npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

strapi-plugin-oidc-sso

v0.1.1

Published

Generic OIDC SSO for the Strapi v5 admin panel.

Readme

strapi-plugin-oidc-sso

Generic OIDC SSO for the Strapi v5 admin panel. Works with any OpenID Connect–compliant identity provider — Microsoft Entra ID, Google, Auth0, Keycloak, Okta, AWS Cognito, etc.

  • Adds a "Login with SSO" button to the Strapi admin login page
  • Full OIDC authorization-code flow with PKCE, state, and nonce
  • ID token signature validation via the provider's JWKS
  • Find-or-create Strapi admin users (configurable auto-provisioning)
  • Issues a real Strapi v5 admin session via strapi.sessionManager — no monkey-patching of Strapi core
  • Customizable button (label, icon, colors)
  • Strapi-styled error banners on the login page when sign-in fails
  • Multi-tenant Microsoft /common/ and /organizations/ endpoints handled via manual iss validation against the actual tenant

Requirements

  • Strapi ^5.24.1 (uses strapi.sessionManager, introduced in 5.24)
  • Node >=20

Installation

npm install strapi-plugin-oidc-sso
# or
pnpm add strapi-plugin-oidc-sso
# or
yarn add strapi-plugin-oidc-sso

Configuration

Add the plugin to config/plugins.ts (or .js):

export default ({ env }) => ({
  'oidc-sso': {
    enabled: env.bool('OIDC_ENABLED', false),
    config: {
      // Either set `issuer` for OIDC discovery, OR set explicit endpoints below.
      issuer: env('OIDC_ISSUER'),
      endpoints: {
        authorization: env('OIDC_AUTHZ_URL'),    // optional override
        token:         env('OIDC_TOKEN_URL'),    // optional override
        userinfo:      env('OIDC_USERINFO_URL'), // optional override
        jwks:          env('OIDC_JWKS_URL'),     // optional override
        endSession:    env('OIDC_END_SESSION_URL'),
      },
      clientId:     env('OIDC_CLIENT_ID'),
      clientSecret: env('OIDC_CLIENT_SECRET'),
      redirectUri:  env('OIDC_REDIRECT_URI', 'http://localhost:1337/oidc-sso/callback'),
      scopes:       env.array('OIDC_SCOPES', ['openid', 'profile', 'email']),

      // Map verified id_token claims → Strapi admin user fields. Throw to reject login.
      userMapping: (claims) => ({
        email:     claims.preferred_username ?? claims.email,
        firstName: claims.given_name ?? claims.name?.split(' ')?.[0],
        lastName:  claims.family_name ?? '',
      }),

      autoCreateUsers: env.bool('OIDC_AUTO_CREATE', false),
      defaultRoles:    env.array('OIDC_DEFAULT_ROLES', []),
      allowedDomains:  env.array('OIDC_ALLOWED_DOMAINS', []),
      useUserinfo:     env.bool('OIDC_USE_USERINFO', false),

      // Button branding (all optional)
      buttonLabel: env('OIDC_BUTTON_LABEL', 'Login with SSO'),
      buttonIcon:  env('OIDC_BUTTON_ICON'), // URL, data URI, or inline <svg>...</svg>
      buttonStyle: {
        background:      env('OIDC_BUTTON_BACKGROUND'),
        color:           env('OIDC_BUTTON_COLOR'),
        borderColor:     env('OIDC_BUTTON_BORDER_COLOR'),
        hoverBackground: env('OIDC_BUTTON_HOVER_BACKGROUND'),
        hoverColor:      env('OIDC_BUTTON_HOVER_COLOR'),
      },
    },
  },
});

Configuration reference

| Field | Required | Description | |-------------------|----------|-------------| | issuer | one of | OIDC issuer URL — used for .well-known/openid-configuration discovery | | endpoints.* | one of | Explicit endpoint overrides; any field set here wins per-field over discovery | | clientId | yes | OAuth client ID | | clientSecret | yes | OAuth client secret | | redirectUri | yes | Must match the URI registered with the IdP. Plugin serves /oidc-sso/callback | | scopes | yes | Defaults to ['openid', 'profile', 'email'] | | userMapping | yes | (claims, ctx) => ({ email, firstName?, lastName?, username?, groups? }). Async allowed. Throw to reject. | | autoCreateUsers | no | If true, unknown emails are auto-provisioned with defaultRoles. Default false | | defaultRoles | no | Strapi admin role codes (e.g. strapi-super-admin, strapi-editor, strapi-author) | | allowedDomains | no | If non-empty, only emails whose domain matches are allowed | | useUserinfo | no | If true, also fetches the userinfo endpoint and merges its claims | | buttonLabel | no | Default Login with SSO | | buttonIcon | no | Image URL, data URI, or inline <svg>...</svg> | | buttonStyle.* | no | CSS color overrides — all default to Strapi's design tokens |

