Skip to content

wdk: add support for non-PKCE authcode flow (required by Apple) #751

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions packages/wallet/wdk/src/dbs/auth-commitments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ const TABLE_NAME = 'auth-commitments'

export type AuthCommitment = {
id: string
kind: 'google-pkce' | 'apple-pkce'
kind: 'google-pkce' | 'apple'
metadata: { [key: string]: string }
verifier: string
challenge: string
verifier?: string
challenge?: string
target: string
isSignUp: boolean
signer?: string
}

export class AuthCommitments extends Generic<AuthCommitment, 'id'> {
Expand Down
46 changes: 46 additions & 0 deletions packages/wallet/wdk/src/identity/challenge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,52 @@ export class IdTokenChallenge extends Challenge {
}
}

export class AuthCodeChallenge extends Challenge {
private handle = ''
private signer?: string

constructor(
readonly issuer: string,
readonly audience: string,
readonly redirectUri: string,
readonly authCode: string,
) {
super()
const authCodeHash = Hash.keccak256(new TextEncoder().encode(this.authCode))
this.handle = Hex.fromBytes(authCodeHash)
}

public getCommitParams(): CommitChallengeParams {
return {
authMode: AuthMode.AuthCode,
identityType: IdentityType.OIDC,
signer: this.signer,
handle: this.handle,
metadata: {
iss: this.issuer,
aud: this.audience,
redirect_uri: this.redirectUri,
},
}
}

public getCompleteParams(): CompleteChallengeParams {
return {
authMode: AuthMode.AuthCode,
identityType: IdentityType.OIDC,
verifier: this.handle,
answer: this.authCode,
}
}

public withSigner(signer: string): AuthCodeChallenge {
const challenge = new AuthCodeChallenge(this.issuer, this.audience, this.redirectUri, this.authCode)
challenge.handle = this.handle
challenge.signer = signer
return challenge
}
}

