OAuth for Projects

OAuth for Projects

Let the users of your app sign in with an account they already have: Google, GitHub, Microsoft, Discord and 15 more identity providers. WoWSQL Auth runs the OAuth 2.0 authorization-code flow for you on your project host: it exchanges the code with the provider, reads the user's profile, creates or updates the user in your project's auth schema and returns the same session (access and refresh token) as email and password sign-in.

This page covers sign-in for your own application's users. Email, password, magic-link and 2FA endpoints are in User Authentication. To let a third-party app such as Zapier or n8n work with one of your WoWSQL projects, see Sign in with WoWSQL (OAuth Connect); it is a separate feature.

Base URL:  https://your-project-slug.wowsqlconnect.com/auth/v1
Also:      https://your-project-slug.wowsqlconnect.com/api/auth   (the gateway rewrites /api/auth/... to /auth/v1/...)

Headers (every call except the provider callback):
  apikey: wowsql_anon_...

Paths below are relative to the base URL. Both prefixes reach the same auth service. The provider callback URL you register is always the /auth/v1 form shown in the dashboard.

Supported Providers

Use the value in the second column as {provider} in every path and SDK call. Default scopes are what the dashboard pre-fills; you can change them. If a provider is saved with no scopes, the auth service requests email profile.

Provider{provider}Default scopesNotes
Appleapplename emailThe client secret must be the signed JWT that Apple requires (generate it from your Apple key; it expires after at most six months). Apple sends its response as a form POST.
Azureazureopenid profile emailSame Microsoft identity platform endpoints as microsoft (the common tenant). Configured separately.
Bitbucketbitbucketaccount emailThe primary address from the Bitbucket email API is used.
Discorddiscordidentify emailWithout the email scope the user gets a placeholder address (username@discord.noreply).
Facebookfacebookemail public_profileUses Graph API v18.0. Accounts without an email address cannot sign in.
Figmafigmafile_readEmail and handle come from GET /v1/me.
GitHubgithubread:user user:emailPrivate emails are read from /user/emails (primary address). If none is available the user gets login@github.noreply.
GitLabgitlabread_user emailgitlab.com only. Self-managed GitLab instances are not supported.
Googlegoogleopenid email profileRequests offline access.
Kakaokakaoaccount_email profile_nicknameEmail comes from kakao_account.email.
Keycloakkeycloakopenid email profileListed in the dashboard but not usable yet: the dashboard has no field for the realm base URL, and the token exchange is not implemented.
LinkedInlinkedinopenid profile emailSign In with LinkedIn using OpenID Connect.
Microsoftmicrosoftopenid profile email offline_accessEmail is mail, or userPrincipalName when mail is empty.
NotionnotionnoneThe user is identified by the Notion integration (bot_id); email comes from the workspace owner.
Slackslackopenid profile emailSign in with Slack (OpenID Connect).
Spotifyspotifyuser-read-email user-read-private
Twitchtwitchuser:read:emailWithout an email the user gets login@twitch.noreply.
Twitter / Xtwittertweet.read users.readX does not return an email address, so every X user gets username@twitter.noreply. Use twitter, not x.
WorkOSworkosnoneWorkOS SSO.
Zoomzoomuser:read:email

The dashboard's Third-Party Auth tab (Firebase, Clerk, Auth0, Amazon Cognito, WorkOS) and the SAML 2.0 and Web3 Wallet rows are marked Coming soon. They are not part of OAuth sign-in.

Set Up a Provider in the Dashboard

Do this once per provider, per project. Auth must already be enabled for the project.

  1. Open Project → Auth → Providers and expand the provider.
  2. Copy the Redirect URI. It is read-only and always has this form:
    https://{slug}.wowsqlconnect.com/auth/v1/oauth/{provider}/callback
  3. In the provider's developer console, create an OAuth app (Google: Cloud Console → Credentials → OAuth client ID; GitHub: Settings → Developer settings → OAuth Apps) and add that exact URI to its authorised redirect or callback URLs.
  4. Paste the provider's Client ID and Client Secret into the dashboard. Leave no leading or trailing spaces.
  5. Adjust Scopes if needed. Separate them with commas or spaces. Keep a scope that returns the user's email; sign-in fails without one (except for the placeholder cases in the table above).
  6. Click Enable. Saving always enables the provider.
  7. Open Auth → URL Configuration and set Frontend URL to your app's URL. It is the default landing page after sign-in.

