# Authentication

Volcano provides a complete authentication system with multiple sign-in methods, session management, and security features.

Volcano provides a complete authentication system with multiple sign-in methods, session management, and security features. This guide covers everything you need to implement user authentication in your application.

## Overview

The SDK supports several authentication methods:

- **Email/Password** - Traditional sign-up and sign-in with email verification
- **OAuth/SSO** - Sign in with Google, GitHub, Microsoft, or Apple
- **Anonymous Users** - Let users explore your app before creating an account
- **Magic Links** - Passwordless authentication via email (coming soon)

All authentication methods work with the same session system and integrate seamlessly with Row-Level Security.

## Email/Password Authentication

### Sign Up

Create a new user account with email and password:

```javascript
const { confirmationRequired, message, error } = await volcano.auth.signUp({
  email: 'alice@example.com',
  password: 'secure-password-123',
  metadata: {
    full_name: 'Alice Smith',
    avatar_url: 'https://example.com/alice.jpg',
  },
});

if (error) {
  // Handle specific error cases
  if (error.message.includes('already exists')) {
    console.error('An account with this email already exists');
  } else if (error.message.includes('weak password')) {
    console.error('Please choose a stronger password');
  } else {
    console.error('Sign up failed:', error.message);
  }
  return;
}

// Sign up is session-less: it returns no user and no session, and the response is
// deliberately identical whether or not the email was already registered. The
// account is created; obtain a session with a separate `signIn`.
if (confirmationRequired) {
  console.log(message ?? 'Check your email to confirm your account, then sign in.');
} else {
  await volcano.auth.signIn({ email: 'alice@example.com', password: 'secure-password-123' });
}
```

The `metadata` field is optional and lets you store additional user information like display names, profile pictures, or preferences. This data is stored securely and accessible via `user.user_metadata` once the user is signed in.

If your project does not require email confirmation and you want the user signed in immediately, pass `signInWhenAllowed: true`. When confirmation is not required, the SDK then performs the follow-up `signIn` for you and the response carries a live `user`/`session`:

```javascript
const { user, session, error } = await volcano.auth.signUp({
  email: 'alice@example.com',
  password: 'secure-password-123',
  signInWhenAllowed: true,
});
```

### Sign In

Authenticate an existing user:

```javascript
const { user, session, error } = await volcano.auth.signIn({
  email: 'alice@example.com',
  password: 'secure-password-123',
});

if (error) {
  if (error.message.includes('Invalid credentials')) {
    console.error('Incorrect email or password');
  } else {
    console.error('Sign in failed:', error.message);
  }
  return;
}

console.log('Signed in as', user.email);
```

After successful sign-in, the SDK automatically:

1. Stores the access token and any returned refresh token in localStorage (browser) or memory (server)
2. Refreshes the access token when a refresh token is available
3. Makes the user available via `volcano.auth.user()`

Profile operations (`getUser`, `updateUser`, `convertAnonymous`, and `confirmEmailChange`) refresh a rejected access token once when the session has a usable refresh token, then replay the original request values.
They do not retry other HTTP failures or ambiguous network failures, and they do not retry under a replacement session.
Authenticated mutations capture the session before reading or serializing your options and metadata.
If a getter or `toJSON()` replaces the session, the operation returns `AuthSessionChangedError` before sending the request.

### Sign Out

End the current session:

```javascript
const { error } = await volcano.auth.signOut();

if (!error) {
  console.log('Signed out successfully');
}
```

Sign-out revokes the captured session and clears local storage. For credentials received together
from a successful sign-in or validated refresh, it uses the refresh token directly, even if the
access token has expired. For supplied credentials, it revokes the access-token session.
On HTTP 401, that path can refresh once and revoke the same session without adopting the renewed credentials locally.
Calling `signOut()` without a current session succeeds without a request. If token revocation fails,
the SDK still clears the captured local session and returns the error so the application can report
it. The cleared session no longer contains credentials needed to retry revocation.

Sign-out waits for an already-running refresh and uses its validated credentials for revocation.
Later refresh attempts for that session return `AuthRefreshDiscardedError`; they do not send a request.
Concurrent sign-out calls share the same revocation result. A separate sign-in or explicit
session adoption remains current and sign-out returns `AuthSessionChangedError`.

## Session Management

### Read the Current Session

