Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 54 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ You are still responsible for your app’s route protection and redirects.
refreshSession(): Promise<void>;
refreshStepUpStatus(): Promise<StepUpStatus | null>;
verifyStepUpWithPasskey(): Promise<StepUpVerificationResult>;
verifyStepUpWithPasskeyPrf(input: PasskeyPrfInput): Promise<StepUpWithPasskeyPrfResult>;
logout(): Promise<void>;
deleteUser(): Promise<void>;
login(identifier: string, passkeyAvailable: boolean): Promise<Response>;
Expand Down Expand Up @@ -185,6 +186,55 @@ function DeleteAccountButton() {

The current step-up backend supports WebAuthn/passkeys. `refreshStepUpStatus()` calls `/step-up/status`, and `verifyStepUpWithPasskey()` performs the `/step-up/webauthn/start` and `/step-up/webauthn/finish` challenge flow.

### WebAuthn PRF

WebAuthn PRF lets a compatible passkey and browser derive local key material during a WebAuthn assertion. Seamless Auth verifies the passkey assertion on the server, while the React SDK returns the PRF output only to the browser caller. PRF output is stripped before `/webAuthn/login/finish` and `/step-up/webauthn/finish`, and should never be logged, stored, or sent to your API.

Browser and authenticator support is not universal. Call `isPasskeyPrfSupported()` before offering PRF-required flows, and keep a fallback for passkeys that authenticate successfully without returning PRF output.

```ts
import { createSeamlessAuthClient } from '@seamless-auth/react';

const authClient = createSeamlessAuthClient({
apiHost: 'https://your.api',
mode: 'server',
});

const prfSupported = await authClient.isPasskeyPrfSupported();

if (prfSupported) {
await authClient.registerPasskey({
metadata: {
friendlyName: 'My laptop',
platform: 'macOS',
browser: 'Chrome',
deviceInfo: navigator.userAgent,
},
requirePrf: true,
});
}
```

For local key unwrap flows such as Seamless Secrets, use PRF during step-up and consume the returned bytes in browser memory:

```ts
const result = await authClient.verifyStepUpWithPasskeyPrf({
salt: vaultSaltBase64url,
credentialId,
});

if (!result.success || !result.prf) {
throw new Error('PRF step-up failed');
}

const vaultUnlockMaterial: { credentialId: string; output: Uint8Array } = {
credentialId: result.credentialId!,
output: result.prf.output,
};
```

The salt may be an `ArrayBuffer`, `ArrayBufferView`, or base64url string. Authentication proves identity and user presence; the PRF output is local key material for your application to use without sending it to Seamless Auth.

## Headless Client

For custom auth UIs, use the exported client directly:
Expand Down Expand Up @@ -221,9 +271,11 @@ The headless client exposes helpers for:

Client methods return raw `Response` objects except for the passkey convenience helpers:

- `loginWithPasskey(): Promise<PasskeyLoginResult>`
- `registerPasskey(metadata): Promise<PasskeyRegistrationResult>`
- `loginWithPasskey(options?: PasskeyLoginOptions): Promise<PasskeyLoginWithPrfResult>`
- `registerPasskey(metadata | { metadata, requestPrf?, requirePrf? }): Promise<PasskeyRegistrationResult>`
- `isPasskeyPrfSupported(): Promise<boolean>`
- `verifyStepUpWithPasskey(): Promise<StepUpVerificationResult>`
- `verifyStepUpWithPasskeyPrf(input): Promise<StepUpWithPasskeyPrfResult>`

## React Hooks For Custom UI

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@seamless-auth/react",
"version": "0.1.1",
"version": "0.2.0",
"description": "A drop-in authentication solution for modern React applications.",
"type": "module",
"exports": {
Expand Down
25 changes: 25 additions & 0 deletions src/AuthProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@

import {
createSeamlessAuthClient,
StepUpWithPasskeyPrfResult,
StepUpStatus,
StepUpVerificationResult,
} from '@/client/createSeamlessAuthClient';
import { PasskeyPrfInput } from '@/client/webauthnPrf';
import { Credential, User } from '@/types';
import React, {
createContext,
Expand Down Expand Up @@ -42,6 +44,9 @@ export interface AuthContextType {
handlePasskeyLogin: () => Promise<boolean>;
refreshStepUpStatus: () => Promise<StepUpStatus | null>;
verifyStepUpWithPasskey: () => Promise<StepUpVerificationResult>;
verifyStepUpWithPasskeyPrf: (
input: PasskeyPrfInput
) => Promise<StepUpWithPasskeyPrfResult>;
loading: boolean;
}

Expand Down Expand Up @@ -222,6 +227,25 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({
return result;
}, [authClient]);

const verifyStepUpWithPasskeyPrf = useCallback(
async (input: PasskeyPrfInput) => {
const result = await authClient.verifyStepUpWithPasskeyPrf(input);

if (result.success) {
setStepUpStatus({
fresh: result.fresh,
method: result.method,
verifiedAt: result.verifiedAt,
expiresAt: result.expiresAt,
maxAgeSeconds: result.maxAgeSeconds,
});
}

return result;
},
[authClient]
);

useEffect(() => {
void validateToken();
}, [validateToken]);
Expand Down Expand Up @@ -254,6 +278,7 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({
handlePasskeyLogin,
refreshStepUpStatus,
verifyStepUpWithPasskey,
verifyStepUpWithPasskeyPrf,
}}
>
{children}
Expand Down
Loading
Loading