To change the credentials later, click Update; leave the secret blank to keep the stored one. Disable deletes the provider configuration, so enabling it again needs the client secret again. Client secrets are encrypted at rest and are never returned by the API.

The same page has project-wide switches that affect OAuth:

SwitchEffect on OAuth
Allow new users to sign upWhen off, OAuth sign-in only works for users who already exist; a new provider account gets 403 Signups disabled.
Allow manual linkingRequired for POST /oauth/{provider}/link. Off by default.
Confirm emailDoes not apply: users who sign in with a provider are marked as email-verified.

Project owners can also manage providers through the dashboard API with a dashboard session token: POST /api/v1/projects/{slug}/auth/oauth-providers with {"provider_name", "client_id", "client_secret", "enabled", "redirect_uri", "scopes": []}, and DELETE /api/v1/projects/{slug}/auth/oauth-providers/{provider}.

How the Sign-in Flow Works

StepWhoWhat happens
1Your appCalls GET /oauth/{provider}?frontend_redirect_uri=... with your anon key. The response contains authorization_url. This call does not redirect.
2Your appSends the browser to authorization_url.
3UserSigns in at the provider and approves the requested scopes.
4ProviderRedirects the browser to https://{slug}.wowsqlconnect.com/auth/v1/oauth/{provider}/callback?code=...&state=.... No API key is needed on this URL.
5WoWSQL AuthExchanges the code, reads the profile, finds the user by provider account ID or email (or creates one), and opens a session.
6WoWSQL AuthRedirects to your frontend_redirect_uri with the session in the query string: access_token, refresh_token, token_type, expires_in, user_id, email.
7Your appReads the parameters, stores the session (for example with setSession), and removes them from the address bar.

The landing page is chosen in this order: frontend_redirect_uri (also accepted as redirect_to or redirect_uri) from step 1, then the project's Frontend URL, then http://localhost:5000. It is carried through the provider in the state parameter. Use a landing URL without its own query string: the token parameters are appended after a ?.

Existing users. If a user with the same email already exists in the project (for example from email and password sign-up), the provider sign-in signs in that same user and records the provider on it. Banned or deactivated users get 403.

API Reference

Start sign-in

GET /oauth/{provider}?frontend_redirect_uri=https://yourapp.com/auth/callback
(POST is also accepted, with the same query parameters)

Headers:
  apikey: wowsql_anon_...

Response 200:
{
  "authorization_url": "https://accounts.google.com/o/oauth2/v2/auth?client_id=...&redirect_uri=...&state=...",
  "provider": "google",
  "backend_callback_url": "https://your-project-slug.wowsqlconnect.com/auth/v1/oauth/google/callback",
  "frontend_redirect_uri": "https://yourapp.com/auth/callback"
}

Legacy form, still supported: GET /authorize?provider=google&redirect_to=https://yourapp.com/auth/callback. It returns the same body.

Provider callback

GET /oauth/{provider}/callback?code=...&state=...
(called by the provider; no apikey)

Response: 302 to
https://yourapp.com/auth/callback?access_token=...&refresh_token=...&token_type=bearer&expires_in=86400&user_id=...&email=...

expires_in is the project's access-token lifetime in seconds (24 hours unless you changed it). The refresh token follows the project's refresh-token lifetime (30 days by default). Refresh with POST /refresh-token, as for any other session.

Exchange a code yourself (advanced)

POST /oauth/{provider}/callback

Headers:
  apikey: wowsql_anon_...
  Content-Type: application/json

Body:
{
  "code": "authorization_code_from_provider",
  "redirect_uri": "https://yourapp.com/auth/callback"
}

Response 200:
{
  "success": true,
  "user": {
    "id": "uuid",
    "email": "user@example.com",
    "full_name": "Jane Doe",
    "avatar_url": "https://...",
    "email_verified": true
  },
  "access_token": "eyJ...",
  "refresh_token": "eyJ...",
  "token_type": "bearer",
  "expires_in": 86400
}

