Admin SSO
Configure SSO for the Dyrected Admin UI using external OIDC providers such as Okta, Azure AD, or Google.
Dyrected supports external Admin single sign-on (SSO) through adminAuth. This lets your editorial or operations team sign into the Admin UI with an OIDC (OpenID Connect, the standard identity layer built on OAuth) provider such as Okta, Azure AD, Google Workspace, or another standards-compliant identity system.
This guide is about Admin login, not your application's end-user auth. Your application can continue using its own auth collections and login flows independently.
What Admin SSO is for
Use Admin SSO when:
- your team already signs into internal tools with an identity provider
- you want Admin access governed separately from customer/member login
- you need central control over who can access the dashboard
- you want Just-in-Time provisioning or pre-approved Admin access rules
Do not use this guide for application-user login such as customers, members, or subscribers. That remains collection-level auth: true.
How the flow works
At a high level:
- The Admin UI discovers that
adminAuth.modeisexternal. - The user clicks the provider on the login screen.
- Dyrected redirects the browser to the OIDC provider.
- The provider redirects back to Dyrected at
/api/admin/auth/:provider/callback. - Dyrected verifies the identity claims.
- Dyrected resolves access using
resolveAccess. - Dyrected provisions or updates an Admin user in the configured Admin auth collection.
- Dyrected issues its own Admin bearer token and the dashboard continues normally.
This means the external provider controls identity, while Dyrected still controls its own Admin session and access rules.
Before you start
You need all of the following:
- A Dyrected app with an Admin UI route such as
/admin - An external identity provider that supports OpenID Connect
- An Admin auth collection in Dyrected
- A stable public callback URL reachable by the identity provider
The Admin auth collection is important. Even with external SSO, Dyrected still needs a collection to store the Admin-side user record and metadata such as:
emailnamerolesauthProviderexternalSubjectlastLoginAt
Define an Admin auth collection
If you do not set adminAuth.collectionSlug, Dyrected prefers __admins. That is the cleanest default for most teams.
import { defineCollection } from '@dyrected/core'
export const Admins = defineCollection({
slug: '__admins',
auth: true,
fields: [
{ name: 'name', label: 'Name', type: 'text' },
{
name: 'roles',
label: 'Roles',
type: 'multiSelect',
options: ['admin', 'editor', 'publisher'],
},
{ name: 'authProvider', label: 'Auth provider', type: 'text', admin: { hidden: true } },
{ name: 'externalSubject', label: 'External subject', type: 'text', admin: { hidden: true } },
{ name: 'lastLoginAt', label: 'Last login', type: 'datetime', admin: { hidden: true } },
],
})If you are migrating from using a different Admin collection, read Separating Admin Auth first.
Configure adminAuth
Add an external Admin auth configuration to defineConfig.
import { defineConfig } from '@dyrected/core'
import { Admins } from './collections/admins'
export default defineConfig({
collections: [Admins],
globals: [],
adminAuth: {
mode: 'external',
collectionSlug: '__admins',
provisioningMode: 'jit_plus_membership_management',
providers: [
{
id: 'okta',
type: 'oidc',
displayName: 'Okta',
issuer: process.env.OKTA_ISSUER!,
clientId: process.env.OKTA_CLIENT_ID!,
clientSecret: process.env.OKTA_CLIENT_SECRET!,
scopes: ['openid', 'profile', 'email'],
autoRedirect: true,
},
],
},
})Minimum OIDC provider fields:
idtype: 'oidc'issuerclientIdclientSecret
Optional but commonly useful:
displayNameredirectUriscopesclaimMappingautoRedirect
Register the callback URL in your identity provider
By default, Dyrected uses this callback pattern:
https://your-app.com/api/admin/auth/<providerId>/callbackExample:
https://cms.example.com/api/admin/auth/okta/callbackIf you set redirectUri explicitly in the provider config, that exact value must be registered with the identity provider.
Important constraints:
- the callback must be publicly reachable by the OIDC provider
- the callback must resolve to the same Dyrected app that serves the Admin UI
- in local development, use whatever callback URL your provider allows, or a local tunnel if necessary
Decide the provisioning mode
provisioningMode controls how Dyrected admits external identities into the Admin layer.
jit_only
Use this when:
- anyone meeting
resolveAccessrules should be created on first login - you do not want Dyrected to be the main source of truth for Admin membership
jit_plus_membership_management
Use this when:
- you want first-login creation
- you also want Dyrected-side membership records to exist and stay updated
This is the best default for most teams.
preprovisioned_only
Use this when:
- accounts must be approved or created before first login
- you cannot allow Dyrected to create new Admin users automatically
In this mode, a valid OIDC identity is still rejected if no matching Admin user has been provisioned already.
Map claims if your provider does not use the defaults
By default, Dyrected expects:
subemailname
If your provider stores roles or groups in custom claim names, map them explicitly.
adminAuth: {
mode: 'external',
collectionSlug: '__admins',
provisioningMode: 'jit_plus_membership_management',
providers: [
{
id: 'azure',
type: 'oidc',
displayName: 'Azure AD',
issuer: process.env.AZURE_ISSUER!,
clientId: process.env.AZURE_CLIENT_ID!,
clientSecret: process.env.AZURE_CLIENT_SECRET!,
claimMapping: {
sub: 'sub',
email: 'email',
name: 'name',
roles: 'roles',
groups: 'groups',
},
},
],
}Common use cases:
rolesclaim from the provider should become Dyrectedrolesgroupsclaim helpsresolveAccessdecide whether the user should be admitted- tenant- or site-specific claims can be passed through custom mapping keys such as
siteIdsorworkspaceIds
Write resolveAccess
resolveAccess is the most important part of the setup. It decides whether the authenticated identity should be allowed into the Admin UI and what Dyrected-side roles or extra data should be stored.
If you do not supply resolveAccess, Dyrected allows the external identity by default after successful authentication. That is too open for most real deployments.
Start with an explicit function.
adminAuth: {
mode: 'external',
collectionSlug: '__admins',
provisioningMode: 'jit_plus_membership_management',
providers: [
{
id: 'okta',
type: 'oidc',
issuer: process.env.OKTA_ISSUER!,
clientId: process.env.OKTA_CLIENT_ID!,
clientSecret: process.env.OKTA_CLIENT_SECRET!,
claimMapping: {
roles: 'roles',
groups: 'groups',
},
},
],
resolveAccess: ({ identity }) => {
const groups = identity.groups ?? []
const isCmsAdmin = groups.includes('cms-admins')
const isCmsEditor = groups.includes('cms-editors')
if (!isCmsAdmin && !isCmsEditor) {
return { allowed: false }
}
return {
allowed: true,
roles: isCmsAdmin ? ['admin'] : ['editor'],
data: {
department: identity.rawClaims?.department,
},
}
},
}Use resolveAccess for:
- group- or role-based allow/deny logic
- tenant- or site-aware restrictions
- mapping external groups to Dyrected
roles - attaching extra fields to the Dyrected Admin user document
resolveAccess receives:
identityproviderIdsiteIdworkspaceId- request metadata
- the current user record if one already exists
Test the full login flow
Do not stop after saving the config. Test the whole redirect chain.
Expected start endpoint
When the login page begins the external flow, it hits:
GET /api/admin/auth/:provider/startExample:
GET /api/admin/auth/okta/startExpected callback endpoint
The OIDC provider should redirect back to:
GET /api/admin/auth/:provider/callbackExample:
GET /api/admin/auth/okta/callbackWhat success looks like
After a successful callback:
- the browser returns to the Admin UI
- Dyrected exchanges the external identity for a Dyrected Admin token
- the Admin UI loads as the provisioned user
/api/collections/__admins/meor your configured Admin collection/meresolves successfully
Example: Okta setup
import { defineConfig } from '@dyrected/core'
import { Admins } from './collections/admins'
export default defineConfig({
collections: [Admins],
globals: [],
adminAuth: {
mode: 'external',
collectionSlug: '__admins',
provisioningMode: 'jit_plus_membership_management',
providers: [
{
id: 'okta',
type: 'oidc',
displayName: 'Okta',
issuer: process.env.OKTA_ISSUER!,
clientId: process.env.OKTA_CLIENT_ID!,
clientSecret: process.env.OKTA_CLIENT_SECRET!,
scopes: ['openid', 'profile', 'email'],
claimMapping: {
roles: 'roles',
groups: 'groups',
},
},
],
resolveAccess: ({ identity }) => {
const groups = identity.groups ?? []
if (!groups.includes('cms-admins') && !groups.includes('cms-editors')) {
return { allowed: false }
}
return {
allowed: true,
roles: groups.includes('cms-admins') ? ['admin'] : ['editor'],
}
},
},
})Provider-side checklist:
- Create an OIDC application in Okta.
- Set the callback URL to
https://your-app.com/api/admin/auth/okta/callback. - Allow
openid,profile, andemailscopes. - Ensure the required
groupsorrolesclaims are present in the token or userinfo response. - Store issuer, client ID, and client secret in environment variables.
The same pattern applies to Azure AD, Google Workspace, Auth0, and other OIDC providers.
Multi-tenant note
If your deployment uses onSchemaFetch, adminAuth can also be resolved per site.
That is useful when:
- different sites have different identity providers
- different sites need different access rules
- a platform builder stores site-specific admin auth settings in a control plane
In that model:
- your request must resolve the correct
siteId onSchemaFetch(siteId)can return tenant-specificadminAuth- your callback URL still points to the Dyrected app, but access resolution can vary by site
Local development tips
Local SSO is usually harder than production because identity providers often reject arbitrary localhost callback URLs.
Practical advice:
- Prefer a provider that allows
http://localhostcallback registration for development. - If not, use a tunnel or development hostname.
- Keep local and production client credentials separate.
- Test both the initial redirect and the callback exchange, not just the login button.
If your provider requires an explicit redirect URI, set redirectUri in the Dyrected provider config so the generated flow matches exactly.
Troubleshooting
Recommended baseline
If you want a conservative setup that works for most teams:
- use
__adminsas the Admin collection - set
adminAuth.modetoexternal - use one OIDC provider
- use
jit_plus_membership_management - map
groups - gate access in
resolveAccess - store Dyrected-side roles in the Admin collection
That gives you a clean split between:
- external identity and group ownership
- Dyrected-side Admin sessions and content permissions