Read a snapshot of the session already held by the client:

```javascript
const {
  data: { session },
  error,
} = await volcano.auth.getSession();

if (!error && session) {
  console.log(session.user?.id);
}
```

`getSession()` reads local SDK state. It does not refresh or validate the access token; use
`getUser()` when server validation is required.

### Start with a Supplied Access Token

Create a separate client for each server request that carries a user's access token:

```javascript
import { VolcanoClient } from '@volcano.dev/sdk';

export async function loadRequestUser(accessToken) {
  if (typeof accessToken !== 'string' || !accessToken.trim()) {
    throw new Error('An access token from the current request is required');
  }
  const client = new VolcanoClient({
    anonKey: process.env.VOLCANO_ANON_KEY,
    accessToken,
  });
  const { user, error } = await client.auth.getUser();
  if (error) throw error;
  return user;
}
```

Call this helper from your request handler with the bearer token from that request.
For a Volcano function, use the access token in `event.__volcano_auth.access_token` supplied for that invocation.
The helper validates the token with Volcano before returning the user.

Refresh must preserve the server session identified by the access JWT, even before a profile is loaded. A different session is rejected, including another session for the same user. Supplied credentials need a readable session identifier to refresh, even when you provide a user profile or load it from the server. Profile data does not prove that access and refresh tokens belong together.
Once a user identity has been validated, a refresh response for another user is also rejected.
Construction makes no request and does not persist the supplied credentials.
`getSession()` initially returns the access token with `refresh_token: null` and `user: null`.
A successful `getUser()` caches the server-validated profile without changing the token.
Without a refresh token, an HTTP 401 remains an authentication error, `refreshSession()` returns an error, and `signOut()` revokes the server session identified by the access token before clearing local state.
Pass `refreshToken` alongside `accessToken` when the client should refresh that session.
`setSession()` still requires a complete session.

### Adopt an Existing Session

Copy a complete session into another client:

```javascript
const {
  data: { session: sourceSession },
} = await source.auth.getSession();

if (sourceSession?.refresh_token && sourceSession.user) {
  const { data, error } = await target.auth.setSession({
    ...sourceSession,
    refresh_token: sourceSession.refresh_token,
    user: sourceSession.user,
  });

  if (!error) {
    console.log(data.session.user.id);
  }
}
```

`setSession()` validates and copies the supplied session into the target client's memory. It does
not contact Volcano, persist tokens, or notify auth listeners. Call it again after creating a new
client or reloading the page.

### Check Current User

Get the current user synchronously (returns cached data):

```javascript
const user = volcano.auth.user();

if (user) {
  console.log('Logged in as:', user.email);
  console.log('User ID:', user.id);
  console.log('Custom data:', user.user_metadata);
} else {
  console.log('Not logged in');
}
```

### Fetch Fresh User Data

Get the latest user data from the server:

```javascript
const { user, error } = await volcano.auth.getUser();

if (user) {
  console.log('User data refreshed');
  console.log('Email:', user.email);
  console.log('Created:', user.created_at);
}
```

This is useful when you need to ensure the user data is current, such as after updating their profile elsewhere.

### Restore Session on Page Load

When your application starts, restore any existing session:

```javascript
// In your app initialization
const { user, error } = await volcano.initialize();

if (user) {
  console.log('Session restored for', user.email);
  // User is authenticated, show dashboard
} else {
  // No session, show login page
}
```

The client also completes browser redirect hand-offs automatically. Managed email/password pages can return a session fragment, while OAuth providers return a short-lived authorization code that the SDK exchanges before an authenticated call proceeds.

### Listen for Auth State Changes

React to authentication events in real-time:

```javascript
const unsubscribe = volcano.auth.onAuthStateChange((user) => {
  if (user) {
    // User signed in or session restored
    updateUI({ isLoggedIn: true, user });
  } else {
    // User signed out
    updateUI({ isLoggedIn: false, user: null });
  }
});

// Clean up when your component unmounts
// unsubscribe();
```

This callback fires immediately with the current state, then again whenever the auth state changes.

### Manual Token Refresh

Refresh the current session with its refresh token:

```javascript
const { session, error } = await volcano.auth.refreshSession();

if (error) {
  console.error('Refresh failed:', error.message);
} else {
  console.log('Token refreshed, expires in', session.expires_in, 'seconds');
}
```