The code is always exchanged with the project callback URL (.../auth/v1/oauth/{provider}/callback) as its redirect_uri, so it must be a code the provider issued for that URL. redirect_uri in the body does not change the exchange. In the normal browser flow the callback in step 4 has already used the code, so you only need this endpoint if your app captures the code before it reaches WoWSQL.

POST /oauth/{provider}/link

Headers:
  apikey: wowsql_anon_...
  Authorization: Bearer USER_ACCESS_TOKEN
  Content-Type: application/json

Body:
{
  "code": "authorization_code_from_provider",
  "redirect_uri": "https://yourapp.com/settings/accounts"
}

Response 200:
{
  "success": true,
  "message": "Identity linked",
  "user_id": "uuid"
}

Linking requires Allow manual linking, an active session, a provider account whose email matches the user's email, and a provider account that is not already linked to another user (409). A user holds one linked provider identity at a time: linking a new provider replaces the previous one. There is no unlink endpoint.

SDK Examples

All eleven SDKs (3.10.0) wrap the start and exchange calls. linkOAuthIdentity is available in TypeScript, Python and Java only. Pass your landing URL as the redirect argument; the SDKs send it as frontend_redirect_uri.

TypeScript / JavaScript

import { ProjectAuthClient } from '@wowsql/sdk';

const auth = new ProjectAuthClient({
  projectUrl: 'your-project-slug',
  apiKey: 'wowsql_anon_your_key_here'
});

// 1. Start sign-in: GET /auth/v1/oauth/google
const { authorizationUrl } = await auth.getOAuthAuthorizationUrl(
  'google',
  'https://yourapp.com/auth/callback'
);
window.location.href = authorizationUrl;
// 2. On https://yourapp.com/auth/callback
const params = new URLSearchParams(window.location.search);
const accessToken = params.get('access_token');
const refreshToken = params.get('refresh_token');

if (accessToken) {
  auth.setSession({ accessToken, refreshToken });
  // Remove the tokens from the address bar and browser history
  window.history.replaceState({}, '', window.location.pathname);
  const user = await auth.getUser();
}
// Link GitHub to the signed-in user (requires "Allow manual linking")
await auth.linkOAuthIdentity('github', {
  code: 'authorization_code_from_provider',
  redirectUri: 'https://yourapp.com/settings/accounts'
});

Python

from wowsql import ProjectAuthClient

auth = ProjectAuthClient(project_url="your-project-slug", api_key="wowsql_anon_your_key_here")

# Start sign-in; send the user's browser to the returned URL
result = auth.get_oauth_authorization_url("github", redirect_uri="https://yourapp.com/auth/callback")
print(result["authorization_url"])

# On your callback route, store the tokens WoWSQL appended to the URL
auth.set_session(access_token=request.args["access_token"], refresh_token=request.args.get("refresh_token"))

Method names in every SDK

SDKStart sign-inExchange a codeLink identity
TypeScriptgetOAuthAuthorizationUrl(provider, redirectUri)exchangeOAuthCallback(provider, code, redirectUri?)linkOAuthIdentity(provider, {code, redirectUri}, accessToken?)
Pythonget_oauth_authorization_url(provider, *, redirect_uri=None)exchange_oauth_callback(provider, *, code, redirect_uri=None)link_oauth_identity(provider, *, code, redirect_uri=None, access_token=None)
JavagetOAuthAuthorizationUrl(provider[, redirectUri])exchangeOAuthCallback(provider, code[, redirectUri])linkOAuthIdentity(provider, code, redirectUri)
KotlingetOAuthAuthorizationUrl(provider, redirectUri = null)exchangeOAuthCallback(provider, code, redirectUri = null)-
GoGetOAuthAuthorizationURL(provider, redirectURI ...string)ExchangeOAuthCallback(provider, code, redirectURI ...string)-
Flutter / DartgetOAuthAuthorizationUrl(provider:, redirectUri:)exchangeOAuthCallback(provider:, code:, redirectUri:)-
Rustget_oauth_authorization_url(provider, Option<&str>)exchange_oauth_callback(provider, code, Option<&str>)-
PHPgetOAuthAuthorizationUrl($provider, $redirectUri = null)exchangeOAuthCallback($provider, $code, $redirectUri = null)-
Rubyget_oauth_authorization_url(provider:, redirect_uri: nil)exchange_oauth_callback(provider:, code:, redirect_uri: nil)-
SwiftgetOAuthAuthorizationUrl(provider:redirectUri:)exchangeOAuthCallback(provider:code:redirectUri:)-
C#GetOAuthAuthorizationUrlAsync(provider, redirectUri = null)ExchangeOAuthCallbackAsync(provider, code, redirectUri = null)-