You must provide either issuer (for discovery) or all of endpoints.{authorization, token, jwks}.

Running behind a reverse proxy

If a proxy serves Strapi under a path prefix — nginx exposing it at /strapi and stripping the prefix before proxying, say — set PUBLIC_URL (i.e. server.url) to the public URL including that prefix:

PUBLIC_URL=https://example.com/strapi

The plugin derives every browser-facing path from Strapi's own admin.absoluteUrl, which Strapi computes from server.url. With PUBLIC_URL set correctly you get:

| | Value | |---|---| | Redirect URI to register with the IdP | https://example.com/strapi/oidc-sso/callback | | Post-login landing | https://example.com/strapi/admin | | Error banner target | https://example.com/strapi/admin/auth/login | | Refresh-cookie path | /strapi/admin |

The admin-side button resolves the prefix from the URL in the address bar, so it requests /strapi/oidc-sso/login rather than /oidc-sso/login.

If PUBLIC_URL omits the prefix, the prefix is invisible to Strapi and sign-in completes but lands the browser on /admin, which the proxy will route to whatever else sits at the site root.

Provider examples

All examples assume OIDC_REDIRECT_URI=http://localhost:1337/oidc-sso/callback in development. Replace with your public URL in production and register the same URI verbatim with the IdP.

Microsoft Entra ID — single tenant

OIDC_ISSUER=https://login.microsoftonline.com/<tenant-id>/v2.0
OIDC_CLIENT_ID=...
OIDC_CLIENT_SECRET=...

Setup:

  1. Entra admin centre → App registrations → New registration.
  2. Authentication → Add platform → Web → set the redirect URI.
  3. Certificates & secrets → New client secret → copy the value into OIDC_CLIENT_SECRET.
  4. API permissions → keep the default User.Read (Microsoft Graph).
userMapping: (claims) => ({
  email:     claims.preferred_username ?? claims.email,
  firstName: claims.given_name,
  lastName:  claims.family_name,
}),

Microsoft Entra ID — multi-tenant (any work/school account)

OIDC_ISSUER=https://login.microsoftonline.com/common/v2.0
# or /organizations/v2.0 to exclude personal MSAs

Switch the app registration to "Accounts in any organizational directory". The plugin auto-detects the {tenantid} placeholder in /common/ discovery and validates the id_token's actual tid against the JWKS — no extra config needed. Combine with allowedDomains to restrict which tenants can sign in.

Google

OIDC_ISSUER=https://accounts.google.com
OIDC_CLIENT_ID=...apps.googleusercontent.com
OIDC_CLIENT_SECRET=...
OIDC_SCOPES=openid,profile,email

Setup:

  1. Google Cloud Console → APIs & Services → OAuth consent screen → configure (Internal if you use Workspace).
  2. Credentials → Create credentials → OAuth 2.0 Client ID → Web application.
  3. Add the redirect URI under Authorized redirect URIs.

Restrict to a single Google Workspace domain via the hd claim:

userMapping: (claims) => {
  if (claims.hd !== 'example.com') throw new Error('Not a company account');
  return {
    email:     claims.email,
    firstName: claims.given_name,
    lastName:  claims.family_name,
  };
},

Okta

OIDC_ISSUER=https://<your-org>.okta.com/oauth2/default
# or a custom auth server: https://<your-org>.okta.com/oauth2/<auth-server-id>
OIDC_CLIENT_ID=...
OIDC_CLIENT_SECRET=...

Setup:

  1. Okta admin → Applications → Create App Integration → OIDC – Web Application.
  2. Sign-in redirect URIs → add the callback URL.
  3. Assign users / groups under Assignments.

Pass Okta groups through if you include the groups scope and configure a groups claim on the auth server:

scopes: ['openid', 'profile', 'email', 'groups'],
userMapping: (claims) => ({
  email:     claims.email,
  firstName: claims.given_name,
  lastName:  claims.family_name,
  groups:    claims.groups as string[] | undefined,
}),

Auth0

OIDC_ISSUER=https://<tenant>.auth0.com/   # trailing slash matters
OIDC_CLIENT_ID=...
OIDC_CLIENT_SECRET=...

Setup:

  1. Auth0 dashboard → Applications → Create Application → Regular Web Application.
  2. Settings → set Allowed Callback URLs to the redirect URI.
  3. (Optional) add a custom claim via an Action to forward roles/groups — Auth0 strips non-OIDC claim names, so namespace them:
// Auth0 Action — Login / Post Login
exports.onExecutePostLogin = async (event, api) => {
  api.idToken.setCustomClaim('https://strapi/roles', event.authorization?.roles ?? []);
};
userMapping: (claims) => ({
  email:     claims.email,
  firstName: claims.given_name,
  lastName:  claims.family_name,
  groups:    claims['https://strapi/roles'] as string[] | undefined,
}),

AWS Cognito

