Skip to content

Commit

Permalink
Add MFA device related API.
Browse files Browse the repository at this point in the history
  • Loading branch information
wparad committed Dec 9, 2023
1 parent 6786e9f commit 7a08151
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 0 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Change log
This is the changelog for [Authress Login](readme.md).

## 2.3 ##
* Add MFA device methods.

## 2.2 ##
* Automatically retry on network connection issues.
* Handle expired requests on code exchanges.
Expand Down
26 changes: 26 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,20 @@ interface TokenParameters {
timeoutInMillis?: number;
}

/** User credentials from the Authress Credentials Vault. */
interface UserCredentials {
/** User access token generated credentials for the connected provider used to log in */
accessToken: string;
}

/** MFA device */
interface Device {
/** Unique Device ID for the this user specified MFA device. */
deviceId: string;
/** User specified name for this device. */
name: string;
}

export class LoginClient {
/**
* @constructor constructs the LoginClient with a given configuration
Expand All @@ -82,6 +91,23 @@ export class LoginClient {
*/
getConnectionCredentials(): Promise<UserCredentials | null>;

/**
* @description Fetch the list of the user's MFA devices.
*/
getDevices(): Promise<Device>;

/**
* @description Remove a MFA device from the user's profile
* @param {string} deviceId The deviceId to delete from the user's profile.
*/
deleteDevice(deviceId: string): Promise<void>;

/**
* @description Starts the MFA device registration flow, requesting the user to insert or attach their MFA device.
* @param {string} deviceName A user suggested name for this device
*/
registerDevice(deviceName: string): Promise<void>;

/**
* @description Async wait for a user session to exist. Will block until {@link userSessionExists} or {@link authenticate} is called.
* @return {Promise<void>}
Expand Down
89 changes: 89 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,95 @@ class LoginClient {
}
}

async getDevices() {
await this.waitForUserSession();

try {
const token = await this.ensureToken();
const deviceResult = await this.httpClient.get('/session/devices', this.enableCredentials, { Authorization: token && `Bearer ${token}` });
return deviceResult.data.devices;
} catch (error) {
return null;
}
}

async deleteDevice(deviceId) {
await this.waitForUserSession();

try {
const token = await this.ensureToken();
await this.httpClient.delete(`/session/devices/${encodeURIComponent(deviceId)}`, this.enableCredentials, { Authorization: token && `Bearer ${token}` });
} catch (error) {
return null;
}
}

async registerDevice(deviceName) {
await this.waitForUserSession();

const userIdentity = await this.getUserIdentity();
const userId = userIdentity.sub;

// https://developer.mozilla.org/en-US/docs/Web/API/CredentialsContainer/create
const publicKeyCredentialCreationOptions = {
challenge: Uint8Array.from(userId, c => c.charCodeAt(0)),
rp: {
name: 'WebAuthN Login'
},
user: {
id: Uint8Array.from(userId, c => c.charCodeAt(0)),
name: userId,
displayName: `Generated User ID: ${userId}`
},
// https://www.iana.org/assignments/cose/cose.xhtml#algorithms (Order Matters)
pubKeyCredParams: [
// Disabled in the library and not currently supported
// IMPORTANT: NEVER ADD TO THE FRONT OF THIS LIST - because we have no idea which algo public key we have saved in the DB, if we guess wrong there is going to be a mismatch.
// => So until we have a code to enable a retry and realistically, we can deterministically know which public key to type use, we must never prepend this list, only append unless the user data contains a preference order
// { type: 'public-key', alg: -8 }, /* EdDSA */
{ type: 'public-key', alg: -36 }, /* ES512 */ { type: 'public-key', alg: -35 }, /* ES384 */ { type: 'public-key', alg: -7 }, /* ES256 */
// { type: 'public-key', alg: -39 }, /* PS512 */ { type: 'public-key', alg: -38 }, /* PS384 */ { type: 'public-key', alg: -37 }, /* PS256 */
{ type: 'public-key', alg: -259 }, /* RS512 */ { type: 'public-key', alg: -258 }, /* RS384 */ { type: 'public-key', alg: -257 } /* RS256 */
],
authenticatorSelection: {
residentKey: 'discouraged',
requireResidentKey: false,
userVerification: 'discouraged'
// authenticatorAttachment: 'cross-platform'
},
timeout: 60000,
attestation: 'direct'
};

const credential = await navigator.credentials.create({
publicKey: publicKeyCredentialCreationOptions
});

const webAuthNTokenRequest = {
authenticatorAttachment: credential.authenticatorAttachment,
credentialId: credential.id,
type: credential.type,
userId: userId,
attestation: btoa(String.fromCharCode(...new Uint8Array(credential.response.attestationObject))),
client: JSON.parse(new TextDecoder('utf-8').decode(credential.response.clientDataJSON))
};

const request = {
name: deviceName,
code: webAuthNTokenRequest,
type: 'WebAuthN'
};

try {
const token = await this.ensureToken();
const deviceCreationResult = await this.httpClient.post('/session/devices', this.enableCredentials, request, { Authorization: token && `Bearer ${token}` });
return deviceCreationResult.data;
} catch (error) {
this.logger && this.logger.log({ title: 'Failed to register new device', error });
throw error;
}
}

/**
* @description Async wait for a user session to exist. Will block until {@link userSessionExists} or {@link authenticate} is called.
* @return {Promise<void>}
Expand Down

0 comments on commit 7a08151

Please sign in to comment.