Skip to content

Commit

Permalink
Add support for onTokenExpired callback (#75)
Browse files Browse the repository at this point in the history
* wip

* Bump version
  • Loading branch information
hwhmeikle authored Oct 2, 2024
1 parent 44e8d08 commit 798baee
Show file tree
Hide file tree
Showing 13 changed files with 167 additions and 104 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@authsignal/browser",
"version": "0.6.0",
"version": "0.6.1",
"type": "module",
"main": "dist/index.js",
"module": "dist/index.js",
Expand Down
48 changes: 27 additions & 21 deletions src/api/email-api-client.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,59 @@
import {ApiClientOptions, ChallengeResponse, EnrollResponse, VerifyResponse} from "./types/shared";
import {buildHeaders, handleTokenExpired} from "./helpers";
import {ApiClientOptions, AuthsignalResponse, ChallengeResponse, EnrollResponse, VerifyResponse} from "./types/shared";

export class EmailApiClient {
tenantId: string;
baseUrl: string;
onTokenExpired?: () => void;

constructor({baseUrl, tenantId}: ApiClientOptions) {
constructor({baseUrl, tenantId, onTokenExpired}: ApiClientOptions) {
this.tenantId = tenantId;
this.baseUrl = baseUrl;
this.onTokenExpired = onTokenExpired;
}

async enroll({token, email}: {token: string; email: string}): Promise<EnrollResponse> {
async enroll({token, email}: {token: string; email: string}) {
const body = {email};

const response = fetch(`${this.baseUrl}/client/user-authenticators/email-otp`, {
const response = await fetch(`${this.baseUrl}/client/user-authenticators/email-otp`, {
method: "POST",
headers: this.buildHeaders(token),
headers: buildHeaders({token, tenantId: this.tenantId}),
body: JSON.stringify(body),
});

return (await response).json();
const responseJson: AuthsignalResponse<EnrollResponse> = await response.json();

handleTokenExpired({response: responseJson, onTokenExpired: this.onTokenExpired});

return responseJson;
}

async challenge({token}: {token: string}): Promise<ChallengeResponse> {
const response = fetch(`${this.baseUrl}/client/challenge/email-otp`, {
async challenge({token}: {token: string}) {
const response = await fetch(`${this.baseUrl}/client/challenge/email-otp`, {
method: "POST",
headers: this.buildHeaders(token),
headers: buildHeaders({token, tenantId: this.tenantId}),
});

return (await response).json();
const responseJson: AuthsignalResponse<ChallengeResponse> = await response.json();

handleTokenExpired({response: responseJson, onTokenExpired: this.onTokenExpired});

return responseJson;
}

async verify({token, code}: {token: string; code: string}): Promise<VerifyResponse> {
async verify({token, code}: {token: string; code: string}) {
const body = {verificationCode: code};

const response = fetch(`${this.baseUrl}/client/verify/email-otp`, {
const response = await fetch(`${this.baseUrl}/client/verify/email-otp`, {
method: "POST",
headers: this.buildHeaders(token),
headers: buildHeaders({token, tenantId: this.tenantId}),
body: JSON.stringify(body),
});

return (await response).json();
}
const responseJson: AuthsignalResponse<VerifyResponse> = await response.json();

private buildHeaders(token?: string) {
const authorizationHeader = token ? `Bearer ${token}` : `Basic ${window.btoa(encodeURIComponent(this.tenantId))}`;
handleTokenExpired({response: responseJson, onTokenExpired: this.onTokenExpired});

return {
"Content-Type": "application/json",
Authorization: authorizationHeader,
};
return responseJson;
}
}
13 changes: 13 additions & 0 deletions src/api/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import {AuthsignalResponse} from "./types/shared";

type BuildHeadersParams = {
token?: string;
tenantId: string;
Expand All @@ -11,3 +13,14 @@ export function buildHeaders({token, tenantId}: BuildHeadersParams) {
Authorization: authorizationHeader,
};
}

type HandleTokenExpiredParams<T> = {
response: AuthsignalResponse<T>;
onTokenExpired?: () => void;
};

export function handleTokenExpired<T extends object>({response, onTokenExpired}: HandleTokenExpiredParams<T>) {
if ("error" in response && response.errorCode === "token_expired" && onTokenExpired) {
onTokenExpired();
}
}
72 changes: 39 additions & 33 deletions src/api/passkey-api-client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {buildHeaders} from "./helpers";
import {buildHeaders, handleTokenExpired} from "./helpers";
import {
AddAuthenticatorRequest,
AddAuthenticatorResponse,
Expand All @@ -15,79 +15,81 @@ import {ApiClientOptions, AuthsignalResponse, ChallengeResponse} from "./types/s
export class PasskeyApiClient {
tenantId: string;
baseUrl: string;
onTokenExpired?: () => void;

constructor({baseUrl, tenantId}: ApiClientOptions) {
constructor({baseUrl, tenantId, onTokenExpired}: ApiClientOptions) {
this.tenantId = tenantId;
this.baseUrl = baseUrl;
this.onTokenExpired = onTokenExpired;
}

async registrationOptions({
token,
username,
authenticatorAttachment,
}: {token: string} & RegistrationOptsRequest): Promise<AuthsignalResponse<RegistrationOptsResponse>> {
async registrationOptions({token, username, authenticatorAttachment}: {token: string} & RegistrationOptsRequest) {
const body: RegistrationOptsRequest = Boolean(authenticatorAttachment)
? {username, authenticatorAttachment}
: {username};

const response = fetch(`${this.baseUrl}/client/user-authenticators/passkey/registration-options`, {
const response = await fetch(`${this.baseUrl}/client/user-authenticators/passkey/registration-options`, {
method: "POST",
headers: buildHeaders({token, tenantId: this.tenantId}),
body: JSON.stringify(body),
});

return (await response).json();
const responseJson: AuthsignalResponse<RegistrationOptsResponse> = await response.json();

handleTokenExpired({response: responseJson, onTokenExpired: this.onTokenExpired});

return responseJson;
}

async authenticationOptions({
token,
challengeId,
}: {token?: string} & AuthenticationOptsRequest): Promise<AuthsignalResponse<AuthenticationOptsResponse>> {
async authenticationOptions({token, challengeId}: {token?: string} & AuthenticationOptsRequest) {
const body: AuthenticationOptsRequest = {challengeId};

const response = fetch(`${this.baseUrl}/client/user-authenticators/passkey/authentication-options`, {
const response = await fetch(`${this.baseUrl}/client/user-authenticators/passkey/authentication-options`, {
method: "POST",
headers: buildHeaders({token, tenantId: this.tenantId}),
body: JSON.stringify(body),
});

return (await response).json();
const responseJson: AuthsignalResponse<AuthenticationOptsResponse> = await response.json();

handleTokenExpired({response: responseJson, onTokenExpired: this.onTokenExpired});

return responseJson;
}

async addAuthenticator({
token,
challengeId,
registrationCredential,
}: {token: string} & AddAuthenticatorRequest): Promise<AuthsignalResponse<AddAuthenticatorResponse>> {
async addAuthenticator({token, challengeId, registrationCredential}: {token: string} & AddAuthenticatorRequest) {
const body: AddAuthenticatorRequest = {
challengeId,
registrationCredential,
};

const response = fetch(`${this.baseUrl}/client/user-authenticators/passkey`, {
const response = await fetch(`${this.baseUrl}/client/user-authenticators/passkey`, {
method: "POST",
headers: buildHeaders({token, tenantId: this.tenantId}),
body: JSON.stringify(body),
});

return (await response).json();
const responseJson: AuthsignalResponse<AddAuthenticatorResponse> = await response.json();

handleTokenExpired({response: responseJson, onTokenExpired: this.onTokenExpired});

return responseJson;
}

async verify({
token,
challengeId,
authenticationCredential,
deviceId,
}: {token?: string} & VerifyRequest): Promise<AuthsignalResponse<VerifyResponse>> {
async verify({token, challengeId, authenticationCredential, deviceId}: {token?: string} & VerifyRequest) {
const body: VerifyRequest = {challengeId, authenticationCredential, deviceId};

const response = fetch(`${this.baseUrl}/client/verify/passkey`, {
const response = await fetch(`${this.baseUrl}/client/verify/passkey`, {
method: "POST",
headers: buildHeaders({token, tenantId: this.tenantId}),
body: JSON.stringify(body),
});

return (await response).json();
const responseJson: AuthsignalResponse<VerifyResponse> = await response.json();

handleTokenExpired({response: responseJson, onTokenExpired: this.onTokenExpired});

return responseJson;
}

async getPasskeyAuthenticator({
Expand All @@ -107,13 +109,17 @@ export class PasskeyApiClient {
return response.json();
}

async challenge(action: string): Promise<AuthsignalResponse<ChallengeResponse>> {
const response = fetch(`${this.baseUrl}/client/challenge`, {
async challenge(action: string) {
const response = await fetch(`${this.baseUrl}/client/challenge`, {
method: "POST",
headers: buildHeaders({tenantId: this.tenantId}),
body: JSON.stringify({action}),
});

return (await response).json();
const responseJson: AuthsignalResponse<ChallengeResponse> = await response.json();

handleTokenExpired({response: responseJson, onTokenExpired: this.onTokenExpired});

return responseJson;
}
}
38 changes: 26 additions & 12 deletions src/api/sms-api-client.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,59 @@
import {buildHeaders} from "./helpers";
import {ApiClientOptions, ChallengeResponse, EnrollResponse, VerifyResponse} from "./types/shared";
import {buildHeaders, handleTokenExpired} from "./helpers";
import {ApiClientOptions, AuthsignalResponse, ChallengeResponse, EnrollResponse, VerifyResponse} from "./types/shared";

export class SmsApiClient {
tenantId: string;
baseUrl: string;
onTokenExpired?: () => void;

constructor({baseUrl, tenantId}: ApiClientOptions) {
constructor({baseUrl, tenantId, onTokenExpired}: ApiClientOptions) {
this.tenantId = tenantId;
this.baseUrl = baseUrl;
this.onTokenExpired = onTokenExpired;
}

async enroll({token, phoneNumber}: {token: string; phoneNumber: string}): Promise<EnrollResponse> {
async enroll({token, phoneNumber}: {token: string; phoneNumber: string}) {
const body = {phoneNumber};

const response = fetch(`${this.baseUrl}/client/user-authenticators/sms`, {
const response = await fetch(`${this.baseUrl}/client/user-authenticators/sms`, {
method: "POST",
headers: buildHeaders({token, tenantId: this.tenantId}),
body: JSON.stringify(body),
});

return (await response).json();
const responseJson: AuthsignalResponse<EnrollResponse> = await response.json();

handleTokenExpired({response: responseJson, onTokenExpired: this.onTokenExpired});

return responseJson;
}

async challenge({token}: {token: string}): Promise<ChallengeResponse> {
const response = fetch(`${this.baseUrl}/client/challenge/sms`, {
async challenge({token}: {token: string}) {
const response = await fetch(`${this.baseUrl}/client/challenge/sms`, {
method: "POST",
headers: buildHeaders({token, tenantId: this.tenantId}),
});

return (await response).json();
const responseJson: AuthsignalResponse<ChallengeResponse> = await response.json();

handleTokenExpired({response: responseJson, onTokenExpired: this.onTokenExpired});

return responseJson;
}

async verify({token, code}: {token: string; code: string}): Promise<VerifyResponse> {
async verify({token, code}: {token: string; code: string}) {
const body = {verificationCode: code};

const response = fetch(`${this.baseUrl}/client/verify/sms`, {
const response = await fetch(`${this.baseUrl}/client/verify/sms`, {
method: "POST",
headers: buildHeaders({token, tenantId: this.tenantId}),
body: JSON.stringify(body),
});

return (await response).json();
const responseJson: AuthsignalResponse<VerifyResponse> = await response.json();

handleTokenExpired({response: responseJson, onTokenExpired: this.onTokenExpired});

return responseJson;
}
}
28 changes: 19 additions & 9 deletions src/api/totp-api-client.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,44 @@
import {buildHeaders} from "./helpers";
import {ApiClientOptions, VerifyResponse} from "./types/shared";
import {buildHeaders, handleTokenExpired} from "./helpers";
import {ApiClientOptions, AuthsignalResponse, VerifyResponse} from "./types/shared";
import {EnrollResponse} from "./types/totp";

export class TotpApiClient {
tenantId: string;
baseUrl: string;
onTokenExpired?: () => void;

constructor({baseUrl, tenantId}: ApiClientOptions) {
constructor({baseUrl, tenantId, onTokenExpired}: ApiClientOptions) {
this.tenantId = tenantId;
this.baseUrl = baseUrl;
this.onTokenExpired = onTokenExpired;
}

async enroll({token}: {token: string}): Promise<EnrollResponse> {
const response = fetch(`${this.baseUrl}/client/user-authenticators/totp`, {
async enroll({token}: {token: string}) {
const response = await fetch(`${this.baseUrl}/client/user-authenticators/totp`, {
method: "POST",
headers: buildHeaders({token, tenantId: this.tenantId}),
});

return (await response).json();
const responseJson: AuthsignalResponse<EnrollResponse> = await response.json();

handleTokenExpired({response: responseJson, onTokenExpired: this.onTokenExpired});

return responseJson;
}

async verify({token, code}: {token: string; code: string}): Promise<VerifyResponse> {
async verify({token, code}: {token: string; code: string}) {
const body = {verificationCode: code};

const response = fetch(`${this.baseUrl}/client/verify/totp`, {
const response = await fetch(`${this.baseUrl}/client/verify/totp`, {
method: "POST",
headers: buildHeaders({token, tenantId: this.tenantId}),
body: JSON.stringify(body),
});

return (await response).json();
const responseJson: AuthsignalResponse<VerifyResponse> = await response.json();

handleTokenExpired({response: responseJson, onTokenExpired: this.onTokenExpired});

return responseJson;
}
}
3 changes: 3 additions & 0 deletions src/api/types/shared.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export type ApiClientOptions = {
baseUrl: string;
tenantId: string;
onTokenExpired?: () => void;
};

export type EnrollResponse = {
Expand All @@ -20,6 +21,8 @@ export type VerifyResponse = {

export type ErrorResponse = {
error: string;
// eslint-disable-next-line @typescript-eslint/ban-types -- This is a valid use case for an empty object
errorCode?: "token_expired" | (string & {});
errorDescription?: string;
};

Expand Down
Loading

0 comments on commit 798baee

Please sign in to comment.