| name | implement-sso |
| description | Implements Modular SSO so an agent can add SAML/OIDC, IdP-initiated login, and the admin portal. Use when the user wants SSO, SAML, OIDC, Modular SSO, or IdP. It does not write SaaSKit login (that's `implement-saaskit`) or provision users from a directory (that's `implement-scim`).
|
Implement SSO
Add SAML/OIDC, IdP-initiated login, and the admin portal. Then stop.
Guardrails
- MUST pass one selector on every authorization URL:
connectionId, organizationId, or loginHint.
- MUST keep
redirectUri identical to the dashboard Allowed callback URL.
- MUST call
validateToken before using claims.
- MUST keep
relay_state as state on IdP-initiated login.
- MUST print dashboard checklists, wait for the user, then continue. Do not click the dashboard.
Gotchas
- Modular SSO is for apps that already manage users and sessions. Scalekit handles SAML/OIDC. The app keeps the user store and session.
- Full-Stack Auth / SaaSKit login already includes SSO. Name
implement-saaskit and stop. Modular SSO or FSA, not both.
- Auth mode: Dashboard → Settings → Authentication Mode → Modular Auth. Path is
/settings/authentication-mode.
- Read
SCALEKIT_ENVIRONMENT_URL, SCALEKIT_CLIENT_ID, SCALEKIT_CLIENT_SECRET. Never SCALEKIT_ENV_URL. Do not prepend https:// if the URL already has a scheme.
- Default language is Node. Default scopes are
openid profile email. Modular SSO does not support offline_access. Do not write refresh tokens or SaaSKit cookies.
- Selector order:
connectionId > organizationId > loginHint. domainHint also works. Without a selector Scalekit cannot pick an IdP.
- Copy organization IDs from the dashboard. Do not invent
org_… values.
- Test proof is Organizations → Test Organization. Do not ask for FSA dryrun or a hosted-login URL.
- IdP click-paths live in references/providers.md. Point IT at the live guide.
Step 1 — Confirm Modular Auth
If the app already uses SaaSKit or Full-Stack Auth login, name implement-saaskit and stop.
Print this checklist. Wait for the user. Then continue.
- app.scalekit.com → Settings → Authentication Mode → Modular Auth
- Developers → Settings → API Credentials. Copy the three env names.
- Authentication → Redirects:
- Allowed callback URLs: the app
/auth/callback (local default http://localhost:3000/auth/callback)
- Initiate login URL: the app
/login (local default http://localhost:3000/login)
Put the three env names in the project env file. Do not invent values.
Done when: mode is Modular Auth, the three env names exist, and both redirect URLs are registered.
Step 2 — Init the SDK
Install @scalekit-sdk/node only when the repo has no Scalekit SDK yet.
import { ScalekitClient } from '@scalekit-sdk/node';
const scalekit = new ScalekitClient(
process.env.SCALEKIT_ENVIRONMENT_URL,
process.env.SCALEKIT_CLIENT_ID,
process.env.SCALEKIT_CLIENT_SECRET
);
const redirectUri = 'http://localhost:3000/auth/callback';
Use the same redirectUri string that is registered in Step 1.
Done when: the client initializes from those env vars.
Step 3 — Login route
Pass one selector. Empty query fails.
app.get('/auth/login', (req, res) => {
const { connectionId, organizationId, loginHint, domainHint } = req.query;
const authorizationUrl = scalekit.getAuthorizationUrl(redirectUri, {
connectionId,
organizationId,
loginHint,
domainHint,
scopes: ['openid', 'profile', 'email'],
});
res.redirect(authorizationUrl);
});
Done when: /auth/login redirects to that URL with one selector.
Step 4 — IdP-initiated /login
Register this exact URL as Initiate login URL.
app.get('/login', async (req, res) => {
const { idp_initiated_login, error, error_description } = req.query;
if (error) return res.status(400).send(error_description);
const claims = await scalekit.getIdpInitiatedLoginClaims(idp_initiated_login);
const authorizationUrl = scalekit.getAuthorizationUrl(redirectUri, {
connectionId: claims.connection_id,
organizationId: claims.organization_id,
loginHint: claims.login_hint,
state: claims.relay_state,
scopes: ['openid', 'profile', 'email'],
});
res.redirect(authorizationUrl);
});
Done when: /login reads idp_initiated_login and redirects with relay_state as state.
Step 5 — Callback and app session
Use the same redirectUri as Step 3. The app creates its own session after validation.
app.get('/auth/callback', async (req, res) => {
const { code, error, error_description } = req.query;
if (error) return res.status(400).send(error_description);
const result = await scalekit.authenticateWithCode(code, redirectUri);
await scalekit.validateToken(result.idToken);
await scalekit.validateToken(result.accessToken);
res.redirect('/dashboard');
});
Do not write SaaSKit HttpOnly cookies. Modular SSO does not return a refresh token.
Done when: the callback exchanges code, validates both tokens, and the app session exists.
Step 6 — Test with the simulator
Print this checklist. Wait for the user. Then continue.
- Dashboard → Organizations → Test Organization
- Copy the test
organization_id or connection_id. Domains are example.com and example.org.
- Open
/auth/login with one of: organizationId=<id>, connectionId=<id>, or loginHint=user@example.com
- Complete the simulator (SP-initiated, then IdP-initiated)
Done when: the callback created the app session for a simulator login.
Step 7 — Admin portal
Shareable link (no code): Dashboard → Organizations → the org → Generate link. Send that URL plus the matching SSO setup guide to the customer's IT admin. Open references/providers.md for Okta, Entra ID, Google Workspace, and JumpCloud.
Embed: register the page origin under Authentication → Redirects → Allowed callback URLs. Generate the link server-side on every load. Links are single-use. orgId is the customer's organization ID from the dashboard.
const portalLink = await scalekit.organization.generatePortalLink(orgId);
res.json({ portalUrl: portalLink.location });
<iframe src="${portalUrl}" width="100%" height="600" frameborder="0" allow="clipboard-write"></iframe>
On message, if event.data.event_type is PORTAL_SESSION_EXPIRY, generate a new link and set iframe.src. Do the same for PORTAL_LOAD_FAILURE when data.error_code is SESSION_EXPIRED.
Done when: a shareable link can be generated, and the iframe uses a fresh portalLink.location.
Step 8 — Stop
Name implement-scim only if the user wants directory provisioning.
Done when: SAML/OIDC, IdP-initiated /login, and the admin portal are in the repo, and this skill has stopped.
Reach for
implement-saaskit if the app wants SaaSKit login instead
implement-scim to provision users from a directory
- references/providers.md for Okta, Entra ID, Google Workspace, and JumpCloud
Live lookups