feat: self-serve signup Phase 2 (frontend cutover) (#162)
Co-authored-by: Michael Chihlas <michael@resolutionflow.com> Co-committed-by: Michael Chihlas <michael@resolutionflow.com>
This commit was merged in pull request #162.
This commit is contained in:
61
frontend/src/lib/oauthState.ts
Normal file
61
frontend/src/lib/oauthState.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* UTF-8-safe base64url encoding for OAuth `state` payloads.
|
||||
*
|
||||
* The /accept-invite flow round-trips an invite code + invited email through
|
||||
* the OAuth provider's `state` parameter. Internationalized email addresses
|
||||
* (e.g., `user@münchen.de`) contain code points > 255, which raw `btoa` /
|
||||
* `atob` cannot represent — they throw `DOMException: The string to be
|
||||
* encoded contains characters outside of the Latin1 range`.
|
||||
*
|
||||
* The classic `unescape(encodeURIComponent(...))` trick maps a UTF-16 string
|
||||
* through its UTF-8 byte representation into a Latin-1 string that `btoa`
|
||||
* accepts. The decode side reverses the transformation.
|
||||
*/
|
||||
|
||||
export interface OAuthStatePayload {
|
||||
csrf: string
|
||||
accountInviteCode: string
|
||||
invitedEmail: string
|
||||
}
|
||||
|
||||
export interface DecodedOAuthState {
|
||||
csrf: string
|
||||
accountInviteCode?: string
|
||||
invitedEmail?: string
|
||||
}
|
||||
|
||||
/** Encode an OAuth state payload as URL-safe base64. UTF-8 safe. */
|
||||
export function encodeOAuthState(payload: OAuthStatePayload): string {
|
||||
const json = JSON.stringify(payload)
|
||||
// unescape(encodeURIComponent(...)) converts UTF-16 -> UTF-8 -> Latin-1
|
||||
// string so btoa can encode it without throwing on non-Latin-1 chars.
|
||||
const b64 = btoa(unescape(encodeURIComponent(json)))
|
||||
return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
|
||||
}
|
||||
|
||||
/** Best-effort base64url-decode. Returns null on legacy random-hex states or
|
||||
* malformed input so the caller can fall back to a simple equality check. */
|
||||
export function decodeOAuthState(raw: string | null): DecodedOAuthState | null {
|
||||
if (!raw) return null
|
||||
try {
|
||||
const padded = raw.replace(/-/g, '+').replace(/_/g, '/')
|
||||
const b64 = padded + '='.repeat((4 - (padded.length % 4)) % 4)
|
||||
// decodeURIComponent(escape(...)) reverses the encode-side transform.
|
||||
const json = decodeURIComponent(escape(atob(b64)))
|
||||
const parsed = JSON.parse(json) as Partial<DecodedOAuthState>
|
||||
if (typeof parsed?.csrf === 'string') {
|
||||
return {
|
||||
csrf: parsed.csrf,
|
||||
accountInviteCode:
|
||||
typeof parsed.accountInviteCode === 'string'
|
||||
? parsed.accountInviteCode
|
||||
: undefined,
|
||||
invitedEmail:
|
||||
typeof parsed.invitedEmail === 'string' ? parsed.invitedEmail : undefined,
|
||||
}
|
||||
}
|
||||
return null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user