@joinmeow/cognito-passwordless-auth
Version:
Passwordless authentication with Amazon Cognito: FIDO2 (WebAuthn, support for Passkeys)
189 lines (188 loc) • 7.33 kB
JavaScript
/**
* 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");
}
}