Skip to content

Commit 1b845b5

Browse files
Add Apple ID token support to wallet WDK
1 parent 695705d commit 1b845b5

7 files changed

Lines changed: 170 additions & 22 deletions

File tree

packages/wallet/wdk/src/sequence/handlers/idtoken.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,16 @@ import type { WdkEnv } from '../../env.js'
1212

1313
type RespondFn = (idToken: string) => Promise<void>
1414

15-
export type PromptIdTokenHandler = (kind: 'google-id-token' | `custom-${string}`, respond: RespondFn) => Promise<void>
15+
export type PromptIdTokenHandler = (
16+
kind: 'google-id-token' | 'apple-id-token' | `custom-${string}`,
17+
respond: RespondFn,
18+
) => Promise<void>
1619

1720
export class IdTokenHandler extends IdentityHandler implements Handler {
1821
private onPromptIdToken: undefined | PromptIdTokenHandler
1922

2023
constructor(
21-
public readonly signupKind: 'google-id-token' | `custom-${string}`,
24+
public readonly signupKind: 'google-id-token' | 'apple-id-token' | `custom-${string}`,
2225
public readonly issuer: string,
2326
public readonly audience: string,
2427
nitro: Identity.IdentityInstrument,
@@ -33,6 +36,9 @@ export class IdTokenHandler extends IdentityHandler implements Handler {
3336
if (this.signupKind === 'google-id-token') {
3437
return Kinds.LoginGoogle
3538
}
39+
if (this.signupKind === 'apple-id-token') {
40+
return Kinds.LoginApple
41+
}
3642
return 'login-' + this.signupKind
3743
}
3844

packages/wallet/wdk/src/sequence/manager.ts

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ export type ManagerOptions = {
107107
apple?: {
108108
enabled: boolean
109109
clientId: string
110+
authMethod?: 'authcode' | 'id-token'
110111
}
111112
customProviders?: CustomIdentityProvider[]
112113
}
@@ -129,6 +130,7 @@ export type ResolvedIdentityOptions = {
129130
apple: {
130131
enabled: boolean
131132
clientId: string
133+
authMethod: 'authcode' | 'id-token'
132134
}
133135
customProviders?: CustomIdentityProvider[]
134136
}
@@ -260,6 +262,7 @@ export const ManagerOptionsDefaults = {
260262
apple: {
261263
enabled: false,
262264
clientId: '',
265+
authMethod: 'authcode' as const,
263266
},
264267
},
265268
}
@@ -716,20 +719,35 @@ export class Manager {
716719
}
717720
}
718721
if (ops.identity.apple?.enabled) {
719-
shared.handlers.set(
720-
Kinds.LoginApple,
721-
new AuthCodeHandler(
722-
'apple',
723-
'https://appleid.apple.com',
724-
'https://appleid.apple.com/auth/authorize',
725-
ops.identity.apple.clientId,
726-
identityInstrument,
727-
modules.signatures,
728-
shared.databases.authCommitments,
729-
shared.databases.authKeys,
730-
shared.env,
731-
),
732-
)
722+
if (ops.identity.apple.authMethod === 'id-token') {
723+
shared.handlers.set(
724+
Kinds.LoginApple,
725+
new IdTokenHandler(
726+
'apple-id-token',
727+
'https://appleid.apple.com',
728+
ops.identity.apple.clientId,
729+
identityInstrument,
730+
modules.signatures,
731+
shared.databases.authKeys,
732+
shared.env,
733+
),
734+
)
735+
} else {
736+
shared.handlers.set(
737+
Kinds.LoginApple,
738+
new AuthCodeHandler(
739+
'apple',
740+
'https://appleid.apple.com',
741+
'https://appleid.apple.com/auth/authorize',
742+
ops.identity.apple.clientId,
743+
identityInstrument,
744+
modules.signatures,
745+
shared.databases.authCommitments,
746+
shared.databases.authKeys,
747+
shared.env,
748+
),
749+
)
750+
}
733751
}
734752
if (ops.identity.customProviders?.length) {
735753
for (const provider of ops.identity.customProviders) {

packages/wallet/wdk/src/sequence/wallets.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,19 @@ function getSignerKindForSignup(kind: SignupArgs['kind'] | AuthCommitment['kind'
2828
if (kind === 'google-id-token' || kind === 'google-pkce') {
2929
return Kinds.LoginGoogle
3030
}
31+
if (kind === 'apple-id-token' || kind === 'apple') {
32+
return Kinds.LoginApple
33+
}
3134
if (kind.startsWith('custom-')) {
3235
return kind
3336
}
3437
return ('login-' + kind) as string
3538
}
3639

37-
function getIdTokenSignupHandler(shared: Shared, kind: typeof Kinds.LoginGoogle | `custom-${string}`): IdTokenHandler {
40+
function getIdTokenSignupHandler(
41+
shared: Shared,
42+
kind: typeof Kinds.LoginGoogle | typeof Kinds.LoginApple | `custom-${string}`,
43+
): IdTokenHandler {
3844
const handler = shared.handlers.get(kind)
3945
if (!handler) {
4046
throw new Error('handler-not-registered')
@@ -82,7 +88,7 @@ export type EmailOtpSignupArgs = CommonSignupArgs & {
8288
}
8389

8490
export type IdTokenSignupArgs = CommonSignupArgs & {
85-
kind: 'google-id-token' | `custom-${string}`
91+
kind: 'google-id-token' | 'apple-id-token' | `custom-${string}`
8692
idToken: string
8793
}
8894

@@ -222,7 +228,7 @@ export interface WalletsInterface {
222228
* - `kind: 'mnemonic'`: Uses a mnemonic phrase as the login credential.
223229
* - `kind: 'passkey'`: Prompts the user to create a WebAuthn passkey.
224230
* - `kind: 'email-otp'`: Initiates an OTP flow to the user's email.
225-
* - `kind: 'google-id-token'`: Completes an OIDC ID token flow when Google is configured with `authMethod: 'id-token'`.
231+
* - `kind: 'google-id-token' | 'apple-id-token'`: Completes an OIDC ID token flow when the provider is configured with `authMethod: 'id-token'`.
226232
* - `kind: 'google-pkce' | 'apple'`: Completes an OAuth redirect flow.
227233
* Common options like `noGuard` or `noRecovery` can customize the wallet's security features.
228234
* @returns A promise that resolves to the address of the newly created wallet, or `undefined` if the sign-up was aborted.
@@ -721,16 +727,20 @@ export class Wallets implements WalletsInterface {
721727
}
722728
}
723729

724-
case 'google-id-token': {
725-
const handler = getIdTokenSignupHandler(this.shared, Kinds.LoginGoogle)
730+
case 'google-id-token':
731+
case 'apple-id-token': {
732+
const handler = getIdTokenSignupHandler(
733+
this.shared,
734+
args.kind === 'google-id-token' ? Kinds.LoginGoogle : Kinds.LoginApple,
735+
)
726736
const [signer, metadata] = await handler.completeAuth(args.idToken)
727737
const loginEmail = metadata.email
728738
this.shared.modules.logger.log('Created new id token signer:', signer.address)
729739

730740
return {
731741
signer,
732742
extra: {
733-
signerKind: Kinds.LoginGoogle,
743+
signerKind: getSignerKindForSignup(args.kind),
734744
},
735745
loginEmail,
736746
}

packages/wallet/wdk/test/identity-auth-dbs.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,42 @@ describe('Identity Authentication Databases', () => {
395395
expect(handlers.has('login-google-pkce')).toBe(false)
396396
})
397397

398+
it('Should register the Apple ID token handler when configured explicitly', async () => {
399+
manager = new Manager({
400+
stateProvider: new State.Local.Provider(new State.Local.IndexedDbStore(`manager-apple-idtoken-${Date.now()}`)),
401+
networks: [
402+
{
403+
name: 'Test Network',
404+
type: Network.NetworkType.MAINNET,
405+
rpcUrl: LOCAL_RPC_URL,
406+
chainId: Network.ChainId.ARBITRUM,
407+
blockExplorer: { url: 'https://arbiscan.io' },
408+
nativeCurrency: {
409+
name: 'Ether',
410+
symbol: 'ETH',
411+
decimals: 18,
412+
},
413+
},
414+
],
415+
relayers: [],
416+
authCommitmentsDb,
417+
authKeysDb,
418+
identity: {
419+
url: 'https://dev-identity.sequence-dev.app',
420+
fetch: window.fetch,
421+
apple: {
422+
enabled: true,
423+
clientId: 'test-apple-client-id',
424+
authMethod: 'id-token',
425+
},
426+
},
427+
})
428+
429+
const handlers = (manager as any).shared.handlers
430+
expect(handlers.has('login-apple-id-token')).toBe(false)
431+
expect(handlers.has('login-apple')).toBe(true)
432+
})
433+
398434
it('Should use auth databases when email authentication is enabled', async () => {
399435
manager = new Manager({
400436
stateProvider: new State.Local.Provider(new State.Local.IndexedDbStore(`manager-email-${Date.now()}`)),

packages/wallet/wdk/test/idtoken.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,22 @@ describe('IdTokenHandler', () => {
101101
expect(handler.kind).toBe(Kinds.LoginGoogle)
102102
})
103103

104+
it('Should normalize apple-id-token handlers to login-apple', () => {
105+
const handler = new IdTokenHandler(
106+
'apple-id-token',
107+
'https://appleid.apple.com',
108+
'test-apple-client-id',
109+
mockNitroInstrument,
110+
mockSignatures,
111+
mockAuthKeys,
112+
)
113+
114+
expect(handler.signupKind).toBe('apple-id-token')
115+
expect(handler.issuer).toBe('https://appleid.apple.com')
116+
expect(handler.audience).toBe('test-apple-client-id')
117+
expect(handler.kind).toBe(Kinds.LoginApple)
118+
})
119+
104120
it('Should initialize without a registered UI callback', () => {
105121
expect(idTokenHandler['onPromptIdToken']).toBeUndefined()
106122
})

packages/wallet/wdk/test/sessions-idtoken.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ describe('Sessions ID token attestation', () => {
6060
apple: {
6161
enabled: true,
6262
clientId: 'test-apple-client-id',
63+
authMethod: 'id-token',
6364
},
6465
},
6566
})

packages/wallet/wdk/test/wallets.test.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,48 @@ describe('Wallets', () => {
7171
expect(configuration.login[0]!.kind).toBe(Kinds.LoginGoogle)
7272
})
7373

74+
it('Should create a new wallet using apple-id-token when Apple ID token auth is enabled', async () => {
75+
manager = newManager({
76+
identity: {
77+
apple: {
78+
enabled: true,
79+
clientId: 'test-apple-client-id',
80+
authMethod: 'id-token',
81+
},
82+
},
83+
})
84+
85+
const handler = (manager as any).shared.handlers.get(Kinds.LoginApple) as IdTokenHandler
86+
const loginMnemonic = Mnemonic.random(Mnemonic.english)
87+
const loginSigner = MnemonicHandler.toSigner(loginMnemonic)
88+
if (!loginSigner) {
89+
throw new Error('Failed to create login signer for test')
90+
}
91+
92+
const completeAuthSpy = vi
93+
.spyOn(handler, 'completeAuth')
94+
.mockResolvedValue([loginSigner as unknown as IdentitySigner, { email: 'apple-user@example.com' }])
95+
96+
const wallet = await manager.wallets.signUp({
97+
kind: 'apple-id-token',
98+
idToken: 'eyJhbGciOiJub25lIn0.eyJleHAiOjQxMDI0NDQ4MDB9.',
99+
noGuard: true,
100+
})
101+
102+
expect(wallet).toBeDefined()
103+
expect(completeAuthSpy).toHaveBeenCalledWith('eyJhbGciOiJub25lIn0.eyJleHAiOjQxMDI0NDQ4MDB9.')
104+
await expect(manager.wallets.has(wallet!)).resolves.toBeTruthy()
105+
106+
const walletEntry = await manager.wallets.get(wallet!)
107+
expect(walletEntry).toBeDefined()
108+
expect(walletEntry!.loginType).toBe(Kinds.LoginApple)
109+
expect(walletEntry!.loginEmail).toBe('apple-user@example.com')
110+
111+
const configuration = await manager.wallets.getConfiguration(wallet!)
112+
expect(configuration.login).toHaveLength(1)
113+
expect(configuration.login[0]!.kind).toBe(Kinds.LoginApple)
114+
})
115+
74116
it('Should register and unregister Google ID token UI callbacks through the manager', async () => {
75117
manager = newManager({
76118
identity: {
@@ -140,6 +182,25 @@ describe('Wallets', () => {
140182
).rejects.toThrow('handler-does-not-support-id-token')
141183
})
142184

185+
it('Should reject apple-id-token signup when Apple is configured for redirect auth', async () => {
186+
manager = newManager({
187+
identity: {
188+
apple: {
189+
enabled: true,
190+
clientId: 'test-apple-client-id',
191+
},
192+
},
193+
})
194+
195+
await expect(
196+
manager.wallets.signUp({
197+
kind: 'apple-id-token',
198+
idToken: 'eyJhbGciOiJub25lIn0.eyJleHAiOjQxMDI0NDQ4MDB9.',
199+
noGuard: true,
200+
}),
201+
).rejects.toThrow('handler-does-not-support-id-token')
202+
})
203+
143204
it('Should reject custom ID token signup when the provider uses redirect auth', async () => {
144205
manager = newManager({
145206
identity: {

0 commit comments

Comments
 (0)