Skip to content

Commit

Permalink
Add TOTP support.
Browse files Browse the repository at this point in the history
  • Loading branch information
wparad committed Mar 22, 2024
1 parent 58cf24c commit 355b7d6
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 60 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ This is the changelog for [Authress Login](readme.md).
* Improve http error handling when there is an issue authenticating.
* Reduce logging level for SESSION continuation.
* Temporarily remove encouragement for generating non-256 backed webauthn keys as some browsers don't support more complex options.
* Support missing TOTP saving of devices.

## 2.2 ##
* Automatically retry on network connection issues.
Expand Down
16 changes: 16 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,22 @@ export interface Device {
export interface DeviceRegistrationParameters {
/** The user selected new device name. */
name: string;
/** The device type */
type?: DeviceType;
/** Device data required for registering a TOTP device */
totp?: TotpData;
}

export interface TotpData {
/** The shared secret used to generate TOTP codes. */
secret: string;
/** Verification code used to validate that the secret has been stored safely. */
verificationCode?: string;
}

export enum DeviceType {
TOTP = 'TOTP',
WebAuthN = 'WebAuthN'
}

export enum UserConfigurationScreen {
Expand Down
2 changes: 0 additions & 2 deletions src/extensionClient.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
const base64url = require('./base64url');

const jwtManager = require('./jwtManager');
const { sanitizeUrl } = require('./util');
const windowManager = require('./windowManager');
Expand Down
132 changes: 74 additions & 58 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,78 +174,94 @@ class LoginClient {
await Promise.resolve();
}

async registerDevice(options = { name: '' }) {
async registerDevice(options = { name: '', type: '', totpData: {} }) {
const userIdentity = await this.getUserIdentity();
if (!userIdentity) {
const e = Error('User must be logged to configure user profile data.');
e.code = 'NotLoggedIn';
throw e;
}
const userId = userIdentity.sub;

// https://developer.mozilla.org/en-US/docs/Web/API/CredentialsContainer/create
// Development Note: To actually test to see if this works on your local development machine, run this code on an https domain in the Web Inspector Console tab.
const publicKeyCredentialCreationOptions = {
challenge: Uint8Array.from(userId, c => c.charCodeAt(0)),
rp: {
// Allow all subdomains, this works because Authress always runs on a subdomain such as login.example.com, where the domain example.com is owned by the authress account owner.
id: this.hostUrl.split('.').slice(1).join('.'),
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
// { 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: btoa(String.fromCharCode(...new Uint8Array(credential.response.clientDataJSON)))
};
if (!options) {
const e = Error("Register Device missing required parameter: 'Options'");
e.code = 'InvalidInput';
throw e;
}

const request = {
name: options && options.name,
code: webAuthNTokenRequest,
type: 'WebAuthN'
};
let request;
if (!options.type || options.type === 'WebAuthN') {
const userId = userIdentity.sub;

// https://developer.mozilla.org/en-US/docs/Web/API/CredentialsContainer/create
// Development Note: To actually test to see if this works on your local development machine, run this code on an https domain in the Web Inspector Console tab.
const publicKeyCredentialCreationOptions = {
challenge: Uint8Array.from(userId, c => c.charCodeAt(0)),
rp: {
// Allow all subdomains, this works because Authress always runs on a subdomain such as login.example.com, where the domain example.com is owned by the authress account owner.
id: this.hostUrl.split('.').slice(1).join('.'),
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
// { 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: btoa(String.fromCharCode(...new Uint8Array(credential.response.clientDataJSON)))
};

request = {
name: options && options.name,
code: webAuthNTokenRequest,
type: 'WebAuthN'
};
} else if (options.type === 'TOTP') {
request = {
name: options.name,
totpData: options.totpData,
type: 'TOTP'
};
}

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, request, credential });
this.logger && this.logger.log({ title: 'Failed to register new device', error, request });
throw error;
}
}
Expand Down

0 comments on commit 355b7d6

Please sign in to comment.