Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow passing action to passkey.signIn() #56

Merged
merged 2 commits into from
Apr 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.4.1",
"version": "0.4.2",
"type": "module",
"main": "dist/index.js",
"module": "dist/index.js",
Expand Down
54 changes: 43 additions & 11 deletions src/api/passkey-api-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
AddAuthenticatorResponse,
AuthenticationOptsRequest,
AuthenticationOptsResponse,
ChallengeResponse,
PasskeyAuthenticatorResponse,
RegistrationOptsRequest,
RegistrationOptsResponse,
Expand All @@ -26,12 +27,12 @@ export class PasskeyApiClient {

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

const response = fetch(`${this.baseUrl}/client/user-authenticators/passkey/registration-options`, {
method: "POST",
Expand All @@ -42,31 +43,52 @@ export class PasskeyApiClient {
return (await response).json();
}

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

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

return (await response).json();
}

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

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

return (await response).json();
}

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

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

return (await response).json();
Expand All @@ -85,6 +107,16 @@ export class PasskeyApiClient {
return response.json();
}

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

return (await response).json();
}

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

Expand Down
12 changes: 7 additions & 5 deletions src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ import {
} from "@simplewebauthn/types";

export type RegistrationOptsRequest = {
userName?: string;
token: string;
username?: string;
authenticatorAttachment?: AuthenticatorAttachment | null;
};

Expand All @@ -17,7 +16,7 @@ export type RegistrationOptsResponse = {
};

export type AuthenticationOptsRequest = {
token?: string;
challengeId?: string;
};

export type AuthenticationOptsResponse = {
Expand All @@ -26,7 +25,6 @@ export type AuthenticationOptsResponse = {
};

export type AddAuthenticatorRequest = {
token: string;
challengeId: string;
registrationCredential: RegistrationResponseJSON;
};
Expand All @@ -38,9 +36,9 @@ export type AddAuthenticatorResponse = {
};

export type VerifyRequest = {
token?: string;
challengeId: string;
authenticationCredential: AuthenticationResponseJSON;
deviceId?: string;
};

export type VerifyResponse = {
Expand All @@ -52,3 +50,7 @@ export type PasskeyAuthenticatorResponse = {
credentialId: string;
verifiedAt: string;
};

export type ChallengeResponse = {
challengeId: string;
};
4 changes: 2 additions & 2 deletions src/authsignal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,6 @@ export class Authsignal {
throw new Error("tenantId is required");
}

this.passkey = new Passkey({tenantId, baseUrl});

const idCookie = getCookie(this.anonymousIdCookieName);

if (idCookie) {
Expand All @@ -59,6 +57,8 @@ export class Authsignal {
secure: document.location.protocol !== "http:",
});
}

this.passkey = new Passkey({tenantId, baseUrl, anonymousId: this.anonymousId});
}

launch(url: string, options?: {mode?: "redirect"} & LaunchOptions): undefined;
Expand Down
24 changes: 19 additions & 5 deletions src/passkey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {AuthenticationResponseJSON, RegistrationResponseJSON, AuthenticatorAttac
type PasskeyOptions = {
baseUrl: string;
tenantId: string;
anonymousId: string;
};

type SignUpParams = {
Expand All @@ -17,13 +18,15 @@ type SignUpParams = {
export class Passkey {
public api: PasskeyApiClient;
private passkeyLocalStorageKey = "as_passkey_credential_id";
private anonymousId: string;

constructor({baseUrl, tenantId}: PasskeyOptions) {
constructor({baseUrl, tenantId, anonymousId}: PasskeyOptions) {
this.api = new PasskeyApiClient({baseUrl, tenantId});
this.anonymousId = anonymousId;
}

async signUp({userName, token, authenticatorAttachment = "platform"}: SignUpParams) {
const optionsResponse = await this.api.registrationOptions({userName, token, authenticatorAttachment});
const optionsResponse = await this.api.registrationOptions({username: userName, token, authenticatorAttachment});

const registrationResponse = await startRegistration(optionsResponse.options);

Expand All @@ -41,21 +44,32 @@ export class Passkey {
}

async signIn(): Promise<string | undefined>;
async signIn(params?: {action: string; autofill?: boolean}): Promise<string | undefined>;
async signIn(params?: {token: string}): Promise<string | undefined>;
async signIn(params?: {autofill: boolean}): Promise<string | undefined>;
async signIn(params?: {token?: string; autofill?: boolean} | undefined) {
async signIn(params?: {token?: string; autofill?: boolean; action?: string} | undefined) {
if (params?.token && params.autofill) {
throw new Error("Autofill is not supported when providing a token");
throw new Error("autofill is not supported when providing a token");
}

const optionsResponse = await this.api.authenticationOptions({token: params?.token});
if (params?.action && params.token) {
throw new Error("action is not supported when providing a token");
}

const challengeResponse = params?.action ? await this.api.challenge(params.action) : null;

const optionsResponse = await this.api.authenticationOptions({
token: params?.token,
challengeId: challengeResponse?.challengeId,
});

const authenticationResponse = await startAuthentication(optionsResponse.options, params?.autofill);

const verifyResponse = await this.api.verify({
challengeId: optionsResponse.challengeId,
authenticationCredential: authenticationResponse,
token: params?.token,
deviceId: this.anonymousId,
});

if (verifyResponse?.isVerified) {
Expand Down