strapi-plugin-oidc
v1.10.9
Published
A Strapi plugin that provides OpenID Connect (OIDC) authentication functionality for the Strapi Admin Panel.
Maintainers
Readme
A Strapi plugin that provides OpenID Connect (OIDC) authentication for the Strapi Admin Panel. Supports Keycloak, Auth0, Okta, Azure AD, Authentik, Authelia, and any other OpenID Connect provider.
Installation
npm install strapi-plugin-oidcConfiguration
Add the plugin to config/plugins.js (or .ts):
module.exports = ({ env }) => ({
'strapi-plugin-oidc': {
enabled: true,
config: {
// Required
OIDC_PUBLIC_URL: env('PUBLIC_URL', 'https://strapi.example.com'), // origin only — we append /strapi-plugin-oidc/oidc/callback
OIDC_ISSUER: env('OIDC_ISSUER'), // https://your-provider or https://your-provider/realms/your-realm
OIDC_CLIENT_ID: env('OIDC_CLIENT_ID'),
OIDC_CLIENT_SECRET: env('OIDC_CLIENT_SECRET'),
// Optional — defaults shown
OIDC_SCOPE: 'openid profile email', // space-separated scopes
OIDC_FAMILY_NAME_FIELD: 'family_name',
OIDC_GIVEN_NAME_FIELD: 'given_name',
OIDC_SSO_BUTTON_TEXT: 'Login via SSO',
OIDC_ENFORCE: null, // null = use Admin UI toggle; true/false = override in config
OIDC_SKIP_LOGIN_PAGE: null, // null = use Admin UI toggle; true/false = override in config
REMEMBER_ME: false, // Persist session across browser restarts
AUDIT_LOG_RETENTION_DAYS: 90, // Set to 0 to disable audit logging; otherwise entries older than this many days are purged daily at midnight
OIDC_GROUP_FIELD: 'groups', // OIDC claim field containing group membership
OIDC_GROUP_ROLE_MAP: '{}', // JSON map of group names to Strapi role names
OIDC_REQUIRE_EMAIL_VERIFIED: true, // Reject logins when provider does not report email_verified=true (set false to disable)
OIDC_TRUSTED_IP_HEADER: '', // Optional: header set by your CDN/proxy containing the real client IP (see note below); only honoured when Koa proxy mode is enabled (see below)
OIDC_FORCE_SECURE_COOKIES: false, // Set true when behind a trusted HTTPS proxy that Strapi can't auto-detect
},
},
});OIDC_PUBLIC_URL is your Strapi instance's origin (e.g. https://myapp.com). The plugin appends /strapi-plugin-oidc/oidc/callback to form the full OIDC redirect URI. If unset, falls back to the PUBLIC_URL environment variable. Only provide the scheme + host + port — no trailing slash or path.
OIDC_ISSUER is your provider's issuer URL (e.g. https://auth.example.com or https://auth.example.com/realms/myrealm). The plugin appends /.well-known/openid-configuration automatically if not present, and fetches the discovery document at startup to configure all endpoints, JWKS URI, and canonical issuer.
Security features
- ID token verification — Enabled automatically when the discovery document includes a
jwks_uri. Validates signature, issuer, audience, and expiry viajose - Email verification —
OIDC_REQUIRE_EMAIL_VERIFIED: true(default) rejects unverified emails - CSRF protection — OIDC state/nonce and POST-only logout endpoint
- Rate limiting — 1 000 req/min per IP+UA (in-process; use a reverse-proxy-level limiter for multi-node)
- Secure cookies —
OIDC_FORCE_SECURE_COOKIESensures cookies are marked Secure
Client IP attribution and reverse proxies
The plugin logs client IPs for rate-limit buckets and audit logs. When Strapi runs behind a reverse proxy, enable Koa proxy mode so Strapi trusts X-Forwarded-For; otherwise all IPs will be the proxy's internal address.
In config/server.ts:
proxy: {
koa: true,
},Set OIDC_TRUSTED_IP_HEADER to the header your CDN or proxy uses to forward the real client IP. The header is only honoured when Koa proxy mode is enabled. Accepted values (all others are silently ignored):
| Header | Provider |
| --------------------------- | ------------------------------------------------- |
| cf-connecting-ip | Cloudflare |
| true-client-ip | Cloudflare Enterprise, Akamai |
| fastly-client-ip | Fastly |
| fly-client-ip | Fly.io |
| x-nf-client-connection-ip | Netlify |
| x-real-ip | nginx (proxy_set_header X-Real-IP $remote_addr) |
Only headers that CDN/proxy vendors guarantee to strip from inbound client requests are accepted, preventing IP spoofing via forged headers.
Login
Navigate to /strapi-plugin-oidc/oidc to start the OIDC flow, or click the Login via SSO button injected into the Strapi login page.
Logout
When the discovery document includes an end_session_endpoint, clicking logout redirects to the provider's end-session URL (RP-initiated logout). If the provider session has already expired, Strapi skips the redirect and goes straight to the login page.
The logout endpoint is POST /strapi-plugin-oidc/logout. Using POST instead of GET prevents CSRF-forced-logout attacks.
Admin Settings
Manage the plugin under Settings → OIDC Plugin.
Default Roles — Strapi admin role(s) assigned to new users on first login.
Whitelist — Restrict access to specific email addresses. When empty, any authenticated OIDC user gets an account. Supports:
- Individual emails with optional role overrides
- JSON import / export
- Bulk delete with confirmation
Audit Logs — Authentication events recorded and visible in the settings page. Filter by action, email, IP, and date. Download exports the current view as NDJSON. Set AUDIT_LOG_RETENTION_DAYS to 0 to disable. Records older than the configured value (default: 90 days) are purged daily.
Enforce OIDC Login — Removes email/password fields from the login page and blocks direct login API calls. Automatically disabled when the whitelist is empty to prevent lockout.
The toggle is grayed out when OIDC_ENFORCE is set in config. Lockout recovery: set OIDC_ENFORCE: false in your plugin config and restart Strapi.
Skip Login Page — Redirects unauthenticated users straight to the OIDC provider without showing the Strapi login page. Toggle under Settings → Login Settings.
The toggle is grayed out when OIDC_SKIP_LOGIN_PAGE is set in config. Set OIDC_SKIP_LOGIN_PAGE: false in your plugin config to disable and restart Strapi.
Group-to-Role Mapping
When your OIDC provider includes group membership in the userinfo response (e.g. a groups claim containing ["strapi-admins", "strapi-editors"]), you can automatically assign Strapi roles based on group membership.
| Setting | Default | Description |
| --------------------- | ---------- | --------------------------------------------------------- |
| OIDC_GROUP_FIELD | 'groups' | OIDC claim field that contains the group membership array |
| OIDC_GROUP_ROLE_MAP | '{}' | JSON map of group names → Strapi role names |
Example configuration
module.exports = ({ env }) => ({
'strapi-plugin-oidc': {
enabled: true,
config: {
// ... other OIDC config ...
OIDC_GROUP_FIELD: 'groups',
OIDC_GROUP_ROLE_MAP: JSON.stringify({
'strapi-admins': ['Super Admin'],
'strapi-editors': ['Editor'],
'strapi-authors': ['Editor', 'Author'],
}),
},
},
});Role names are the display names shown in Settings → Roles (e.g. "Editor", "Super Admin", "Author"). IDs are not supported — use names for clarity.
Role assignment precedence
- OIDC groups match
OIDC_GROUP_ROLE_MAP→ mapped Strapi roles - No match or no mapping → default OIDC roles (new users only)
Role updates on subsequent logins
- New users — Roles assigned on first login (group-mapped or default).
- Existing users with group match — Roles updated to reflect current mapping.
- Existing users without group match — Roles left unchanged. Manually-assigned roles are never overwritten.
Whitelist API
The whitelist can be managed programmatically using a Strapi API token. All endpoints are under /api/strapi-plugin-oidc and require Authorization: Bearer <token>.
Full-access tokens can call all routes. Custom tokens must be granted one of the following scopes (Settings → API Tokens → Custom → plugin permissions):
| Scope | Routes |
| --------------------------------------------- | ----------------------------------------------- |
| plugin::strapi-plugin-oidc.whitelist.read | GET /whitelist, GET /whitelist/export |
| plugin::strapi-plugin-oidc.whitelist.write | POST /whitelist, POST /whitelist/import |
| plugin::strapi-plugin-oidc.whitelist.delete | DELETE /whitelist, DELETE /whitelist/:email |
| Method | Path | Description |
| -------- | ------------------------------------------ | ---------------------- |
| GET | /api/strapi-plugin-oidc/whitelist | List all entries |
| GET | /api/strapi-plugin-oidc/whitelist/export | Export as JSON |
| POST | /api/strapi-plugin-oidc/whitelist | Add one or more emails |
| POST | /api/strapi-plugin-oidc/whitelist/import | Bulk import |
| DELETE | /api/strapi-plugin-oidc/whitelist/:email | Remove by email |
| DELETE | /api/strapi-plugin-oidc/whitelist | Remove all entries |
API calls write directly to the database — there is no unsaved state.
Import format
Accepted by both the API import endpoint and the Admin UI import button. If the email already exists as a Strapi admin user, their current roles are used automatically.
[{ "email": "[email protected]" }, { "email": "[email protected]" }]Duplicate emails within the payload and emails already in the whitelist are silently skipped.
Examples
# List
curl -H "Authorization: Bearer <token>" \
https://strapi.example.com/api/strapi-plugin-oidc/whitelist
# Export
curl -H "Authorization: Bearer <token>" \
https://strapi.example.com/api/strapi-plugin-oidc/whitelist/export \
-o whitelist.json
# Add
curl -X POST -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
-d '{"email": "[email protected]"}' \
https://strapi.example.com/api/strapi-plugin-oidc/whitelist
# Bulk import
curl -X POST -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
-d '{"users": [{"email": "[email protected]"}, {"email": "[email protected]"}]}' \
https://strapi.example.com/api/strapi-plugin-oidc/whitelist/import
# Delete one (by email)
curl -X DELETE -H "Authorization: Bearer <token>" \
"https://strapi.example.com/api/strapi-plugin-oidc/whitelist/user%40example.com"
# Delete all
curl -X DELETE -H "Authorization: Bearer <token>" \
https://strapi.example.com/api/strapi-plugin-oidc/whitelistAudit Log API
Audit log entries can be fetched programmatically using a Strapi API token. Endpoints are under /api/strapi-plugin-oidc and require Authorization: Bearer <token>.
Full-access tokens can call all routes. Custom tokens must be granted one of the following scopes:
| Scope | Routes |
| ----------------------------------------- | ------------------------------------------- |
| plugin::strapi-plugin-oidc.audit.read | GET /audit-logs, GET /audit-logs/export |
| plugin::strapi-plugin-oidc.audit.delete | DELETE /audit-logs |
| Method | Path | Description |
| -------- | ------------------------------------------- | ----------------------------------- |
| GET | /api/strapi-plugin-oidc/audit-logs | Paginated list of log entries |
| GET | /api/strapi-plugin-oidc/audit-logs/export | Matching records as NDJSON download |
| DELETE | /api/strapi-plugin-oidc/audit-logs | Delete all audit log entries (204) |
Query parameters (GET /audit-logs, GET /audit-logs/export)
| Parameter | Default | Description |
| ---------- | ------- | ---------------------------------------------- |
| page | 1 | Page number (list endpoint only) |
| pageSize | 25 | Results per page, max 100 (list only) |
| filters | — | Field/operator filters, same on both endpoints |
Results are sorted newest-first. The response shape is:
{
"results": [
{
"id": 42,
"action": "login_success",
"email": "[email protected]",
"ip": "203.0.113.42",
"details": null,
"createdAt": "2026-04-08T12:00:00.000Z",
"updatedAt": "2026-04-08T12:00:00.000Z"
}
],
"pagination": { "page": 1, "pageSize": 25, "total": 1, "pageCount": 1 }
}The NDJSON export emits one row per line with { datetime, action, email, ip, details } where datetime is the entry's createdAt timestamp.
Filtering
Use filters[<field>][<operator>]=<value> to narrow results. Invalid filters return a 400.
| Field | Operators | Value |
| ----------- | ---------------------------------------------------- | ------------------------------------------------------- |
| action | $eq, $in | One of the recorded actions |
| email | $eq, $contains, $endsWith, $null, $notNull | String (use true/false with $null / $notNull) |
| ip | $eq, $contains, $endsWith, $null, $notNull | String (use true/false with $null / $notNull) |
| createdAt | $gte, $lt, $lte, $between, $in | ISO-8601 UTC timestamp, e.g. 2026-04-08T00:00:00.000Z |
$between takes a [start, end] pair. $in on createdAt takes a list of day-start timestamps and matches anything within that UTC day.
# Failed logins on one day
curl -H "Authorization: Bearer <token>" -G \
--data-urlencode 'filters[action][$eq]=login_failure' \
--data-urlencode 'filters[createdAt][$gte]=2026-04-08T00:00:00.000Z' \
--data-urlencode 'filters[createdAt][$lt]=2026-04-09T00:00:00.000Z' \
https://strapi.example.com/api/strapi-plugin-oidc/audit-logsRecorded actions
| Action | Trigger |
| ----------------------- | ----------------------------------------------------------------- |
| login_success | Successful OIDC authentication |
| user_created | New Strapi admin user created during login |
| login_failure | Unexpected error during the OIDC login flow |
| missing_code | Callback received without an authorisation code |
| state_mismatch | CSRF state cookie does not match callback parameter |
| nonce_mismatch | ID token nonce does not match the session nonce |
| token_exchange_failed | Provider returned an error during token exchange |
| whitelist_rejected | Email not present in the active whitelist |
| email_not_verified | Provider did not report email_verified=true |
| id_token_invalid | ID token failed signature, issuer, audience, or expiry validation |
| logout | User logged out via /logout |
| session_expired | Logout attempted but provider session already stale |
Each event is also emitted on Strapi's internal eventHub as strapi-plugin-oidc::auth.<action>, which Enterprise audit log listeners pick up automatically.
Examples
# Paginated list
curl -H "Authorization: Bearer <token>" \
"https://strapi.example.com/api/strapi-plugin-oidc/audit-logs?page=1&pageSize=50"
# NDJSON export
curl -H "Authorization: Bearer <token>" \
https://strapi.example.com/api/strapi-plugin-oidc/audit-logs/export \
-o oidc-audit-log.ndjsonCredits & Changes
This plugin is a hard fork of strapi-plugin-sso by yasudacloud. Huge thanks to them for creating the foundation of this plugin!
Changes from the original:
- OIDC-only (removed other SSO methods)
- Redesigned whitelist and role management UI using native Strapi components
- OIDC enforcement and skip-login-page via admin toggles, overridable via
OIDC_ENFORCE/OIDC_SKIP_LOGIN_PAGEconfig - RP-initiated logout with smart session detection
- Migrated to Vitest with e2e coverage
- Config variable names aligned with OIDC discovery document field names
- Login via SSO button always injected; text configurable via
OIDC_SSO_BUTTON_TEXT - Whitelist REST API with JSON import/export, bulk delete, delete by email
- Hardened OIDC flow: server-generated state and nonce, PKCE, Bearer token auth for userinfo, generic error messages on failure
- Audit log: records all auth events to a queryable table with UI, JSON/NDJSON export, and REST API
