Silent sign-in
A visitor lands on your site. They already signed in to Allegro somewhere else — another site under the same parent domain, or an earlier visit — and you want to greet them as a member rather than showing them a sign-in prompt they do not need. Silent sign-in answers "is anyone signed in on this browser?" without interrupting the visitor.
The answer, when it is yes, is a short-lived authorization code that your backend redeems for the member's JWT through the normal OAuth token endpoint. Read the OAuth 2.0 guide first — everything here builds on the client registration and the code exchange described there.
Two mechanisms
| Mechanism | How it runs | Use it when |
|---|---|---|
| Session probe | Cross-origin fetch from your page. No navigation. | Your site and the Allegro host are on different origins. This is the usual choice. |
prompt=none | Top-level redirect through /oauth/authorize. | You already own a full-page redirect in your flow, or your page is same-origin with the Allegro host. |
Both depend on the same prerequisite, and it is the one thing that most often makes silent sign-in look broken.
Prerequisite: the session cookie
Allegro's browser SDK stores the member's session JWT in a cookie named
_ALLEGROT, written client-side with SameSite=Lax. Silent sign-in works by
reading that cookie on the Allegro host, so the browser has to be willing to
send it there.
A SameSite=Lax cookie is only attached to a cross-origin request when the two
hosts are same-site — that is, when they share a registrable domain. And the
cookie is only visible to the Allegro host at all if it was written with a
Domain covering both hosts.
That means an operator must set the tenant's Cookie Domain to the shared
parent domain. With Cookie Domain set to .acme.com, a member who signs in on
community.acme.com has a cookie the browser will send to members.acme.com,
and silent sign-in works. Left blank, the cookie is scoped to the exact hostname
that served the SDK, and the Allegro host never sees it.
If the probe returns unauthenticated for a visitor you know is signed in,
check Cookie Domain before anything else. There is no error and no warning — the
browser simply does not send a cookie, and Allegro correctly reports that nobody
is signed in on this browser.
Ask the operator to set Organization Settings → Browser → Cookie Domain to the parent domain shared by your site and the Allegro host. See Browser Settings.
Silent sign-in therefore cannot work at all when Allegro is served from a domain
unrelated to your site — app.example.com calling members.acme.com will get
unauthenticated forever, no matter who is signed in. Those visitors need the
interactive flow.
The session probe
GET https://<your-tenant-domain>/oauth/session?client_id=<your-client-id>
The probe is read-only in every path. It creates no member, mints no session, and modifies nothing — a visitor who loads your page and leaves has changed nothing in Allegro.
The request
Send it as a credentialed cross-origin fetch. The browser supplies Origin
itself; you cannot set it.
const response = await fetch(
`https://<your-tenant-domain>/oauth/session?client_id=${CLIENT_ID}`,
{ credentials: 'include' },
);
const session = await response.json();
if (session.status === 'authenticated') {
// Hand session.code to your own backend to redeem.
await fetch('/allegro/adopt-session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code: session.code }),
});
}
credentials: 'include' is required. Without it the browser sends no cookie and
you will always be told nobody is signed in.
Call the tenant's canonical domain. Every other hostname answers a GET
with a 302, and a redirect carries no CORS headers, so the fetch fails as a
CORS error rather than following it. See
Canonical host and issuer.
The responses
Somebody is signed in:
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
Access-Control-Allow-Origin: https://community.acme.com
Access-Control-Allow-Credentials: true
Vary: Origin
{
"status": "authenticated",
"code": "aB3…64-characters…9zQ",
"expires_in": 30
}
Nobody is signed in:
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
Access-Control-Allow-Origin: https://community.acme.com
Access-Control-Allow-Credentials: true
Vary: Origin
{ "status": "unauthenticated" }
Both are 200. Nobody being signed in is a normal answer, not an error, so do
not treat a non-authenticated status as a failure to retry.
Cache-Control: no-store is set on both, because the first response carries a
credential and the second would otherwise be cached as a permanent "no".
| Field | Present when | Description |
|---|---|---|
status | Always | authenticated or unauthenticated. |
code | authenticated | A single-use authorization code. Redeem it immediately. |
expires_in | authenticated | Seconds the code stays redeemable. Always 30. |
Why a code and not a token
Anything your page can read, a script on your page can steal. A member JWT is a bearer credential: whoever holds it can act as the member until it expires. A 30-second single-use authorization code cannot be spent by whoever holds it, because redeeming it requires the client secret — which only your backend has. So the probe hands the browser the weakest possible thing that still gets your backend a token.
Registered origins
The probe only answers a request whose Origin header is on the calling
client's allowed origins list. Origins are matched byte for byte: no
wildcards, no suffix matching, no trailing slash. Every site that probes needs
its own entry, and a parent domain does not cover its subdomains.
A refusal is a 403 with no CORS headers at all:
HTTP/1.1 403 Forbidden
Content-Type: application/json
{ "error": "invalid_request" }
Because the CORS headers are absent, the browser surfaces this to your
JavaScript as a generic CORS failure and your page cannot tell what was wrong.
That is deliberate — a distinguishable response would let anyone enumerate which
clients and origins exist. The same 403 covers all of it:
- No
Originheader on the request - No
client_id, or aclient_idthat does not exist - A client that is disabled
- A client with no registered origins
- An origin that is not registered against this client
- An origin that is a suffix of a registered one
When you hit this, the fix is on the Allegro side: ask the operator to add your exact origin to the client's allowed origins.
Preflight
A plain credentialed GET with no custom headers is a simple request and is not
preflighted, so most integrations never see an OPTIONS. If yours does add a
header that triggers one, the endpoint answers it:
OPTIONS /oauth/session?client_id=<your-client-id> HTTP/1.1
Host: <your-tenant-domain>
Origin: https://community.acme.com
Access-Control-Request-Method: GET
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://community.acme.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET
Access-Control-Allow-Headers: Content-Type
Access-Control-Max-Age: 600
Vary: Origin
Access-Control-Allow-Origin always echoes the exact requesting origin rather
than *, which credentialed requests forbid. An unregistered origin gets the
same headerless 403 as above.
Redeeming the code
Hand the code to your own backend and exchange it exactly as you would a code
from the interactive flow — same endpoint, same grant_type, same client
credentials:
curl https://<your-tenant-domain>/oauth/token \
-d grant_type=authorization_code \
-d code="<probe-code>" \
-d redirect_uri="<your-first-registered-redirect-uri>" \
-d client_id="<your-client-id>" \
-d client_secret="<your-client-secret>"
Two differences from a redirect-issued code:
redirect_uriis your registered value, not one you chose. A probe code never traveled through a redirect, so there is no URI to echo back. It is stamped with the client's first registered redirect URI, and the token endpoint still compares them exactly, so send that value.- No
code_verifier. A probe code carries no PKCE challenge, so none is required — including for a client that requires PKCE on the interactive flow.
You receive the same response as the interactive flow:
{
"access_token": "<member-jwt>",
"token_type": "Bearer",
"expires_in": 31536000
}
The token rides the visitor's existing device session rather than a new one, so a probe on every page load does not multiply that visitor's sessions, and a sign-out on the Allegro side invalidates the token you were issued.
Codes expire in 30 seconds and are single-use. Redeem immediately; a second
attempt with the same code returns invalid_grant.
Rate limits
The probe is expected to run once per visitor per page load, so the per-browser limit is generous while a second ceiling still bounds any single host:
| Limit | Scope |
|---|---|
| 120 requests per minute | Per browser, per client |
| 600 requests per minute | Per IP address |
Exceeding either returns 429. Probe once per page load and cache the answer
for the life of the page; do not poll.
prompt=none
The authorize endpoint also accepts prompt=none, which asks it to answer from
the existing session or not at all — it renders no page in either outcome.
GET https://<your-tenant-domain>/oauth/authorize
?client_id=<your-client-id>
&redirect_uri=https://your-app.example.com/callback
&response_type=code
&prompt=none
&state=<state>
Signed in, you get the same callback redirect as the interactive flow:
https://your-app.example.com/callback?code=<authorization-code>&state=<state>
Not signed in, you get an error back at the same place:
https://your-app.example.com/callback?error=login_required&state=<state>
none is the only supported prompt value, and OpenID Connect forbids
combining it with any other, so prompt=none login is rejected with
error=invalid_request. A prompt=none request against a client that requires
PKCE and without a code_challenge also comes back as error=invalid_request
rather than an error page, since there is no person to read one.
Codes issued this way get the full 10-minute lifetime, ride the visitor's
existing device session, and redeem with the redirect_uri you sent — an
ordinary code exchange in every respect.
prompt=none needs a top-level navigation, not a frameTwo separate things rule out the hidden-iframe pattern, and it is worth knowing both, because fixing one does not fix the other.
The SameSite=Lax cookie described above rides along on a top-level navigation,
so a full-page redirect works. It is not sent on a cross-site subresource
load, so prompt=none in an iframe on a page that is not same-site with the
Allegro host gets you a permanent login_required.
Being same-site is not enough either. Allegro is served behind an
X-Frame-Options response header, so the browser refuses to render the frame at
all for any origin but Allegro's own — and a sibling host under the same parent
domain is still a different origin. The cookie would be sent; the frame is
blocked before that matters.
Use a top-level redirect, or use the session probe. The probe needs no
navigation and no frame, and X-Frame-Options does not apply to it: it governs
framing, not fetch.
Troubleshooting
| Symptom | Cause |
|---|---|
{"status":"unauthenticated"} for a visitor you know is signed in | Cookie Domain is not set to the shared parent domain, or credentials: 'include' is missing. |
A CORS error in the console, 403 in the network tab | Your origin is not registered against this client, or client_id is wrong or disabled. |
A CORS error with a 302 in the network tab | You are calling a non-canonical hostname. Use the tenant's canonical domain. |
invalid_grant when redeeming a probe code | The code expired (30 seconds), was already used, or redirect_uri is not the client's first registered URI. |
error=login_required every time on prompt=none | A hidden iframe. Use a top-level redirect or the session probe. |
429 | Polling. Probe once per page load. |
Related
- OAuth 2.0 — Client registration, the interactive flow, and the token endpoint
- Browser Settings — Cookie Domain and CORS allowed origins
- JWT Verification — Verifying the token you receive