(state: string | null)
| 80 | export type OAuthStateVerificationFailureReason = |
| 81 | | 'state_missing' |
| 82 | | 'state_malformed' |
| 83 | | 'signature_invalid' |
| 84 | | 'state_expired' |
| 85 | | 'state_from_future'; |
| 86 | |
| 87 | export type OAuthStateVerificationResult = |
| 88 | | { status: 'valid'; state: VerifiedOAuthState } |
| 89 | | { status: 'invalid'; reason: OAuthStateVerificationFailureReason }; |
| 90 | |
| 91 | const OAuthStatePayloadSchema = z.object({ |
| 92 | owner: z.string(), |
| 93 | uid: z.string(), |
| 94 | iat: z.number().finite(), |
| 95 | nonce: z.string().min(1), |
| 96 | returnTo: z.unknown().optional(), |
| 97 | purpose: z.enum(['provider_install']).optional(), |
| 98 | }); |
| 99 | |
| 100 | export function verifyOAuthStateDetailed(state: string | null): OAuthStateVerificationResult { |
| 101 | const result = verifySignedTokenDetailed(state, { |
| 102 | ttlSeconds: OAUTH_STATE_TTL_SECONDS, |
| 103 | parse: payload => { |
| 104 | const parsed = OAuthStatePayloadSchema.safeParse(payload); |
| 105 | if (!parsed.success) return null; |
| 106 | |
| 107 | const returnTo = |
| 108 | typeof parsed.data.returnTo === 'string' ? validateReturnPath(parsed.data.returnTo) : null; |
| 109 | |
| 110 | return { |
| 111 | owner: parsed.data.owner, |
| 112 | userId: parsed.data.uid, |
| 113 | issuedAt: parsed.data.iat, |
| 114 | ...(returnTo ? { returnTo } : {}), |
| 115 | ...(parsed.data.purpose ? { purpose: parsed.data.purpose } : {}), |
| 116 | }; |
| 117 | }, |
| 118 | }); |
| 119 | |
| 120 | if (result.status === 'valid') return { status: 'valid', state: result.value }; |
| 121 | |
| 122 | switch (result.reason) { |
| 123 | case 'token_missing': |
| 124 | return { status: 'invalid', reason: 'state_missing' }; |
| 125 | case 'token_malformed': |
| 126 | case 'payload_invalid': |
| 127 | return { status: 'invalid', reason: 'state_malformed' }; |
| 128 | case 'signature_invalid': |
| 129 | return { status: 'invalid', reason: 'signature_invalid' }; |
no test coverage detected