export class AuthCodePkceChallenge extends Challenge {
private verifier?: string
private authCode?: string
Expand Down
2 changes: 1 addition & 1 deletion packages/wallet/wdk/src/identity/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { IdentityInstrument, IdentityType, KeyType } from './nitro/index.js'
export type { CommitChallengeParams, CompleteChallengeParams, Challenge } from './challenge.js'
export { IdTokenChallenge, AuthCodePkceChallenge, OtpChallenge } from './challenge.js'
export { IdTokenChallenge, AuthCodeChallenge, AuthCodePkceChallenge, OtpChallenge } from './challenge.js'
export { IdentitySigner } from './signer.js'
72 changes: 10 additions & 62 deletions packages/wallet/wdk/src/sequence/handlers/authcode-pkce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,19 @@ import { Signatures } from '../signatures.js'
import * as Identity from '../../identity/index.js'
import { SignerUnavailable, SignerReady, SignerActionable, BaseSignatureRequest } from '../types/signature-request.js'
import { IdentitySigner } from '../../identity/signer.js'
import { IdentityHandler } from './identity.js'

export class AuthCodePkceHandler extends IdentityHandler implements Handler {
private redirectUri: string = ''
import { AuthCodeHandler } from './authcode.js'

export class AuthCodePkceHandler extends AuthCodeHandler implements Handler {
constructor(
public readonly signupKind: 'google-pkce' | 'apple-pkce',
public readonly issuer: string,
public readonly audience: string,
signupKind: 'google-pkce',
issuer: string,
audience: string,
nitro: Identity.IdentityInstrument,
signatures: Signatures,
private readonly commitments: Db.AuthCommitments,
commitments: Db.AuthCommitments,
authKeys: Db.AuthKeys,
) {
super(nitro, authKeys, signatures, Identity.IdentityType.OIDC)
}

public get kind() {
return 'login-' + this.signupKind
}

public setRedirectUri(redirectUri: string) {
this.redirectUri = redirectUri
super(signupKind, issuer, audience, nitro, signatures, commitments, authKeys)
}

public async commitAuth(target: string, isSignUp: boolean, state?: string, signer?: string) {
Expand Down Expand Up @@ -70,54 +60,12 @@ export class AuthCodePkceHandler extends IdentityHandler implements Handler {
code: string,
): Promise<[IdentitySigner, { [key: string]: string }]> {
const challenge = new Identity.AuthCodePkceChallenge('', '', '')
if (!commitment.verifier) {
throw new Error('Missing verifier in commitment')
}
const signer = await this.nitroCompleteAuth(challenge.withAnswer(commitment.verifier, code))

await this.commitments.del(commitment.id)

return [signer, commitment.metadata]
}

async status(
address: Address.Address,
_imageHash: Hex.Hex | undefined,
request: BaseSignatureRequest,
): Promise<SignerUnavailable | SignerReady | SignerActionable> {
// Normalize address
const normalizedAddress = Address.checksum(address)
const signer = await this.getAuthKeySigner(normalizedAddress)
if (signer) {
return {
address: normalizedAddress,
handler: this,
status: 'ready',
handle: async () => {
await this.sign(signer, request)
return true
},
}
}

return {
address: normalizedAddress,
handler: this,
status: 'actionable',
message: 'request-redirect',
handle: async () => {
const url = await this.commitAuth(window.location.pathname, false, request.id, normalizedAddress)
window.location.href = url
return true
},
}
}

private 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')
}
}
}
116 changes: 116 additions & 0 deletions packages/wallet/wdk/src/sequence/handlers/authcode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { Hex, Address, Bytes } from 'ox'
import { Handler } from './handler.js'
import * as Db from '../../dbs/index.js'
import { Signatures } from '../signatures.js'
import * as Identity from '../../identity/index.js'
import { SignerUnavailable, SignerReady, SignerActionable, BaseSignatureRequest } from '../types/signature-request.js'
import { IdentitySigner } from '../../identity/signer.js'
import { IdentityHandler } from './identity.js'

export class AuthCodeHandler extends IdentityHandler implements Handler {
protected redirectUri: string = ''

constructor(
public readonly signupKind: 'apple' | 'google-pkce',
public readonly issuer: string,
public readonly audience: string,
nitro: Identity.IdentityInstrument,
signatures: Signatures,
protected readonly commitments: Db.AuthCommitments,
authKeys: Db.AuthKeys,
) {
super(nitro, authKeys, signatures, Identity.IdentityType.OIDC)
}

public get kind() {
return 'login-' + this.signupKind
}

public setRedirectUri(redirectUri: string) {
this.redirectUri = redirectUri
}

public async commitAuth(target: string, isSignUp: boolean, state?: string, signer?: string) {
if (!state) {
state = Hex.fromBytes(Bytes.random(32))
}

await this.commitments.set({
id: state,
kind: this.signupKind,
signer,
target,
metadata: {},
isSignUp,
})

const searchParams = new URLSearchParams({
client_id: this.audience,
redirect_uri: this.redirectUri,
response_type: 'code',
scope: 'openid',
state,
})

const oauthUrl = this.oauthUrl()
return `${oauthUrl}?${searchParams.toString()}`
}

public async completeAuth(
commitment: Db.AuthCommitment,
code: string,
): Promise<[IdentitySigner, { [key: string]: string }]> {
let challenge = new Identity.AuthCodeChallenge(this.issuer, this.audience, this.redirectUri, code)
if (commitment.signer) {
challenge = challenge.withSigner(commitment.signer)
}
await this.nitroCommitVerifier(challenge)
const signer = await this.nitroCompleteAuth(challenge)

return [signer, {}]
}

async status(
address: Address.Address,
_imageHash: Hex.Hex | undefined,
request: BaseSignatureRequest,
): Promise<SignerUnavailable | SignerReady | SignerActionable> {
// Normalize address
const normalizedAddress = Address.checksum(address)
const signer = await this.getAuthKeySigner(normalizedAddress)
if (signer) {
return {
address: normalizedAddress,
handler: this,
status: 'ready',
handle: async () => {
await this.sign(signer, request)
return true
},
}
}

return {
address: normalizedAddress,
handler: this,
status: 'actionable',
message: 'request-redirect',
handle: async () => {
const url = await this.commitAuth(window.location.pathname, false, request.id, normalizedAddress)
window.location.href = url
return true
},
}
}

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')
}
}
}
1 change: 0 additions & 1 deletion packages/wallet/wdk/src/sequence/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ export type {
MnemonicSignupArgs,
EmailOtpSignupArgs,
CompleteRedirectArgs,
AuthCodePkceSignupArgs,
SignupArgs,
LoginToWalletArgs,
LoginToMnemonicArgs,
Expand Down
9 changes: 5 additions & 4 deletions packages/wallet/wdk/src/sequence/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { WalletSelectionUiHandler } from './types/wallet.js'
import { Cron } from './cron.js'
import { Recovery } from './recovery.js'
import { RecoveryHandler } from './handlers/recovery.js'
import { AuthCodeHandler } from './handlers/authcode.js'

export type ManagerOptions = {
verbose?: boolean
Expand Down Expand Up @@ -298,9 +299,9 @@ export class Manager {
}
if (ops.identity.apple?.enabled) {
shared.handlers.set(
Kinds.LoginApplePkce,
new AuthCodePkceHandler(
'apple-pkce',
Kinds.LoginApple,
new AuthCodeHandler(
'apple',
'https://appleid.apple.com',
ops.identity.apple.clientId,
nitro,
Expand Down Expand Up @@ -455,7 +456,7 @@ export class Manager {

public async setRedirectPrefix(prefix: string) {
this.shared.handlers.forEach((handler) => {
if (handler instanceof AuthCodePkceHandler) {
if (handler instanceof AuthCodeHandler) {
handler.setRedirectUri(prefix + '/' + handler.signupKind)
}
})
Expand Down
2 changes: 1 addition & 1 deletion packages/wallet/wdk/src/sequence/types/signer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const Kinds = {
LoginMnemonic: 'login-mnemonic', // Todo: do not name it login-mnemonic, just mnemonic
LoginEmailOtp: 'login-email-otp',
LoginGooglePkce: 'login-google-pkce',
LoginApplePkce: 'login-apple-pkce',
LoginApple: 'login-apple',
Recovery: 'recovery-extension',
Unknown: 'unknown',
} as const
Expand Down
Loading