Skip to main content

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

MechanismHow it runsUse it when
Session probeCross-origin fetch from your page. No navigation.Your site and the Allegro host are on different origins. This is the usual choice.
prompt=noneTop-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.

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.

The single most common failure

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".

FieldPresent whenDescription
statusAlwaysauthenticated or unauthenticated.
codeauthenticatedA single-use authorization code. Redeem it immediately.
expires_inauthenticatedSeconds 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 Origin header on the request
  • No client_id, or a client_id that 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_uri is 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:

LimitScope
120 requests per minutePer browser, per client
600 requests per minutePer 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.

note
prompt=none needs a top-level navigation, not a frame

Two 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

SymptomCause
{"status":"unauthenticated"} for a visitor you know is signed inCookie Domain is not set to the shared parent domain, or credentials: 'include' is missing.
A CORS error in the console, 403 in the network tabYour origin is not registered against this client, or client_id is wrong or disabled.
A CORS error with a 302 in the network tabYou are calling a non-canonical hostname. Use the tenant's canonical domain.
invalid_grant when redeeming a probe codeThe 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=noneA hidden iframe. Use a top-level redirect or the session probe.
429Polling. Probe once per page load.