BaasixBaasix
GuidesAuthentication

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

  1. Overview
  2. Supported Providers
  3. Environment Configuration
  4. Provider Setup Guides
  5. API Endpoints
  6. Client Integration
  7. Advanced Configuration
  8. Security Best Practices
  9. 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 GET link/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/signin returns 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):

  1. Client links/redirects the browser to GET /auth/signin/:provider?redirect_url=...
  2. Baasix redirects to the provider's login page
  3. After successful login, the provider redirects back to the Baasix callback URL
  4. Baasix exchanges the authorization code for tokens, fetches the profile, and creates/links the account
  5. Baasix 302s back to your redirect_url with ?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.

ProviderProvider IDEnv PrefixExtra config
AppleappleAPPLE_Yes — APPLE_TEAM_ID, APPLE_KEY_ID, APPLE_PRIVATE_KEY
AtlassianatlassianATLASSIAN_-
AWS CognitocognitoCOGNITO_Yes — COGNITO_DOMAIN, COGNITO_REGION
DiscorddiscordDISCORD_-
DropboxdropboxDROPBOX_-
FacebookfacebookFACEBOOK_-
FigmafigmaFIGMA_-
GitHubgithubGITHUB_-
GitLabgitlabGITLAB_-
GooglegoogleGOOGLE_-
Hugging FacehuggingfaceHUGGINGFACE_-
KakaokakaoKAKAO_-
KickkickKICK_-
LINElineLINE_-
LinearlinearLINEAR_-
LinkedInlinkedinLINKEDIN_-
Microsoft (Entra ID)microsoftMICROSOFT_Yes — MICROSOFT_TENANT_ID (default common)
NavernaverNAVER_-
NotionnotionNOTION_-
PaybinpaybinPAYBIN_-
PayPalpaypalPAYPAL_-
PolarpolarPOLAR_-
RailwayrailwayRAILWAY_-
RedditredditREDDIT_-
RobloxrobloxROBLOX_-
SalesforcesalesforceSALESFORCE_-
SlackslackSLACK_-
SpotifyspotifySPOTIFY_-
TikToktiktokTIKTOK_Yes — TIKTOK_CLIENT_KEY (and TIKTOK_CLIENT_ID must also be set; TikTok currently uses TIKTOK_CLIENT_ID as its registration gate)
TwitchtwitchTWITCH_-
Twitter (X)twitterTWITTER_-
VercelvercelVERCEL_-
VKvkVK_-
WeChatwechatWECHAT_Note — WECHAT_CLIENT_ID holds the WeChat appid, WECHAT_CLIENT_SECRET holds the WeChat secret
ZoomzoomZOOM_-

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,ZOOM
  • LOCAL = email/password authentication (default if unset)
  • PASSKEY = WebAuthn passkeys — see the Passkeys guide
  • TWOFACTOR = 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-secret

Providers 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-secret

Additional 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

  1. Go to Google Cloud Console
  2. Create a new project or select an existing one
  3. Navigate to APIs & Services > Credentials
  4. Click Create Credentials > OAuth Client ID
  5. Select Web application as application type
  6. Configure authorized redirect URIs:
    https://api.yourdomain.com/auth/callback/google
  7. Copy the Client ID and Client Secret

Google OAuth Scopes

Default scopes requested:

  • openid - OpenID Connect
  • email - User's email address
  • profile - User's basic profile (name, picture)

GitHub OAuth Setup

  1. Go to GitHub Developer Settings
  2. Click New OAuth App
  3. 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
  4. Click Register application
  5. Generate a new Client Secret

GitHub OAuth Scopes

Default scopes requested:

  • read:user - Read user profile
  • user:email - Access email addresses

Discord OAuth Setup

  1. Go to the Discord Developer Portal
  2. Create a new application
  3. Under OAuth2 > General, add a redirect:
    https://api.yourdomain.com/auth/callback/discord
  4. Copy the Client ID and Client Secret

Apple Sign In Setup

Apple Sign In requires more setup than other providers:

  1. Go to Apple Developer Portal
  2. Navigate to Certificates, Identifiers & Profiles
  3. Create an App ID with Sign In with Apple capability
  4. 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
  5. Create a Key with Sign In with Apple enabled:
    • Note the Key ID
    • Download the .p8 private key file
  6. 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

Start Sign-In

Redirect the browser directly to this endpoint — no JSON request required.

Endpoint: GET /auth/signin/:provider

Query Parameters:

ParameterTypeRequiredDescription
redirect_urlstringYesWhere to send the browser after auth completes. Must match an allow-listed app URL (AUTH_APP_URL origins or settings-configured URLs).
scopesstringNoComma-separated additional OAuth scopes
authModestringNojwt (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 your redirect_url with the result in the query string:
    • Success: <redirect_url>?token=<jwt>
    • Failure: <redirect_url>?error=<message>
  • If the flow was started via the legacy POST /auth/social/signin flow, 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"
}
FieldTypeRequiredDescription
providerstringYesAny of the 35 supported provider ids
callbackURLstringNoCustom callback URL (uses default if not specified)
errorCallbackURLstringNoURL to redirect on error
scopesstring[]NoAdditional OAuth scopes
authModestringNojwt (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 code
  • state: State parameter
  • id_token: Apple ID token
  • user: 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 FieldGoogleGitHubFacebookApple
emailemailemailemailemail
firstNamegiven_namename (parsed)first_name(from user object)
lastNamefamily_namename (parsed)last_name(from user object)
avatarpictureavatar_urlpicture.data.url-

Multi-tenant Social Auth

When using multi-tenant mode, social sign-in follows these rules:

  1. New Users: Created without tenant association
  2. Existing Users: Matched by email and logged in
  3. 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 seconds

Authentication 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.):

ModeValueDescription
JWTjwt (default)Token returned in response body
CookiecookieToken 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/posts

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
});

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 seconds

When to Use Each Mode

Use CaseRecommended Mode
Same-domain SPAcookie
Cross-domain SPAjwt
Mobile appsjwt
Server-to-serverjwt
Third-party integrationsjwt

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=cookie

For 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.com

2. 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.com

3. 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).

For production:

AUTH_COOKIE_HTTP_ONLY=true    # Prevent JavaScript access
AUTH_COOKIE_SECURE=true       # HTTPS only
AUTH_COOKIE_SAME_SITE=strict  # Prevent CSRF

6. 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:

  1. Ensure Redis is running and configured
  2. Check if DATA_CACHE_ENABLED=true
  3. OAuth state expires after 10 minutes - retry the flow

"redirect_uri_mismatch" Error

Cause: Callback URL doesn't match provider configuration.

Solution:

  1. Verify the exact callback URL in provider settings:
    https://api.yourdomain.com/auth/callback/google
  2. Check for trailing slashes
  3. Ensure protocol matches (https vs http)
  4. Confirm BASE_URL is set correctly on the server — an unset BASE_URL produces a relative (host-less) redirect URI

Apple Sign In Issues

Issue: "Invalid client_secret"

Solution:

  1. Verify the private key format
  2. Check if the key is associated with the correct Key ID
  3. 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=debug

Testing 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/google

Note: Some providers (like Facebook) require HTTPS even for development. Use tools like ngrok for local testing.


On this page