OAuth 2.0
Allegro lets your service sign a member in through Allegro and receive that member's session token — the same RS256 JWT the browser SDK uses to act as the member. This is the flow to reach for when you run your own application (a mobile app, a companion site, a partner portal) and want "Sign in with Allegro" without building your own login for the tenant's audience.
Allegro implements the standard OAuth 2.0 Authorization Code flow, with PKCE supported and strongly recommended. PKCE is optional by default; a tenant can require it for your client (see PKCE). The member authenticates on an Allegro-hosted page, which emails a sign-in link and a one-time code in the same message — the member can either click the link or type the code back into the page — alongside any social providers the tenant has enabled. Your backend then exchanges a short-lived code for the member's JWT.
A member who opens the emailed link in a different browser than the one holding the authorization request would otherwise be stranded, because the request lives in the original tab. Offering the one-time code gives them a way to finish in the tab they started in.
Only a short-lived, single-use authorization code passes through the redirect URL, and that code is useless without your client secret (and your PKCE verifier when PKCE is in use). The JWT itself is only ever returned to your backend over a direct server-to-server request.
Before you begin
A tenant administrator registers an OAuth client for you from the tenant's admin settings and gives you:
| Value | Description |
|---|---|
client_id | Public identifier for your integration. Safe to include in browser redirects. |
client_secret | Secret shown once at creation. Store it securely on your backend; it is never exposed in the browser. |
| Redirect URI(s) | The exact URLs Allegro is allowed to send the member back to. |
| Allowed origins | The exact browser origins allowed to call the session probe. Only needed for silent sign-in. |
Redirect URIs are matched exactly — no wildcards, no trailing-slash
flexibility, no query-string variance. Each URI you use must be registered with
the client ahead of time and must be HTTPS (except http://localhost and
http://127.0.0.1 during local development).
A tenant administrator can edit a client after creation — updating its redirect URIs, renaming it, enabling or disabling it, and toggling whether PKCE is required — from the tenant's OAuth client settings. The client secret is not editable; it can only be regenerated (which invalidates the previous secret).
Everything below happens against the tenant's canonical domain, for example
https://<your-tenant-domain>. You can confirm the endpoints programmatically
from the discovery documents, and you should — see
Canonical host and issuer for why only one of a
tenant's hostnames serves these endpoints.
If your integration also needs to know whether a visitor is already signed in — without sending them through the sign-in page — see Silent sign-in.
The flow
Your app / backend Member's browser Allegro (tenant domain)
│ │ │
│ 1. redirect to authorize │ │
│─────────────────────────────>│ GET /oauth/authorize │
│ │──────────────────────────────>│ validate client_id
│ │ 2. member signs in │ + redirect_uri
│ │<───────── login page ──────────│
│ │ │
│ │ 3. redirect back with code │
│ redirect_uri?code=&state= │<──────────────────────────────│
│<─────────────────────────────│ │
│ │ │
│ 4. POST /oauth/token (server-to-server, no browser) │
│───────────────────────────────────────────────────────────────>│ verify secret,
│ │ code + PKCE
│ 5. { access_token: <member JWT>, token_type, expires_in } │
│<───────────────────────────────────────────────────────────────│
Step 1 — Redirect the member to the authorize endpoint
GET https://<your-tenant-domain>/oauth/authorize
Generate a random state value and store it so you can verify it when the
member returns. If you are using PKCE — recommended, and required when the
tenant has enabled it for your client — generate the code verifier and challenge
first; see PKCE.
Send the member's browser to the authorize endpoint with the following query parameters:
| Parameter | Required | Description |
|---|---|---|
client_id | Yes | Your client identifier. |
redirect_uri | Yes | One of your registered redirect URIs. Matched exactly. |
response_type | Yes | Must be code. |
code_challenge | If enabled | The PKCE challenge (see PKCE). Required unless PKCE is disabled for your client. |
code_challenge_method | If enabled | Must be S256 when code_challenge is present. |
state | No | An opaque value round-tripped back to you unchanged. Use it for CSRF protection. |
scope | No | Reserved for future use. |
const params = new URLSearchParams({
client_id: CLIENT_ID,
redirect_uri: 'https://your-app.example.com/callback',
response_type: 'code',
// Include code_challenge / code_challenge_method only when using PKCE.
code_challenge: codeChallenge,
code_challenge_method: 'S256',
state,
});
const authorizeUrl = `https://<your-tenant-domain>/oauth/authorize?${params}`;
// Redirect the member's browser to authorizeUrl.
Allegro renders a sign-in page hosting the tenant's configured login methods. If
the member already has a valid session on the tenant domain, they see a
"Continue as {email}" prompt instead. Either way the member takes one explicit
action, after which Allegro redirects the browser back to your redirect_uri.
If the client_id is unknown or disabled, or the redirect_uri is not on the
client's allowlist, Allegro renders an error page and does not redirect.
This is deliberate — it prevents an attacker from using a malformed request to
bounce a code to an unapproved destination. Double-check that the exact redirect
URI you send is registered.
Step 2 — Handle the redirect back
Once the member authenticates, Allegro redirects the browser to:
https://your-app.example.com/callback?code=<authorization-code>&state=<state>
Verify that state matches the value you generated in step 1, then take the
code. The code is single-use and expires 10 minutes after it is issued.
Exchange it as soon as the member lands on your callback; nothing is gained by
holding it.
Step 3 — Exchange the code for a token
From your backend (never the browser — this request carries your client secret), POST to the token endpoint:
POST https://<your-tenant-domain>/oauth/token
Content-Type: application/x-www-form-urlencoded
| Parameter | Required | Description |
|---|---|---|
grant_type | Yes | Must be authorization_code. |
code | Yes | The authorization code from step 2. |
redirect_uri | Yes | The same redirect URI used in step 1. Matched exactly. |
client_id | Yes | Your client identifier. |
client_secret | Yes | Your client secret. |
code_verifier | If enabled | The PKCE verifier (see PKCE). Required whenever you sent a code_challenge in step 1. |
The only supported client authentication method is client_secret_post: send
client_id and client_secret as form fields. An HTTP Authorization: Basic
header is not read, so a library configured for client_secret_basic will fail
with invalid_client.
curl https://<your-tenant-domain>/oauth/token \
-d grant_type=authorization_code \
-d code="<authorization-code>" \
-d redirect_uri="https://your-app.example.com/callback" \
-d client_id="<your-client-id>" \
-d client_secret="<your-client-secret>" \
-d code_verifier="<your-code-verifier>"
On success you receive the member's JWT:
{
"access_token": "<member-jwt>",
"token_type": "Bearer",
"expires_in": 31536000
}
| Field | Description |
|---|---|
access_token | The member's RS256 JWT. Use it as a Bearer token. |
token_type | Always Bearer. |
expires_in | Token lifetime in seconds. |
Those three fields are the whole response. There is no refresh_token and no
id_token — start a new authorization request when the token expires, and read
the member's identity from the JWT's own claims.
The token represents a fresh, independent session minted for your
integration. It appears among the member's sessions and can be revoked without
affecting the member's own browser session. The token also carries an azp
(authorized party) claim set to your client_id, so a partner-issued token is
distinguishable from a browser SDK session token.
Step 4 — Use the token
The access_token is a standard Allegro member JWT. Send it as a bearer token
to the Browser SDK API, or verify it yourself against the
tenant's public keys. See the JWT Verification guide for the token
structure, the JWKS endpoint, and verification examples.
What a partner token cannot do
A token carrying azp acts as the member but may not manage the session it
rides on. Two Session API endpoints refuse it:
| Endpoint | Why it is refused |
|---|---|
DELETE /api/session | Signing out would end the member's own session on the tenant, not just your integration. |
POST /api/session/refresh | The re-minted token would drop azp, turning a partner token into a first-party one. |
Both answer 403 with the error code PARTNER_TOKEN_NOT_PERMITTED:
{
"data": {
"message": "This endpoint cannot be called with a partner-issued token.",
"code": "PARTNER_TOKEN_NOT_PERMITTED"
},
"status": 403
}
Every other endpoint that hands you back a JWT — form submission, setting a user
attribute — re-mints it with azp intact, so a partner token stays a partner
token for its whole life. Get a new token by running the authorization flow
again.
Discovery
Allegro advertises its endpoints in two discovery documents, so OAuth-aware libraries can configure themselves automatically:
| Document | Specification |
|---|---|
/.well-known/openid-configuration | OpenID Connect Discovery 1.0 |
/.well-known/oauth-authorization-server | RFC 8414, OAuth 2.0 Authorization Server Metadata |
Fetch whichever one your library reaches for. They are generated from the same
source and agree on every field except subject_types_supported, which only the
OpenID document carries.
GET https://<your-tenant-domain>/.well-known/oauth-authorization-server
{
"issuer": "https://<your-tenant-domain>",
"authorization_endpoint": "https://<your-tenant-domain>/oauth/authorize",
"token_endpoint": "https://<your-tenant-domain>/oauth/token",
"jwks_uri": "https://<your-tenant-domain>/.well-known/jwks.json",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code"],
"token_endpoint_auth_methods_supported": ["client_secret_post"],
"code_challenge_methods_supported": ["S256"],
"prompt_values_supported": ["none"]
}
Both documents are cacheable for a day (Cache-Control: public, max-age=86400).
Every value is claimed on behalf of something the server really implements.
response_types_supported is ["code"] alone: the implicit flow is not
supported, and response_type=token is rejected. prompt_values_supported is
["none"], covered in Silent sign-in.
id_token is issued, and no signing algorithm is advertisedAllegro is an OAuth 2.0 authorization server that serves the OpenID discovery
document for the convenience of clients that look there first. It issues no
id_token, so both documents deliberately omit
id_token_signing_alg_values_supported even though OpenID Connect Discovery
lists that field as required. A strict OIDC client library may reject the
document for that reason; treat Allegro as a plain OAuth 2.0 server and read the
member's identity from the access token instead. The algorithm behind the tokens
Allegro does issue is discoverable from the alg on each key at
/.well-known/jwks.json — see the JWT Verification guide.
Canonical host and issuer
A tenant is reachable at more than one hostname: its platform subdomain, its
per-property hostnames, and its custom domain when one is configured. Only one
of them — the canonical domain, which is the custom domain when set and the
platform subdomain otherwise — serves the OAuth and discovery endpoints. That
same URL is the issuer in both discovery documents and the iss and aud
claims in every token Allegro issues, which is what lets a client verify a token
against the document it discovered.
Requests that arrive on another one of the tenant's hostnames are handled three different ways:
| Request | Response |
|---|---|
GET /oauth/authorize, GET /.well-known/openid-configuration, GET /.well-known/oauth-authorization-server | 302 to the same path on the canonical host |
GET /.well-known/jwks.json | Served directly on whichever host asked |
POST /oauth/token, POST /oauth/authorize | 400 {"error":"invalid_request"} |
The redirect is a 302 rather than a 301 on purpose: a tenant's custom domain
can change, and browsers cache a 301 indefinitely. Do not rewrite it as a
permanent redirect in your own client.
The key set is the exception — it is served wherever it is requested rather
than redirected to the canonical host. The discovery documents are pinned to the
canonical host because they name it as the issuer, but the key set names no
host, so it is safe to answer everywhere. This matters because a verifier that
follows the redirect can lose the response body it fetched the URL for, and some
verifiers don't follow redirects at all. You can still read the jwks_uri from
a discovery document and use the canonical-host URL it gives you — that keeps
working.
POSTs are refused rather than redirected because following a redirect downgrades
the request to GET and drops the body, which would turn your code exchange
into a silent failure. The refusal names the host you should be using:
{
"error": "invalid_request",
"error_description": "This endpoint is served from <your-tenant-domain>."
}
If you see this, you hard-coded the wrong hostname. Read the endpoints from a discovery document instead.
If you pin iss when verifying tokens, a tenant that adds, changes, or removes
its custom domain changes the value you are pinning, and every token
verification starts failing. Operators should tell integrators before making
that change; on your side, prefer reading issuer from discovery over
hard-coding it.
Errors
The token endpoint returns a 400 response with an OAuth-style error code:
| Error | Cause |
|---|---|
invalid_client | The client_id is unknown or disabled, or the client_secret is wrong. |
invalid_grant | The code is unknown, expired, or already used; the redirect_uri does not match the one bound to the code; or the code_verifier fails PKCE validation. |
{ "error": "invalid_grant" }
Because codes are single-use and short-lived, a retried or double-submitted
exchange returns invalid_grant. Start a new authorization request if that
happens. Two concurrent exchanges of the same code are resolved the same way:
exactly one gets a token and the other gets invalid_grant.
The authorize endpoint reports its errors differently. Once the client_id and
redirect_uri are known-good, a malformed request is reported back to your
callback as a query parameter, echoing state:
https://your-app.example.com/callback?error=invalid_request&state=<state>
| Error | Cause |
|---|---|
invalid_request | An unsupported prompt value, or a missing code_challenge on a client that requires PKCE. |
login_required | A prompt=none request on a browser with no signed-in session. See Silent sign-in. |
An unknown or disabled client_id, or a redirect_uri that is not registered,
never redirects at all — see the warning in
step 1.
PKCE
PKCE (S256) is strongly recommended. It is optional by default; a tenant
administrator can require it for an individual client from the client's
settings. Leaving it optional accommodates a server-side integration that
authenticates with its client secret and cannot generate a verifier. Regardless
of the requirement, if you send a code_challenge it is always enforced at the
token endpoint (you cannot start a PKCE-protected flow and then drop the
verifier), so we recommend using PKCE whenever your integration can.
When you use PKCE, generate a random code verifier and derive its code challenge before you redirect the member in step 1.
- The verifier is a random string between 43 and 128 characters.
- The challenge is the base64url-encoded SHA-256 hash of the verifier, with
padding removed. The
S256method is required.
import crypto from 'node:crypto';
const base64url = (buffer) =>
buffer
.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
const codeVerifier = base64url(crypto.randomBytes(32));
const codeChallenge = base64url(
crypto.createHash('sha256').update(codeVerifier).digest(),
);
Send code_challenge (with code_challenge_method=S256) on the authorize
request in step 1, and
keep the code_verifier on your backend (or in secure session storage) to send
on the token exchange in step 3.
Security notes
- PKCE (
S256) is strongly recommended. It is optional by default; a tenant can require it for your client. Whenever acode_challengeis sent, a matchingcode_verifieris required at the token endpoint. - Authorization codes are single-use, expire in 10 minutes, and are bound to the
client, the exact
redirect_uri, and the PKCE challenge. - Redirect URIs use exact-match allowlisting — register every URI you intend to use.
- Always send and verify
stateto protect against CSRF. - Keep the
client_secretandcode_verifieron your backend. The only value that legitimately passes through the browser is the authorization code. - Read the endpoints from a discovery document rather than hard-coding them, so a tenant's move to a custom domain does not break your integration.
Related
- Silent sign-in — Find out whether a visitor is already signed in, without sending them to the sign-in page
- JWT Verification — Token structure, JWKS, and verification examples
- Member Authentication — How Allegro sessions work