Single Sign-On
Single Sign-On on Strapi allows you to configure additional sign-in and sign-up methods for your administration panel.
- A Strapi application running on version 3.5.0 or higher is required.
- To configure SSO on your application, you will need an EE license with a Gold plan.
- Make sure the SSO feature is enabled in the admin panel.
- Make sure Strapi is part of the applications you can access with your provider. For example, with Microsoft (Azure) Active Directory, you must first ask someone with the right permissions to add Strapi to the list of allowed applications. Please refer to your provider(s) documentation to learn more about that.
It is currently not possible to associate a unique SSO provider to an email address used for a Strapi account, meaning that the access to a Strapi account cannot be restricted to only one SSO provider. For more information and workarounds to solve this issue, please refer to the dedicated GitHub issue.
SSO configuration lives in the server configuration of the application, found at ./config/admin.js
.
Accessing the configuration
The providers' configuration should be written within the auth.providers
path of the admin panel configuration.
auth.providers
is an array of provider configuration.
- JavaScript
- TypeScript
module.exports = ({ env }) => ({
// ...
auth: {
providers: [], // The providers' configuration lives there
},
});
export default ({ env }) => ({
// ...
auth: {
providers: [], // The providers' configuration lives there
},
});
Setting up provider configuration
A provider's configuration is a JavaScript object built with the following properties:
Name | Required | Type | Description |
---|---|---|---|
uid | true | string | The UID of the strategy. It must match the strategy's name |
displayName | true | string | The name that will be used on the login page to reference the provider |
icon | false | string | An image URL. If specified, it will replace the displayName on the login page |
createStrategy | true | function | A factory that will build and return a new passport strategy for your provider. Takes the strapi instance as parameter |
The uid
property is the unique identifier of each strategy and is generally found in the strategy's package. If you are not sure of what it refers to, please contact the maintainer of the strategy.
By default, Strapi security policy does not allow loading images from external URLs, so provider logos will not show up on the login screen of the admin panel unless a security exception is added.
Example: Security exception for provider logos
- JavaScript
- TypeScript
module.exports = [
// ...
{
name: 'strapi::security',
config: {
contentSecurityPolicy: {
useDefaults: true,
directives: {
'connect-src': ["'self'", 'https:'],
'img-src': [
"'self'",
'data:',
'blob:',
'dl.airtable.com',
'www.okta.com', // Base URL of the provider's logo
],
'media-src': [
"'self'",
'data:',
'blob:',
'dl.airtable.com',
'www.okta.com', // Base URL of the provider's logo
],
upgradeInsecureRequests: null,
},
},
},
},
// ...
]
export default [
// ...
{
name: 'strapi::security',
config: {
contentSecurityPolicy: {
useDefaults: true,
directives: {
'connect-src': ["'self'", 'https:'],
'img-src': [
"'self'",
'data:',
'blob:',
'dl.airtable.com',
'www.okta.com', // Base URL of the provider's logo
],
'media-src': [
"'self'",
'data:',
'blob:',
'dl.airtable.com',
'www.okta.com', // Base URL of the provider's logo
],
upgradeInsecureRequests: null,
},
},
},
},
// ...
]
When deploying the admin panel to a different location or on a different subdomain, an additional configuration is required to set the common domain for the cookies. This is required to ensure the cookies are shared across the domains.
Deploying the admin and backend on entirely different unrelated domains is not possible at this time when using SSO.
Example: Setting custom cookie domain
- JavaScript
- TypeScript
module.exports = ({ env }) => ({
auth: {
domain: env("ADMIN_SSO_DOMAIN", ".test.example.com"),
providers: [
// ...
],
},
url: env("ADMIN_URL", "http://admin.test.example.com"),
// ...
});
export default ({ env }) => ({
auth: {
domain: env("ADMIN_SSO_DOMAIN", ".test.example.com"),
providers: [
// ...
],
},
url: env("ADMIN_URL", "http://admin.test.example.com"),
// ...
});
The createStrategy
Factory
A passport strategy is usually built by instantiating it using 2 parameters: the configuration object, and the verify function.
Configuration Object
The configuration object depends on the strategy needs, but often asks for a callback URL to be redirected to once the connection has been made on the provider side.
A specific callback URL can be generated for your provider using the getStrategyCallbackURL
method. This URL also needs to be written on the provider side in order to allow redirection from it.
The format of the callback URL is the following: /admin/connect/<provider_uid>
.
strapi.admin.services.passport.getStrategyCallbackURL
is a Strapi helper you can use to get a callback URL for a specific provider. It takes a provider name as a parameter and returns a URL.
If needed, this is also where you will put your client ID and secret key for your OAuth2 application.
Verify Function
The verify function is used here as a middleware allowing the user to transform and make extra processing on the data returned from the provider API.
This function always takes a done
method as last parameter which is used to transfer needed data to the Strapi layer of SSO.
Its signature is the following: void done(error: any, data: object);
and it follows the following rules:
- If
error
is not set tonull
, then the data sent is ignored, and the controller will throw an error. - If the SSO's auto-registration feature is disabled, then the
data
object only need to be composed of anemail
property. - If the SSO's auto-registration feature is enabled, then you will need to define (in addition to the
email
) either ausername
property or bothfirstname
andlastname
within thedata
object.
Adding a provider
Adding a new provider means adding a new way for your administrators to log-in.
Strapi uses Passport.js, which enables a large selection of providers. Any valid passport strategy that doesn't need additional custom data should therefore work with Strapi.
Strategies such as ldapauth don't work out of the box since they require extra data to be sent from the admin panel. If you want to add an LDAP provider to your application, you will need to write a custom strategy. You can also use services such as Okta and Auth0 as bridge services.
Configuring the provider
To configure a provider, follow the procedure below:
- Make sure to import your strategy in your admin configuration file, either from an installed package or a local file.
- You'll need to add a new item to the
auth.providers
array in your admin panel configuration that will match the format given above - Restart your application, the provider should appear on your admin login page.
Provider configuration examples
Google
Using: passport-google-oauth2
- yarn
- npm
yarn add passport-google-oauth2
npm install --save passport-google-oauth2
Configuration example for Google:
- JavaScript
- TypeScript
const GoogleStrategy = require("passport-google-oauth2");
module.exports = ({ env }) => ({
auth: {
// ...
providers: [
{
uid: "google",
displayName: "Google",
icon: "https://cdn2.iconfinder.com/data/icons/social-icons-33/128/Google-512.png",
createStrategy: (strapi) =>
new GoogleStrategy(
{
clientID: env("GOOGLE_CLIENT_ID"),
clientSecret: env("GOOGLE_CLIENT_SECRET"),
scope: [
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/userinfo.profile",
],
callbackURL:
strapi.admin.services.passport.getStrategyCallbackURL("google"),
},
(request, accessToken, refreshToken, profile, done) => {
done(null, {
email: profile.email,
firstname: profile.given_name,
lastname: profile.family_name,
});
}
),
},
],
},
});
import {Strategy as GoogleStrategy } from "passport-google-oauth2";
export default ({ env }) => ({
auth: {
// ...
providers: [
{
uid: "google",
displayName: "Google",
icon: "https://cdn2.iconfinder.com/data/icons/social-icons-33/128/Google-512.png",
createStrategy: (strapi) =>
new GoogleStrategy(
{
clientID: env("GOOGLE_CLIENT_ID"),
clientSecret: env("GOOGLE_CLIENT_SECRET"),
scope: [
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/userinfo.profile",
],
callbackURL:
strapi.admin.services.passport.getStrategyCallbackURL("google"),
},
(request, accessToken, refreshToken, profile, done) => {
done(null, {
email: profile.email,
firstname: profile.given_name,
lastname: profile.family_name,
});
}
),
},
],
},
});
Github
Using: passport-github
- yarn
- npm
yarn add passport-github2
npm install --save passport-github2
Configuration example for Github:
- JavaScript
- TypeScript
const GithubStrategy = require("passport-github2");
module.exports = ({ env }) => ({
auth: {
// ...
providers: [
{
uid: "github",
displayName: "Github",
icon: "https://cdn1.iconfinder.com/data/icons/logotypes/32/github-512.png",
createStrategy: (strapi) =>
new GithubStrategy(
{
clientID: env("GITHUB_CLIENT_ID"),
clientSecret: env("GITHUB_CLIENT_SECRET"),
scope: ["user:email"],
callbackURL:
strapi.admin.services.passport.getStrategyCallbackURL("github"),
},
(accessToken, refreshToken, profile, done) => {
done(null, {
email: profile.emails[0].value,
username: profile.username,
});
}
),
},
],
},
});
import { Strategy as GithubStrategy } from "passport-github2";
export default ({ env }) => ({
auth: {
// ...
providers: [
{
uid: "github",
displayName: "Github",
icon: "https://cdn1.iconfinder.com/data/icons/logotypes/32/github-512.png",
createStrategy: (strapi) =>
new GithubStrategy(
{
clientID: env("GITHUB_CLIENT_ID"),
clientSecret: env("GITHUB_CLIENT_SECRET"),
scope: ["user:email"],
callbackURL:
strapi.admin.services.passport.getStrategyCallbackURL("github"),
},
(accessToken, refreshToken, profile, done) => {
done(null, {
email: profile.emails[0].value,
username: profile.username,
});
}
),
},
],
},
});
Discord
Using: passport-discord
- yarn
- npm
yarn add passport-discord
npm install --save passport-discord
Configuration example for Discord:
- JavaScript
- TypeScript
const DiscordStrategy = require("passport-discord");
module.exports = ({ env }) => ({
auth: {
// ...
providers: [
{
uid: "discord",
displayName: "Discord",
icon: "https://cdn0.iconfinder.com/data/icons/free-social-media-set/24/discord-512.png",
createStrategy: (strapi) =>
new DiscordStrategy(
{
clientID: env("DISCORD_CLIENT_ID"),
clientSecret: env("DISCORD_SECRET"),
callbackURL:
strapi.admin.services.passport.getStrategyCallbackURL(
"discord"
),
scope: ["identify", "email"],
},
(accessToken, refreshToken, profile, done) => {
done(null, {
email: profile.email,
username: `${profile.username}#${profile.discriminator}`,
});
}
),
},
],
},
});
import { Strategy as DiscordStrategy } from "passport-discord";
export default ({ env }) => ({
auth: {
// ...
providers: [
{
uid: "discord",
displayName: "Discord",
icon: "https://cdn0.iconfinder.com/data/icons/free-social-media-set/24/discord-512.png",
createStrategy: (strapi) =>
new DiscordStrategy(
{
clientID: env("DISCORD_CLIENT_ID"),
clientSecret: env("DISCORD_SECRET"),
callbackURL:
strapi.admin.services.passport.getStrategyCallbackURL(
"discord"
),
scope: ["identify", "email"],
},
(accessToken, refreshToken, profile, done) => {
done(null, {
email: profile.email,
username: `${profile.username}#${profile.discriminator}`,
});
}
),
},
],
},
});
Microsoft
Using: passport-azure-ad-oauth2
- yarn
- npm
yarn add passport-azure-ad-oauth2 jsonwebtoken
npm install --save passport-azure-ad-oauth2 jsonwebtoken
Configuration example for Microsoft:
- JavaScript
- TypeScript
const AzureAdOAuth2Strategy = require("passport-azure-ad-oauth2");
const jwt = require("jsonwebtoken");
module.exports = ({ env }) => ({
auth: {
// ...
providers: [
{
uid: "azure_ad_oauth2",
displayName: "Microsoft",
icon: "https://upload.wikimedia.org/wikipedia/commons/thumb/9/96/Microsoft_logo_%282012%29.svg/320px-Microsoft_logo_%282012%29.svg.png",
createStrategy: (strapi) =>
new AzureAdOAuth2Strategy(
{
clientID: env("MICROSOFT_CLIENT_ID", ""),
clientSecret: env("MICROSOFT_CLIENT_SECRET", ""),
scope: ["user:email"],
tenant: env("MICROSOFT_TENANT_ID", ""),
callbackURL:
strapi.admin.services.passport.getStrategyCallbackURL(
"azure_ad_oauth2"
),
},
(accessToken, refreshToken, params, profile, done) => {
let waadProfile = jwt.decode(params.id_token, "", true);
done(null, {
email: waadProfile.email,
username: waadProfile.email,
firstname: waadProfile.given_name, // optional if email and username exist
lastname: waadProfile.family_name, // optional if email and username exist
});
}
),
},
],
},
});
import { Strategy as AzureAdOAuth2Strategy} from "passport-azure-ad-oauth2";
import jwt from "jsonwebtoken";
export default ({ env }) => ({
auth: {
// ...
providers: [
{
uid: "azure_ad_oauth2",
displayName: "Microsoft",
icon: "https://upload.wikimedia.org/wikipedia/commons/thumb/9/96/Microsoft_logo_%282012%29.svg/320px-Microsoft_logo_%282012%29.svg.png",
createStrategy: (strapi) =>
new AzureAdOAuth2Strategy(
{
clientID: env("MICROSOFT_CLIENT_ID", ""),
clientSecret: env("MICROSOFT_CLIENT_SECRET", ""),
scope: ["user:email"],
tenant: env("MICROSOFT_TENANT_ID", ""),
callbackURL:
strapi.admin.services.passport.getStrategyCallbackURL(
"azure_ad_oauth2"
),
},
(accessToken, refreshToken, params, profile, done) => {
let waadProfile = jwt.decode(params.id_token, "", true);
done(null, {
email: waadProfile.email,
username: waadProfile.email,
firstname: waadProfile.given_name, // optional if email and username exist
lastname: waadProfile.family_name, // optional if email and username exist
});
}
),
},
],
},
});
Keycloak (OpenID Connect)
Using: passport-keycloak-oauth2-oidc
- yarn
- npm
yarn add passport-keycloak-oauth2-oidc
npm install --save passport-keycloak-oauth2-oidc
Configuration example for Keycloak (OpenID Connect):
- JavaScript
- TypeScript
const KeyCloakStrategy = require("passport-keycloak-oauth2-oidc");
module.exports = ({ env }) => ({
auth: {
// ...
providers: [
{
uid: "keycloak",
displayName: "Keycloak",
icon: "https://raw.githubusercontent.com/keycloak/keycloak-admin-ui/main/themes/keycloak/logo.svg",
createStrategy: (strapi) =>
new KeyCloakStrategy(
{
clientID: env("KEYCLOAK_CLIENT_ID", ""),
realm: env("KEYCLOAK_REALM", ""),
publicClient: env.bool("KEYCLOAK_PUBLIC_CLIENT", false),
clientSecret: env("KEYCLOAK_CLIENT_SECRET", ""),
sslRequired: env("KEYCLOAK_SSL_REQUIRED", "external"),
authServerURL: env("KEYCLOAK_AUTH_SERVER_URL", ""),
callbackURL:
strapi.admin.services.passport.getStrategyCallbackURL(
"keycloak"
),
},
(accessToken, refreshToken, profile, done) => {
done(null, {
email: profile.email,
username: profile.username,
});
}
),
},
],
},
});
import { Strategy as KeyCloakStrategy } from "passport-keycloak-oauth2-oidc";
export default ({ env }) => ({
auth: {
// ...
providers: [
{
uid: "keycloak",
displayName: "Keycloak",
icon: "https://raw.githubusercontent.com/keycloak/keycloak-admin-ui/main/themes/keycloak/logo.svg",
createStrategy: (strapi) =>
new KeyCloakStrategy(
{
clientID: env("KEYCLOAK_CLIENT_ID", ""),
realm: env("KEYCLOAK_REALM", ""),
publicClient: env.bool("KEYCLOAK_PUBLIC_CLIENT", false),
clientSecret: env("KEYCLOAK_CLIENT_SECRET", ""),
sslRequired: env("KEYCLOAK_SSL_REQUIRED", "external"),
authServerURL: env("KEYCLOAK_AUTH_SERVER_URL", ""),
callbackURL:
strapi.admin.services.passport.getStrategyCallbackURL(
"keycloak"
),
},
(accessToken, refreshToken, profile, done) => {
done(null, {
email: profile.email,
username: profile.username,
});
}
),
},
],
},
});
Okta
Using: passport-okta-oauth20
- yarn
- npm
yarn add passport-okta-oauth20
npm install --save passport-okta-oauth20
When setting the OKTA_DOMAIN
environment variable, make sure to include the protocol (e.g. https://example.okta.com
). If you do not, you will end up in a redirect loop.
Configuration example for Okta:
- JavaScript
- TypeScript
const OktaOAuth2Strategy = require("passport-okta-oauth20").Strategy;
module.exports = ({ env }) => ({
auth: {
// ...
providers: [
{
uid: "okta",
displayName: "Okta",
icon: "https://www.okta.com/sites/default/files/Okta_Logo_BrightBlue_Medium-thumbnail.png",
createStrategy: (strapi) =>
new OktaOAuth2Strategy(
{
clientID: env("OKTA_CLIENT_ID"),
clientSecret: env("OKTA_CLIENT_SECRET"),
audience: env("OKTA_DOMAIN"),
scope: ["openid", "email", "profile"],
callbackURL:
strapi.admin.services.passport.getStrategyCallbackURL("okta"),
},
(accessToken, refreshToken, profile, done) => {
done(null, {
email: profile.email,
username: profile.username,
});
}
),
},
],
},
});
import { Strategy as OktaOAuth2Strategy } from "passport-okta-oauth20";
export default ({ env }) => ({
auth: {
// ...
providers: [
{
uid: "okta",
displayName: "Okta",
icon: "https://www.okta.com/sites/default/files/Okta_Logo_BrightBlue_Medium-thumbnail.png",
createStrategy: (strapi) =>
new OktaOAuth2Strategy(
{
clientID: env("OKTA_CLIENT_ID"),
clientSecret: env("OKTA_CLIENT_SECRET"),
audience: env("OKTA_DOMAIN"),
scope: ["openid", "email", "profile"],
callbackURL:
strapi.admin.services.passport.getStrategyCallbackURL("okta"),
},
(accessToken, refreshToken, profile, done) => {
done(null, {
email: profile.email,
username: profile.username,
});
}
),
},
],
},
});
Performing advanced customization
Admin panel URL
If the administration panel lives on a host/port different from the Strapi server, the admin panel URL needs to be updated:
update the url
key in the ./config/admin.js
configuration file (see admin panel customization documentation).
Custom Logic
In some scenarios, you will want to write additional logic for your connection workflow such as:
- restricting connection and registration for a specific domain
- triggering actions on connection attempt
- adding analytics
The easiest way to do so is to plug into the verify function of your strategy and write some code.
For example, if you want to allow only people with an official strapi.io email address, you can instantiate your strategy like this:
- JavaScript
- TypeScript
const strategyInstance = new Strategy(configuration, ({ email, username }, done) => {
// If the email ends with @strapi.io
if (email.endsWith('@strapi.io')) {
// then we continue with the data given by the provider
return done(null, { email, username });
}
// Otherwise, we continue by sending an error to the done function
done(new Error('Forbidden email address'));
});
const strategyInstance = new Strategy(configuration, ({ email, username }, done) => {
// If the email ends with @strapi.io
if (email.endsWith('@strapi.io')) {
// then we continue with the data given by the provider
return done(null, { email, username });
}
// Otherwise, we continue by sending an error to the done function
done(new Error('Forbidden email address'));
});
Authentication Events
The SSO feature adds a new authentication event: onSSOAutoRegistration
.
This event is triggered whenever a user is created using the auto-register feature added by SSO.
It contains the created user (event.user
), and the provider used to make the registration (event.provider
).
- JavaScript
- TypeScript
module.exports = () => ({
auth: {
// ...
events: {
onConnectionSuccess(e) {},
onConnectionError(e) {},
// ...
onSSOAutoRegistration(e) {
const { user, provider } = e;
console.log(
`A new user (${user.id}) has been automatically registered using ${provider}`
);
},
},
},
});
export default () => ({
auth: {
// ...
events: {
onConnectionSuccess(e) {},
onConnectionError(e) {},
// ...
onSSOAutoRegistration(e) {
const { user, provider } = e;
console.log(
`A new user (${user.id}) has been automatically registered using ${provider}`
);
},
},
},
});