From 0c23375ebb4f1fa2089281cfa64e98850023d387 Mon Sep 17 00:00:00 2001 From: Patryk Kalinowski Date: Thu, 16 Oct 2025 16:29:28 +0200 Subject: [PATCH 1/2] Add support for custom auth providers (authcode & authcode-pkce only) --- .../wallet/wdk/src/dbs/auth-commitments.ts | 2 +- .../src/sequence/handlers/authcode-pkce.ts | 8 +-- .../wdk/src/sequence/handlers/authcode.ts | 19 ++----- packages/wallet/wdk/src/sequence/manager.ts | 49 +++++++++++++++++++ packages/wallet/wdk/src/sequence/signers.ts | 6 ++- .../wallet/wdk/src/sequence/types/signer.ts | 2 +- packages/wallet/wdk/src/sequence/wallets.ts | 29 +++++++++-- 7 files changed, 89 insertions(+), 26 deletions(-) diff --git a/packages/wallet/wdk/src/dbs/auth-commitments.ts b/packages/wallet/wdk/src/dbs/auth-commitments.ts index dcb97e566..a3f360639 100644 --- a/packages/wallet/wdk/src/dbs/auth-commitments.ts +++ b/packages/wallet/wdk/src/dbs/auth-commitments.ts @@ -5,7 +5,7 @@ const TABLE_NAME = 'auth-commitments' export type AuthCommitment = { id: string - kind: 'google-pkce' | 'apple' + kind: 'google-pkce' | 'apple' | `custom-${string}` metadata: { [key: string]: string } verifier?: string challenge?: string diff --git a/packages/wallet/wdk/src/sequence/handlers/authcode-pkce.ts b/packages/wallet/wdk/src/sequence/handlers/authcode-pkce.ts index 6029f4114..0b4706c0e 100644 --- a/packages/wallet/wdk/src/sequence/handlers/authcode-pkce.ts +++ b/packages/wallet/wdk/src/sequence/handlers/authcode-pkce.ts @@ -8,15 +8,16 @@ import { AuthCodeHandler } from './authcode.js' export class AuthCodePkceHandler extends AuthCodeHandler implements Handler { constructor( - signupKind: 'google-pkce', + signupKind: 'google-pkce' | `custom-${string}`, issuer: string, + oauthUrl: string, audience: string, nitro: Identity.IdentityInstrument, signatures: Signatures, commitments: Db.AuthCommitments, authKeys: Db.AuthKeys, ) { - super(signupKind, issuer, audience, nitro, signatures, commitments, authKeys) + super(signupKind, issuer, oauthUrl, audience, nitro, signatures, commitments, authKeys) } public async commitAuth(target: string, isSignUp: boolean, state?: string, signer?: string) { @@ -50,8 +51,7 @@ export class AuthCodePkceHandler extends AuthCodeHandler implements Handler { state, }) - const oauthUrl = this.oauthUrl() - return `${oauthUrl}?${searchParams.toString()}` + return `${this.oauthUrl}?${searchParams.toString()}` } public async completeAuth( diff --git a/packages/wallet/wdk/src/sequence/handlers/authcode.ts b/packages/wallet/wdk/src/sequence/handlers/authcode.ts index bb8a1b315..f73f9ec5d 100644 --- a/packages/wallet/wdk/src/sequence/handlers/authcode.ts +++ b/packages/wallet/wdk/src/sequence/handlers/authcode.ts @@ -11,8 +11,9 @@ export class AuthCodeHandler extends IdentityHandler implements Handler { protected redirectUri: string = '' constructor( - public readonly signupKind: 'apple' | 'google-pkce', + public readonly signupKind: 'apple' | 'google-pkce' | `custom-${string}`, public readonly issuer: string, + protected readonly oauthUrl: string, public readonly audience: string, nitro: Identity.IdentityInstrument, signatures: Signatures, @@ -48,12 +49,11 @@ export class AuthCodeHandler extends IdentityHandler implements Handler { client_id: this.audience, redirect_uri: this.redirectUri, response_type: 'code', - scope: 'openid', + scope: 'openid profile email', state, }) - const oauthUrl = this.oauthUrl() - return `${oauthUrl}?${searchParams.toString()}` + return `${this.oauthUrl}?${searchParams.toString()}` } public async completeAuth( @@ -100,15 +100,4 @@ export class AuthCodeHandler extends IdentityHandler implements Handler { }, } } - - protected oauthUrl() { - switch (this.issuer) { - case 'https://accounts.google.com': - return 'https://accounts.google.com/o/oauth2/v2/auth' - case 'https://appleid.apple.com': - return 'https://appleid.apple.com/auth/authorize' - default: - throw new Error('unsupported-issuer') - } - } } diff --git a/packages/wallet/wdk/src/sequence/manager.ts b/packages/wallet/wdk/src/sequence/manager.ts index 0dbe84ef8..9df36d68f 100644 --- a/packages/wallet/wdk/src/sequence/manager.ts +++ b/packages/wallet/wdk/src/sequence/manager.ts @@ -79,6 +79,13 @@ export type ManagerOptions = { enabled: boolean clientId: string } + customProviders?: { + kind: `custom-${string}` + authMethod: 'id-token' | 'authcode' | 'authcode-pkce' + issuer: string + oauthUrl: string + clientId: string + }[] } } @@ -466,6 +473,7 @@ export class Manager { new AuthCodePkceHandler( 'google-pkce', 'https://accounts.google.com', + 'https://accounts.google.com/o/oauth2/v2/auth', ops.identity.google.clientId, identityInstrument, modules.signatures, @@ -480,6 +488,7 @@ export class Manager { new AuthCodeHandler( 'apple', 'https://appleid.apple.com', + 'https://appleid.apple.com/auth/authorize', ops.identity.apple.clientId, identityInstrument, modules.signatures, @@ -488,6 +497,46 @@ export class Manager { ), ) } + if (ops.identity.customProviders?.length) { + for (const provider of ops.identity.customProviders) { + switch (provider.authMethod) { + case 'id-token': + throw new Error('id-token is not supported yet') + case 'authcode': + shared.handlers.set( + provider.kind, + new AuthCodeHandler( + provider.kind, + provider.issuer, + provider.oauthUrl, + provider.clientId, + identityInstrument, + modules.signatures, + shared.databases.authCommitments, + shared.databases.authKeys, + ), + ) + break + case 'authcode-pkce': + shared.handlers.set( + provider.kind, + new AuthCodePkceHandler( + provider.kind, + provider.issuer, + provider.oauthUrl, + provider.clientId, + identityInstrument, + modules.signatures, + shared.databases.authCommitments, + shared.databases.authKeys, + ), + ) + break + default: + throw new Error('unsupported auth method') + } + } + } shared.modules = modules this.shared = shared diff --git a/packages/wallet/wdk/src/sequence/signers.ts b/packages/wallet/wdk/src/sequence/signers.ts index 051e03270..cf79c43c7 100644 --- a/packages/wallet/wdk/src/sequence/signers.ts +++ b/packages/wallet/wdk/src/sequence/signers.ts @@ -8,7 +8,11 @@ export function isWitnessExtraSignerKind(extra: any): extra is WitnessExtraSigne } function toKnownKind(kind: string): Kind { - if (Object.values(Kinds).includes(kind as Kind)) { + if (kind.startsWith('custom-')) { + return kind as Kind + } + + if (Object.values(Kinds).includes(kind as (typeof Kinds)[keyof typeof Kinds])) { return kind as Kind } diff --git a/packages/wallet/wdk/src/sequence/types/signer.ts b/packages/wallet/wdk/src/sequence/types/signer.ts index ac4b5b51f..30a2a5073 100644 --- a/packages/wallet/wdk/src/sequence/types/signer.ts +++ b/packages/wallet/wdk/src/sequence/types/signer.ts @@ -12,7 +12,7 @@ export const Kinds = { Unknown: 'unknown', } as const -export type Kind = (typeof Kinds)[keyof typeof Kinds] +export type Kind = (typeof Kinds)[keyof typeof Kinds] | `custom-${string}` export type WitnessExtraSignerKind = { signerKind: string diff --git a/packages/wallet/wdk/src/sequence/wallets.ts b/packages/wallet/wdk/src/sequence/wallets.ts index a859ca797..f393c224a 100644 --- a/packages/wallet/wdk/src/sequence/wallets.ts +++ b/packages/wallet/wdk/src/sequence/wallets.ts @@ -14,7 +14,7 @@ import { PasskeysHandler } from './handlers/passkeys.js' import { GuardRole } from './guards.js' export type StartSignUpWithRedirectArgs = { - kind: 'google-pkce' | 'apple' + kind: 'google-pkce' | 'apple' | `custom-${string}` target: string metadata: { [key: string]: string } } @@ -55,7 +55,7 @@ export type CompleteRedirectArgs = CommonSignupArgs & { } export type AuthCodeSignupArgs = CommonSignupArgs & { - kind: 'google-pkce' | 'apple' + kind: 'google-pkce' | 'apple' | `custom-${string}` commitment: AuthCommitment code: string target: string @@ -693,10 +693,30 @@ export class Wallets implements WalletsInterface { } } } + + if (args.kind.startsWith('custom-')) { + // TODO: support other custom auth methods (e.g. id-token) + const handler = this.shared.handlers.get(args.kind) as AuthCodeHandler + if (!handler) { + throw new Error('handler-not-registered') + } + + const [signer, metadata] = await handler.completeAuth(args.commitment, args.code) + return { + signer, + extra: { + signerKind: args.kind, + }, + loginEmail: metadata.email, + } + } + + throw new Error('invalid-signup-kind') } async startSignUpWithRedirect(args: StartSignUpWithRedirectArgs) { - const handler = this.shared.handlers.get('login-' + args.kind) as AuthCodeHandler + const kind = args.kind.startsWith('custom-') ? args.kind : 'login-' + args.kind + const handler = this.shared.handlers.get(kind) as AuthCodeHandler if (!handler) { throw new Error('handler-not-registered') } @@ -721,7 +741,8 @@ export class Wallets implements WalletsInterface { use4337: args.use4337, }) } else { - const handler = this.shared.handlers.get('login-' + commitment.kind) as AuthCodeHandler + const kind = commitment.kind.startsWith('custom-') ? commitment.kind : 'login-' + commitment.kind + const handler = this.shared.handlers.get(kind) as AuthCodeHandler if (!handler) { throw new Error('handler-not-registered') } From 0530eeed4e72d4e5d56d5e887319d663de5c0eef Mon Sep 17 00:00:00 2001 From: Patryk Kalinowski Date: Thu, 16 Oct 2025 16:45:17 +0200 Subject: [PATCH 2/2] fix authcode tests --- .../wallet/wdk/test/authcode-pkce.test.ts | 14 ++--- packages/wallet/wdk/test/authcode.test.ts | 62 ++++--------------- 2 files changed, 17 insertions(+), 59 deletions(-) diff --git a/packages/wallet/wdk/test/authcode-pkce.test.ts b/packages/wallet/wdk/test/authcode-pkce.test.ts index dc6ecfc73..2e62b8afd 100644 --- a/packages/wallet/wdk/test/authcode-pkce.test.ts +++ b/packages/wallet/wdk/test/authcode-pkce.test.ts @@ -56,6 +56,7 @@ describe('AuthCodePkceHandler', () => { handler = new AuthCodePkceHandler( 'google-pkce', 'https://accounts.google.com', + 'https://accounts.google.com/o/oauth2/v2/auth', 'test-google-client-id', mockNitroInstrument, mockSignatures, @@ -81,8 +82,6 @@ describe('AuthCodePkceHandler', () => { email: 'user@example.com', } }) - - vi.spyOn(handler as any, 'oauthUrl').mockReturnValue('https://accounts.google.com/oauth/authorize') }) afterEach(() => { @@ -116,7 +115,7 @@ describe('AuthCodePkceHandler', () => { }) // Verify OAuth URL is constructed correctly - expect(result).toMatch(/^https:\/\/accounts\.google\.com\/oauth\/authorize\?/) + expect(result).toMatch(/^https:\/\/accounts\.google\.com\/o\/oauth2\/v2\/auth\?/) expect(result).toContain('code_challenge=mock-challenge-hash') expect(result).toContain('code_challenge_method=S256') expect(result).toContain('client_id=test-google-client-id') @@ -335,10 +334,6 @@ describe('AuthCodePkceHandler', () => { const newRedirectUri = 'https://newdomain.com/callback' handler.setRedirectUri(newRedirectUri) - // Verify redirect URI is used in OAuth URL construction - const mockUrl = 'https://accounts.google.com/oauth/authorize' - vi.spyOn(handler as any, 'oauthUrl').mockReturnValue(mockUrl) - return handler.commitAuth('https://example.com/success', true).then((result) => { expect(result).toContain(`redirect_uri=${encodeURIComponent(newRedirectUri)}`) }) @@ -346,8 +341,9 @@ describe('AuthCodePkceHandler', () => { it('Should work with different issuer and audience configurations', () => { const customHandler = new AuthCodePkceHandler( - 'google-pkce', + 'custom-provider', 'https://custom-issuer.com', + 'https://custom-issuer.com/o/oauth2/v2/auth', 'custom-client-id', mockNitroInstrument, mockSignatures, @@ -357,7 +353,7 @@ describe('AuthCodePkceHandler', () => { expect(customHandler['issuer']).toBe('https://custom-issuer.com') expect(customHandler['audience']).toBe('custom-client-id') - expect(customHandler.signupKind).toBe('google-pkce') + expect(customHandler.signupKind).toBe('custom-provider') }) }) }) diff --git a/packages/wallet/wdk/test/authcode.test.ts b/packages/wallet/wdk/test/authcode.test.ts index 06e4372af..121a03348 100644 --- a/packages/wallet/wdk/test/authcode.test.ts +++ b/packages/wallet/wdk/test/authcode.test.ts @@ -129,6 +129,7 @@ describe('AuthCodeHandler', () => { authCodeHandler = new AuthCodeHandler( 'google-pkce', 'https://accounts.google.com', + 'https://accounts.google.com/o/oauth2/v2/auth', 'test-audience', mockIdentityInstrument, mockSignatures, @@ -148,6 +149,7 @@ describe('AuthCodeHandler', () => { const handler = new AuthCodeHandler( 'google-pkce', 'https://accounts.google.com', + 'https://accounts.google.com/o/oauth2/v2/auth', 'google-client-id', mockIdentityInstrument, mockSignatures, @@ -165,6 +167,7 @@ describe('AuthCodeHandler', () => { const handler = new AuthCodeHandler( 'apple', 'https://appleid.apple.com', + 'https://appleid.apple.com/auth/authorize', 'apple-client-id', mockIdentityInstrument, mockSignatures, @@ -189,6 +192,7 @@ describe('AuthCodeHandler', () => { const googleHandler = new AuthCodeHandler( 'google-pkce', 'https://accounts.google.com', + 'https://accounts.google.com/o/oauth2/v2/auth', 'test-audience', mockIdentityInstrument, mockSignatures, @@ -203,6 +207,7 @@ describe('AuthCodeHandler', () => { const appleHandler = new AuthCodeHandler( 'apple', 'https://appleid.apple.com', + 'https://appleid.apple.com/auth/authorize', 'test-audience', mockIdentityInstrument, mockSignatures, @@ -293,6 +298,7 @@ describe('AuthCodeHandler', () => { const appleHandler = new AuthCodeHandler( 'apple', 'https://appleid.apple.com', + 'https://appleid.apple.com/auth/authorize', 'apple-client-id', mockIdentityInstrument, mockSignatures, @@ -491,13 +497,14 @@ describe('AuthCodeHandler', () => { }) }) - // === OAUTH URL METHOD === + // === OAUTH URL PROPERTY === - describe('oauthUrl()', () => { + describe('oauthUrl', () => { it('Should return Google OAuth URL for Google issuer', () => { const googleHandler = new AuthCodeHandler( 'google-pkce', 'https://accounts.google.com', + 'https://accounts.google.com/o/oauth2/v2/auth', 'test-audience', mockIdentityInstrument, mockSignatures, @@ -505,7 +512,7 @@ describe('AuthCodeHandler', () => { mockAuthKeys, ) - const url = googleHandler['oauthUrl']() + const url = googleHandler['oauthUrl'] expect(url).toBe('https://accounts.google.com/o/oauth2/v2/auth') }) @@ -513,6 +520,7 @@ describe('AuthCodeHandler', () => { const appleHandler = new AuthCodeHandler( 'apple', 'https://appleid.apple.com', + 'https://appleid.apple.com/auth/authorize', 'test-audience', mockIdentityInstrument, mockSignatures, @@ -520,23 +528,9 @@ describe('AuthCodeHandler', () => { mockAuthKeys, ) - const url = appleHandler['oauthUrl']() + const url = appleHandler['oauthUrl'] expect(url).toBe('https://appleid.apple.com/auth/authorize') }) - - it('Should throw error for unsupported issuer', () => { - const unsupportedHandler = new AuthCodeHandler( - 'google-pkce', - 'https://unsupported.provider.com', - 'test-audience', - mockIdentityInstrument, - mockSignatures, - mockAuthCommitments, - mockAuthKeys, - ) - - expect(() => unsupportedHandler['oauthUrl']()).toThrow('unsupported-issuer') - }) }) // === INHERITED METHODS FROM IDENTITYHANDLER === @@ -711,38 +705,6 @@ describe('AuthCodeHandler', () => { expect(metadata.email).toBe('test@example.com') }) - it('Should handle different OAuth providers correctly', async () => { - const providers = [ - { - signupKind: 'google-pkce' as const, - issuer: 'https://accounts.google.com', - expectedUrl: 'https://accounts.google.com/o/oauth2/v2/auth', - }, - { - signupKind: 'apple' as const, - issuer: 'https://appleid.apple.com', - expectedUrl: 'https://appleid.apple.com/auth/authorize', - }, - ] - - for (const provider of providers) { - const handler = new AuthCodeHandler( - provider.signupKind, - provider.issuer, - 'test-audience', - mockIdentityInstrument, - mockSignatures, - mockAuthCommitments, - mockAuthKeys, - ) - handler.setRedirectUri('https://example.com/callback') - - const url = await handler.commitAuth('/target', false) - expect(url).toContain(provider.expectedUrl) - expect(handler.kind).toBe(`login-${provider.signupKind}`) - } - }) - it('Should handle signup vs login flows correctly', async () => { authCodeHandler.setRedirectUri('https://example.com/callback')