Social Authentication
Gate Identity supports OAuth 2.0 / OIDC social login for your end-users. You can let users register and sign in with their existing accounts from 10 major providers, with zero infrastructure to maintain.
Supported Providers
| Provider | Provider ID |
|---|---|
| GitHub | github |
google | |
| GitLab | gitlab |
| Microsoft | microsoft |
| Discord | discord |
facebook | |
linkedin | |
| Slack | slack |
| Twitch | twitch |
| Spotify | spotify |
How It Works
A social login is a round-trip through the end user's browser — they leave for GitHub or Google and come back — but the session it produces is needed by your backend. Gate bridges the two, and the session token never travels through the browser:
- Your backend starts a login flow with your Gate key and submits the chosen provider to it.
- Gate answers with an address at the provider; your backend redirects the user's browser there.
- The user signs in at the provider. The provider sends the browser back to Gate.
- Gate takes the session for itself and redirects the browser to one of your registered return URLs, carrying a short-lived, one-time
code— and your ownstate, if you sent one. - Your backend sends that
code, with your Gate key, toPOST /v1/gate/auth/oidc/exchangeand receives thesession_tokenand the session.
The token is never in a URL. A URL ends up in browser history, in your server's access logs and in Referer headers; a session token there is a session handed to all three. What travels through the browser is a code that is worth nothing without a Gate key of the same project, works once, and expires after about a minute.
This is also what makes social login usable from a server-side integration at all. Without a registered return URL, Gate has nowhere to send the browser back to, and the only client that can finish a social login is one that keeps the cookie Kratos sets when the provider is submitted — a native app, not your backend. Registering a return URL is what turns the above on.
Verne's post-registration hook fires for both password and social registrations, so your tenant's user record is provisioned automatically, and a person who signs in with a social account for the first time is recorded as a user of the project whose key started the login.
Enabling Providers in the Dashboard
By default, no social providers are visible to your end-users. You control the list from Dashboard → Users & sign-in → Authentication.
Each provider appears as a card with a toggle. Turning a provider on makes it available in your end-users' login and registration UI. You can enable or disable providers at any time without redeploying anything.
Note: Verne runs a shared Kratos instance. Provider credentials (Client ID / Secret) are configured at the platform level. Your toggle controls whether the button appears in your tenant's UI — you do not need to register OAuth apps for your end-users' social login.
API: Manage Social Providers
All provider management endpoints use your Gate API key in the Authorization header — the same key you use for identity management.
Get enabled providers
GET /v1/gate/settings/oidc-providers
Returns the current configuration for all 10 providers, as a JSON array.
Response (200 OK)
[
{ "provider": "github", "enabled": true },
{ "provider": "google", "enabled": true },
{ "provider": "gitlab", "enabled": false },
{ "provider": "microsoft", "enabled": false },
{ "provider": "discord", "enabled": false },
{ "provider": "facebook", "enabled": false },
{ "provider": "linkedin", "enabled": false },
{ "provider": "slack", "enabled": false },
{ "provider": "twitch", "enabled": false },
{ "provider": "spotify", "enabled": false }
]
Example
curl https://api.vernesoft.com/v1/gate/settings/oidc-providers \
-H 'Authorization: Bearer vrn_9f8a7...'
Update enabled providers
PUT /v1/gate/settings/oidc-providers
Sets the enabled flag for one or more providers. Any provider not listed in the request body is left unchanged.
Request body
{
"providers": [
{ "provider": "github", "enabled": true },
{ "provider": "google", "enabled": true },
{ "provider": "discord", "enabled": false }
]
}
Response (200 OK)
{ "status": "ok" }
Read the list back with GET to see the result. A provider id that is not one of the ten above is refused with 400.
Example
curl -X PUT https://api.vernesoft.com/v1/gate/settings/oidc-providers \
-H 'Authorization: Bearer vrn_9f8a7...' \
-H 'Content-Type: application/json' \
-d '{ "providers": [{ "provider": "github", "enabled": true }] }'
Public Endpoint: Fetch Active Providers for a Tenant
Your end-user auth UI can query which providers are currently enabled for a given tenant, without authentication.
GET /public/gate/providers/{tenant_id}
This is the endpoint you call from your login/registration page before rendering the social buttons.
Response (200 OK)
{
"providers": ["github", "google"]
}
Example
curl https://api.vernesoft.com/public/gate/providers/3f1c2a9e-5b7d-4e8a-9c01-2d6f8b4a7e10
Use the returned list to decide which social login buttons to render in your UI. This endpoint requires no authentication.
Return URLs
A return URL is an address on your backend that receives the user after a social login. You register them per project, with the Gate key of that project.
GET /v1/gate/settings/return-urls
PUT /v1/gate/settings/return-urls
PUT replaces the whole list. The first address is the default, used by a login that does not name one. An empty list switches the feature off for the project.
curl -X PUT https://api.vernesoft.com/v1/gate/settings/return-urls \
-H 'Authorization: Bearer vrn_9f8a7...' \
-H 'Content-Type: application/json' \
-d '{ "return_urls": ["https://app.example.com/auth/callback", "http://localhost:3000/auth/callback"] }'
# → 200 { "return_urls": ["https://app.example.com/auth/callback", "http://localhost:3000/auth/callback"] }
The answer lists the addresses as they will be used: the host is lower-cased, the default port and dot-segments are dropped, an empty path is /. At most 10 addresses per project.
An address is matched exactly — scheme, host, port and path must all be equal. There is no prefix matching, so https://app.example.com/auth/callback/extra is not covered by https://app.example.com/auth/callback, and neither is the same path on another port. Register every address you use, one by one. What is refused, with 400 invalid_payload naming the entry:
| Refused | Why |
|---|---|
http:// for anything but localhost and 127.0.0.1 | A sign-in code must not cross the network unencrypted |
| A query string or a fragment, even an empty one | Gate adds its own code (and state) to the query |
user:pass@ — or an @ anywhere | Credentials in a URL, and the usual way to disguise one host as another |
A wildcard (*) | Each address matches only itself |
A bare IP address (except 127.0.0.1), IPv6, a relative address, javascript:, a backslash, whitespace, non-ASCII characters | Write an internationalised host in its punycode form |
The same project-level list drives both GET /v1/gate/auth/login and GET /v1/gate/auth/registration.
Integrating Social Login in Your App
Everything below runs on your backend. The only thing the user's browser does is follow redirects.
Security: Your Gate API key must stay on your server. The
codethat reaches your callback is single-use and expires after about a minute, but it is still a credential: exchange it at once, do not log it, and redirect the user on so it does not stay in the address bar.
1. Start the flow and send the user to the provider
GET /v1/gate/auth/login takes two optional parameters, which matter once the project has a return URL:
| Parameter | Meaning |
|---|---|
return_to | One of the project's registered return URLs, exactly as registered. Optional — the first one is used when it is absent. Anything else is 400 invalid_payload. |
state | An opaque value of yours, 1–256 characters of letters, digits, -, ., _ and ~. It is returned unchanged on the redirect. |
Always send a state. Bind it to the browser that started the login (a session cookie) and compare it at your callback. Without that, an attacker can complete a login of their own and send your user the link to your callback, which would sign the user in as the attacker.
The flow lists only the providers your tenant enabled. To use one, POST to the flow's ui.action — a public route; no key, no cookie, no CSRF token — and Kratos answers 422 with the address to send the browser to. That 422 is the expected answer, not an error:
# 1. Start the flow with your Gate key.
curl 'https://api.vernesoft.com/v1/gate/auth/login?state=7c2f1e9a-4b8d-4f06-a3d1-5e9b0c8d2a14' \
-H 'Authorization: Bearer vrn_9f8a7...'
# → 200 { "id": "…", "type": "api", "ui": { "action": "https://api.vernesoft.com/auth/self-service/login?flow=…", "nodes": [ … ] }, … }
# 2. Submit the provider to the flow's action URL.
curl -X POST 'https://api.vernesoft.com/auth/self-service/login?flow=…' \
-H 'Content-Type: application/json' \
-d '{ "method": "oidc", "provider": "github" }'
# → 422 { "redirect_browser_to": "https://github.com/login/oauth/authorize?…", … }
Redirect the user's browser to redirect_browser_to. A social sign-up is the same with GET /v1/gate/auth/registration and POST …/self-service/registration?flow=….
2. The user comes back to your return URL
After the provider, Gate sends the browser to your registered address:
https://app.example.com/auth/callback?code=gxc_9d3e…&state=7c2f1e9a-4b8d-4f06-a3d1-5e9b0c8d2a14
There is no session token in it — only the code, and your state.
3. Your backend exchanges the code
POST /v1/gate/auth/oidc/exchange
curl -X POST https://api.vernesoft.com/v1/gate/auth/oidc/exchange \
-H 'Authorization: Bearer vrn_9f8a7...' \
-H 'Content-Type: application/json' \
-d '{ "code": "gxc_9d3e…" }'
# → 200 { "session_token": "ory_st_…", "session": { "identity": { "id": "…", … }, … } }
The answer has the shape a password login returns. The session_token is what GET /v1/gate/auth/session and the settings flow take as X-Session-Token; keep it on your server.
The code is bound to the project whose key started the login:
- it works once — a second exchange is
404; - it lives about 60 seconds — then it is
404; - a key of any other project, in your account or another, gets the same
404as a code that never existed, and does not use the code up.
403 means the key is not a Gate key; 400 means the body is not { "code": "…" }.
The whole path, in Node.js
import express from 'express';
import session from 'express-session';
import { randomUUID } from 'node:crypto';
const API = 'https://api.vernesoft.com';
const gate = { Authorization: `Bearer ${process.env.VERNE_GATE_KEY}` };
const app = express();
app.use(session({ secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false }));
// 1. The user clicks "Sign in with GitHub".
app.get('/login/github', async (req, res) => {
// Bind the login to this browser.
req.session.loginState = randomUUID();
const flow = await fetch(`${API}/v1/gate/auth/login?state=${req.session.loginState}`, { headers: gate })
.then(r => r.json());
// Submit the provider. The 422 is the answer, not a failure.
const submitted = await fetch(flow.ui.action, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ method: 'oidc', provider: 'github' }),
}).then(r => r.json());
res.redirect(submitted.redirect_browser_to);
});
// 2. Gate sends the browser back here — your registered return URL.
app.get('/auth/callback', async (req, res) => {
const { code, state } = req.query;
if (!code || !state || state !== req.session.loginState) {
return res.status(400).send('Sign-in could not be verified. Start again.');
}
delete req.session.loginState;
// 3. Trade the one-time code for the session.
const r = await fetch(`${API}/v1/gate/auth/oidc/exchange`, {
method: 'POST',
headers: { ...gate, 'Content-Type': 'application/json' },
body: JSON.stringify({ code }),
});
if (!r.ok) return res.status(400).send('Sign-in expired. Start again.');
const { session_token, session: kratosSession } = await r.json();
req.session.userId = kratosSession.identity.id;
req.session.gateSessionToken = session_token; // server-side only
res.redirect('/'); // takes the code out of the address bar
});
What a failed return looks like
A code exists only after a sign-in has completed, so a user who abandons it at the provider never reaches your callback with one. If anything is wrong when the browser comes back to Gate — the link was already used, has expired, or the return URL was removed from the project in the meantime — the browser is not redirected; it receives an error response:
{ "error": { "code": "invalid_return", "message": "This sign-in link is not valid, has expired, or was already used. Start the sign-in again.", "request_id": "req_…" } }
A person who signs in with an account that belongs to another tenant, or an account that is not an end user, is refused with 403 forbidden in the same way — Gate will not hand your backend a session for someone else's user:
{ "error": { "code": "forbidden", "message": "This sign-in cannot be used with this project.", "request_id": "req_…" } }
One social account, one tenant
A social account — one particular Google or GitHub identity — can be an end user of only one tenant. The first tenant whose login it completes gets it. If it later signs in through a second tenant's key, that login ends with the 403 forbidden above, no code is issued, and the first tenant's end user is not affected.
This is about the account, not the email address. A different social account with the same email address is a different person and signs in to the second tenant as its own new end user. The same holds for a password end user: the same address can be an end user of two tenants, each a separate person (see Limits).
Within one tenant, an address is one end user. A password registration with the address of a social end user of the same tenant fails with message 4000007 instead of creating a second person.
Callback URL pattern
The address the provider sends the browser to is handled by Verne and is the same for everyone:
https://api.vernesoft.com/auth/self-service/methods/oidc/callback/{provider}
You do not need to register your own OAuth app. Verne manages the OAuth credentials for all supported providers on behalf of all tenants. The return URLs above are a different thing: where you receive the user after Verne is done.
Social Login for Verne Tenants
In addition to end-user social auth, Verne tenants can register and log in to the Verne Dashboard itself using GitHub or Google.
This uses dedicated provider IDs (github-tenant, google-tenant) separate from the end-user providers, so the same GitHub/Google account can simultaneously be both a Verne tenant and an end-user of a tenant's application without any credential conflicts.
The GitHub and Google buttons appear automatically on the registration and login pages at app.vernesoft.com when the corresponding OAuth credentials are configured at the platform level.
Traits populated via social login
When a tenant registers via GitHub or Google, Gate automatically populates:
| Trait | GitHub source | Google source |
|---|---|---|
email | email claim | email claim |
display_name | name → login → "GitHub User" | name → given_name → "Google User" |
locale | en (default) | locale claim (trimmed to 2 chars, e.g. en-US → en) |