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 scopes | Notes |
|---|---|---|---|
| Apple | apple | name email | The 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. |
| Azure | azure | openid profile email | Same Microsoft identity platform endpoints as microsoft (the common tenant). Configured separately. |
| Bitbucket | bitbucket | account email | The primary address from the Bitbucket email API is used. |
| Discord | discord | identify email | Without the email scope the user gets a placeholder address (username@discord.noreply). |
facebook | email public_profile | Uses Graph API v18.0. Accounts without an email address cannot sign in. | |
| Figma | figma | file_read | Email and handle come from GET /v1/me. |
| GitHub | github | read:user user:email | Private emails are read from /user/emails (primary address). If none is available the user gets login@github.noreply. |
| GitLab | gitlab | read_user email | gitlab.com only. Self-managed GitLab instances are not supported. |
google | openid email profile | Requests offline access. | |
| Kakao | kakao | account_email profile_nickname | Email comes from kakao_account.email. |
| Keycloak | keycloak | openid email profile | Listed in the dashboard but not usable yet: the dashboard has no field for the realm base URL, and the token exchange is not implemented. |
linkedin | openid profile email | Sign In with LinkedIn using OpenID Connect. | |
| Microsoft | microsoft | openid profile email offline_access | Email is mail, or userPrincipalName when mail is empty. |
| Notion | notion | none | The user is identified by the Notion integration (bot_id); email comes from the workspace owner. |
| Slack | slack | openid profile email | Sign in with Slack (OpenID Connect). |
| Spotify | spotify | user-read-email user-read-private | |
| Twitch | twitch | user:read:email | Without an email the user gets login@twitch.noreply. |
| Twitter / X | twitter | tweet.read users.read | X does not return an email address, so every X user gets username@twitter.noreply. Use twitter, not x. |
| WorkOS | workos | none | WorkOS SSO. |
| Zoom | zoom | user: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.
- Open Project → Auth → Providers and expand the provider.
- Copy the Redirect URI. It is read-only and always has this form:
https://{slug}.wowsqlconnect.com/auth/v1/oauth/{provider}/callback - 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.
- Paste the provider's Client ID and Client Secret into the dashboard. Leave no leading or trailing spaces.
- 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).
- Click Enable. Saving always enables the provider.
- 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:
| Switch | Effect on OAuth |
|---|---|
| Allow new users to sign up | When off, OAuth sign-in only works for users who already exist; a new provider account gets 403 Signups disabled. |
| Allow manual linking | Required for POST /oauth/{provider}/link. Off by default. |
| Confirm email | Does 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
| Step | Who | What happens |
|---|---|---|
| 1 | Your app | Calls GET /oauth/{provider}?frontend_redirect_uri=... with your anon key. The response contains authorization_url. This call does not redirect. |
| 2 | Your app | Sends the browser to authorization_url. |
| 3 | User | Signs in at the provider and approves the requested scopes. |
| 4 | Provider | Redirects the browser to https://{slug}.wowsqlconnect.com/auth/v1/oauth/{provider}/callback?code=...&state=.... No API key is needed on this URL. |
| 5 | WoWSQL Auth | Exchanges the code, reads the profile, finds the user by provider account ID or email (or creates one), and opens a session. |
| 6 | WoWSQL Auth | Redirects to your frontend_redirect_uri with the session in the query string: access_token, refresh_token, token_type, expires_in, user_id, email. |
| 7 | Your app | Reads 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.
Link a provider to the signed-in user
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
| SDK | Start sign-in | Exchange a code | Link identity |
|---|---|---|---|
| TypeScript | getOAuthAuthorizationUrl(provider, redirectUri) | exchangeOAuthCallback(provider, code, redirectUri?) | linkOAuthIdentity(provider, {code, redirectUri}, accessToken?) |
| Python | get_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) |
| Java | getOAuthAuthorizationUrl(provider[, redirectUri]) | exchangeOAuthCallback(provider, code[, redirectUri]) | linkOAuthIdentity(provider, code, redirectUri) |
| Kotlin | getOAuthAuthorizationUrl(provider, redirectUri = null) | exchangeOAuthCallback(provider, code, redirectUri = null) | - |
| Go | GetOAuthAuthorizationURL(provider, redirectURI ...string) | ExchangeOAuthCallback(provider, code, redirectURI ...string) | - |
| Flutter / Dart | getOAuthAuthorizationUrl(provider:, redirectUri:) | exchangeOAuthCallback(provider:, code:, redirectUri:) | - |
| Rust | get_oauth_authorization_url(provider, Option<&str>) | exchange_oauth_callback(provider, code, Option<&str>) | - |
| PHP | getOAuthAuthorizationUrl($provider, $redirectUri = null) | exchangeOAuthCallback($provider, $code, $redirectUri = null) | - |
| Ruby | get_oauth_authorization_url(provider:, redirect_uri: nil) | exchange_oauth_callback(provider:, code:, redirect_uri: nil) | - |
| Swift | getOAuthAuthorizationUrl(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
| Status | Message | Meaning |
|---|---|---|
| 401 | Missing API key... | The start, exchange or link call was sent without the apikey header. |
| 400 | OAuth provider {provider} not configured | No configuration saved for this provider in this project. |
| 400 | OAuth provider {provider} not enabled / not configured/enabled | The provider is disabled or missing (callback and link return the combined message). |
| 400 | Keycloak requires 'keycloak_base_url' in provider config JSON | Keycloak is not usable yet (see Supported Providers). |
| 400 | Token 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. |
| 400 | User-info fetch failed (...) | The provider rejected the profile request, usually because of missing scopes. |
| 400 | No email from OAuth provider | The provider returned no email address. Add the email scope or make the email visible in the provider account. |
| 403 | Signups disabled | New user, and Allow new users to sign up is off. |
| 403 | Account banned / Account inactive | The matching user is banned or deactivated. |
| 422 | validation error | The callback was called without code, for example after the user cancelled at the provider. |
| 401 | Bearer token required / Invalid or expired session | Link called without a valid user access token. |
| 403 | Identity linking disabled | Link called while Allow manual linking is off. |
| 400 | OAuth email must match account email | Link: the provider account uses a different email. |
| 409 | OAuth identity already linked to another user | Link: 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, includinghttps, the project slug and the provider ID, with no trailing slash. - The start call returns 401. Send the anon key in the
apikeyheader. 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 passfrontend_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.noreplyor@twitter.noreplyaddresses. 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
Refererheader. - Only pass landing URLs on domains you control, and set the Frontend URL so the default is never
localhostin 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_loginanduser.oauth_link.