OIDC_ISSUER=https://cognito-idp.<region>.amazonaws.com/<user-pool-id>
OIDC_CLIENT_ID=...
OIDC_CLIENT_SECRET=...

Setup:

  1. Cognito → User pools → your pool → App integration → create an App client (with secret).
  2. Configure a Hosted UI domain and add the callback URL under Allowed callback URLs.
  3. Enable identity providers (Cognito User Pool, Google, SAML, etc.) on the app client.

Cognito puts emails in email and the username in cognito:username:

userMapping: (claims) => ({
  email:     claims.email,
  username:  claims['cognito:username'],
  firstName: claims.given_name,
  lastName:  claims.family_name,
  groups:    claims['cognito:groups'] as string[] | undefined,
}),

Keycloak

OIDC_ISSUER=https://keycloak.example.com/realms/<realm>
OIDC_CLIENT_ID=...
OIDC_CLIENT_SECRET=...

Setup:

  1. Keycloak admin → realm → Clients → Create client → OpenID Connect, Client authentication ON.
  2. Valid redirect URIs → add the callback URL.
  3. Credentials tab → copy the client secret.

Forward realm/client roles via the built-in roles scope:

scopes: ['openid', 'profile', 'email', 'roles'],
userMapping: (claims) => ({
  email:     claims.email,
  firstName: claims.given_name,
  lastName:  claims.family_name,
  groups:    (claims.realm_access as { roles?: string[] } | undefined)?.roles,
}),

Authentik

OIDC_ISSUER=https://authentik.example.com/application/o/<slug>/
OIDC_CLIENT_ID=...
OIDC_CLIENT_SECRET=...

Create an OAuth2/OpenID provider, then an application that points at it. The issuer ends with the application slug and a trailing slash.

GitHub

GitHub OAuth apps do not implement OpenID Connect — they don't issue an id_token, so this plugin can't talk to GitHub directly. Bridge GitHub through an OIDC-capable IdP:

  • Auth0 — add GitHub as a social connection, then point the plugin at your Auth0 tenant (see above).
  • Keycloak — add a GitHub Identity provider under your realm, then point the plugin at the realm.
  • Authentik — add a GitHub Source, then expose it through an OAuth2/OpenID provider.
  • AWS Cognito — federate GitHub via a custom OIDC/SAML identity provider on the user pool.

The plugin sees the bridge IdP; the bridge handles the GitHub OAuth dance and re-issues a proper OIDC id_token carrying the GitHub claims.

How it works

[Login Page]                       [Strapi Server]              [IdP]
     │                                    │                       │
  click "Login with SSO"                  │                       │
     │── GET /oidc-sso/login ────────────▶│                       │
     │◀── 302 to IdP (state, nonce, PKCE)─│                       │
     │── user authenticates ────────────────────────────────────▶│
     │◀── 302 /oidc-sso/callback?code ──────────────────────────│
     │── GET /oidc-sso/callback ─────────▶│                       │
     │                                    │── code → tokens ─────▶│
     │                                    │── verify id_token     │
     │                                    │── find-or-create user │
     │                                    │── sessionManager → JWT│
     │◀── HTML handoff (sets jwtToken,    │                       │
     │    redirects to /admin) ───────────│                       │
     │                                                            │
  user lands in /admin, fully logged in                           │

The handoff page is a per-request CSP-noncedened HTML response — no React route needed, no auth context plumbing. The Strapi admin shell takes over via its standard localStorage.jwtToken boot sequence.

Security

| Threat | Mitigation | |------------------------------|-------------------------------------------------------------------| | Authorization-code injection | state parameter, signed cookie, one-shot | | ID-token replay | nonce parameter, verified | | ID-token forgery | JWKS signature check, iss, aud, exp validated | | Stolen authorization code | PKCE (S256 code_challenge + code_verifier) | | Token leakage via referer | Access token only sent in HTML body, never in URL | | Session fixation | Fresh Strapi access + refresh tokens issued on every successful login | | Disabled users | isActive/blocked flags checked before issuing tokens | | Unwanted sign-ups | autoCreateUsers is false by default | | Cookie tampering | State cookie HMAC-signed with ADMIN_JWT_SECRET |

Provider-specific quirks the plugin handles

  • Microsoft /common/ and /organizations/ — the discovery doc returns iss: "https://login.microsoftonline.com/{tenantid}/v2.0" literally. The plugin detects the {tenantid} placeholder, decodes the id_token to get the real tid, and validates against the expanded issuer URL using jose + the discovered JWKS.
  • Empty allow-list parsed as [""] — env.array('FOO', []) parses "" as [""]. The plugin filters empty strings before checking allowedDomains.

Development

pnpm install
pnpm build      # SDK build + tsc declaration emit
pnpm verify     # @strapi/sdk-plugin package shape check
pnpm watch:link # live-link into a host Strapi app

License

MIT