@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
JavaScript
;
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