@v4/cortex-sso
EuroLeague ID sign-on through Cortex's hosted microsite: NextAuth with a public PKCE client, a stateless cookie and the header's session hook.
Overview
@v4/cortex-sso is the site's EuroLeague ID sign-on, through Cortex (FanScore). Readers log in and manage their account on Cortex's hosted microsite; the site holds only a short, stateless session. It is the only package that imports next-auth (pinned at 4.24.15), apart from the app's own NextAuth route. Club-only: it is not a template domain (Club integrations has the shape it shares with the club's other third-party seams).
This page is the package view. The reader journeys (login, logout from the site, logout from the microsite, a failed login) are in Single sign-on; the plan and its rulings are in .claude/reference/SSO/sso-mvp-plan.md, beside Cortex's own SSO Developer Overview - Euroleague (October 2026).pdf.
Three facts shape everything else:
- A public OAuth client. PKCE,
stateandnonce, no client secret, scopeopenid profile email. The provider is written out by hand rather than discovered: Cortex's discovery document names the website as both the login address and the issuer while being served from the OAuth host, which a standard discovery check rejects. Cortex's code exchange also wants adevice_id, so the token request is the package's own, but it still goes through the OpenID client NextAuth passes in — that call is what verifies the ID token's signature, issuer, audience, expiry and nonce (src/options.ts). - No session store. The session is NextAuth's encrypted cookie and nothing else. It keeps the ID token's
sub, the first name,emailVerified, the ID token itself (logout needs it) andloginAt. No access or refresh token is kept (src/session.ts). The first name is read from Cortex's non-standardfirst_nameclaim, falling back togiven_name. - The lifetime is absolute. A session ends
sessionMaxAgeSecondsafter login, whatever the activity (isSessionLive); NextAuth's sliding renewal does not extend it.
Entry points
| Import | What it holds | Imports next-auth |
|---|---|---|
@v4/cortex-sso |
Settings, links, cookie names, session types. Server-safe and client-safe | No |
@v4/cortex-sso/server |
The NextAuth options and the login / logout route handlers | Yes |
@v4/cortex-sso/client |
"use client": the header's session hook and the login / logout actions |
Via next-auth/react |
@v4/cortex-sso (src/index.ts)
- Settings —
readCortexSsoSettings(env),DEFAULT_SESSION_MAX_AGE_SECONDS(86 400), typeCortexSsoSettings(baseUrl,micrositeUrl,clientId,issuer,siteUrl,sessionMaxAgeSeconds). - Links (
src/links.ts)AUTH_PATHS—callback(/api/auth/callback/cortex),loginReturn,logout,logoutReturn,logoutLocal, all under/api/auth/. The callback and the two return paths are the addresses registered with Cortex.CORTEX_PROVIDER_ID("cortex", part of the registered callback),RETURN_TO_PARAM(returnTo).SIGNOUT_PARAM/SIGNOUT_VALUE— the?signout=truethe microsite adds when a reader logs out there.LOGIN_FAILED_PARAM/LOGIN_FAILED_PATH— a failed login lands on/?loginfailed=true. Nothing reads the marker yet.micrositeAuthorizeUrl,micrositeAccountUrl,micrositeRegisterUrl,micrositeLogoutUrl(withid_token_hintandpost_logout_redirect_uri),logoutReturnUrl,loginReturnPath.safeReturnPath— the open-redirect guard. A return address must be one of our own paths: another origin,//host, a backslash, a control character or our own/api/auth/*routes fall back to/.
- Cookies (
src/cookies.ts) —SIGNED_IN_HINT_COOKIE(elb-signed-in, valueSIGNED_IN_HINT_VALUE"1"): a readable hint that carries no identity and grants nothing;LOGOUT_RETURN_COOKIE(elb-logout-return, five minutes): the page a reader signed out from, kept while the browser visits the microsite;sessionCookieName(secure)(__Secure-next-auth.session-tokenon https). - Session —
isSessionLive, typesAccountSession(what the browser may read:firstName,userId— the ID token'ssub— andexpires) andSessionToken(what the cookie holds).
@v4/cortex-sso/server (src/server.ts)
cortexProvider(settings),createCortexAuthOptions(settings)— the NextAuth provider and options.- Route handlers (
src/routes.ts):handleLoginReturn(GET) — sets the signed-in hint, but only if a session cookie came with the request, then sends the reader back to their page.handleLogout(POST, a form) — clears our cookies FIRST, then sends the browser to the microsite's logout page. The browser has to visit it: the microsite keeps its session in its own local storage, which no server call can clear.handleLocalLogout(POST, 204) — ends only our session; used for?signout=true.handleLogoutReturn(GET) — where Cortex returns after a logout.- type
ReadSessionToken.
readSessionProfileandSessionProfile— the reader's name and email from the session's ID token, for the Code of Conduct report.
@v4/cortex-sso/client (src/client/index.ts)
startLogin(returnTo?)— NextAuth'ssignIn, a POST carrying the CSRF token. It is a button action, never a link.startLogout(returnTo?)— builds and submits a hidden form POST to the logout route.useAccountSession(options)with typesAccountState(signedOut, optionallychecked, orsignedInwithfirstNameanduserId) andUseAccountSessionOptions. It replaces NextAuth'sSessionProvider:- It starts signed out on the server and on the first paint, so the shell stays cacheable.
- It calls
/api/auth/sessiononly when the hint cookie is present, so an anonymous page view makes no session call; a stale hint is cleared. - It handles
?signout=trueby calling the local logout and stripping the parameter.
Configuration
readCortexSsoSettings(process.env) reads everything (src/settings.ts). CORTEX_SSO_BASE_URL is the switch.
| Variable | Meaning |
|---|---|
CORTEX_SSO_BASE_URL |
Cortex's OAuth platform. Unset: sign-on is OFF — no login control, every auth route answers 404. CI and verify build this way |
CORTEX_MICROSITE_URL |
The hosted login and account microsite. Discovery does not name it |
CORTEX_SSO_CLIENT_ID |
The OAuth client id (EUROLEAGUE on stage) |
CORTEX_SSO_ISSUER |
The iss Cortex writes into its ID tokens — compared character for character, trailing slash included |
NEXTAUTH_URL |
This site's public address: the callback and logout-return addresses are built from it |
AUTH_SESSION_MAX_AGE_SECONDS |
Optional. A whole number above zero; default 86 400 (24 hours) |
NEXTAUTH_SECRET |
Read by NextAuth itself at request time, never through the club config. Each deployed environment sets its own |
With the switch set, every other required variable must be set too, or the config throws naming the missing ones — at build as well as at runtime, because club.config.ts loads in both.
.env.example ships the stage values (https://oauth-stage.fanscore.com, client EUROLEAGUE), so a developer runs with sign-on on. The local site must be on http://localhost:3000: that is the callback Cortex has registered.
How the app wires it
| File | Role |
|---|---|
apps/web/club.config.ts |
readCortexSsoSettings(process.env) → features.auth = { adapter: { id: "cortex", options } } (an adapter seam, not env markers) |
apps/web/src/lib/auth.ts |
The ONE config point, server-only: cortexSso (settings or undefined) and authOff() (404, no-store) |
apps/web/src/app/api/auth/[...nextauth]/route.ts |
NextAuth(createCortexAuthOptions(cortexSso)) |
apps/web/src/app/api/auth/login/return/route.ts, logout/route.ts, logout/return/route.ts, logout/local/route.ts |
The package's handlers; each answers authOff() when sign-on is off |
apps/web/src/app/[locale]/layout.tsx |
Passes micrositeAccountUrl(cortexSso) to the chrome — the same for every visitor, so the shell stays cached |
apps/web/src/components/site-chrome.tsx |
useAccountSession and startLogin; signed in, the header shows the reader's initial, linking to the microsite's account page |
apps/web/src/lib/analytics.ts |
Reads the hint cookie so a possibly signed-in reader's first page_viewed waits for user_id |
apps/web/src/lib/code-of-conduct.ts, packages/code-of-conduct/src/server/reporter-route.ts |
readSessionProfile for the report's reporter |
apps/web/next.config.ts |
Listed in transpilePackages (raw TS source, no build) |
Only the /api/auth/* routes read the session. The app runs cacheComponents; a layout or page that called getServerSession() or cookies() would become per-visitor and uncacheable. The header learns the session in the browser instead, through useAccountSession.
The stage-test app
packages/cortex-sso/stage-test/ is a standalone Next app for testing a REAL login and logout against Cortex's stage platform without Contentful or a full site build. It mounts the package's own route handlers, options and client hook (no copies), on the same five /api/auth/ paths, and runs cacheComponents with a cached page so the caching rule is exercised too.
cp packages/cortex-sso/stage-test/.env.example packages/cortex-sso/stage-test/.env.local # then set NEXTAUTH_SECRET
pnpm --filter @v4/cortex-sso stage-test # next dev on :3000, then open /news
Port 3000 is mandatory (the registered addresses). Its README has the journey table and the troubleshooting:
invalid_originfrom Cortex — the Issuer configured in Cortex's dashboard does not match.- An error page straight after login —
CORTEX_SSO_ISSUERis not exactly the token'siss. - A logout that ends on
?signout=trueinstead of your page — the post-logout address is not registered exactly.
Scripts and tests
test / test:watch (vitest, jsdom), lint, check-types, stage-test. Suites: settings, links, options, routes, profile, client/use-account-session, client/start-logout.
Where it lives
| File | What |
|---|---|
packages/cortex-sso/src/settings.ts |
The environment variables and the on/off switch |
packages/cortex-sso/src/options.ts |
The hand-written provider, the token exchange, the callbacks |
packages/cortex-sso/src/routes.ts |
The login-return and the three logout handlers |
packages/cortex-sso/src/links.ts |
Addresses, parameters and the return-path guard |
packages/cortex-sso/src/client/ |
useAccountSession, startLogin, startLogout |
packages/cortex-sso/stage-test/ |
The standalone stage login app and its README |