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
101 changes: 100 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
- `createSeamlessAuthClient()`
- `useAuthClient()`
- `usePasskeySupport()`
- types including `AuthMode`, `AuthContextType`, `Credential`, `User`, `StepUpStatus`, and the headless client input/result types
- types including `AuthMode`, `AuthContextType`, `Credential`, `User`, `OAuthProvider`, `StepUpStatus`, and the headless client input/result types

## Installation

Expand Down Expand Up @@ -104,6 +104,9 @@ You are still responsible for your app’s route protection and redirects.
hasSignedInBefore: boolean;
markSignedIn(): void;
hasRole(role: string): boolean | undefined;
listOAuthProviders(): Promise<OAuthProvidersResult>;
startOAuthLogin(input: StartOAuthLoginInput): Promise<StartOAuthLoginResult>;
finishOAuthLogin(input: FinishOAuthLoginInput): Promise<void>;
refreshSession(): Promise<void>;
refreshStepUpStatus(): Promise<StepUpStatus | null>;
verifyStepUpWithPasskey(): Promise<StepUpVerificationResult>;
Expand Down Expand Up @@ -235,6 +238,95 @@ const vaultUnlockMaterial: { credentialId: string; output: Uint8Array } = {

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.

### OAuth Login

OAuth lets your app offer external identity providers such as Google, GitHub, Facebook, or custom
OIDC-style providers configured on the Seamless Auth API. The React SDK does not receive provider
access tokens. It only starts the provider redirect and completes the callback so Seamless Auth can
issue the normal access/refresh session.

Use `listOAuthProviders()` when you want to render enabled providers dynamically:

```tsx
import { useEffect, useState } from 'react';
import { useAuth } from '@seamless-auth/react';
import type { OAuthProvider } from '@seamless-auth/react';

function OAuthButtons() {
const { listOAuthProviders, startOAuthLogin } = useAuth();
const [providers, setProviders] = useState<OAuthProvider[]>([]);

useEffect(() => {
void listOAuthProviders().then((result) => setProviders(result.providers));
}, [listOAuthProviders]);

async function signIn(providerId: string) {
const result = await startOAuthLogin({
providerId,
redirectUri: `${window.location.origin}/oauth/callback`,
returnTo: `${window.location.origin}/dashboard`,
});

window.location.assign(result.authorizationUrl);
}

return (
<div>
{providers.map((provider) => (
<button key={provider.id} onClick={() => void signIn(provider.id)}>
Continue with {provider.name}
</button>
))}
</div>
);
}
```

Create a callback route that reads the provider query params and asks Seamless Auth to complete the
login:

```tsx
import { useEffect } from 'react';
import { useAuth } from '@seamless-auth/react';

function OAuthCallback() {
const { finishOAuthLogin } = useAuth();

useEffect(() => {
const params = new URLSearchParams(window.location.search);
const providerId = 'google';
const code = params.get('code');
const state = params.get('state');

if (!code || !state) {
return;
}

void finishOAuthLogin({ providerId, code, state }).then(() => {
window.location.assign('/dashboard');
});
}, [finishOAuthLogin]);

return <p>Finishing sign-in...</p>;
}
```

For fully custom UI without `useAuth()`, call the headless client directly:

```ts
const providers = await authClient.listOAuthProviders();
const started = await authClient.startOAuthLogin({
providerId: providers.providers[0].id,
redirectUri: `${window.location.origin}/oauth/callback`,
});

window.location.assign(started.authorizationUrl);
```

OAuth must be enabled on the Seamless Auth API with `LOGIN_METHODS` including `oauth` and at least
one configured `oauth_providers` entry. Provider client secrets live on the server and are referenced
by environment variable name; they are never passed through this SDK.

## Headless Client

For custom auth UIs, use the exported client directly:
Expand Down Expand Up @@ -264,6 +356,7 @@ The headless client exposes helpers for:
- registration
- phone OTP and email OTP
- magic-link request, verify, and polling
- OAuth provider listing, start, and callback completion
- passkey registration
- step-up status and passkey verification
- logout and delete-user
Expand All @@ -276,6 +369,9 @@ Client methods return raw `Response` objects except for the passkey convenience
- `isPasskeyPrfSupported(): Promise<boolean>`
- `verifyStepUpWithPasskey(): Promise<StepUpVerificationResult>`
- `verifyStepUpWithPasskeyPrf(input): Promise<StepUpWithPasskeyPrfResult>`
- `listOAuthProviders(): Promise<OAuthProvidersResult>`
- `startOAuthLogin(input): Promise<StartOAuthLoginResult>`
- `finishOAuthLogin(input): Promise<Response>`

## React Hooks For Custom UI

Expand Down Expand Up @@ -348,6 +444,9 @@ The built-in flows assume compatible endpoints for:
- `/magic-link`
- `/magic-link/check`
- `/magic-link/verify/:token`
- `/oauth/providers`
- `/oauth/:providerId/start`
- `/oauth/:providerId/callback`
- `/step-up/status`
- `/step-up/webauthn/start`
- `/step-up/webauthn/finish`
Expand Down
25 changes: 25 additions & 0 deletions src/AuthProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

import {
createSeamlessAuthClient,
FinishOAuthLoginInput,
OAuthProvidersResult,
StartOAuthLoginInput,
StartOAuthLoginResult,
StepUpWithPasskeyPrfResult,
StepUpStatus,
StepUpVerificationResult,
Expand Down Expand Up @@ -40,6 +44,9 @@ export interface AuthContextType {
organizations: Organization[];
activeOrganization: Organization | null;
switchOrganization: (organizationId: string) => Promise<void>;
listOAuthProviders: () => Promise<OAuthProvidersResult>;
startOAuthLogin: (input: StartOAuthLoginInput) => Promise<StartOAuthLoginResult>;
finishOAuthLogin: (input: FinishOAuthLoginInput) => Promise<void>;
stepUpStatus: StepUpStatus | null;
updateCredential: (credential: Credential) => Promise<Credential>;
deleteCredential: (credentialId: string) => Promise<void>;
Expand Down Expand Up @@ -235,6 +242,21 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({
await validateToken();
};

const listOAuthProviders = () => authClient.listOAuthProviders();

const startOAuthLogin = (input: StartOAuthLoginInput) =>
authClient.startOAuthLogin(input);

const finishOAuthLogin = async (input: FinishOAuthLoginInput) => {
const response = await authClient.finishOAuthLogin(input);

if (!response.ok) {
throw new Error('Failed to finish OAuth login');
}

await validateToken();
};

const refreshStepUpStatus = useCallback(async () => {
const response = await authClient.getStepUpStatus();

Expand Down Expand Up @@ -311,6 +333,9 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({
organizations,
activeOrganization,
switchOrganization,
listOAuthProviders,
startOAuthLogin,
finishOAuthLogin,
stepUpStatus,
updateCredential,
deleteCredential,
Expand Down
73 changes: 72 additions & 1 deletion src/client/createSeamlessAuthClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export interface LoginInput {
passkeyAvailable: boolean;
}

export type LoginMethod = 'passkey' | 'magic_link' | 'email_otp' | 'phone_otp';
export type LoginMethod = 'passkey' | 'magic_link' | 'email_otp' | 'phone_otp' | 'oauth';

export interface LoginStartResult {
message?: string;
Expand Down Expand Up @@ -101,6 +101,34 @@ export interface OrganizationMembersResult {
total: number;
}

export interface OAuthProvider {
id: string;
name: string;
scopes: string[];
}

export interface OAuthProvidersResult {
providers: OAuthProvider[];
}

export interface StartOAuthLoginInput {
providerId: string;
redirectUri?: string;
returnTo?: string;
}

export interface StartOAuthLoginResult {
provider: OAuthProvider;
state: string;
authorizationUrl: string;
}

export interface FinishOAuthLoginInput {
providerId: string;
code: string;
state: string;
}

export interface PasskeyLoginResult {
success: boolean;
mfaRequired: boolean;
Expand Down Expand Up @@ -166,6 +194,9 @@ export interface SeamlessAuthClient {
requestMagicLink: () => Promise<Response>;
checkMagicLink: () => Promise<Response>;
verifyMagicLink: (token: string) => Promise<Response>;
listOAuthProviders: () => Promise<OAuthProvidersResult>;
startOAuthLogin: (input: StartOAuthLoginInput) => Promise<StartOAuthLoginResult>;
finishOAuthLogin: (input: FinishOAuthLoginInput) => Promise<Response>;
registerPasskey: (
input: PasskeyMetadata | RegisterPasskeyOptions
) => Promise<PasskeyRegistrationResult>;
Expand Down Expand Up @@ -444,6 +475,46 @@ export const createSeamlessAuthClient = (
},
}),

listOAuthProviders: async () => {
const response = await fetchWithAuth(`/oauth/providers`, {
method: 'GET',
});

if (!response.ok) {
throw new Error('Failed to list OAuth providers.');
}

return response.json();
},

startOAuthLogin: async input => {
const response = await fetchWithAuth(
`/oauth/${encodeURIComponent(input.providerId)}/start`,
{
method: 'POST',
body: JSON.stringify({
...(input.redirectUri ? { redirectUri: input.redirectUri } : {}),
...(input.returnTo ? { returnTo: input.returnTo } : {}),
}),
}
);

if (!response.ok) {
throw new Error('Failed to start OAuth login.');
}

return response.json();
},

finishOAuthLogin: input =>
fetchWithAuth(`/oauth/${encodeURIComponent(input.providerId)}/callback`, {
method: 'POST',
body: JSON.stringify({
code: input.code,
state: input.state,
}),
}),

registerPasskey: async input => {
const registerInput = normalizeRegisterPasskeyInput(input);
const challengeRes = await fetchWithAuth(buildRegisterStartPath(registerInput), {
Expand Down
10 changes: 10 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@ import {
createSeamlessAuthClient,
CreateOrganizationInput,
CurrentUserResult,
FinishOAuthLoginInput,
LoginInput,
LoginMethod,
LoginStartResult,
OAuthProvider,
OAuthProvidersResult,
OrganizationMemberInput,
OrganizationMemberUpdateInput,
OrganizationMembersResult,
Expand All @@ -26,6 +29,8 @@ import {
RegisterPasskeyOptions,
SeamlessAuthClient,
SeamlessAuthClientOptions,
StartOAuthLoginInput,
StartOAuthLoginResult,
StepUpMethod,
StepUpStatus,
StepUpWithPasskeyPrfResult,
Expand Down Expand Up @@ -61,9 +66,12 @@ export type {
Credential,
CreateOrganizationInput,
CurrentUserResult,
FinishOAuthLoginInput,
LoginInput,
LoginMethod,
LoginStartResult,
OAuthProvider,
OAuthProvidersResult,
Organization,
OrganizationMemberInput,
OrganizationMembership,
Expand All @@ -81,6 +89,8 @@ export type {
RegisterPasskeyOptions,
SeamlessAuthClient,
SeamlessAuthClientOptions,
StartOAuthLoginInput,
StartOAuthLoginResult,
StepUpMethod,
StepUpStatus,
StepUpWithPasskeyPrfResult,
Expand Down
Loading
Loading