Customer Authentication
Customer authentication provides login, registration, and identity management for your end users. Foir supports password-based login, one-time passwords (OTP), and OAuth/OIDC providers.
Overview
Authentication Methods
| Method | Description | Use Case |
|---|---|---|
| Password | Email + password | Traditional login |
| OTP | One-time password sent via email | Passwordless login |
| OAuth/OIDC | External identity providers (Google, Auth0, etc.) | Social login, SSO |
All methods return an access token and a refresh token on success. The access token is sent in the Authorization header to identify the customer.
For the hosted Login with Foir flow — where Foir hosts the login UI so you don’t rebuild it and your app gets back the same EdDSA customer tokens — see the Login with Foir guide.
Tokens
- Access token: a short-lived (15-minute) EdDSA JWT sent as
Authorization: Bearer …. To verify it server-side, fetch the public keys fromhttps://api.foir.dev/.well-known/jwks.json(issuerfoir-customer) — never ship a verifier to the browser. - Refresh token: longer-lived (30 days), rotated on every use; exchange it via
customerRefreshTokenfor a fresh access token.
In the Admin
Setting Up Auth Providers
- Go to Settings > Authentication
- View the list of configured auth providers
- Click Add Provider to configure a new one:
- Key: unique identifier (e.g.,
google) - Name: display name (e.g., “Google Login”)
- Type: OAuth, OIDC, or other supported type
- Client ID / Secret: credentials from the provider
- Redirect URL: the callback URL for OAuth flows
- Enabled: toggle to activate or deactivate
- Priority: ordering for display in login UIs
- Key: unique identifier (e.g.,
- Save
Token signing
Token signing keys and lifetimes are managed by Foir — access tokens are EdDSA, 15 minutes; refresh tokens are 30 days and rotate on use. There’s nothing to configure here. What you do control per project is which login methods are enabled (password, OTP, signup) and your auth providers, under Settings > Authentication.
Via the CLI
# List configured auth providers
foir auth-providers list
# Get details for a specific provider
foir auth-providers get <id>
# Create a new auth provider.
# --key, --name and --type are required; valid types are
# OAUTH2, OIDC, SAML, TOKEN_SSO, OTP_VERIFY, EXTERNAL.
# -d/--data carries the provider config only (NOT key/name/type/enabled).
foir auth-providers create \
--key google \
--name "Google Login" \
--type OIDC \
--enabled \
--data '{
"clientId": "your-client-id",
"clientSecret": "your-client-secret",
"redirectUrl": "https://yoursite.com/auth/callback"
}'
# Update an auth provider's config
foir auth-providers update <id> --data '{ "redirectUrl": "https://yoursite.com/auth/callback" }'
# Disable a provider
foir auth-providers update <id> --no-enabled
# Delete an auth provider
foir auth-providers delete <id>Via the API
Standard Login (Password)
Every token-issuing mutation returns the same CustomerAuthResult { token, refreshToken, customerId, email } (the token is a short-lived EdDSA access token you send as a Bearer).
mutation CustomerLogin($email: String!, $password: String!) {
customerLogin(email: $email, password: $password) {
token
refreshToken
customerId
email
}
}Registration
mutation CustomerRegister($email: String!, $password: String!) {
customerRegister(email: $email, password: $password) {
token
refreshToken
customerId
email
}
}Logout
Returns Boolean (invalidates the refresh-token family server-side):
mutation CustomerLogout {
customerLogout
}OTP Authentication
Request a one-time password (returns Boolean), then use it to log in:
mutation RequestOTP($email: String!) {
customerRequestOTP(email: $email)
}mutation LoginWithOTP($email: String!, $otp: String!) {
customerLoginOTP(email: $email, otp: $otp) {
token
refreshToken
customerId
email
}
}Password Reset
Request a reset (returns Boolean):
mutation RequestReset($email: String!) {
customerRequestPasswordReset(email: $email)
}Reset with the emailed token (returns a CustomerAuthResult — the customer is logged in):
mutation ResetPassword($token: String!, $newPassword: String!) {
customerResetPassword(token: $token, newPassword: $newPassword) {
token
refreshToken
customerId
email
}
}Update Password (Authenticated)
Returns Boolean:
mutation UpdatePassword($currentPassword: String!, $newPassword: String!) {
customerUpdatePassword(
currentPassword: $currentPassword
newPassword: $newPassword
)
}Token Refresh
Refresh tokens rotate on every use — persist the new one before responding to the user:
mutation RefreshToken($refreshToken: String!) {
customerRefreshToken(refreshToken: $refreshToken) {
token
refreshToken
customerId
email
}
}Current User
query CurrentUser {
currentUser {
id
email
status
emailVerifiedAt
createdAt
}
}Auth Providers
List available providers for your login UI:
query ListProviders {
authProviders {
id
key
name
type
enabled
isDefault
priority
}
}OAuth Callback
After the user completes the OAuth flow, exchange the code:
mutation ProviderCallback($code: String!, $state: String!) {
customerProviderCallback(input: {
providerKey: "google"
code: $code
state: $state
}) {
token
refreshToken
customerId
email
}
}Auth Configuration
Query the project’s auth settings to build dynamic login forms:
query AuthConfig {
authConfig {
passwordEnabled
otpEnabled
providers {
key
name
type
}
}
}Complete Login Flow Example
// 1. Check auth config to know which methods are available
const { data: config } = await client.query({ query: AUTH_CONFIG });
// 2. Login with email and password
const { data: login } = await client.mutate({
mutation: CUSTOMER_LOGIN,
variables: { email, password }
});
const session = login.customerLogin;
if (session) {
// 3. Store tokens (prefer an httpOnly cookie set from your server over localStorage)
localStorage.setItem('accessToken', session.token);
localStorage.setItem('refreshToken', session.refreshToken);
// 4. Use the access token for authenticated requests
const { data: user } = await client.query({
query: CURRENT_USER,
context: {
headers: {
Authorization: `Bearer ${session.token}`
}
}
});
}Using Access Tokens
Include the access token in subsequent requests:
curl -X POST https://api.foir.dev/graphql \
-H "Content-Type: application/json" \
-H "x-api-key: pk_..." \
-H "Authorization: Bearer eyJhbGc..." \
-d '{"query": "{ currentUser { id email } }"}'Config System
Auth providers can also be defined in foir.config.ts using defineAuthProvider. See the Configuration reference for details.
Project Email & Brand
Every project has a brand block that controls how customer-facing emails (welcome, password reset, OTP, email verification) look and where they link. These are project-level settings, not per-user.
| Field | Purpose |
|---|---|
displayName | Human-readable project name shown in emails (“Bob’s Country Bunker”) |
logoUrl | URL of the project logo embedded in email templates. Defaults to app.foir.io/logo.png if unset. |
primaryColor | Brand colour applied to email buttons and accents (hex string, e.g. #2c4433) |
fromName | Display name on the From header (From: Bob's Country Bunker <noreply@…>) |
replyTo | Address customers reach when replying to platform emails |
supportEmail | Address shown to customers in error states (“Contact support@…”) |
appBaseUrl | Public base URL of your storefront/portal — used as the base for links in emails ({appBaseUrl}/reset-password?token=…). Required for password reset, OTP, and welcome emails to work. |
customerWelcomeEmailEnabled | Toggle: send a welcome email on customerRegister. On by default. |
Configuring brand fields
In the admin: Settings → Project → Brand. Fill in display name, logo URL, primary colour, support address, and your customer portal URL.
If appBaseUrl is unset, password-reset emails fail at send time with a clear “app base URL is not configured” error — they can’t generate the reset link without it. Set it before turning on registration flows.
Welcome emails
When customerWelcomeEmailEnabled is true, the platform sends a welcome email with the project’s branding to every new customer registered via customerRegister. Toggle it off if your application sends its own welcome email and you don’t want a duplicate.
Email service
Foir sends all customer-facing emails (welcome, password reset, OTP, email verification) through a single outbound pipeline. Templates are project-aware — every send pulls the project’s brand block to render the template. If you maintain multiple projects, each project’s emails use its own brand independently.
Best Practices
- Always handle token refresh — implement automatic refresh when access tokens expire.
- Query auth config first — build login UIs dynamically based on the enabled methods (
passwordEnabled,otpEnabled,providers). - Use OTP for passwordless flows — simpler for users and avoids password management.
- Verify tokens server-side — validate the EdDSA access token against the JWKS endpoint (
https://api.foir.dev/.well-known/jwks.json) or use thecurrentUserquery to validate sessions. - Set appropriate token lifetimes — balance security with user convenience.