Passwordless authentication with Amazon Cognito: FIDO2 (WebAuthn, support for Passkeys)
npm install @joinmeow/cognito-passwordless-auth
A client-side library for implementing passwordless authentication with Amazon Cognito. This package provides the frontend implementation for various secure passwordless authentication methods:
- FIDO2: aka WebAuthn, i.e. sign in with Face, Touch, YubiKey, etc. This includes support for Passkeys (i.e. usernameless authentication).
- Device Authentication: allow users to authenticate with trusted devices after initial verification.
- TOTP MFA: support for Time-Based One-Time Password multi-factor authentication.
This is an opinionated fork of the original Amazon Cognito Passwordless Auth solution, maintained by @joinmeow. Unlike the original library, this fork focuses exclusively on secure authentication methods and eliminates less secure options, creating a more streamlined and security-focused implementation. This client-only version is intended for use with an already configured backend.
This package is published on npm and can be installed directly:
``shell`
npm install @joinmeow/cognito-passwordless-auth
Or with Yarn:
`shell`
yarn add @joinmeow/cognito-passwordless-auth
This library provides implementations for different frontend frameworks:
`javascript
import {
configure,
initialize,
signUp,
confirmSignUp,
prepareFido2SignIn,
authenticateWithFido2,
authenticateWithSRP,
} from "@joinmeow/cognito-passwordless-auth";
// Configure the client
configure({
userPoolId: "us-east-1_example",
clientId: "abcdefghijklmnopqrstuvwxyz",
// Other configuration parameters as needed
});
// Initialize (handles things like redirects)
initialize();
// Sign up a new user
await signUp({
username: "user@example.com",
password: "securePassword123",
userAttributes: [
{ name: "email", value: "user@example.com" },
{ name: "name", value: "Jane Doe" },
],
});
// Confirm sign up with verification code
await confirmSignUp({
username: "user@example.com",
confirmationCode: "123456",
});
// Optionally pre-fetch the WebAuthn assertion (e.g. for conditional mediation)
const prepared = await prepareFido2SignIn({
username: "user@example.com",
});
// Start the sign-in process with FIDO2. Pass the prepared bundle to skip
// re-triggering navigator.credentials.get(). If you omit prepared, the
// helper will prompt the user as usual.
const { signedIn } = await authenticateWithFido2({ prepared });
// Wait for the sign-in process to complete
await signedIn;
// Sign-in with SRP without MFA (no rememberDevice callback needed)
const { signedIn } = await authenticateWithSRP({
username: "user@example.com",
password: "password123",
});
// If your user pool enforces an OTP second factor, provide the callback so it can
// ask the user after they enter the OTP:
const { signedIn: signedInWithOtp } = await authenticateWithSRP({
username: "user@example.com",
password: "password123",
rememberDevice: async () => {
return window.confirm("Remember this device?");
},
});
// Sign out
await signOut();
`
`jsx
import React, { useState, useEffect } from "react";
import {
PasswordlessContextProvider,
usePasswordless,
useTotpMfa,
} from "@joinmeow/cognito-passwordless-auth/react";
function App() {
return (
);
}
function YourApp() {
const {
signInStatus,
prepareFido2SignIn,
authenticateWithFido2,
authenticateWithSRP,
tokens,
updateDeviceStatus,
} = usePasswordless();
const [showRememberDevice, setShowRememberDevice] = useState(false);
// Check if we need to ask the user about remembering this device
useEffect(() => {
if (tokens?.userConfirmationNecessary) {
setShowRememberDevice(true);
}
}, [tokens]);
// Handle user's choice about remembering the device
const handleRememberDevice = async (remember) => {
if (tokens?.deviceKey && tokens?.accessToken) {
await updateDeviceStatus({
deviceKey: tokens.deviceKey,
deviceRememberedStatus: remember ? "remembered" : "not_remembered",
});
setShowRememberDevice(false);
}
};
// For TOTP MFA setup
const { setupStatus, secretCode, qrCodeUrl, beginSetup, verifySetup } =
useTotpMfa();
if (signInStatus === "SIGNED_IN") {
return (
{/ Device remembering prompt /}
{showRememberDevice && (
Do you want to remember this device? You won't need MFA next time.
return (
onSubmit={(e) => {
Sign in with Google (OAuth2 Redirect)
When you call
signInWithGoogle(), the library builds the Google OAuth2 authorization URL (including client ID, PKCE challenge, state, scopes, etc.) and redirects the browser. After the user signs in, Google redirects back to your registered redirectUri with ?code=...&state=....On your callback page, call
handleGoogleCallback() from @joinmeow/cognito-passwordless-auth/client/google to complete the flow:Plain-JS / Multi-Page Example:
`html
`React Single-Page App Example:
`jsx
// GoogleCallback.jsx
import { useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { handleGoogleCallback } from "@joinmeow/cognito-passwordless-auth/client/google";export function GoogleCallback() {
const navigate = useNavigate();
useEffect(() => {
(async () => {
try {
await handleGoogleCallback();
navigate("/dashboard");
} catch (err) {
console.error("Google callback error", err);
navigate("/login?error=oauth");
}
})();
}, [navigate]);
return
Signing you in…;
}
`Steps:
1. Configure Google OAuth in
configure({ google: { clientId, redirectUri, scopes } }).
2. Initiate sign-in:`js
import { signInWithGoogle } from "@joinmeow/cognito-passwordless-auth/client/google";
;
`3. After redirect, your callback page or component calls
handleGoogleCallback() to exchange the code, store tokens, clean up the URL, and return the tokens.
4. Redirect users to your protected routes (e.g. /dashboard).Advanced Features
$3
The complete sign-up flow consists of:
`javascript
// 1. Register a new user
await signUp({
username: "user@example.com",
password: "securePassword123",
userAttributes: [{ name: "email", value: "user@example.com" }],
});// 2. If an error occurs during sign-up, you can resend the confirmation code
await resendConfirmationCode({ username: "user@example.com" });
// 3. Confirm the sign-up with the verification code that was sent to the user
await confirmSignUp({
username: "user@example.com",
confirmationCode: "123456", // Code received via email or SMS
});
// 4. After sign-up confirmation, the user can sign in
const { signedIn } = await authenticateWithSRP({
username: "user@example.com",
password: "securePassword123",
rememberDevice: async () => {
return window.confirm("Remember this device?");
},
});
`$3
Device authentication allows users to bypass MFA on subsequent sign-ins from the same device. The library handles most of this automatically:
`javascript
// 1. User signs in with SRP or another method
const { signedIn } = await authenticateWithSRP({
username: "user@example.com",
password: "securePassword123",
rememberDevice: async () => {
return window.confirm("Remember this device?");
},
});// 2. Wait for the sign-in to complete - including any "remember device" decisions
const tokens = await signedIn;
// 3. The signedIn promise won't resolve until:
// - User explicitly said "don't remember", or
// - The UpdateDeviceStatus call has completed, or
// - No rememberDevice callback was supplied
`How device authentication works:
1. Device Confirmation (automatic): When a user signs in with MFA on a new device, the library automatically registers the device with Cognito.
2. Device Remembering (user choice): When
userConfirmationNecessary is true, your app's rememberDevice callback is invoked, giving you control over when and how to ask the user.3. Atomic Authentication Flow: The
signedIn promise doesn't resolve until the entire flow (including device status updates) is complete.4. Subsequent Sign-ins: On remembered devices, users bypass MFA automatically.
Important: The
rememberDevice callback is only invoked after successful MFA authentication (like TOTP or SMS). For users without MFA enabled, or for sign-ins that don't trigger an MFA step, the callback will never be invoked and device remembering is not possible.$3
Set up Time-Based One-Time Password MFA for a user:
`javascript
// Configure the TOTP issuer (defaults to "YourApp")
configure({
// ... other configuration
totp: {
issuer: "YourCompany", // The name shown in authenticator apps
},
});// In a React component
const { setupStatus, secretCode, qrCodeUrl, beginSetup, verifySetup } =
useTotpMfa();
// Start setup
await beginSetup();
// Show QR code to user (qrCodeUrl contains the otpauth:// URL)
// The QR code will show "YourCompany:username" in authenticator apps
// ...
// Verify the code from the user's authenticator app
await verifySetup(userEnteredCode, "My Authenticator");
`> Note: TOTP MFA setup requires proper configuration. The
issuer setting controls what name appears in authenticator apps like Google Authenticator or Authy.$3
The library provides comprehensive device management capabilities for trusted device authentication:
#### React Hook Usage
`jsx
import React, { useState } from "react";
import { usePasswordless } from "@joinmeow/cognito-passwordless-auth/react";function DeviceManager() {
const {
deviceKey,
confirmDevice,
updateDeviceStatus,
forgetDevice,
clearDeviceKey,
tokens,
} = usePasswordless();
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const handleConfirmDevice = async () => {
if (!deviceKey) return;
try {
setLoading(true);
setError(null);
await confirmDevice("My Laptop");
console.log("Device confirmed successfully");
} catch (error) {
console.error("Failed to confirm device:", error);
setError(error.message);
} finally {
setLoading(false);
}
};
const handleRememberDevice = async (remember: boolean) => {
if (!deviceKey) return;
try {
setLoading(true);
setError(null);
await updateDeviceStatus({
deviceKey,
deviceRememberedStatus: remember ? "remembered" : "not_remembered",
});
} catch (error) {
console.error("Failed to update device status:", error);
setError(error.message);
} finally {
setLoading(false);
}
};
const handleForgetDevice = async () => {
try {
setLoading(true);
setError(null);
await forgetDevice(); // Forgets current device
// Or forget a specific device: await forgetDevice(specificDeviceKey);
} catch (error) {
console.error("Failed to forget device:", error);
setError(error.message);
} finally {
setLoading(false);
}
};
return (
{error && Error: {error}} {deviceKey && (
Device Key: {deviceKey}
)}
);
}
`#### Device Management Flow
`javascript
// 1. After successful authentication with MFA, a device key is available
const { signedIn } = await authenticateWithSRP({
username: "user@example.com",
password: "password123",
rememberDevice: async () => {
// This callback is called when userConfirmationNecessary is true
return window.confirm("Remember this device for future sign-ins?");
},
});// 2. The device is automatically confirmed and optionally remembered
const tokens = await signedIn;
// 3. Manually manage device status later
if (tokens.deviceKey) {
// Update device status
await updateDeviceStatus({
deviceKey: tokens.deviceKey,
deviceRememberedStatus: "remembered",
});
// Or forget the device entirely
await forgetDevice(tokens.deviceKey);
}
`$3
Manage WebAuthn/FIDO2 credentials for passwordless authentication:
#### React Hook Usage
`jsx
import React, { useState } from "react";
import { usePasswordless } from "@joinmeow/cognito-passwordless-auth/react";function FIDO2Manager() {
const {
fido2Credentials,
creatingCredential,
fido2CreateCredential,
userVerifyingPlatformAuthenticatorAvailable,
} = usePasswordless();
const [error, setError] = useState(null);
const handleCreateCredential = async () => {
try {
setError(null);
await fido2CreateCredential({
friendlyName: "My Face ID",
});
console.log("FIDO2 credential created successfully");
} catch (error) {
console.error("Failed to create FIDO2 credential:", error);
setError(error.message);
}
};
const handleUpdateCredential = async (credential, newName) => {
try {
setError(null);
await credential.update({ friendlyName: newName });
console.log("Credential updated successfully");
} catch (error) {
console.error("Failed to update credential:", error);
setError(error.message);
}
};
const handleDeleteCredential = async (credential) => {
if (!window.confirm("Are you sure you want to delete this credential?")) {
return;
}
try {
setError(null);
await credential.delete();
console.log("Credential deleted successfully");
} catch (error) {
console.error("Failed to delete credential:", error);
setError(error.message);
}
};
return (
FIDO2 Credentials
{error &&
Error: {error}} {userVerifyingPlatformAuthenticatorAvailable && (
)}
{fido2Credentials?.map((credential) => (
key={credential.credentialId}
style={{ border: "1px solid #ccc", margin: "10px", padding: "10px" }}
>
{credential.friendlyName || "Unnamed Credential"}
Created: {new Date(credential.createdAt).toLocaleDateString()}
Last Used: {new Date(credential.lastUseDate).toLocaleDateString()}
onClick={() => {
const newName = prompt(
"Enter new name:",
credential.friendlyName
);
if (newName) handleUpdateCredential(credential, newName);
}}
disabled={credential.busy}
>
{credential.busy ? "Updating..." : "Update Name"}
onClick={() => handleDeleteCredential(credential)}
disabled={credential.busy}
style={{
marginLeft: "10px",
backgroundColor: "#ff4444",
color: "white",
}}
>
{credential.busy ? "Deleting..." : "Delete"}
))} {fido2Credentials?.length === 0 && (
No FIDO2 credentials registered.
)}
#### FIDO2 Authentication Flow
`javascript
import {
prepareFido2SignIn,
authenticateWithFido2,
fido2CreateCredential,
detectMediationCapabilities,
} from "@joinmeow/cognito-passwordless-auth";// 1. Check if platform authenticator is available
if (userVerifyingPlatformAuthenticatorAvailable) {
// 2. Detect browser capabilities for enhanced WebAuthn UX
const { conditional } = await detectMediationCapabilities();
// 3. Optionally create a new credential using conditional create (autofill)
await fido2CreateCredential({
friendlyName: "iPhone Face ID",
conditionalCreate: conditional,
});
// 4. Prepare the WebAuthn assertion (conditional mediation when supported)
const prepared = await prepareFido2SignIn({
username: "user@example.com",
mediation: conditional ? "conditional" : undefined,
// Optional: specify credentials to bias the allow list
credentials: fido2Credentials?.map((c) => ({
id: c.credentialId,
transports: c.transports,
})),
});
// 5. Complete the Cognito challenge using the prepared bundle
const { signedIn } = await authenticateWithFido2({ prepared });
await signedIn;
}
`> Tip: For advanced capability checks (e.g., detecting upcoming "immediate" mediation or hybrid transports), call
>
getClientCapabilities() directly. It returns the full WebAuthn client capabilities dictionary when supported by the
> browser.$3
The
useLocalUserCache() hook manages a cache of recently signed-in users for improved UX:#### Setup and Usage
`jsx
import React, { useCallback } from "react";
import {
PasswordlessContextProvider,
usePasswordless,
useLocalUserCache,
} from "@joinmeow/cognito-passwordless-auth/react";// 1. Enable local user cache in the provider
function App() {
return (
);
}
// 2. Use the cache in your components
function UserSelector() {
const {
currentUser,
lastSignedInUsers,
clearLastSignedInUsers,
updateFidoPreference,
signingInStatus,
authMethod,
} = useLocalUserCache();
const { authenticateWithFido2, authenticateWithSRP } = usePasswordless();
const handleQuickSignIn = useCallback(
async (user) => {
try {
if (user.useFido === "YES" && user.credentials) {
// Sign in with FIDO2 using stored credentials
await authenticateWithFido2({
username: user.username,
credentials: user.credentials,
});
} else {
// Fall back to password authentication
const password = prompt("Enter your password:");
if (password) {
await authenticateWithSRP({
username: user.username,
password,
});
}
}
} catch (error) {
console.error("Quick sign-in failed:", error);
alert("Sign-in failed: " + error.message);
}
},
[authenticateWithFido2, authenticateWithSRP]
);
const handleFidoPreferenceChange = useCallback(
(useFido) => {
updateFidoPreference({ useFido });
},
[updateFidoPreference]
);
return (
Recent Users
{currentUser && (
style={{ border: "1px solid #ccc", padding: "10px", margin: "10px" }}
>
Current User: {currentUser.username}
Email: {currentUser.email}
Auth Method: {currentUser.authMethod}
FIDO2 Preference: {currentUser.useFido}
)}
Quick Sign-In
{lastSignedInUsers?.map((user) => (
onClick={() => handleQuickSignIn(user)}
disabled={signingInStatus !== "SIGNED_OUT"}
>
{user.email || user.username}
{user.useFido === "YES" && " (FIDO2)"}
))}
{signingInStatus !== "SIGNED_OUT" &&
Status: {signingInStatus}
}
#### StoredUser Object Structure
`typescript
type StoredUser = {
username: string;
email?: string;
useFido?: "YES" | "NO" | "ASK";
credentials?: { id: string; transports?: AuthenticatorTransport[] }[];
authMethod?: "SRP" | "FIDO2" | "PLAINTEXT" | "REDIRECT";
};
`$3
#### useAwaitableState
Convert any state value into a promise that can be awaited:
`jsx
import React, { useState } from "react";
import { useAwaitableState } from "@joinmeow/cognito-passwordless-auth/react";function AsyncStateExample() {
const [data, setData] = useState(null);
const awaitableData = useAwaitableState(data);
const [waiting, setWaiting] = useState(false);
const handleWaitForData = async () => {
try {
setWaiting(true);
console.log("Waiting for data...");
// This will wait until data is set to a truthy value
const result = await awaitableData.awaitable();
console.log("Data received:", result);
} catch (error) {
console.error("Waiting failed:", error);
} finally {
setWaiting(false);
}
};
const handleSetData = () => {
setData("Hello World");
// Resolve the awaitable with the current data value
awaitableData.resolve();
};
const handleRejectData = () => {
awaitableData.reject(new Error("Data loading failed"));
};
return (
Current data: {data}
Awaited data: {awaitableData.awaited?.value}
);
}
`API Reference
$3
Complete reference of all available properties and methods:
#### State Properties
`typescript
// Token and authentication state
tokens?: TokensFromStorage // Raw JWT tokens
tokensParsed?: { // Parsed token contents
idToken: CognitoIdTokenPayload;
accessToken: CognitoAccessTokenPayload;
expireAt: Date;
}
signInStatus: string // Overall auth status
signingInStatus: BusyState | IdleState // Current operation status
busy: boolean // Is any auth operation in progress
lastError?: Error // Last error that occurred
authMethod?: string // Current auth method used// Token refresh state
isRefreshingTokens: boolean // Is token refresh in progress
// FIDO2 state
userVerifyingPlatformAuthenticatorAvailable?: boolean // Platform auth available
fido2Credentials?: Fido2Credential[] // User's FIDO2 credentials
creatingCredential: boolean // Is creating FIDO2 credential
// Device management
deviceKey: string | null // Current device key
// TOTP MFA state
totpMfaStatus: {
enabled: boolean;
preferred: boolean;
availableMfaTypes: string[];
}
// Activity tracking (when enabled)
timeSinceLastActivityMs: number | null // Milliseconds since last activity
timeSinceLastActivitySeconds: number | null // Seconds since last activity
`#### Methods
`typescript
// Authentication methods
prepareFido2SignIn(options?: {
username?: string;
credentials?: { id: string; transports?: AuthenticatorTransport[] }[];
mediation?: "conditional" | "immediate" | "optional" | "required" | "silent";
signal?: AbortSignal;
}) => PromiseauthenticateWithFido2(options?: {
username?: string;
credentials?: { id: string; transports?: AuthenticatorTransport[] }[];
clientMetadata?: Record;
mediation?: "conditional" | "immediate" | "optional" | "required" | "silent";
prepared?: PreparedFido2SignIn;
}) => { signedIn: Promise }
authenticateWithSRP(options: {
username: string;
password: string;
smsMfaCode?: () => Promise;
otpMfaCode?: () => Promise;
newPassword?: () => Promise;
clientMetadata?: Record;
rememberDevice?: () => Promise;
}) => { signedIn: Promise }
authenticateWithPlaintextPassword(options: {
username: string;
password: string;
smsMfaCode?: () => Promise;
otpMfaCode?: () => Promise;
clientMetadata?: Record;
rememberDevice?: () => Promise;
}) => { signedIn: Promise }
signInWithRedirect(options?: {
provider?: string;
customState?: string;
}) => void
signOut(options?: {
skipTokenRevocation?: boolean
}) => { signedOut: Promise }
// Token management
refreshTokens(abort?: AbortSignal) => Promise
forceRefreshTokens(abort?: AbortSignal) => Promise
markUserActive() => void // Mark user as active for activity tracking
// Device management
confirmDevice(deviceName: string) => Promise
updateDeviceStatus(options: {
deviceKey: string;
deviceRememberedStatus: "remembered" | "not_remembered";
}) => Promise
forgetDevice(deviceKeyToForget?: string) => Promise
clearDeviceKey() => void
// FIDO2 credential management
fido2CreateCredential(options: {
friendlyName: string | (() => string | Promise);
conditionalCreate?: boolean;
}) => Promise
// WebAuthn helpers
detectMediationCapabilities(): Promise<{ conditional: boolean; immediate: boolean }>
getClientCapabilities(): Promise
// TOTP MFA
refreshTotpMfaStatus() => Promise
`> Note: Activity tracking features require enabling in your configuration:
>
>
`javascript
> configure({
> // ... other configuration
> tokenRefresh: {
> useActivityTracking: true,
> },
> });
> `$3
`typescript
// Properties
currentUser?: StoredUser // Currently signed-in user
lastSignedInUsers?: StoredUser[] // Last 10 signed-in users
signingInStatus: BusyState | IdleState // Current signing status
authMethod?: string // Current auth method// Methods
updateFidoPreference(options: {
useFido: "YES" | "NO"
}) => void
clearLastSignedInUsers() => void
`$3
`typescript
// Properties
setupStatus: "IDLE" | "GENERATING" | "READY" | "VERIFYING" | "VERIFIED" | "ERROR"
secretCode?: string // Secret for QR code
qrCodeUrl?: string // QR code URL
errorMessage?: string // Setup error message
totpMfaStatus: { // Current MFA status
enabled: boolean;
preferred: boolean;
availableMfaTypes: string[];
}// Methods
beginSetup() => Promise<{ SecretCode: string }>
verifySetup(code: string, deviceName?: string) => Promise<{ Status: string }>
resetSetup() => void
`$3
`typescript
// Methods
awaitable() => Promise // Get current promise
resolve() => void // Resolve with current state
reject(reason: Error) => void // Reject the promise// Properties
awaited?: { value: T } // Resolved value (if any)
`Library Architecture
The library is organized into several modules that handle different aspects of authentication:
- Core Configuration:
config.ts - Central configuration for the library
- Authentication APIs:
- cognito-api.ts - Direct interactions with Cognito Identity Provider API
- srp.ts - Secure Remote Password implementation
- fido2.ts - WebAuthn/FIDO2 authentication
- plaintext.ts - Basic password authentication
- device.ts - Device remembering and authentication
- Token Management:
- storage.ts - Token persistence and retrieval
- refresh.ts` - Token refresh scheduling and automatic renewalApache-2.0 © Amazon.com, Inc. and its affiliates.
This is a fork by Meow Technologies Inc. (https://meow.com), based on the original work by Amazon. All modifications are also licensed under Apache-2.0.