From 1cc3973b9809bf761b5368edcfe0938f057907b0 Mon Sep 17 00:00:00 2001 From: Ashley Frieze Date: Tue, 26 Mar 2024 17:49:39 +0000 Subject: [PATCH 1/3] feat: create device binding emulator client --- package-lock.json | 29 +++- packages/javascript-sdk/package.json | 5 +- packages/javascript-sdk/src/auth/enums.ts | 2 + .../DeviceBindingEmulator.ts | 131 ++++++++++++++++++ packages/javascript-sdk/src/index.ts | 2 + 5 files changed, 163 insertions(+), 6 deletions(-) create mode 100644 packages/javascript-sdk/src/fr-device-binding-emulation/DeviceBindingEmulator.ts diff --git a/package-lock.json b/package-lock.json index c89aacaf8..a151cc51a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20172,6 +20172,14 @@ "dev": true, "license": "MIT" }, + "node_modules/jose": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.2.3.tgz", + "integrity": "sha512-KUXdbctm1uHVL8BYhnyHkgp3zDX5KW8ZhAKVFEfUbU2P8Alpzjb+48hHvjOdQIyPshoblhzsuqOwEEAbtHVirA==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/joycon": { "version": "3.1.1", "dev": true, @@ -32414,14 +32422,17 @@ }, "packages/javascript-sdk": { "name": "@forgerock/javascript-sdk", - "version": "4.4.1", - "license": "MIT" + "version": "4.4.0", + "license": "MIT", + "dependencies": { + "jose": "^5.2.3" + } }, "packages/ping-protect": { "name": "@forgerock/ping-protect", - "version": "4.4.1", + "version": "4.4.0", "peerDependencies": { - "@forgerock/javascript-sdk": "^4.4.1" + "@forgerock/javascript-sdk": "^4.4.0" } }, "packages/token-vault": { @@ -36868,7 +36879,10 @@ "dev": true }, "@forgerock/javascript-sdk": { - "version": "file:packages/javascript-sdk" + "version": "file:packages/javascript-sdk", + "requires": { + "jose": "^5.2.3" + } }, "@forgerock/ping-protect": { "version": "file:packages/ping-protect", @@ -48299,6 +48313,11 @@ "version": "1.4.0", "dev": true }, + "jose": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.2.3.tgz", + "integrity": "sha512-KUXdbctm1uHVL8BYhnyHkgp3zDX5KW8ZhAKVFEfUbU2P8Alpzjb+48hHvjOdQIyPshoblhzsuqOwEEAbtHVirA==" + }, "joycon": { "version": "3.1.1", "dev": true diff --git a/packages/javascript-sdk/package.json b/packages/javascript-sdk/package.json index 0b7ffdd03..7092ceda0 100644 --- a/packages/javascript-sdk/package.json +++ b/packages/javascript-sdk/package.json @@ -26,5 +26,8 @@ }, "main": "./src/index.cjs", "module": "./src/index.js", - "types": "./src/index.d.ts" + "types": "./src/index.d.ts", + "dependencies": { + "jose": "^5.2.3" + } } diff --git a/packages/javascript-sdk/src/auth/enums.ts b/packages/javascript-sdk/src/auth/enums.ts index 3874922a6..a850942a5 100644 --- a/packages/javascript-sdk/src/auth/enums.ts +++ b/packages/javascript-sdk/src/auth/enums.ts @@ -25,7 +25,9 @@ enum CallbackType { BooleanAttributeInputCallback = 'BooleanAttributeInputCallback', ChoiceCallback = 'ChoiceCallback', ConfirmationCallback = 'ConfirmationCallback', + DeviceBindingCallback = 'DeviceBindingCallback', DeviceProfileCallback = 'DeviceProfileCallback', + DeviceSigningVerifierCallback = 'DeviceSigningVerifierCallback', HiddenValueCallback = 'HiddenValueCallback', KbaCreateCallback = 'KbaCreateCallback', MetadataCallback = 'MetadataCallback', diff --git a/packages/javascript-sdk/src/fr-device-binding-emulation/DeviceBindingEmulator.ts b/packages/javascript-sdk/src/fr-device-binding-emulation/DeviceBindingEmulator.ts new file mode 100644 index 000000000..46c425762 --- /dev/null +++ b/packages/javascript-sdk/src/fr-device-binding-emulation/DeviceBindingEmulator.ts @@ -0,0 +1,131 @@ +import { v4 } from 'uuid'; +import FRCallback from '../fr-auth/callbacks'; +import { CallbackType } from '../auth/enums'; +import * as jose from 'jose'; + +const ALGORITHM = 'ES256'; + +/** + * An emulator for a device binding client. Not for production use. + * Can help with testing scenarios. + */ +export default class DeviceBindingEmulator { + /** + * Factory method to create a virtual device + * @returns Promise that will resolve to a new device emulator object + */ + public static async createDevice(): Promise { + const { publicKey, privateKey } = await jose.generateKeyPair('ES256', { + extractable: true, + }); + + const privateJwk = await jose.exportJWK(privateKey); + const publicJwk = await jose.exportJWK(publicKey); + const kid = v4(); + + return new DeviceBindingEmulator(publicJwk, privateJwk, kid); + } + + private publicJwk: jose.JWK; + private privateJwk: jose.JWK; + private kid: string; + private deviceId: string = v4(); + private _userId = ''; + + private constructor(publicJwk: jose.JWK, privateJwk: jose.JWK, kid: string) { + this.publicJwk = publicJwk; + this.privateJwk = privateJwk; + this.kid = kid; + } + + /** + * Execute device binding for a user and store the user + * in this object. Will satisfy the device binding challenge + * from the server, posing as an iOS client. + * @param callback device binding callback received from a journey + */ + public async bind(callback: FRCallback): Promise { + if (callback.getType() !== CallbackType.DeviceBindingCallback) { + throw new Error( + `Expecting a callback of type DeviceBindingCallback but got ${callback.getType()}`, + ); + } + + const userId = callback.getOutputValue(0) as string; + this._userId = userId; + + const challenge = callback.getOutputValue(3) as string; + + const jwt = await this.createDeviceBindingJwt(challenge); + + callback.setInputValue(jwt, 0); + callback.setInputValue('iPhone', 1); + callback.setInputValue(this.deviceId, 2); + } + + /** + * Perform the device signing operation, satisfying a device + * signing verifier callback. This emulates login with face id or + * thumbprint, etc. It also acts as though the iOS SDK. + * @param callback the device signing callback + */ + public async signIn(callback: FRCallback): Promise { + if (callback.getType() !== CallbackType.DeviceSigningVerifierCallback) { + throw new Error( + `Expecting a callback of type DeviceSigningVerifierCallback but got ${callback.getType()}`, + ); + } + + const challenge = callback.getOutputValue(1) as string; + + const jwt = await this.createDeviceLoginJwt(challenge); + + callback.setInputValue(jwt, 0); + } + + public get userId() { + return this._userId; + } + + public set userId(userId: string) { + this._userId = userId; + } + + private async createDeviceBindingJwt(challenge: string): Promise { + const { privateJwk, kid, publicJwk, _userId: sub } = this; + + return new jose.SignJWT({ + platform: 'ios', + iss: 'com.forgerock.unsummit', + sub, + challenge, + }) + .setProtectedHeader({ + typ: 'JWS', + jwk: { kid, ...publicJwk }, + kid, + alg: ALGORITHM, + }) + .setIssuedAt() + .setExpirationTime('1m') + .sign(await jose.importJWK(privateJwk, ALGORITHM)); + } + + private async createDeviceLoginJwt(challenge: string): Promise { + const { privateJwk, kid, _userId: sub } = this; + + return new jose.SignJWT({ + iss: 'com.forgerock.unsummit', + sub, + challenge, + }) + .setProtectedHeader({ + typ: 'JWS', + kid, + alg: ALGORITHM, + }) + .setIssuedAt() + .setExpirationTime('1m') + .sign(await jose.importJWK(privateJwk, ALGORITHM)); + } +} diff --git a/packages/javascript-sdk/src/index.ts b/packages/javascript-sdk/src/index.ts index 895456fc1..c74dab854 100644 --- a/packages/javascript-sdk/src/index.ts +++ b/packages/javascript-sdk/src/index.ts @@ -77,6 +77,7 @@ import Deferred from './util/deferred'; import PKCE from './util/pkce'; import LocalStorage from './util/storage'; import type { LoggerFunctions, StepOptions } from './config/interfaces'; +import DeviceBindingEmulator from './fr-device-binding-emulation/DeviceBindingEmulator'; export type { AuthResponse, @@ -113,6 +114,7 @@ export { Config, ConfirmationCallback, Deferred, + DeviceBindingEmulator, DeviceProfileCallback, ErrorCode, FRAuth, From 56a4c734889fd0b96d85980b76aec49dd9f48e0b Mon Sep 17 00:00:00 2001 From: Ashley Frieze Date: Wed, 3 Apr 2024 13:01:46 +0100 Subject: [PATCH 2/3] feat: allow the issuer to be configurable --- .../DeviceBindingEmulator.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/javascript-sdk/src/fr-device-binding-emulation/DeviceBindingEmulator.ts b/packages/javascript-sdk/src/fr-device-binding-emulation/DeviceBindingEmulator.ts index 46c425762..b069d6ffe 100644 --- a/packages/javascript-sdk/src/fr-device-binding-emulation/DeviceBindingEmulator.ts +++ b/packages/javascript-sdk/src/fr-device-binding-emulation/DeviceBindingEmulator.ts @@ -12,9 +12,12 @@ const ALGORITHM = 'ES256'; export default class DeviceBindingEmulator { /** * Factory method to create a virtual device + * @param issuer the issuer to use * @returns Promise that will resolve to a new device emulator object */ - public static async createDevice(): Promise { + public static async createDevice( + issuer = 'com.forgerock.unsummit', + ): Promise { const { publicKey, privateKey } = await jose.generateKeyPair('ES256', { extractable: true, }); @@ -23,7 +26,7 @@ export default class DeviceBindingEmulator { const publicJwk = await jose.exportJWK(publicKey); const kid = v4(); - return new DeviceBindingEmulator(publicJwk, privateJwk, kid); + return new DeviceBindingEmulator(publicJwk, privateJwk, kid, issuer); } private publicJwk: jose.JWK; @@ -31,11 +34,13 @@ export default class DeviceBindingEmulator { private kid: string; private deviceId: string = v4(); private _userId = ''; + private issuer: string; - private constructor(publicJwk: jose.JWK, privateJwk: jose.JWK, kid: string) { + private constructor(publicJwk: jose.JWK, privateJwk: jose.JWK, kid: string, issuer: string) { this.publicJwk = publicJwk; this.privateJwk = privateJwk; this.kid = kid; + this.issuer = issuer; } /** @@ -96,7 +101,7 @@ export default class DeviceBindingEmulator { return new jose.SignJWT({ platform: 'ios', - iss: 'com.forgerock.unsummit', + iss: this.issuer, sub, challenge, }) @@ -115,7 +120,7 @@ export default class DeviceBindingEmulator { const { privateJwk, kid, _userId: sub } = this; return new jose.SignJWT({ - iss: 'com.forgerock.unsummit', + iss: this.issuer, sub, challenge, }) From cce0b76b0687264c0173fcd8d0e15f02e5ae8ef8 Mon Sep 17 00:00:00 2001 From: Ashley Frieze Date: Wed, 3 Apr 2024 13:45:09 +0100 Subject: [PATCH 3/3] chore: update package lock --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index a151cc51a..2a1a3ee9f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32422,7 +32422,7 @@ }, "packages/javascript-sdk": { "name": "@forgerock/javascript-sdk", - "version": "4.4.0", + "version": "4.4.1", "license": "MIT", "dependencies": { "jose": "^5.2.3" @@ -32430,9 +32430,9 @@ }, "packages/ping-protect": { "name": "@forgerock/ping-protect", - "version": "4.4.0", + "version": "4.4.1", "peerDependencies": { - "@forgerock/javascript-sdk": "^4.4.0" + "@forgerock/javascript-sdk": "^4.4.1" } }, "packages/token-vault": {