blocks-iam-sso-oidc-implementation
Skill42 starsChanged 12 days agoMITView on GitHub
Extend or debug the hosted SSO/OIDC login flow `blocks new web` scaffolds into every Blocks app: redirectToProvider → `/login/callback` → session, through the single `blocksClient`. Covers `AuthProvider` status/claims, `RequireAuth`/`RedirectIfAuthenticated` guards, and token refresh. Use for a login button, the OIDC callback, protected routes, redirect loops, or a session that doesn't stick. Needs a registered OIDC client and HTTPS on the real domain to test.
- Reads credentials
In this repository27 other files it gives its agents.
- AGENTS.mdAGENTS.md
- bootstrap/agents.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- blocks-bootstrapSkill
- blocks-captchaSkill
- blocks-data-gateway-configurationSkill
- blocks-data-gateway-crudSkill
- blocks-data-storageSkill
- blocks-frontend-local-httpsSkill
- blocks-functionsSkill
- blocks-iam-access-controlSkill
- blocks-iam-accountSkill
- blocks-iam-mfaSkill
- blocks-iam-organizationsSkill
- blocks-iam-sso-oidc-configurationSkill
- blocks-iam-usersSkill
- blocks-localization-configurationSkill
- blocks-localization-implementationSkill
- blocks-mailSkill
- blocks-notificationSkill
- blocks-notifierSkill
- blocks-proxySkill
- blocks-release-deploymentSkill
- blocks-secretsSkill
- blocksSkill
- blocks-storage-configurationSkill
- blocks-workflowSkill
The file
We split this skill into the rules it gives an agent
2 rules. About 0 are common, 0 are distinctive, and 2 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 6
When invoking a project-scoped blocks command, either use the resolved account's saved selection or pass --project <tenantId> for that one …
- Very specific: line 36
If data.error is present, completeLogin returns { ok: false, message } and CallbackPage shows an inline error Alert plus a button back to …
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
When invoking a project-scoped blocks command, either use the resolved account's saved selection or pass --project <tenantId> for that one command without changing saved state. --project applies to CLI commands only, never SDK calls.
Blocks IAM — SSO / OIDC Implementation (scaffolded frontend)
blocks new web <name> already generates a complete, working hosted-login flow. Don't reinvent it — read what's there, extend it, or fix it. Every Blocks call in this flow goes through the single blocksClient instance (src/lib/blocks/client.ts, @seliseblocks/client); there is no raw fetch/curl anywhere in this stack.
The files, and what each one actually does
| File | Role |
src/lib/blocks/config.ts | Reads VITE_BLOCKS_* env vars; isLoginConfigured() = apiUrl && oidcUrl && oidcClientId all present |
src/lib/blocks/client.ts | The one blocksClient = createBlocksClient({...}) instance, with oidc: { clientId, scope, url: oidcUrl } |
src/lib/blocks/auth.ts | startLogin, completeLogin, fetchSessionClaims, logout, getValidAccessToken — the session/token logic |
src/lib/blocks/jwt.ts | decodeJwtPayload/isJwtExpired — only relevant if a tenant's OIDC config returns bearer tokens in the body |
src/app/providers/AuthProvider.tsx | React context: status/claims/login/logout/refresh, polling + visibility-driven refresh |
src/app/router/guards.tsx | RequireAuth, RedirectIfAuthenticated |
src/app/router/routes.tsx | Wires /login, /login/callback, and the protected route table (/, /assets, /profile, /error) |
src/features/auth/LoginPage.tsx | The login button |
src/features/auth/CallbackPage.tsx | The /login/callback handler |
The flow, traced through the generated code
- Login button.
LoginPage's button callsuseAuth().login(returnTo), which isAuthProvider'slogincallingstartLogin(returnTo)inlib/blocks/auth.ts.startLoginthrows a clear error ifoidcClientIdisn't set ("Login is not configured. Set VITE_BLOCKS_OIDC_CLIENT_ID in .env."), stashesreturnTo(default"/") insessionStorage, then callsblocksClient.auth.idp.redirectToProvider()with no arguments — it relies entirely on the client's configuredoidcdefaults.- The button itself is
disabled={!configured || pending}— ifisLoginConfigured()is false,LoginPagerenders a warningAlertwith the exact callback URL ({origin}/login/callback) to register, instead of letting the click fail. "Login button does nothing" is almost always an emptyVITE_BLOCKS_OIDC_CLIENT_ID.
- The button itself is
redirectToProvider()(SDK,auth-client.ts) callsauth.idp.initiate(), thenwindow.location.assign(response.redirect_uri).initiateitself is also directly callable (e.g. to get the URL without immediately navigating, such as opening it in a new tab) but the scaffold never calls it directly; onlyredirectToProvideris wired to the button.- The user authenticates on Blocks-hosted IAM.
- IAM redirects back to
<origin>/login/callback?code=...&state=.... That path is the SDK's defaultredirectUri— the scaffold'sclient.tsnever passes an explicitredirectUri, socreateBlocksClientderives${window.location.origin}/login/callbackat runtime (seebrowserRedirectUri()in the SDK'sclient.ts). This is exactly the routeroutes.tsxhandles, so it lines up with zero config — but it means the OIDC client's registeredredirect_urismust include/login/callbackunder every origin this app runs on (dev HTTPS origin and prod origin both — see the scaffold's own README andblocks-iam-sso-oidc-configuration). routes.tsxmatchespath === "/login/callback"and rendersCallbackPagedirectly — not wrapped inRequireAuthorRedirectIfAuthenticated, since the user is by definition not yet authenticated when they land here.CallbackPage's one-shot effect (guarded with auseRefso React 18 Strict Mode's double-invoke doesn't run it twice) callscompleteLogin(window.location.href).completeLoginreads and clears the stashedreturnTo, then callsblocksClient.auth.idp.callback(callbackUrl), passing the full URL so the SDK parsescode/state/erroritself.- On the default cookie flow, IAM sets the session as a Secure, httpOnly cookie via
Set-Cookieon this response and returns no token in the body —completeLoginonly caches a bearer token if the response body actually contains one (a non-default, explicit-token OIDC config). The SDK never stores tokens itself either way; every call setscredentials: "include"so the cookie rides along automatically once IAM has set it. - If
data.erroris present,completeLoginreturns{ ok: false, message }andCallbackPageshows an inline errorAlertplus a button back to/login— it never silently strands the user on a blank screen.
- On the default cookie flow, IAM sets the session as a Secure, httpOnly cookie via
- On success,
CallbackPagecallsrefresh()(fromAuthProvider) and thenonNavigate(result.returnTo).refresh()callsfetchSessionClaims()→blocksClient.auth.userInfo()to confirm the cookie actually landed and to populateclaims/statusbefore the app navigates away from the callback screen.
Session state and route guards
AuthProvideris the single source of truth forstatus("loading" | "authenticated" | "unauthenticated") andclaims. It callsrefresh()on mount, every 5 minutes (STATUS_POLL_MS, a backup interval — not the primary signal), and immediately whenever the tab regains visibility (catches sign-out in another tab or session expiry while backgrounded). It never inspects local storage to decide auth state — asking IAM directly (userInfo()) is the only source of truth, because the default flow holds no locally readable token by design.RequireAuthwraps every protected route inroutes.tsx(/,/assets,/profile,/error). Whilestatus !== "authenticated"it rendersLoadingScreen; oncestatusresolves to"unauthenticated"it navigates to/login?returnTo=<currentPath>from auseEffect(not render-time — readingwindow.locationlive at render would double-nest thereturnToparam under Strict Mode's double-invoked effects).RedirectIfAuthenticatedwraps/loginitself so an already-signed-in user hitting/loginbounces straight to/instead of seeing the login button again.- Adding a new protected page: add it to the
protectedRoutesmap inroutes.tsx— it's automatically wrapped inRequireAuthandAppShellby the existing router code, nothing else to wire.
The @seliseblocks/client methods behind all of this
All under blocksClient.auth:
idp.initiate(request?)— starts the flow, returns{ redirect_uri }. Uses the client's configuredoidcdefaults (clientId,redirectUri) unless you pass overrides per call.idp.redirectToProvider(request?)— callsinitiatethenwindow.location.assign(...). This is whatstartLogin(and therefore the login button) actually calls; reach for this directly in any new login entry point rather than re-implementing initiate+navigate.idp.callback(callbackUrlOrObject)— completes the flow. Passwindow.location.hrefdirectly (whatcompleteLogindoes), or{ code, state, error?, error_description? }if you've parsed the URL yourself. Returns IAM's auth response as-is; the SDK never stores tokens — your app decides what, if anything, to keep (the scaffold keeps nothing in the default cookie flow).idp.uiConfig()— public UI config (e.g. captcha settings). Not currently called anywhere in the scaffold — if you're extendingLoginPagewith captcha or tenant-specific login UI, call this before rendering that UI, not before.oidc.refreshToken(request?)— a separate call from the IdP-controller hosted flow, using a refresh-token grant.getValidAccessToken()inlib/blocks/auth.tsis already wired as the 401-retry/expiry path: it returns a cached, unexpired token if present, otherwise calls this (de-duplicating concurrent callers viarefreshInFlight) if a refresh token happens to be cached. In the default cookie-only flow there's usually nothing cached to refresh, so this mostly matters for tenants whose OIDC config explicitly returns tokens in the response body.isAuthenticated()— returns a plain boolean. The scaffold's ownfetchSessionClaims()calls the lower-leveluserInfo()instead (same underlying check) becauseAuthProviderneeds the claims payload, not just a boolean — reach forisAuthenticated()yourself for a one-off check that doesn't need claims, rather than hand-rolling another call.
Config
createBlocksClient needs an oidc block: clientId (required), url (required — kept for app metadata, not used to build the authorize URL), redirectUri/scope (optional, default to ${origin}/login/callback / openid profile). The scaffold populates this from VITE_BLOCKS_OIDC_CLIENT_ID / VITE_BLOCKS_OIDC_URL / VITE_BLOCKS_OIDC_SCOPE in .env.
This clientId is the public OIDC client registered for this app — see the sibling blocks-iam-sso-oidc-configuration skill for how to resolve or create one (blocks auth oidc-clients list / save, no portal needed). Don't confuse it with blocks login itself, which authenticates the CLI with no setup and needs no registration at all (see blocks-bootstrap) — the two are unrelated and neither can substitute for the other.
--client-id and --app-domain are non-interactive-unsafe when omitted. blocks new web's client-id and domain resolution both fall back to an interactive selection prompt ("Choose an OIDC client... or create/skip" / "Multiple domains found... choose one") when the flag is missing and there's more than one candidate (or, for the client id, always — even zero candidates offers "Create"/"Skip"). There is no stdin in a non-interactive/agent-driven run, so this hangs waiting for a selection instead of quietly scaffolding with a blank/default value. A blank oidcClientId only happens if a human sitting at the terminal interactively picks "Skip". An agent running blocks new web should always resolve and pass both --client-id and --app-domain explicitly up front — see blocks-iam-sso-oidc-configuration for resolving/creating an OIDC client via auth oidc-clients list/save, and project.applications[].domain (from the project record) for the app domain — rather than omitting either and hoping for a graceful non-interactive default.
Gotchas
- Disabled login button, no error →
isLoginConfigured()is false, almost always becauseVITE_BLOCKS_OIDC_CLIENT_IDis empty in.env. Don't assumeblocks new webwas just run without--client-idand "left this blank on purpose" — omitting--client-id(or--app-domain, when a project has multiple domains) requires an interactive selection and fails withinteractive_input_requiredin an agent-driven run. A blank client id only results from a human interactively choosing "Skip." Always pass--client-idexplicitly (see the Config section above). - Login redirects back but the app still shows logged out → this is an HTTPS/cookie problem, not an app-logic bug — the session cookie is Secure and won't be stored/sent on
http://localhost. Cross-referenceblocks-frontend-local-httpsrather than debuggingAuthProvider. - Redirect URI mismatch → the SDK derives
redirectUrifromwindow.location.originat runtime; if the app runs under more than one origin (dev HTTPS host, prod domain), the registered OIDC client'sredirect_urismust list/login/callbackunder each of them, or IAM rejects the authorize request for the ones missing. - Activation is a separate concern. Already-activated users go straight through this flow. Only users invited/created inactive via the portal or API need a one-time
/activatestep first — out of scope here, seeblocks-iam-account. - Don't add a
RequireAuth/RedirectIfAuthenticatedguard around/login/callback— it must stay reachable while the user is still unauthenticated, by design. - Don't hand-roll a "check if logged in" fetch — call
blocksClient.auth.isAuthenticated()or reuseAuthProvider'sstatus/refresh(), never infer auth state fromsessionStorage/localStorage(the default flow keeps no readable token there at all). - Custom app domain, session never sticks (cookie calls silently fail) → on a custom (non-
*.seliseblocks.com) app domain, the hosted-login session cookie is only stored/sent ifVITE_BLOCKS_API_URLshares the app's registrable domain. The defaulthttps://api.seliseblocks.comdoes not share a registrable domain with e.g.abc.slsblx.com, so the browser never stores the cross-site cookie and cookie-based calls (userInfo()//iam/me,logout, the OIDC callback flow this skill documents) silently fail. For a custom domain,VITE_BLOCKS_API_URLmust behttps://blocksapi.<registrable-domain>(e.g.abc.slsblx.com→https://blocksapi.slsblx.com), not the default.
Example trigger prompts
- "Add a login button and handle the OIDC callback"
- "Why is my login button disabled?"
- "Add a new protected page that requires the user to be signed in"
- "The user gets redirected back from IAM but the app still shows them as logged out"
- "Wire up token refresh for when the session expires"
- "How does this scaffolded app know if someone is logged in?"
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