On success, `refreshSession()` returns the refreshed token fields in the existing
`{ session, error }` response and replaces the client's current session. Read the full snapshot,
including its user, with `getSession()`.

If Volcano rejects the refresh token with `401` or `403`, the SDK clears that session. A transport
error or server failure leaves the current session unchanged so the application can retry. A late
refresh response never replaces a newer session.

Authenticated profile, session-list/deletion, email-change request/cancellation,
linked-provider, provider-token, and provider-API operations refresh a usable
session once after HTTP 401 and replay the original request values. They preserve
an explicitly replaced session and do not retry other HTTP failures or ambiguous
network failures. Deleting the current server session also clears its refreshed
local credentials; a separately adopted session remains current.

## Hosted Auth Pages (Managed Login)

Instead of building your own login UI, you can redirect users to Volcano's hosted (managed) login and sign-up pages. **Always start the flow with `signInWithHostedAuth()`** (or `getHostedAuthUrl()`): it stores a one-time nonce and passes it as `state`, which the SDK validates when the session comes back. This binds the returned session to the flow you started and prevents an attacker from tricking a user into adopting an attacker-controlled session (login CSRF / session fixation).

```javascript
// On your login button/page:
volcano.auth.signInWithHostedAuth(); // or { action: 'signup' }
// You can also get the URL without navigating:
//   const url = volcano.auth.getHostedAuthUrl();
//   window.location.assign(url);
```

Both methods also accept `{ projectId }`, which takes precedence over the anon
key's project unless it is blank. The project ID is trimmed and percent-encoded as
one URL path segment, and both methods throw when it is `.` or `..`, contains
`/`, or has a lone UTF-16 surrogate.

After a successful login or sign-up, Volcano redirects the user back to your configured `post_auth_redirect_url` with the session in the URL fragment:

```text
https://your-app.com/callback#access_token=...&refresh_token=...&token_type=bearer&expires_in=...&state=<nonce>
```

The SDK handles this hand-off for you. On your redirect page, just create your client — the session's `state` is validated against the stored nonce, then it's detected, stored, and stripped from the URL automatically. You never parse the fragment yourself:

```javascript
// On your post-auth redirect page (e.g. /callback)
const volcano = new VolcanoAuth({
  apiUrl: 'https://api.example.com',
  anonKey: 'ak-your-anon-key',
});

// The redirect session was already adopted when the client was created,
// so the user is authenticated immediately.
const { user, error } = await volcano.auth.getUser();
if (error) throw error;
console.log('Signed in as', user.email);
```

> Security: a fragment whose `state` doesn't match a nonce this client stored (e.g. an unsolicited `#access_token=...` link, or a flow not started via the SDK) is **rejected** and scrubbed from the URL — it will not authenticate the user.

Because the session is adopted at construction (the same as a session restored from storage), **you don't have to call `getUser()` before making authenticated requests** — any authenticated call works right away:

```javascript
const volcano = new VolcanoAuth({ apiUrl, anonKey }); // session adopted from URL

// Authenticated immediately — no getUser() required first.
const { user } = await volcano.auth.updateUser({ metadata: { onboarded: true } });
const { data } = await volcano.database('app-db').from('orders').select('*');
```

Notes:

- This is browser-only (it reads `window.location.hash`).
- The tokens are removed from the URL automatically so they don't linger in browser history or get bookmarked.
- A normal app fragment (for example `#/dashboard`) is ignored and left untouched.
- `getUser()` and `initialize()` also re-check the URL, so they still work if the client was created before the redirect fragment was present (e.g. in a single-page app).

## OAuth/SSO Authentication

Sign users in with their existing accounts from popular providers.

### Available Providers

- `google` - Google accounts
- `github` - GitHub accounts
- `microsoft` - Microsoft/Azure AD accounts
- `apple` - Apple ID

### Sign In with OAuth

Each provider has a convenience method:

```javascript
// Sign in with Google
volcano.auth.signInWithGoogle();

// Sign in with GitHub
volcano.auth.signInWithGitHub();

// Sign in with Microsoft
volcano.auth.signInWithMicrosoft();

// Sign in with Apple
volcano.auth.signInWithApple();

// Or use the generic method with any provider
volcano.auth.signInWithOAuth('google');
```

