Skip to Content
FeaturesCustomer Authentication

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

MethodDescriptionUse Case
PasswordEmail + passwordTraditional login
OTPOne-time password sent via emailPasswordless login
OAuth/OIDCExternal 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 from https://api.foir.dev/.well-known/jwks.json (issuer foir-customer) — never ship a verifier to the browser.
  • Refresh token: longer-lived (30 days), rotated on every use; exchange it via customerRefreshToken for a fresh access token.

In the Admin

Setting Up Auth Providers

  1. Go to Settings > Authentication
  2. View the list of configured auth providers
  3. 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
  4. 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.

FieldPurpose
displayNameHuman-readable project name shown in emails (“Bob’s Country Bunker”)
logoUrlURL of the project logo embedded in email templates. Defaults to app.foir.io/logo.png if unset.
primaryColorBrand colour applied to email buttons and accents (hex string, e.g. #2c4433)
fromNameDisplay name on the From header (From: Bob's Country Bunker <noreply@…>)
replyToAddress customers reach when replying to platform emails
supportEmailAddress shown to customers in error states (“Contact support@…”)
appBaseUrlPublic 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.
customerWelcomeEmailEnabledToggle: 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 the currentUser query to validate sessions.
  • Set appropriate token lifetimes — balance security with user convenience.
Last updated on