UNPKG

@turnkey/react-wallet-kit

Version:

The easiest and most powerful way to integrate Turnkey's Embedded Wallets into your React applications.

232 lines (228 loc) 7.56 kB
'use strict'; var sdkTypes = require('@turnkey/sdk-types'); var config = require('./config.js'); var storage = require('./storage.js'); /** * Builds the OAuth state parameter string */ function buildOAuthState(params) { const { provider, flow, publicKey, nonce, additionalState } = params; let state = `provider=${provider}&flow=${flow}&publicKey=${encodeURIComponent(publicKey)}`; if (nonce) { state += `&nonce=${nonce}`; } if (additionalState) { const extra = Object.entries(additionalState).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&"); if (extra) { state += `&${extra}`; } } return state; } /** * Opens a centered OAuth popup window */ function openOAuthPopup() { const width = config.popupWidth; const height = config.popupHeight; const left = window.screenX + (window.innerWidth - width) / 2; const top = window.screenY + (window.innerHeight - height) / 2; return window.open("about:blank", "_blank", `width=${width},height=${height},top=${top},left=${left},scrollbars=yes,resizable=yes`); } /** * Redirects to OAuth provider URL */ function redirectToOAuthProvider(url) { window.location.href = url; const timeLimit = 5 * 60 * 1000; // 5 minutes return new Promise((_, reject) => { const timeout = setTimeout(() => { reject(new Error("Authentication timed out.")); }, timeLimit); window.addEventListener("beforeunload", () => clearTimeout(timeout)); }); } /** * Cleans up OAuth parameters from URL (removes hash and search) */ function cleanupOAuthUrl() { window.history.replaceState(null, document.title, window.location.pathname); } /** * Cleans up OAuth hash from URL but preserves search parameters */ function cleanupOAuthUrlPreserveSearch() { window.history.replaceState(null, document.title, window.location.pathname + window.location.search); } /** * Parses the OAuth state parameter string into an object */ function parseStateParam(stateParam) { if (!stateParam) return {}; const result = {}; const pairs = stateParam.split("&"); for (const pair of pairs) { const [key, value] = pair.split("="); if (key && value !== undefined) { result[key] = decodeURIComponent(value); } } return result; } /** * Unified OAuth response parser for both popup and redirect flows. * Handles both: * - PKCE flows (Facebook, Discord, X): code in search parameters * - Non-PKCE flows (Google, Apple): id_token in hash parameters * - Apple's non-standard hash format where state parameters are directly embedded * * @param url - The full URL to parse (from popup or redirect) * @param expectedProvider - Optional provider if already known (for popup flows) * @returns Parsed OAuth response data including tokens, codes, and state parameters, or null if invalid */ function parseOAuthResponse(url, expectedProvider) { let idToken = null; let authCode = null; let stateString = null; const parsedUrl = new URL(url); const search = parsedUrl.search.substring(1); // Remove leading '?' const hash = parsedUrl.hash.substring(1); // Remove leading '#' // Handle PKCE flows (code in search parameters) if (search && search.includes("code=")) { const searchParams = new URLSearchParams(search); authCode = searchParams.get("code"); stateString = searchParams.get("state"); } // Handle non-PKCE flows (id_token in hash) else if (hash) { // Handle Apple's non-standard format: state=provider=apple&flow=redirect&...&code=...&id_token=... if (hash.startsWith("state=provider=apple")) { // Extract id_token from the end const idTokenMatch = hash.match(/id_token=([^&]+)$/); idToken = idTokenMatch ? idTokenMatch[1] : null; // Extract state content between "state=" and "&code=" const stateEndIndex = hash.indexOf("&code="); if (stateEndIndex !== -1) { stateString = hash.substring(6, stateEndIndex); // Remove "state=" prefix } } else { // Standard OAuth format - parse as URLSearchParams const hashParams = new URLSearchParams(hash); idToken = hashParams.get("id_token"); stateString = hashParams.get("state"); } } if (!stateString) { throw new sdkTypes.TurnkeyError("Missing OAuth state in redirect response", sdkTypes.TurnkeyErrorCodes.INVALID_OAUTH_STATE); } // Validate state parameter storage.consumeOAuthState(stateString); // Parse state parameters const { provider, flow, publicKey, openModal, sessionKey, oauthIntent, nonce } = parseStateParam(stateString); // If we have an expected provider (popup flow), validate it matches if (expectedProvider) { const config$1 = config.OAUTH_PROVIDER_CONFIGS[expectedProvider]; if (config$1.usesPKCE) { // PKCE providers must have authCode and matching provider in state if (!authCode || !stateString || provider !== expectedProvider) { return null; } } else { // Non-PKCE providers must have idToken if (!idToken) { return null; } } } return { idToken, authCode, provider: provider ?? null, flow: flow ?? null, publicKey: publicKey ?? null, openModal: openModal ?? null, sessionKey: sessionKey ?? undefined, oauthIntent: oauthIntent ?? null, nonce: nonce ?? null }; } /** * Builds the complete OAuth authorization URL for a provider */ function buildOAuthUrl(params) { const { provider, clientId, redirectUri, publicKey, nonce, flow, codeChallenge, additionalState } = params; const config$1 = config.OAUTH_PROVIDER_CONFIGS[provider]; const authUrl = new URL(config$1.authUrl); authUrl.searchParams.set("client_id", clientId); authUrl.searchParams.set("redirect_uri", redirectUri); authUrl.searchParams.set("response_type", config$1.responseType); if (config$1.scopes) { authUrl.searchParams.set("scope", config$1.scopes); } if (config$1.responseMode) { authUrl.searchParams.set("response_mode", config$1.responseMode); } // PKCE parameters if (config$1.usesPKCE && codeChallenge) { authUrl.searchParams.set("code_challenge", codeChallenge); authUrl.searchParams.set("code_challenge_method", "S256"); } // Nonce handling - some providers want it in URL params, others in state if (config$1.nonceInParams) { authUrl.searchParams.set("nonce", nonce); } // Google-specific: prompt for account selection if (provider === sdkTypes.OAuthProviders.GOOGLE) { authUrl.searchParams.set("prompt", "select_account"); } // Build state parameter const stateParams = { provider, flow, publicKey }; // Include nonce in state for providers that need it there (not in URL params) if (!config$1.nonceInParams) { stateParams.nonce = nonce; } if (additionalState) { stateParams.additionalState = additionalState; } // Generate state string and store for validation const state = buildOAuthState(stateParams); authUrl.searchParams.set("state", state); storage.storeOAuthState(state); return authUrl.toString(); } exports.buildOAuthState = buildOAuthState; exports.buildOAuthUrl = buildOAuthUrl; exports.cleanupOAuthUrl = cleanupOAuthUrl; exports.cleanupOAuthUrlPreserveSearch = cleanupOAuthUrlPreserveSearch; exports.openOAuthPopup = openOAuthPopup; exports.parseOAuthResponse = parseOAuthResponse; exports.parseStateParam = parseStateParam; exports.redirectToOAuthProvider = redirectToOAuthProvider; //# sourceMappingURL=url.js.map