The start call returns the authorization_url (TypeScript: authorizationUrl) together with backend_callback_url and frontend_redirect_uri. The exchange call stores the session in the client, like signIn.

Mobile apps. Open authorization_url in the system browser or an auth session (ASWebAuthenticationSession, Custom Tabs) and use an https landing URL that your app handles as a universal or app link, then read the tokens from that URL.

Two-Factor Authentication and OAuth

Provider sign-in does not ask for the WoWSQL TOTP code. A user who has enabled two-factor authentication still gets a full session from the provider callback, and the response never contains requires_2fa. Only POST /login (email and password) returns the 2FA challenge. Rely on the provider's own multi-factor settings for OAuth users, or check GET /2fa/status in your app after an OAuth sign-in if you need to enforce TOTP.

Errors

StatusMessageMeaning
401Missing API key...The start, exchange or link call was sent without the apikey header.
400OAuth provider {provider} not configuredNo configuration saved for this provider in this project.
400OAuth provider {provider} not enabled / not configured/enabledThe provider is disabled or missing (callback and link return the combined message).
400Keycloak requires 'keycloak_base_url' in provider config JSONKeycloak is not usable yet (see Supported Providers).
400Token exchange failed (...)The provider rejected the code: wrong client secret, a redirect URI that does not match, or a code that was already used or expired. The provider's own error text follows.
400User-info fetch failed (...)The provider rejected the profile request, usually because of missing scopes.
400No email from OAuth providerThe provider returned no email address. Add the email scope or make the email visible in the provider account.
403Signups disabledNew user, and Allow new users to sign up is off.
403Account banned / Account inactiveThe matching user is banned or deactivated.
422validation errorThe callback was called without code, for example after the user cancelled at the provider.
401Bearer token required / Invalid or expired sessionLink called without a valid user access token.
403Identity linking disabledLink called while Allow manual linking is off.
400OAuth email must match account emailLink: the provider account uses a different email.
409OAuth identity already linked to another userLink: that provider account belongs to another user in the project.

Errors on the provider callback are returned as JSON in the user's browser; they are not forwarded to your landing page.

Troubleshooting

  • The provider says the redirect URI does not match (redirect_uri_mismatch). Register exactly the Redirect URI from the dashboard, including https, the project slug and the provider ID, with no trailing slash.
  • The start call returns 401. Send the anon key in the apikey header. The SDKs do this for you.
  • After sign-in the browser lands on http://localhost:5000. No landing URL was passed and the project has no Frontend URL. Set it in Auth → URL Configuration or pass frontend_redirect_uri.
  • The landing page gets a malformed URL. Your landing URL already had a query string. Use a path without one.
  • The user sees a JSON error page after the provider. Read the message against the table above; the most common cause is a wrong client secret (Token exchange failed).
  • OAuth users show @github.noreply or @twitter.noreply addresses. The provider returned no email. These users cannot receive email from your app.
  • Credentials were rotated at the provider. Click Update and paste the new secret; existing sessions stay valid.

Security Notes

  • Your provider client secret stays on the server and is encrypted at rest. Never put it in client code; the browser only needs your anon key.
  • Tokens arrive in the landing URL's query string. Store them and remove them from the address bar straight away (see the TypeScript example), and do not load third-party scripts, analytics or images on the landing page before you do, so the tokens do not leak through history, logs or the Referer header.
  • Only pass landing URLs on domains you control, and set the Frontend URL so the default is never localhost in production.
  • Sign-in matches existing users by email. Prefer providers that only return verified email addresses, and request the scopes that expose the verified address.
  • Provider sign-in bypasses WoWSQL TOTP (see Two-Factor Authentication and OAuth).
  • Disable providers you no longer use. Disabling deletes the stored credentials.
  • Sign-ins and links are written to the project audit log as user.oauth_login and user.oauth_link.