Skip to content

Commit 25d5b0d

Browse files
committed
add support for non-PKCE authcode flow (required by Apple)
1 parent 6b719f8 commit 25d5b0d

File tree

9 files changed

+193
-84
lines changed

9 files changed

+193
-84
lines changed

packages/wallet/wdk/src/dbs/auth-commitments.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ const TABLE_NAME = 'auth-commitments'
44

55
export type AuthCommitment = {
66
id: string
7-
kind: 'google-pkce' | 'apple-pkce'
7+
kind: 'google-pkce' | 'apple'
88
metadata: { [key: string]: string }
9-
verifier: string
10-
challenge: string
9+
verifier?: string
10+
challenge?: string
1111
target: string
1212
isSignUp: boolean
13+
signer?: string
1314
}
1415

1516
export class AuthCommitments extends Generic<AuthCommitment, 'id'> {

packages/wallet/wdk/src/identity/challenge.ts

+46
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,52 @@ export class IdTokenChallenge extends Challenge {
6161
}
6262
}
6363

64+
export class AuthCodeChallenge extends Challenge {
65+
private handle = ''
66+
private signer?: string
67+
68+
constructor(
69+
readonly issuer: string,
70+
readonly audience: string,
71+
readonly redirectUri: string,
72+
readonly authCode: string,
73+
) {
74+
super()
75+
const authCodeHash = Hash.keccak256(new TextEncoder().encode(this.authCode))
76+
this.handle = Hex.fromBytes(authCodeHash)
77+
}
78+
79+
public getCommitParams(): CommitChallengeParams {
80+
return {
81+
authMode: AuthMode.AuthCode,
82+
identityType: IdentityType.OIDC,
83+
signer: this.signer,
84+
handle: this.handle,
85+
metadata: {
86+
iss: this.issuer,
87+
aud: this.audience,
88+
redirect_uri: this.redirectUri,
89+
},
90+
}
91+
}
92+
93+
public getCompleteParams(): CompleteChallengeParams {
94+
return {
95+
authMode: AuthMode.AuthCode,
96+
identityType: IdentityType.OIDC,
97+
verifier: this.handle,
98+
answer: this.authCode,
99+
}
100+
}
101+
102+
public withSigner(signer: string): AuthCodeChallenge {
103+
const challenge = new AuthCodeChallenge(this.issuer, this.audience, this.redirectUri, this.authCode)
104+
challenge.handle = this.handle
105+
challenge.signer = signer
106+
return challenge
107+
}
108+
}
109+
64110
export class AuthCodePkceChallenge extends Challenge {
65111
private verifier?: string
66112
private authCode?: string
+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
export { IdentityInstrument, IdentityType, KeyType } from './nitro/index.js'
22
export type { CommitChallengeParams, CompleteChallengeParams, Challenge } from './challenge.js'
3-
export { IdTokenChallenge, AuthCodePkceChallenge, OtpChallenge } from './challenge.js'
3+
export { IdTokenChallenge, AuthCodeChallenge, AuthCodePkceChallenge, OtpChallenge } from './challenge.js'
44
export { IdentitySigner } from './signer.js'

packages/wallet/wdk/src/sequence/handlers/authcode-pkce.ts

+8-62
Original file line numberDiff line numberDiff line change
@@ -5,29 +5,19 @@ import { Signatures } from '../signatures.js'
55
import * as Identity from '../../identity/index.js'
66
import { SignerUnavailable, SignerReady, SignerActionable, BaseSignatureRequest } from '../types/signature-request.js'
77
import { IdentitySigner } from '../../identity/signer.js'
8-
import { IdentityHandler } from './identity.js'
9-
10-
export class AuthCodePkceHandler extends IdentityHandler implements Handler {
11-
private redirectUri: string = ''
8+
import { AuthCodeHandler } from './authcode.js'
129

10+
export class AuthCodePkceHandler extends AuthCodeHandler implements Handler {
1311
constructor(
14-
public readonly signupKind: 'google-pkce' | 'apple-pkce',
15-
public readonly issuer: string,
16-
public readonly audience: string,
12+
signupKind: 'google-pkce',
13+
issuer: string,
14+
audience: string,
1715
nitro: Identity.IdentityInstrument,
1816
signatures: Signatures,
19-
private readonly commitments: Db.AuthCommitments,
17+
commitments: Db.AuthCommitments,
2018
authKeys: Db.AuthKeys,
2119
) {
22-
super(nitro, authKeys, signatures, Identity.IdentityType.OIDC)
23-
}
24-
25-
public get kind() {
26-
return 'login-' + this.signupKind
27-
}
28-
29-
public setRedirectUri(redirectUri: string) {
30-
this.redirectUri = redirectUri
20+
super(signupKind, issuer, audience, nitro, signatures, commitments, authKeys)
3121
}
3222

3323
public async commitAuth(target: string, isSignUp: boolean, state?: string, signer?: string) {
@@ -70,54 +60,10 @@ export class AuthCodePkceHandler extends IdentityHandler implements Handler {
7060
code: string,
7161
): Promise<[IdentitySigner, { [key: string]: string }]> {
7262
const challenge = new Identity.AuthCodePkceChallenge('', '', '')
73-
const signer = await this.nitroCompleteAuth(challenge.withAnswer(commitment.verifier, code))
63+
const signer = await this.nitroCompleteAuth(challenge.withAnswer(commitment.verifier ?? '', code))
7464

7565
await this.commitments.del(commitment.id)
7666

7767
return [signer, commitment.metadata]
7868
}
79-
80-
async status(
81-
address: Address.Address,
82-
_imageHash: Hex.Hex | undefined,
83-
request: BaseSignatureRequest,
84-
): Promise<SignerUnavailable | SignerReady | SignerActionable> {
85-
// Normalize address
86-
const normalizedAddress = Address.checksum(address)
87-
const signer = await this.getAuthKeySigner(normalizedAddress)
88-
if (signer) {
89-
return {
90-
address: normalizedAddress,
91-
handler: this,
92-
status: 'ready',
93-
handle: async () => {
94-
await this.sign(signer, request)
95-
return true
96-
},
97-
}
98-
}
99-
100-
return {
101-
address: normalizedAddress,
102-
handler: this,
103-
status: 'actionable',
104-
message: 'request-redirect',
105-
handle: async () => {
106-
const url = await this.commitAuth(window.location.pathname, false, request.id, normalizedAddress)
107-
window.location.href = url
108-
return true
109-
},
110-
}
111-
}
112-
113-
private oauthUrl() {
114-
switch (this.issuer) {
115-
case 'https://accounts.google.com':
116-
return 'https://accounts.google.com/o/oauth2/v2/auth'
117-
case 'https://appleid.apple.com':
118-
return 'https://appleid.apple.com/auth/authorize'
119-
default:
120-
throw new Error('unsupported-issuer')
121-
}
122-
}
12369
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { Hex, Address, Bytes } from 'ox'
2+
import { Handler } from './handler.js'
3+
import * as Db from '../../dbs/index.js'
4+
import { Signatures } from '../signatures.js'
5+
import * as Identity from '../../identity/index.js'
6+
import { SignerUnavailable, SignerReady, SignerActionable, BaseSignatureRequest } from '../types/signature-request.js'
7+
import { IdentitySigner } from '../../identity/signer.js'
8+
import { IdentityHandler } from './identity.js'
9+
10+
export class AuthCodeHandler extends IdentityHandler implements Handler {
11+
protected redirectUri: string = ''
12+
13+
constructor(
14+
public readonly signupKind: 'apple' | 'google-pkce',
15+
public readonly issuer: string,
16+
public readonly audience: string,
17+
nitro: Identity.IdentityInstrument,
18+
signatures: Signatures,
19+
protected readonly commitments: Db.AuthCommitments,
20+
authKeys: Db.AuthKeys,
21+
) {
22+
super(nitro, authKeys, signatures, Identity.IdentityType.OIDC)
23+
}
24+
25+
public get kind() {
26+
return 'login-' + this.signupKind
27+
}
28+
29+
public setRedirectUri(redirectUri: string) {
30+
this.redirectUri = redirectUri
31+
}
32+
33+
public async commitAuth(target: string, isSignUp: boolean, state?: string, signer?: string) {
34+
if (!state) {
35+
state = Hex.fromBytes(Bytes.random(32))
36+
}
37+
38+
await this.commitments.set({
39+
id: state,
40+
kind: this.signupKind,
41+
signer,
42+
target,
43+
metadata: {},
44+
isSignUp,
45+
})
46+
47+
const searchParams = new URLSearchParams({
48+
client_id: this.audience,
49+
redirect_uri: this.redirectUri,
50+
response_type: 'code',
51+
scope: 'openid',
52+
state,
53+
})
54+
55+
const oauthUrl = this.oauthUrl()
56+
return `${oauthUrl}?${searchParams.toString()}`
57+
}
58+
59+
public async completeAuth(
60+
commitment: Db.AuthCommitment,
61+
code: string,
62+
): Promise<[IdentitySigner, { [key: string]: string }]> {
63+
let challenge = new Identity.AuthCodeChallenge(this.issuer, this.audience, this.redirectUri, code)
64+
if (commitment.signer) {
65+
challenge = challenge.withSigner(commitment.signer)
66+
}
67+
await this.nitroCommitVerifier(challenge)
68+
const signer = await this.nitroCompleteAuth(challenge)
69+
70+
return [signer, {}]
71+
}
72+
73+
async status(
74+
address: Address.Address,
75+
_imageHash: Hex.Hex | undefined,
76+
request: BaseSignatureRequest,
77+
): Promise<SignerUnavailable | SignerReady | SignerActionable> {
78+
// Normalize address
79+
const normalizedAddress = Address.checksum(address)
80+
const signer = await this.getAuthKeySigner(normalizedAddress)
81+
if (signer) {
82+
return {
83+
address: normalizedAddress,
84+
handler: this,
85+
status: 'ready',
86+
handle: async () => {
87+
await this.sign(signer, request)
88+
return true
89+
},
90+
}
91+
}
92+
93+
return {
94+
address: normalizedAddress,
95+
handler: this,
96+
status: 'actionable',
97+
message: 'request-redirect',
98+
handle: async () => {
99+
const url = await this.commitAuth(window.location.pathname, false, request.id, normalizedAddress)
100+
window.location.href = url
101+
return true
102+
},
103+
}
104+
}
105+
106+
protected oauthUrl() {
107+
switch (this.issuer) {
108+
case 'https://accounts.google.com':
109+
return 'https://accounts.google.com/o/oauth2/v2/auth'
110+
case 'https://appleid.apple.com':
111+
return 'https://appleid.apple.com/auth/authorize'
112+
default:
113+
throw new Error('unsupported-issuer')
114+
}
115+
}
116+
}

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

-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ export type {
1212
MnemonicSignupArgs,
1313
EmailOtpSignupArgs,
1414
CompleteRedirectArgs,
15-
AuthCodePkceSignupArgs,
1615
SignupArgs,
1716
LoginToWalletArgs,
1817
LoginToMnemonicArgs,

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

+5-4
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { WalletSelectionUiHandler } from './types/wallet.js'
3535
import { Cron } from './cron.js'
3636
import { Recovery } from './recovery.js'
3737
import { RecoveryHandler } from './handlers/recovery.js'
38+
import { AuthCodeHandler } from './handlers/authcode.js'
3839

3940
export type ManagerOptions = {
4041
verbose?: boolean
@@ -298,9 +299,9 @@ export class Manager {
298299
}
299300
if (ops.identity.apple?.enabled) {
300301
shared.handlers.set(
301-
Kinds.LoginApplePkce,
302-
new AuthCodePkceHandler(
303-
'apple-pkce',
302+
Kinds.LoginApple,
303+
new AuthCodeHandler(
304+
'apple',
304305
'https://appleid.apple.com',
305306
ops.identity.apple.clientId,
306307
nitro,
@@ -455,7 +456,7 @@ export class Manager {
455456

456457
public async setRedirectPrefix(prefix: string) {
457458
this.shared.handlers.forEach((handler) => {
458-
if (handler instanceof AuthCodePkceHandler) {
459+
if (handler instanceof AuthCodeHandler) {
459460
handler.setRedirectUri(prefix + '/' + handler.signupKind)
460461
}
461462
})

packages/wallet/wdk/src/sequence/types/signer.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export const Kinds = {
66
LoginMnemonic: 'login-mnemonic', // Todo: do not name it login-mnemonic, just mnemonic
77
LoginEmailOtp: 'login-email-otp',
88
LoginGooglePkce: 'login-google-pkce',
9-
LoginApplePkce: 'login-apple-pkce',
9+
LoginApple: 'login-apple',
1010
Recovery: 'recovery-extension',
1111
Unknown: 'unknown',
1212
} as const

0 commit comments

Comments
 (0)