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

Added stepup client method #33

Merged
merged 13 commits into from
Jun 11, 2024
78 changes: 69 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,61 @@ 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
* @param {function} callback - The optional callback function to handle the result of the step-up process.
* Receives the token as the first argument and optional additional arguments.
* @returns {object} - The result of the step-up process. If a callback function is provided, it returns
* the result of the callback function; otherwise, it returns the result directly.
*/
public async stepup(stepup: StepupRequest, callback?: (token: string | undefined, args?: any) => any) {
try {
this.assertBrowserSupported();
this.handleAbort();

if (!stepup.signinMethod) {
throw new Error("You need to provide the signInMethod");
jrmccannon marked this conversation as resolved.
Show resolved Hide resolved
}

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;

const signInComplete = await this.signinComplete(credential, signin.session);

return callback === undefined || callback === null
? signInComplete
: callback(signInComplete.token);
} catch (caughtError: any) {
const errorMessage = getErrorMessage(caughtError);
const error = {
from: "client",
errorCode: "unknown",
title: errorMessage,
};
console.error(caughtError);
jrmccannon marked this conversation as resolved.
Show resolved Hide resolved
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 +308,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