UNPKG

@joinmeow/cognito-passwordless-auth

Version:

Passwordless authentication with Amazon Cognito: FIDO2 (WebAuthn, support for Passkeys)

189 lines (188 loc) 7.33 kB
/** * Copyright Amazon.com, Inc. and its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). You * may not use this file except in compliance with the License. A copy of * the License is located at * * http://aws.amazon.com/apache2.0/ * * or in the "license" file accompanying this file. This file is * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF * ANY KIND, either express or implied. See the License for the specific * language governing permissions and limitations under the License. */ import { createFetchWithRetry } from "./retry.js"; let config_ = undefined; export function configure(config) { if (config) { let cognitoIdpEndpoint = config.cognitoIdpEndpoint || config.userPoolId?.split("_")[0]; if (!cognitoIdpEndpoint) { throw new Error("Invalid configuration provided: either cognitoIdpEndpoint or userPoolId must be provided"); } // If endpoint lacks protocol and is not an AWS region, prefix with https:// const regionRegex = /^[a-z]{2}-[a-z]+-\d$/; if (!cognitoIdpEndpoint.startsWith("http") && !regionRegex.test(cognitoIdpEndpoint)) { cognitoIdpEndpoint = `https://${cognitoIdpEndpoint}`; } // Wrap the user-provided or default fetch in retry logic (pass debug callback) const baseFetch = (config.fetch ?? Defaults.fetch).bind(globalThis); config_ = { ...config, cognitoIdpEndpoint, crypto: config.crypto ?? Defaults.crypto, storage: config.storage ?? Defaults.storage, fetch: createFetchWithRetry(baseFetch, config.debug), location: config.location ?? Defaults.location, history: config.history ?? Defaults.history, totp: config.totp ?? { issuer: "YourApp", }, tokenRefresh: { inactivityThreshold: config.tokenRefresh?.inactivityThreshold ?? 30 * 60 * 1000, // 30 minutes useActivityTracking: config.tokenRefresh?.useActivityTracking ?? false, }, /** Whether to use the new GetTokensFromRefreshToken API. Default: true */ useGetTokensFromRefreshToken: typeof config.useGetTokensFromRefreshToken === "boolean" ? config.useGetTokensFromRefreshToken : config.useGetTokensFromRefreshToken == null ? true : (() => { throw new Error(`Invalid configuration: useGetTokensFromRefreshToken must be a boolean, got ${typeof config.useGetTokensFromRefreshToken}`); })(), }; if (config.hostedUi) { config_.hostedUi = { redirectSignIn: config.hostedUi.redirectSignIn, scopes: config.hostedUi.scopes ?? ["openid", "email", "profile"], responseType: config.hostedUi.responseType ?? "code", ...(config.hostedUi.domain && { domain: config.hostedUi.domain }), }; config_.debug?.("Cognito Hosted UI configured, will use cognitoIdpEndpoint for OAuth domain"); } config_.debug?.("Configuration loaded:", config); } else { if (!config_) { throw new Error("Call configure(config) first"); } } return config_; } function normalizeEndpoint(endpoint) { // Ensure the endpoint has a protocol and no trailing slash const withProtocol = endpoint.startsWith("http") ? endpoint : `https://${endpoint}`; return withProtocol.replace(/\/+$/, ""); // trim trailing slashes } export function getAuthorizeEndpoint() { const { cognitoIdpEndpoint, hostedUi } = configure(); const domainBase = hostedUi?.domain ?? cognitoIdpEndpoint; const base = normalizeEndpoint(domainBase); return `${base}/oauth2/authorize`; } /** * Get the full OAuth token endpoint URL with protocol */ export function getTokenEndpoint() { const { cognitoIdpEndpoint, hostedUi } = configure(); const domainBase = hostedUi?.domain ?? cognitoIdpEndpoint; const base = normalizeEndpoint(domainBase); return `${base}/oauth2/token`; } /** * Get the full Cognito IDP endpoint URL with protocol */ export function getCognitoIdpEndpointWithProtocol() { const config = configure(); const endpoint = config.cognitoIdpEndpoint; return endpoint.startsWith("http") ? endpoint : `https://${endpoint}`; } export function configureFromAmplify(amplifyConfig) { const { region, userPoolId, userPoolWebClientId } = isAmplifyConfig(amplifyConfig) ? amplifyConfig.Auth : amplifyConfig; if (typeof region !== "string") { throw new Error("Invalid Amplify configuration provided: invalid or missing region"); } if (typeof userPoolId !== "string") { throw new Error("Invalid Amplify configuration provided: invalid or missing userPoolId"); } if (typeof userPoolWebClientId !== "string") { throw new Error("Invalid Amplify configuration provided: invalid or missing userPoolWebClientId"); } configure({ cognitoIdpEndpoint: region, userPoolId, clientId: userPoolWebClientId, }); return { with: (config) => { return configure({ cognitoIdpEndpoint: region, userPoolId, clientId: userPoolWebClientId, ...config, }); }, }; } function isAmplifyConfig(c) { return !!c && typeof c === "object" && "Auth" in c; } class MemoryStorage { constructor() { this.memory = new Map(); } getItem(key) { return this.memory.get(key); } setItem(key, value) { this.memory.set(key, value); } removeItem(key) { this.memory.delete(key); } } export class UndefinedGlobalVariableError extends Error { } class Defaults { static getFailingProxy(expected) { const message = `"${expected}" is not available as a global variable in your JavaScript runtime, so you must configure it explicitly with Passwordless.configure()`; return new Proxy((() => undefined), { apply() { throw new UndefinedGlobalVariableError(message); }, get() { throw new UndefinedGlobalVariableError(message); }, }); } static get storage() { return typeof globalThis.localStorage !== "undefined" ? globalThis.localStorage : new MemoryStorage(); } static get crypto() { if (typeof globalThis.crypto !== "undefined") return globalThis.crypto; return Defaults.getFailingProxy("crypto"); } static get fetch() { if (typeof globalThis.fetch !== "undefined") return globalThis.fetch; return Defaults.getFailingProxy("fetch"); } static get location() { if (typeof globalThis.location !== "undefined") return globalThis.location; return Defaults.getFailingProxy("location"); } static get history() { if (typeof globalThis.history !== "undefined") return globalThis.history; return Defaults.getFailingProxy("history"); } }