Skip to content

Commit

Permalink
Added stepup client method (#33)
Browse files Browse the repository at this point in the history
- added step up method and changed signinbegin to accept purpose
  • Loading branch information
jrmccannon authored Jun 11, 2024
1 parent bfccce2 commit f8f81c9
Show file tree
Hide file tree
Showing 2 changed files with 76 additions and 9 deletions.
72 changes: 63 additions & 9 deletions src/passwordless.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
RegisterBeginResponse,
SigninBeginResponse,
SigninMethod,
StepupRequest,
TokenResponse
} from './types';

Expand All @@ -19,7 +20,7 @@ export class Client {
apiUrl: 'https://v4.passwordless.dev',
apiKey: '',
origin: window.location.origin,
rpid: window.location.hostname
rpid: window.location.hostname,
};
private abortController: AbortController = new AbortController();

Expand Down Expand Up @@ -147,7 +148,7 @@ export class Client {
body: JSON.stringify({
token,
RPID: this.config.rpid,
Origin: this.config.origin
Origin: this.config.origin,
}),
});

Expand Down Expand Up @@ -221,11 +222,6 @@ export class Client {
return signin;
}

signin.data.challenge = base64UrlToArrayBuffer(signin.data.challenge);
signin.data.allowCredentials?.forEach((cred) => {
cred.id = base64UrlToArrayBuffer(cred.id);
});

const credential = await navigator.credentials.get({
publicKey: signin.data,
mediation: 'autofill' in signinMethod ? "conditional" as CredentialMediationRequirement : undefined, // Typescript doesn't know about 'conditational' yet
Expand All @@ -249,7 +245,55 @@ export class Client {
}
}

private async signinBegin(signinMethod: SigninMethod): PromiseResult<SigninBeginResponse> {
/**
* Performs a step-up authentication process. This is essentially an overload for the sign-in workflow. It allows for
* a user authentication to be given a purpose or context for the sign-in, enabling a "step-up" authentication flow.
*
* @param {StepupRequest} stepup - The step-up request object. This includes the sign-in method and the purpose of the authentication
*
* @returns {token} - The result of the step-up sign-in process.
*/
public async stepup(stepup: StepupRequest) : PromiseResult<TokenResponse> {
try {
this.assertBrowserSupported();
this.handleAbort();

if (!stepup.signinMethod) {
throw new Error("You need to provide the signinMethod");
}

if (!stepup.purpose) {
stepup.purpose = "step-up";
}

const signin = await this.signinBegin(stepup.signinMethod, stepup.purpose);

if (signin.error) {
return signin;
}

const credential = await navigator.credentials.get({
publicKey: signin.data,
mediation: 'autofill' in stepup.signinMethod ? "conditional" as CredentialMediationRequirement : undefined, // Typescript doesn't know about 'conditional' yet
signal: this.abortController.signal,
}) as PublicKeyCredential;

return await this.signinComplete(credential, signin.session);
} catch (caughtError: any) {
const errorMessage = getErrorMessage(caughtError);
const error = {
from: "client",
errorCode: "unknown",
title: errorMessage,
};
console.error(caughtError);
console.error(error);

return { error };
}
}

private async signinBegin(signinMethod: SigninMethod, purpose?: string): PromiseResult<SigninBeginResponse> {
const response = await fetch(`${this.config.apiUrl}/signin/begin`, {
method: 'POST',
headers: this.createHeaders(),
Expand All @@ -258,12 +302,22 @@ export class Client {
alias: "alias" in signinMethod ? signinMethod.alias : undefined,
RPID: this.config.rpid,
Origin: this.config.origin,
purpose: purpose
}),
});

const res = await response.json();
if (response.ok) {
return res;
return {
...res,
data: {
...res.data,
challenge: base64UrlToArrayBuffer(res.data.challenge),
allowCredentials: res.data.allowCredentials?.map((cred: PublicKeyCredentialDescriptor) => {
return { ...cred, id: base64UrlToArrayBuffer(cred.id) };
})
}
};
}

return { error: { ...res, from: "server" } };
Expand Down
13 changes: 13 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
export type AtLeast<T, K extends keyof T> = Partial<T> & Pick<T, K>;

/**
* Represents a sign-in method.
*/
export type SigninMethod = { userId: string } | { alias: string } | { autofill: boolean } | { discoverable: boolean };

/**
* Represents a step-up request to initiate a specific action or operation.
*
* @interface StepupRequest
*/
export interface StepupRequest {
signinMethod: SigninMethod;
purpose: string;
}

export type RegisterBeginResponse = {
session: string;
data: PublicKeyCredentialCreationOptions;
Expand Down

0 comments on commit f8f81c9

Please sign in to comment.