SSO & Social Authentication
This guide covers implementing Single Sign-On (SSO) and social authentication providers in Baasix. Baasix supports 35 OAuth 2.0 providers out of the box, with full better-auth provider parity.
Table of Contents
- Overview
- Supported Providers
- Environment Configuration
- Provider Setup Guides
- API Endpoints
- Client Integration
- Advanced Configuration
- Security Best Practices
- Troubleshooting
Overview
Baasix implements OAuth 2.0 with PKCE (Proof Key for Code Exchange) for secure social authentication. There are two ways to drive the flow:
- Browser redirect flow (recommended) — a plain
GETlink/redirect to/auth/signin/:provider. The server handles the provider redirect and the callback, then 302s back to your app with the token in the query string. No JSON request needed to start the flow. - Programmatic flow (legacy/advanced) —
POST /auth/social/signinreturns a JSON payload with the provider URL, which your client then navigates to. Useful when you need to control the request (e.g. custom scopes from a mobile WebView) before redirecting.
Typical flow (browser redirect):
- Client links/redirects the browser to
GET /auth/signin/:provider?redirect_url=... - Baasix redirects to the provider's login page
- After successful login, the provider redirects back to the Baasix callback URL
- Baasix exchanges the authorization code for tokens, fetches the profile, and creates/links the account
- Baasix 302s back to your
redirect_urlwith?token=<jwt>(or?error=<message>on failure)
Key Features
- 35 Providers: Full better-auth provider parity — see Supported Providers
- Automatic Account Linking: Link social accounts to existing users by email
- PKCE Support: Enhanced security for OAuth flows
- Cookie or JWT Mode: Flexible token delivery methods
- Multi-tenant Support: Social auth works with multi-tenant configurations
- Discovery-driven: Clients can fetch the enabled provider list at runtime via
GET /— no need to hardcode which providers are active
Supported Providers
All 35 providers share the same <PROVIDERID>_CLIENT_ID / <PROVIDERID>_CLIENT_SECRET convention (uppercased provider id). Providers marked "Extra config" require additional environment variables beyond the standard client id/secret pair — see Environment Configuration.
| Provider | Provider ID | Env Prefix | Extra config |
|---|---|---|---|
| Apple | apple | APPLE_ | Yes — APPLE_TEAM_ID, APPLE_KEY_ID, APPLE_PRIVATE_KEY |
| Atlassian | atlassian | ATLASSIAN_ | - |
| AWS Cognito | cognito | COGNITO_ | Yes — COGNITO_DOMAIN, COGNITO_REGION |
| Discord | discord | DISCORD_ | - |
| Dropbox | dropbox | DROPBOX_ | - |
facebook | FACEBOOK_ | - | |
| Figma | figma | FIGMA_ | - |
| GitHub | github | GITHUB_ | - |
| GitLab | gitlab | GITLAB_ | - |
google | GOOGLE_ | - | |
| Hugging Face | huggingface | HUGGINGFACE_ | - |
| Kakao | kakao | KAKAO_ | - |
| Kick | kick | KICK_ | - |
| LINE | line | LINE_ | - |
| Linear | linear | LINEAR_ | - |
linkedin | LINKEDIN_ | - | |
| Microsoft (Entra ID) | microsoft | MICROSOFT_ | Yes — MICROSOFT_TENANT_ID (default common) |
| Naver | naver | NAVER_ | - |
| Notion | notion | NOTION_ | - |
| Paybin | paybin | PAYBIN_ | - |
| PayPal | paypal | PAYPAL_ | - |
| Polar | polar | POLAR_ | - |
| Railway | railway | RAILWAY_ | - |
reddit | REDDIT_ | - | |
| Roblox | roblox | ROBLOX_ | - |
| Salesforce | salesforce | SALESFORCE_ | - |
| Slack | slack | SLACK_ | - |
| Spotify | spotify | SPOTIFY_ | - |
| TikTok | tiktok | TIKTOK_ | Yes — TIKTOK_CLIENT_KEY (and TIKTOK_CLIENT_ID must also be set; TikTok currently uses TIKTOK_CLIENT_ID as its registration gate) |
| Twitch | twitch | TWITCH_ | - |
| Twitter (X) | twitter | TWITTER_ | - |
| Vercel | vercel | VERCEL_ | - |
| VK | vk | VK_ | - |
wechat | WECHAT_ | Note — WECHAT_CLIENT_ID holds the WeChat appid, WECHAT_CLIENT_SECRET holds the WeChat secret | |
| Zoom | zoom | ZOOM_ | - |
A provider only becomes active when it is both listed in AUTH_SERVICES_ENABLED and has its
<PROVIDERID>_CLIENT_ID / <PROVIDERID>_CLIENT_SECRET set. If either is missing, Baasix skips the provider and
logs a startup warning — it will not appear in the discovery socialProviders list.
Environment Configuration
Enabling Providers
Configure which authentication services are enabled using the AUTH_SERVICES_ENABLED environment variable. It accepts LOCAL, PASSKEY, TWOFACTOR, and any of the 35 provider ids (uppercased), comma-separated:
# Enable email/password plus a handful of social providers
AUTH_SERVICES_ENABLED=LOCAL,GOOGLE,GITHUB,DISCORD
# Enable everything (illustrative — only enable what you actually configure credentials for)
AUTH_SERVICES_ENABLED=LOCAL,PASSKEY,TWOFACTOR,APPLE,ATLASSIAN,COGNITO,DISCORD,DROPBOX,FACEBOOK,FIGMA,GITHUB,GITLAB,GOOGLE,HUGGINGFACE,KAKAO,KICK,LINE,LINEAR,LINKEDIN,MICROSOFT,NAVER,NOTION,PAYBIN,PAYPAL,POLAR,RAILWAY,REDDIT,ROBLOX,SALESFORCE,SLACK,SPOTIFY,TIKTOK,TWITCH,TWITTER,VERCEL,VK,WECHAT,ZOOMLOCAL= email/password authentication (default if unset)PASSKEY= WebAuthn passkeys — see the Passkeys guideTWOFACTOR= TOTP two-factor authentication — see the Two-Factor guide- Any provider id (uppercased) = that OAuth provider, once credentialed
Provider Credentials
Every provider follows the same <PROVIDERID>_CLIENT_ID / <PROVIDERID>_CLIENT_SECRET convention:
# Google OAuth credentials
GOOGLE_CLIENT_ID=your-google-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your-google-client-secret
# GitHub OAuth credentials
GITHUB_CLIENT_ID=your-github-client-id
GITHUB_CLIENT_SECRET=your-github-client-secret
# Discord OAuth credentials
DISCORD_CLIENT_ID=your-discord-client-id
DISCORD_CLIENT_SECRET=your-discord-client-secretProviders with Extra Configuration
A few providers need additional environment variables beyond the standard client id/secret pair:
# Apple Sign In — requires a signing key in addition to the Services ID
APPLE_CLIENT_ID=your-apple-service-id
APPLE_CLIENT_SECRET= # Optional, auto-generated from the private key below
APPLE_TEAM_ID=your-apple-team-id
APPLE_KEY_ID=your-apple-key-id
APPLE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"
# Microsoft (Entra ID) — tenant scoping, defaults to "common" (any Microsoft account)
MICROSOFT_CLIENT_ID=your-microsoft-application-id
MICROSOFT_CLIENT_SECRET=your-microsoft-client-secret
MICROSOFT_TENANT_ID=common # or your organization's tenant ID
# TikTok — uses a separate client key alongside the client id
TIKTOK_CLIENT_ID=your-tiktok-client-id
TIKTOK_CLIENT_SECRET=your-tiktok-client-secret
TIKTOK_CLIENT_KEY=your-tiktok-client-key
# AWS Cognito — requires your user pool domain and region
COGNITO_CLIENT_ID=your-cognito-app-client-id
COGNITO_CLIENT_SECRET=your-cognito-app-client-secret
COGNITO_DOMAIN=your-cognito-domain
COGNITO_REGION=us-east-1
# WeChat — CLIENT_ID/CLIENT_SECRET are repurposed as appid/secret
WECHAT_CLIENT_ID=your-wechat-appid
WECHAT_CLIENT_SECRET=your-wechat-secretAdditional Configuration
# Base URL for OAuth callbacks (used to build {BASE_URL}/auth/callback/{provider})
BASE_URL=https://api.yourdomain.com
# Allowed application URLs for OAuth redirects
AUTH_APP_URL=https://app.yourdomain.com,https://admin.yourdomain.com
# Cookie configuration (when using cookie mode)
AUTH_COOKIE_HTTP_ONLY=true
AUTH_COOKIE_SECURE=true
AUTH_COOKIE_SAME_SITE=strict
AUTH_COOKIE_DOMAIN=.yourdomain.com
AUTH_COOKIE_PATH=/BASE_URL is read at startup but nothing validates its presence — if it's unset, the OAuth callback redirect URI
silently falls back to a relative path (no scheme/host), which breaks provider redirects. Always set BASE_URL to
your API's public origin when any OAuth provider is enabled. BASE_URL (your API's own origin, used to build the
callback URL) is distinct from AUTH_APP_URL (the comma-separated list of trusted frontend app origins used for
CORS and post-login redirect validation).
Provider Setup Guides
The setup steps below cover the four most commonly used providers. The remaining 31 providers follow the same shape — register an OAuth app with the provider, set the callback URL to {BASE_URL}/auth/callback/{provider-id}, and copy the client id/secret into the matching env vars.
Google OAuth Setup
- Go to Google Cloud Console
- Create a new project or select an existing one
- Navigate to APIs & Services > Credentials
- Click Create Credentials > OAuth Client ID
- Select Web application as application type
- Configure authorized redirect URIs:
https://api.yourdomain.com/auth/callback/google - Copy the Client ID and Client Secret
Google OAuth Scopes
Default scopes requested:
openid- OpenID Connectemail- User's email addressprofile- User's basic profile (name, picture)
GitHub OAuth Setup
- Go to GitHub Developer Settings
- Click New OAuth App
- Fill in the application details:
- Application name: Your app name
- Homepage URL:
https://yourdomain.com - Authorization callback URL:
https://api.yourdomain.com/auth/callback/github
- Click Register application
- Generate a new Client Secret
GitHub OAuth Scopes
Default scopes requested:
read:user- Read user profileuser:email- Access email addresses
Discord OAuth Setup
- Go to the Discord Developer Portal
- Create a new application
- Under OAuth2 > General, add a redirect:
https://api.yourdomain.com/auth/callback/discord - Copy the Client ID and Client Secret
Apple Sign In Setup
Apple Sign In requires more setup than other providers:
- Go to Apple Developer Portal
- Navigate to Certificates, Identifiers & Profiles
- Create an App ID with Sign In with Apple capability
- Create a Services ID for web authentication:
- Note the Identifier (this is your
APPLE_CLIENT_ID) - Configure domains and redirect URLs:
Domain: api.yourdomain.com Return URL: https://api.yourdomain.com/auth/callback/apple
- Note the Identifier (this is your
- Create a Key with Sign In with Apple enabled:
- Note the Key ID
- Download the
.p8private key file
- Note your Team ID from the top right of the developer portal
Converting Apple Private Key
The private key should be in PEM format. If using environment variables, replace newlines with \n:
APPLE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nMIGT...your-key-here...\n-----END PRIVATE KEY-----"API Endpoints
Browser Redirect Flow (Recommended)
Start Sign-In
Redirect the browser directly to this endpoint — no JSON request required.
Endpoint: GET /auth/signin/:provider
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
redirect_url | string | Yes | Where to send the browser after auth completes. Must match an allow-listed app URL (AUTH_APP_URL origins or settings-configured URLs). |
scopes | string | No | Comma-separated additional OAuth scopes |
authMode | string | No | jwt (default) or cookie |
Example:
<a href="https://api.yourdomain.com/auth/signin/github?redirect_url=https://app.yourdomain.com/auth/callback">
Continue with GitHub
</a>This responds with a 302 redirect straight to the provider's login page.
OAuth Callback
The provider redirects here after the user authorizes (or denies) access. You never call this directly — it's only reached via the redirect chain started by GET /auth/signin/:provider or the legacy POST flow below.
Endpoint: GET /auth/callback/:provider
Behavior:
- If the flow was started via
GET /auth/signin/:provider, this endpoint 302s back to yourredirect_urlwith the result in the query string:- Success:
<redirect_url>?token=<jwt> - Failure:
<redirect_url>?error=<message>
- Success:
- If the flow was started via the legacy
POST /auth/social/signinflow, this endpoint instead returns a JSON response body (see below), preserving the original behavior for existing integrations.
Programmatic Flow (Legacy / Advanced)
Initiate Social Sign In
Request an authorization URL as JSON instead of redirecting directly. Useful when you need to control the request from JavaScript before navigating (e.g. showing a loading state, custom scopes from a mobile WebView).
Endpoint: POST /auth/social/signin
Request Body:
{
"provider": "google",
"callbackURL": "https://api.yourdomain.com/auth/callback/google",
"errorCallbackURL": "https://app.yourdomain.com/auth/error",
"scopes": ["email", "profile"],
"authMode": "jwt"
}| Field | Type | Required | Description |
|---|---|---|---|
| provider | string | Yes | Any of the 35 supported provider ids |
| callbackURL | string | No | Custom callback URL (uses default if not specified) |
| errorCallbackURL | string | No | URL to redirect on error |
| scopes | string[] | No | Additional OAuth scopes |
| authMode | string | No | jwt (default) or cookie |
Response:
{
"redirect": true,
"url": "https://accounts.google.com/o/oauth2/v2/auth?client_id=...&redirect_uri=...&state=..."
}Your client is responsible for navigating to url (e.g. window.location.href = data.url).
JSON callback response (when the flow was started via this POST endpoint):
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"authMode": "jwt",
"user": {
"id": "user-uuid",
"email": "user@example.com",
"firstName": "John",
"lastName": "Doe"
},
"role": {
"id": "role-uuid",
"name": "user"
},
"permissions": [...],
"tenant": null
}Apple Sign In Callback (POST)
Apple Sign In uses a POST callback with form data.
Endpoint: POST /auth/callback/apple
Form Data:
code: Authorization codestate: State parameterid_token: Apple ID tokenuser: User info (first sign-in only)
Client Integration
The recommended approach is to discover which providers are actually enabled at runtime via GET / (or the SDK's auth.getAuthMethods()) rather than hardcoding a fixed set of buttons. This keeps your login UI in sync with server configuration without a redeploy.
React/Next.js Example (Discovery-Driven)
import { useEffect, useState } from 'react';
const API_URL = 'https://api.yourdomain.com';
export function SocialLoginButtons() {
const [providers, setProviders] = useState<string[]>([]);
const [loading, setLoading] = useState<string | null>(null);
useEffect(() => {
fetch(`${API_URL}/`)
.then((res) => res.json())
.then((data) => setProviders(data.project?.auth?.socialProviders ?? []));
}, []);
const handleSocialLogin = (provider: string) => {
setLoading(provider);
const redirectUrl = `${window.location.origin}/auth/callback`;
window.location.href = `${API_URL}/auth/signin/${provider}?redirect_url=${encodeURIComponent(redirectUrl)}`;
};
return (
<div className="flex flex-col gap-3">
{providers.map((provider) => (
<button
key={provider}
onClick={() => handleSocialLogin(provider)}
disabled={loading !== null}
className="flex items-center justify-center gap-2 px-4 py-2 border rounded-lg hover:bg-gray-50"
>
<ProviderIcon provider={provider} />
{loading === provider ? 'Redirecting...' : `Continue with ${capitalize(provider)}`}
</button>
))}
</div>
);
}
function capitalize(s: string) {
return s.charAt(0).toUpperCase() + s.slice(1);
}Using the SDK instead of raw fetch (see the JavaScript SDK guide):
const methods = await baasix.auth.getAuthMethods();
// methods.socialProviders: string[] — only providers actually enabled + credentialed
methods.socialProviders.forEach((provider) => {
const url = baasix.auth.getOAuthUrl({
provider,
redirectUrl: `${window.location.origin}/auth/callback`,
});
// Render a button that does: window.location.href = url;
});Handling OAuth Callback in Client
Create a callback page to handle the redirect back from Baasix. With the browser redirect flow, the token arrives directly in the query string — no additional request is needed.
// pages/auth/callback.tsx
import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
export default function OAuthCallback() {
const router = useRouter();
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const token = params.get('token');
const errorMsg = params.get('error');
if (errorMsg) {
setError(errorMsg);
return;
}
if (token) {
// Store token (e.g., in localStorage or state management)
localStorage.setItem('authToken', token);
router.push('/dashboard');
}
}, [router]);
if (error) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<h1 className="text-xl font-bold text-red-600">Authentication Error</h1>
<p className="mt-2">{error}</p>
<button onClick={() => router.push('/login')} className="mt-4 px-4 py-2 bg-blue-500 text-white rounded">
Back to Login
</button>
</div>
</div>
);
}
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto"></div>
<p className="mt-4">Completing authentication...</p>
</div>
</div>
);
}Mobile App Integration (React Native)
For mobile apps, use a WebView or in-app browser for OAuth. With the browser redirect flow, you can point the WebView directly at /auth/signin/:provider and watch for the callback navigation:
import { useState } from 'react';
import { WebView } from 'react-native-webview';
import { Modal } from 'react-native';
const API_URL = 'https://api.yourdomain.com';
export function SocialLogin() {
const [oauthUrl, setOauthUrl] = useState<string | null>(null);
const handleSocialLogin = (provider: string) => {
const redirectUrl = 'myapp://auth/callback';
setOauthUrl(`${API_URL}/auth/signin/${provider}?redirect_url=${encodeURIComponent(redirectUrl)}`);
};
const handleNavigationStateChange = (navState: any) => {
// Watch for the final redirect carrying ?token= or ?error=
if (navState.url.startsWith('myapp://auth/callback')) {
const url = new URL(navState.url.replace('myapp://', 'https://'));
const token = url.searchParams.get('token');
// Store the token and close the WebView
setOauthUrl(null);
}
};
return (
<>
<Button title="Sign in with Google" onPress={() => handleSocialLogin('google')} />
<Modal visible={!!oauthUrl} animationType="slide">
{oauthUrl && <WebView source={{ uri: oauthUrl }} onNavigationStateChange={handleNavigationStateChange} />}
</Modal>
</>
);
}Advanced Configuration
Custom User Profile Mapping
Each provider returns different profile data. Baasix automatically maps common fields (shown here for a few representative providers — the same mapping logic applies across all 35):
| Baasix Field | GitHub | Apple | ||
|---|---|---|---|---|
| firstName | given_name | name (parsed) | first_name | (from user object) |
| lastName | family_name | name (parsed) | last_name | (from user object) |
| avatar | picture | avatar_url | picture.data.url | - |
Multi-tenant Social Auth
When using multi-tenant mode, social sign-in follows these rules:
- New Users: Created without tenant association
- Existing Users: Matched by email and logged in
- Tenant Selection: After login, user can be invited to or select a tenant
Session Management with Social Auth
Social auth sessions are managed the same way as regular sessions:
# Session configuration
ACCESS_TOKEN_EXPIRES_IN=604800 # 7 days in secondsAuthentication Modes (authMode)
Baasix supports two authentication modes for token delivery. You can specify the authMode parameter in any authentication request (login, register, social sign-in, etc.):
| Mode | Value | Description |
|---|---|---|
| JWT | jwt (default) | Token returned in response body |
| Cookie | cookie | Token set as HTTP-only cookie |
JWT Mode (Default)
In JWT mode, the token is returned in the response body (or, for the browser redirect flow, appended to the redirect URL's query string). Your client application must store and manage the token:
// Request
{
"email": "user@example.com",
"password": "password123",
"authMode": "jwt"
}
// Response
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"authMode": "jwt",
"user": { ... }
}Usage in subsequent requests:
curl -H "Authorization: Bearer YOUR_TOKEN_HERE" \
https://api.yourdomain.com/items/postsCookie Mode
In cookie mode, the token is automatically set as an HTTP-only cookie. This is more secure for same-domain SPAs as JavaScript cannot access the token:
// Request
{
"email": "user@example.com",
"password": "password123",
"authMode": "cookie"
}
// Response
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"message": "Authentication successful",
"authMode": "cookie",
"user": { ... }
}
// Cookie is also set: token=eyJ...Cookie is automatically sent with requests (no manual header needed):
// Fetch with credentials
fetch('https://api.yourdomain.com/items/posts', {
credentials: 'include', // Important: include cookies
});
// Axios with credentials
axios.get('https://api.yourdomain.com/items/posts', {
withCredentials: true, // Important: include cookies
});Cookie Configuration
Configure cookie behavior with environment variables:
# Cookie security settings
AUTH_COOKIE_HTTP_ONLY=true # Prevent JavaScript access (default: true)
AUTH_COOKIE_SECURE=true # HTTPS only (default: true in production)
AUTH_COOKIE_SAME_SITE=strict # CSRF protection: strict, lax, or none (default: strict in prod, lax in dev)
AUTH_COOKIE_DOMAIN=.yourdomain.com # Share across subdomains (optional)
AUTH_COOKIE_PATH=/ # Cookie path (default: /)
# Token expiration (applies to both modes)
ACCESS_TOKEN_EXPIRES_IN=604800 # 7 days in secondsWhen to Use Each Mode
| Use Case | Recommended Mode |
|---|---|
| Same-domain SPA | cookie |
| Cross-domain SPA | jwt |
| Mobile apps | jwt |
| Server-to-server | jwt |
| Third-party integrations | jwt |
Social Auth with authMode
For the browser redirect flow, pass authMode as a query parameter:
GET /auth/signin/google?redirect_url=https://app.yourdomain.com/callback&authMode=cookieFor the legacy programmatic flow, pass it in the request body:
// Request to /auth/social/signin
{
"provider": "google",
"authMode": "cookie" // Token will be set as cookie after OAuth callback
}The authMode is stored with the OAuth state and applied when the user completes the OAuth flow.
Security Best Practices
1. Use HTTPS Only
Always use HTTPS in production for OAuth callbacks:
BASE_URL=https://api.yourdomain.com
AUTH_APP_URL=https://app.yourdomain.com2. Validate Redirect URIs
Baasix validates both the OAuth callback URL and the redirect_url query parameter (browser flow) against AUTH_APP_URL:
# Comma-separated list of allowed app URLs
AUTH_APP_URL=https://app.yourdomain.com,https://admin.yourdomain.com3. State Parameter Protection
Baasix automatically generates and validates state parameters to prevent CSRF attacks.
4. PKCE Support
PKCE (Proof Key for Code Exchange) is enabled by default for providers that support it (Google).
5. Secure Cookie Configuration
For production:
AUTH_COOKIE_HTTP_ONLY=true # Prevent JavaScript access
AUTH_COOKIE_SECURE=true # HTTPS only
AUTH_COOKIE_SAME_SITE=strict # Prevent CSRF6. Keep Secrets Secure
- Never commit OAuth secrets to version control
- Use environment variables or secrets management
- Rotate secrets periodically
Troubleshooting
Common Issues
"Provider not found" Error
Cause: Provider not enabled or credentials not configured.
Solution: Check your environment variables:
# Ensure provider is in AUTH_SERVICES_ENABLED
AUTH_SERVICES_ENABLED=LOCAL,GOOGLE
# Ensure credentials are set
GOOGLE_CLIENT_ID=...
GOOGLE_CLIENT_SECRET=...You can also check GET / — if the provider isn't in project.auth.socialProviders, it wasn't constructed at startup (see Public Discovery in the Authentication overview).
"Invalid or expired state" Error
Cause: OAuth state parameter expired or cache issue.
Solution:
- Ensure Redis is running and configured
- Check if
DATA_CACHE_ENABLED=true - OAuth state expires after 10 minutes - retry the flow
"redirect_uri_mismatch" Error
Cause: Callback URL doesn't match provider configuration.
Solution:
- Verify the exact callback URL in provider settings:
https://api.yourdomain.com/auth/callback/google - Check for trailing slashes
- Ensure protocol matches (https vs http)
- Confirm
BASE_URLis set correctly on the server — an unsetBASE_URLproduces a relative (host-less) redirect URI
Apple Sign In Issues
Issue: "Invalid client_secret"
Solution:
- Verify the private key format
- Check if the key is associated with the correct Key ID
- Ensure the Service ID (client ID) is correct
Issue: "Invalid redirect_uri"
Solution: Apple requires domains to be verified in the developer portal.
Debug Mode
Enable debugging for more detailed logs:
LOG_LEVEL=debugTesting Locally
For local development, use localhost callbacks:
BASE_URL=http://localhost:3000
AUTH_APP_URL=http://localhost:3001
# In provider settings, add:
# http://localhost:3000/auth/callback/googleNote: Some providers (like Facebook) require HTTPS even for development. Use tools like ngrok for local testing.
Related Documentation
- Authentication API - Email/password authentication, discovery, and 2FA challenge
- Passkeys - WebAuthn passwordless sign-in
- Two-Factor Authentication - TOTP setup and login challenge
- Multi-tenant Guide - Tenant-based authentication
- Session Limits Feature - Control concurrent sessions
- Deployment Guide - Environment configuration
- Integration Guide - Client-side implementation