These methods redirect the user to the provider's login page. After successful authentication, they're redirected back to your application with an active session.

**Note:** OAuth sign-in only works in browser environments. Calling these methods on the server will throw an error. See the [Next.js guide](https://docs.volcano.dev/sdk/js/nextjs.md) for server-side OAuth handling.

### Handle OAuth Callback

The SDK requests the platform's authorization-code callback mode. After the OAuth redirect, the user returns with a short-lived, single-use `code` and the flow's `state` nonce. The SDK validates the nonce, removes both values from the URL, and exchanges the code for a session before `initialize()`, `getUser()`, or another authenticated operation proceeds. In this mode, access and refresh tokens are never placed in the OAuth callback URL. Platform deployments retain the established session-fragment response for older clients that do not request authorization-code mode.

When the exchange uses an eligible HttpOnly cookie session, its response can omit the refresh token. The SDK keeps the access token, clears any stale stored refresh token, and reports `refresh_token: null` from `getSession()`.

By default the user returns to the page that called `signInWithOAuth()`; pass `{ redirectTo }` to override:

```javascript
// On your callback page or app initialization
const { user, error } = await volcano.initialize();

if (user) {
  console.log('OAuth sign-in successful:', user.email);
}
```

The callback URL must exactly match one of the project's registered redirect URLs, including its query string. Callback URLs cannot use the reserved OAuth response query parameters `code`, `state`, `error`, `error_description`, `error_uri`, `iss`, or `vh_state`.

### Link OAuth Provider to Existing Account

Let users add OAuth sign-in to an existing email/password account:

```javascript
// User must be signed in first
const { data, error } = await volcano.auth.linkOAuthProvider('google');

if (error) {
  console.error('Failed to link provider:', error.message);
  return;
}

if (!data?.authorization_url) {
  throw new Error('The provider did not return an authorization URL');
}

// Redirect user to complete linking
window.location.href = data.authorization_url;
```

The result is discarded with `AuthSessionChangedError` if the active session changes while the
request is in flight.

### Unlink OAuth Provider

Remove an OAuth provider from an account:

```javascript
const { error } = await volcano.auth.unlinkOAuthProvider('google');

if (!error) {
  console.log('Google account unlinked');
}
```

The result is discarded with `AuthSessionChangedError` if the active session changes while the
request is in flight.

### List Linked Providers

See which OAuth providers are connected to the current account:

```javascript
const { providers, error } = await volcano.auth.getLinkedOAuthProviders();

if (providers) {
  providers.forEach((p) => {
    console.log(`${p.provider} linked on ${p.linked_at}`);
  });
}
```

The result is discarded with `AuthSessionChangedError` if the active session changes while the
request is in flight.

### Check Provider Token Status

Check whether Volcano has a valid server-held provider token:

```javascript
const { provider, expiresIn, error } = await volcano.auth.getOAuthProviderToken('github');
```

This method returns provider and expiry metadata, not the credential. Volcano refreshes an expired
token on the server. A stale result returns `AuthSessionChangedError`.

Refresh a provider token explicitly when the application needs a new validity window:

```javascript
const { provider, expiresIn, error } = await volcano.auth.refreshOAuthToken('github');
```

The refresh credential and new access token remain on the server. A stale result returns
`AuthSessionChangedError`.

### Access Provider APIs

After OAuth sign-in, you can make authenticated requests to the provider's API:

```javascript
// Get the user's GitHub repositories
const { data, error } = await volcano.auth.callOAuthAPI('github', {
  endpoint: '/user/repos',
  method: 'GET',
});

if (error) {
  console.error('Provider request failed:', error);
} else if (Array.isArray(data)) {
  data.forEach((repo) => {
    console.log(repo.full_name);
  });
}
```

Volcano automatically handles token refresh and passes the correct credentials to the provider.
`data` is the provider's raw JSON value, or `null` when the provider returns no body.
Provider bodies are limited to 8 MiB after decompression. Transport failures,
invalid JSON, and oversized responses produce a Volcano 502 error.

Check `error` for Volcano request failures. The current SDK does not expose the
provider's HTTP status: a valid JSON response from a provider, including a 4xx or
5xx response, is returned as `data` with `error: null`. Inspect the provider's
documented response shape before treating the operation as successful.
The response is discarded with `AuthSessionChangedError` if the active session changes while the
request is in flight.

## Anonymous Users

Let users explore your app without creating an account, then convert them to full users later.

### Create Anonymous User

```javascript
const { user, session, error } = await volcano.auth.signInAnonymously({
  preferred_theme: 'dark',
});

if (user) {
  console.log('Anonymous user created:', user.id);
  // User can now use the app with limited features
}
```

Anonymous users get a unique ID and can store data, but they don't have an email address or password.
They cannot recover the account after signing out unless they first convert it to a full account.
`signUpAnonymous()` remains available as a compatibility alias.

### Convert to Full Account

When an anonymous user is ready to create a real account:

```javascript
const { user, error } = await volcano.auth.convertAnonymous({
  email: 'alice@example.com',
  password: 'secure-password-123',
  metadata: {
    full_name: 'Alice Smith',
  },
});

if (user) {
  console.log('Account converted! Welcome,', user.email);
  // All their data is preserved with the same user ID
}
```

The conversion preserves the user's ID and all associated data.

## Email Verification

If your project requires email verification, users receive a confirmation email after signing up.

### Confirm Email

When the user clicks the confirmation link, extract the token and confirm:

```javascript
// Token comes from the URL query parameter
const token = new URLSearchParams(window.location.search).get('token');

const { message, error } = await volcano.auth.confirmEmail(token);

if (error) {
  console.error('Confirmation failed:', error.message);
} else {
  console.log('Email confirmed!');
}
```

Confirmation does not sign in the confirmed account or change an unrelated local session.

### Resend Confirmation Email

If the user didn't receive the email:

```javascript
const { message, error } = await volcano.auth.resendConfirmation('alice@example.com');

if (!error) {
  console.log('Confirmation email sent');
}
```

The response does not reveal whether the account exists or is already confirmed. Delivery occurs
only for an existing unconfirmed account when transactional email is configured.

## Password Recovery

### Request Password Reset

Request a password reset email. Volcano sends it when transactional email is configured:

```javascript
const { message, error } = await volcano.auth.resetPasswordForEmail('alice@example.com');

if (!error) {
  console.log('Password reset email sent');
}
```

For security, this always succeeds even if the email doesn't exist in your system.
`forgotPassword()` remains available as a deprecated compatibility alias.

### Reset Password

When the user clicks the reset link, extract the token and set a new password:

```javascript
const token = new URLSearchParams(window.location.search).get('token');

const { message, error } = await volcano.auth.resetPassword({
  token,
  newPassword: 'new-secure-password-456',
});

if (error) {
  console.error('Password reset failed:', error.message);
} else {
  console.log('Password updated successfully');
}
```

Resetting the password revokes the recovered account's existing sessions and does not sign it in.
The client keeps any unrelated local session unchanged; sign in with the new password when the reset
flow completes.

## Email Change

### Request Email Change

Allow users to change their email address:

```javascript
const { message, newEmail, error } = await volcano.auth.requestEmailChange('newemail@example.com');

if (!error) {
  console.log('Confirmation email sent to', newEmail);
}
```

If another authentication operation replaces the active session before a successful acknowledgement
arrives, the method returns an `AuthSessionChangedError` and leaves the newer session untouched. The
server may still have started the email change for the original account, so handle this result
separately from a rejected request.

### Confirm Email Change

After the user confirms via email:

```javascript
const token = new URLSearchParams(window.location.search).get('token');

const { user, error } = await volcano.auth.confirmEmailChange(token);

if (user) {
  console.log('Email updated to', user.email);
}
```

The returned user reflects the confirmed address. If another authentication operation replaces the
active session before a successful confirmation arrives, the method returns an
`AuthSessionChangedError` and leaves the newer session's user untouched.

### Cancel Email Change

If the user changes their mind:

```javascript
const { message, error } = await volcano.auth.cancelEmailChange();

if (!error) {
  console.log('Email change cancelled');
}
```

If another authentication operation replaces the active session before a successful cancellation
arrives, the method returns an `AuthSessionChangedError` and leaves the newer session untouched. The
server may still have cancelled the pending change for the original account.

## Update User Profile

Modify the current user's password or metadata:

```javascript
const { user, error } = await volcano.auth.updateUser({
  password: 'new-password-789', // Optional
  metadata: {
    full_name: 'Alice Johnson',
    avatar_url: 'https://example.com/alice-new.jpg',
    notification_preferences: {
      email: true,
      push: false,
    },
  },
});

if (user) {
  console.log('Profile updated');
}
```

## Multi-Device Session Management

Users can manage their active sessions across devices.

### List Sessions

Get all active sessions for the current user:

```javascript
const { sessions, total, error } = await volcano.auth.getSessions({
  page: 1,
  limit: 20,
});

if (sessions) {
  sessions.forEach((session) => {
    console.log('---');
    console.log('Device:', session.user_agent);
    console.log('IP:', session.ip_address);
    console.log('Last active:', session.last_activity_at);
    console.log('Current session:', session.is_current);
  });
}
```

`getSessions()` uses offset pagination and defaults to page `1` with `20` sessions per page.
The response is rejected with `AuthSessionChangedError` if another authentication operation
replaces the local session while the request is in flight.

### Revoke a Session

Sign out from a specific device:

```javascript
const { error } = await volcano.auth.deleteSession(sessionId);

if (!error) {
  console.log('Session revoked');
}
```

The request uses the current access token. Deleting that token's own session clears local
credentials, including when the request outcome is uncertain; deleting another session preserves
them. If another authentication operation replaces the session before deletion finishes, the method
returns an `AuthSessionChangedError` instead of clearing the replacement or acknowledging a stale
result.

The session ID is percent-encoded as one URL path segment. An ID that is blank, is `.` or `..`,
contains `/`, or has a lone UTF-16 surrogate returns an error without a request. Numbers, bigints and
booleans are sent as strings; other non-string IDs are refused. An empty or `.` ID would address
`/auth/user/sessions/`, the route for deleting every other session, because URL parsers resolve
`/auth/user/sessions/.` to that path.

### Sign Out All Other Devices

Keep only the current session active:

```javascript
const { error } = await volcano.auth.deleteAllOtherSessions();

if (!error) {
  console.log('All other sessions have been signed out');
}
```

The session whose access token authorizes the request remains active. Do not replace the client's
session while this request is in flight: the server may revoke that replacement as an "other"
session. If replacement occurs, the method returns an `AuthSessionChangedError` instead of
acknowledging a stale result.

## Security Best Practices

### Protect Service Keys

The SDK prevents service keys (starting with `sk-`) from being used in browser environments. Service keys bypass Row-Level Security and should only be used in secure server-side code:

```javascript
// This will throw an error in the browser:
const volcano = new VolcanoAuth({
  apiUrl: 'https://api.example.com',
  anonKey: 'sk-service-key-here', // ERROR in browser!
});
```

### Use HTTPS

Always use HTTPS URLs for your API endpoint to protect tokens in transit.

### Secure Password Requirements

Enforce strong passwords in your UI before calling the SDK:

```javascript
function validatePassword(password) {
  if (password.length < 8) {
    return 'Password must be at least 8 characters';
  }
  if (!/[A-Z]/.test(password)) {
    return 'Password must contain an uppercase letter';
  }
  if (!/[0-9]/.test(password)) {
    return 'Password must contain a number';
  }
  return null;
}

const passwordError = validatePassword(password);
if (passwordError) {
  showError(passwordError);
  return;
}

// Password is valid, proceed with sign up
await volcano.auth.signUp({ email, password });
```

### Handle Token Expiration

If an authenticated request receives HTTP 401 and cannot refresh successfully, its error retains `status: 401` and any server `code` and `retryAfter` metadata. The compatible message remains `Session expired`. A network failure has no HTTP status.

Access tokens expire after a configured time (default: 1 hour). The SDK handles refresh automatically, but you should handle the case where refresh fails:

```javascript
volcano.auth.onAuthStateChange((user) => {
  if (!user) {
    // Session expired or user signed out
    redirectToLogin();
  }
});
```

## Next Steps

- [Database](https://docs.volcano.dev/sdk/js/database.md) - Query your PostgreSQL database with Row-Level Security
- [OAuth APIs](https://docs.volcano.dev/sdk/js/authentication.md#access-provider-apis) - Use OAuth tokens to access provider APIs
- [Next.js](https://docs.volcano.dev/sdk/js/nextjs.md) - Handle authentication in server components and middleware
