Merge pull request #20634 from strapi/enh/u-p-provider-registration

enhancement: add a way to register a u&p provider
This commit is contained in:
Alexandre BODIN 2024-07-02 09:39:34 +02:00 committed by GitHub
commit d1acea1041
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 495 additions and 440 deletions

View File

@ -1,140 +0,0 @@
'use strict';
module.exports = (baseURL) => ({
email: {
enabled: true,
icon: 'envelope',
},
discord: {
enabled: false,
icon: 'discord',
key: '',
secret: '',
callback: `${baseURL}/discord/callback`,
scope: ['identify', 'email'],
},
facebook: {
enabled: false,
icon: 'facebook-square',
key: '',
secret: '',
callback: `${baseURL}/facebook/callback`,
scope: ['email'],
},
google: {
enabled: false,
icon: 'google',
key: '',
secret: '',
callback: `${baseURL}/google/callback`,
scope: ['email'],
},
github: {
enabled: false,
icon: 'github',
key: '',
secret: '',
callback: `${baseURL}/github/callback`,
scope: ['user', 'user:email'],
},
microsoft: {
enabled: false,
icon: 'windows',
key: '',
secret: '',
callback: `${baseURL}/microsoft/callback`,
scope: ['user.read'],
},
twitter: {
enabled: false,
icon: 'twitter',
key: '',
secret: '',
callback: `${baseURL}/twitter/callback`,
},
instagram: {
enabled: false,
icon: 'instagram',
key: '',
secret: '',
callback: `${baseURL}/instagram/callback`,
scope: ['user_profile'],
},
vk: {
enabled: false,
icon: 'vk',
key: '',
secret: '',
callback: `${baseURL}/vk/callback`,
scope: ['email'],
},
twitch: {
enabled: false,
icon: 'twitch',
key: '',
secret: '',
callback: `${baseURL}/twitch/callback`,
scope: ['user:read:email'],
},
linkedin: {
enabled: false,
icon: 'linkedin',
key: '',
secret: '',
callback: `${baseURL}/linkedin/callback`,
scope: ['r_liteprofile', 'r_emailaddress'],
},
cognito: {
enabled: false,
icon: 'aws',
key: '',
secret: '',
subdomain: 'my.subdomain.com',
callback: `${baseURL}/cognito/callback`,
scope: ['email', 'openid', 'profile'],
},
reddit: {
enabled: false,
icon: 'reddit',
key: '',
secret: '',
state: true,
callback: `${baseURL}/reddit/callback`,
scope: ['identity'],
},
auth0: {
enabled: false,
icon: '',
key: '',
secret: '',
subdomain: 'my-tenant.eu',
callback: `${baseURL}/auth0/callback`,
scope: ['openid', 'email', 'profile'],
},
cas: {
enabled: false,
icon: 'book',
key: '',
secret: '',
callback: `${baseURL}/cas/callback`,
scope: ['openid email'], // scopes should be space delimited
subdomain: 'my.subdomain.com/cas',
},
patreon: {
enabled: false,
icon: '',
key: '',
secret: '',
callback: `${baseURL}/patreon/callback`,
scope: ['identity', 'identity[email]'],
},
keycloak: {
enabled: false,
icon: '',
key: '',
secret: '',
subdomain: 'myKeycloakProvider.com/realms/myrealm',
callback: `${baseURL}/keycloak/callback`,
scope: ['openid', 'email', 'profile'],
},
});

View File

@ -9,22 +9,26 @@
*/ */
const crypto = require('crypto'); const crypto = require('crypto');
const _ = require('lodash'); const _ = require('lodash');
const urljoin = require('url-join');
const { getService } = require('../utils'); const { getService } = require('../utils');
const getGrantConfig = require('./grant-config');
const usersPermissionsActions = require('./users-permissions-actions'); const usersPermissionsActions = require('./users-permissions-actions');
const initGrant = async (pluginStore) => { const initGrant = async (pluginStore) => {
const apiPrefix = strapi.config.get('api.rest.prefix'); const allProviders = getService('providers-registry').getAll();
const baseURL = urljoin(strapi.config.server.url, apiPrefix, 'auth');
const grantConfig = getGrantConfig(baseURL); const grantConfig = Object.entries(allProviders).reduce((acc, [name, provider]) => {
const { icon, enabled, grantConfig } = provider;
acc[name] = {
icon,
enabled,
...grantConfig,
};
return acc;
}, {});
const prevGrantConfig = (await pluginStore.get({ key: 'grant' })) || {}; const prevGrantConfig = (await pluginStore.get({ key: 'grant' })) || {};
// store grant auth config to db
// when plugin_users-permissions_grant is not existed in db if (!prevGrantConfig || !_.isEqual(prevGrantConfig, grantConfig)) {
// or we have added/deleted provider here.
if (!prevGrantConfig || !_.isEqual(_.keys(prevGrantConfig), _.keys(grantConfig))) {
// merge with the previous provider config. // merge with the previous provider config.
_.keys(grantConfig).forEach((key) => { _.keys(grantConfig).forEach((key) => {
if (key in prevGrantConfig) { if (key in prevGrantConfig) {

View File

@ -2,6 +2,7 @@
const { strict: assert } = require('assert'); const { strict: assert } = require('assert');
const jwt = require('jsonwebtoken'); const jwt = require('jsonwebtoken');
const urljoin = require('url-join');
const jwkToPem = require('jwk-to-pem'); const jwkToPem = require('jwk-to-pem');
const getCognitoPayload = async ({ idToken, jwksUrl, purest }) => { const getCognitoPayload = async ({ idToken, jwksUrl, purest }) => {
@ -45,8 +46,22 @@ const getCognitoPayload = async ({ idToken, jwksUrl, purest }) => {
} }
}; };
const getInitialProviders = ({ purest }) => ({ const initProviders = ({ baseURL, purest }) => ({
async discord({ accessToken }) { email: {
enabled: true,
icon: 'envelope',
grantConfig: {},
},
discord: {
enabled: false,
icon: 'discord',
grantConfig: {
key: '',
secret: '',
callbackUrl: `${baseURL}/discord/callback`,
scope: ['identify', 'email'],
},
async authCallback({ accessToken }) {
const discord = purest({ provider: 'discord' }); const discord = purest({ provider: 'discord' });
return discord return discord
@ -62,16 +77,17 @@ const getInitialProviders = ({ purest }) => ({
}; };
}); });
}, },
async cognito({ query, providers }) {
const jwksUrl = new URL(providers.cognito.jwksurl);
const idToken = query.id_token;
const tokenPayload = await getCognitoPayload({ idToken, jwksUrl, purest });
return {
username: tokenPayload['cognito:username'],
email: tokenPayload.email,
};
}, },
async facebook({ accessToken }) { facebook: {
enabled: false,
icon: 'facebook-square',
grantConfig: {
key: '',
secret: '',
callbackUrl: `${baseURL}/facebook/callback`,
scope: ['email'],
},
async authCallback({ accessToken }) {
const facebook = purest({ provider: 'facebook' }); const facebook = purest({ provider: 'facebook' });
return facebook return facebook
@ -84,7 +100,17 @@ const getInitialProviders = ({ purest }) => ({
email: body.email, email: body.email,
})); }));
}, },
async google({ accessToken }) { },
google: {
enabled: false,
icon: 'google',
grantConfig: {
key: '',
secret: '',
callbackUrl: `${baseURL}/google/callback`,
scope: ['email'],
},
async authCallback({ accessToken }) {
const google = purest({ provider: 'google' }); const google = purest({ provider: 'google' });
return google return google
@ -97,7 +123,17 @@ const getInitialProviders = ({ purest }) => ({
email: body.email, email: body.email,
})); }));
}, },
async github({ accessToken }) { },
github: {
enabled: false,
icon: 'github',
grantConfig: {
key: '',
secret: '',
callbackUrl: `${baseURL}/github/callback`,
scope: ['user', 'user:email'],
},
async authCallback({ accessToken }) {
const github = purest({ const github = purest({
provider: 'github', provider: 'github',
defaults: { defaults: {
@ -126,7 +162,17 @@ const getInitialProviders = ({ purest }) => ({
: null, : null,
}; };
}, },
async microsoft({ accessToken }) { },
microsoft: {
enabled: false,
icon: 'windows',
grantConfig: {
key: '',
secret: '',
callbackUrl: `${baseURL}/microsoft/callback`,
scope: ['user.read'],
},
async authCallback({ accessToken }) {
const microsoft = purest({ provider: 'microsoft' }); const microsoft = purest({ provider: 'microsoft' });
return microsoft return microsoft
@ -138,7 +184,17 @@ const getInitialProviders = ({ purest }) => ({
email: body.userPrincipalName, email: body.userPrincipalName,
})); }));
}, },
async twitter({ accessToken, query, providers }) { },
twitter: {
enabled: false,
icon: 'twitter',
grantConfig: {
key: '',
secret: '',
callbackUrl: `${baseURL}/twitter/callback`,
},
async authCallback({ accessToken, query, providers }) {
const twitter = purest({ const twitter = purest({
provider: 'twitter', provider: 'twitter',
defaults: { defaults: {
@ -159,7 +215,17 @@ const getInitialProviders = ({ purest }) => ({
email: body.email, email: body.email,
})); }));
}, },
async instagram({ accessToken }) { },
instagram: {
enabled: false,
icon: 'instagram',
grantConfig: {
key: '',
secret: '',
callbackUrl: `${baseURL}/instagram/callback`,
scope: ['user_profile'],
},
async authCallback({ accessToken }) {
const instagram = purest({ provider: 'instagram' }); const instagram = purest({ provider: 'instagram' });
return instagram return instagram
@ -172,7 +238,17 @@ const getInitialProviders = ({ purest }) => ({
email: `${body.username}@strapi.io`, // dummy email as Instagram does not provide user email email: `${body.username}@strapi.io`, // dummy email as Instagram does not provide user email
})); }));
}, },
async vk({ accessToken, query }) { },
vk: {
enabled: false,
icon: 'vk',
grantConfig: {
key: '',
secret: '',
callbackUrl: `${baseURL}/vk/callback`,
scope: ['email'],
},
async authCallback({ accessToken, query }) {
const vk = purest({ provider: 'vk' }); const vk = purest({ provider: 'vk' });
return vk return vk
@ -185,7 +261,18 @@ const getInitialProviders = ({ purest }) => ({
email: query.raw.email, email: query.raw.email,
})); }));
}, },
async twitch({ accessToken, providers }) { },
twitch: {
enabled: false,
icon: 'twitch',
grantConfig: {
key: '',
secret: '',
callbackUrl: `${baseURL}/twitch/callback`,
scope: ['user:read:email'],
},
async authCallback({ accessToken, providers }) {
const twitch = purest({ const twitch = purest({
provider: 'twitch', provider: 'twitch',
config: { config: {
@ -211,7 +298,18 @@ const getInitialProviders = ({ purest }) => ({
email: body.data[0].email, email: body.data[0].email,
})); }));
}, },
async linkedin({ accessToken }) { },
linkedin: {
enabled: false,
icon: 'linkedin',
grantConfig: {
key: '',
secret: '',
callbackUrl: `${baseURL}/linkedin/callback`,
scope: ['r_liteprofile', 'r_emailaddress'],
},
async authCallback({ accessToken }) {
const linkedIn = purest({ provider: 'linkedin' }); const linkedIn = purest({ provider: 'linkedin' });
const { const {
body: { localizedFirstName }, body: { localizedFirstName },
@ -230,7 +328,39 @@ const getInitialProviders = ({ purest }) => ({
email: email.emailAddress, email: email.emailAddress,
}; };
}, },
async reddit({ accessToken }) { },
cognito: {
enabled: false,
icon: 'aws',
grantConfig: {
key: '',
secret: '',
subdomain: 'my.subdomain.com',
callback: `${baseURL}/cognito/callback`,
scope: ['email', 'openid', 'profile'],
},
async authCallback({ query, providers }) {
const jwksUrl = new URL(providers.cognito.jwksurl);
const idToken = query.id_token;
const tokenPayload = await getCognitoPayload({ idToken, jwksUrl, purest });
return {
username: tokenPayload['cognito:username'],
email: tokenPayload.email,
};
},
},
reddit: {
enabled: false,
icon: 'reddit',
grantConfig: {
key: '',
secret: '',
callback: `${baseURL}/reddit/callback`,
scope: ['identity'],
},
async authCallback({ accessToken }) {
const reddit = purest({ const reddit = purest({
provider: 'reddit', provider: 'reddit',
config: { config: {
@ -257,7 +387,19 @@ const getInitialProviders = ({ purest }) => ({
email: `${body.name}@strapi.io`, // dummy email as Reddit does not provide user email email: `${body.name}@strapi.io`, // dummy email as Reddit does not provide user email
})); }));
}, },
async auth0({ accessToken, providers }) { },
auth0: {
enabled: false,
icon: '',
grantConfig: {
key: '',
secret: '',
subdomain: 'my-tenant.eu',
callback: `${baseURL}/auth0/callback`,
scope: ['openid', 'email', 'profile'],
},
async authCallback({ accessToken, providers }) {
const auth0 = purest({ provider: 'auth0' }); const auth0 = purest({ provider: 'auth0' });
return auth0 return auth0
@ -275,7 +417,19 @@ const getInitialProviders = ({ purest }) => ({
}; };
}); });
}, },
async cas({ accessToken, providers }) { },
cas: {
enabled: false,
icon: 'book',
grantConfig: {
key: '',
secret: '',
callback: `${baseURL}/cas/callback`,
scope: ['openid email'], // scopes should be space delimited
subdomain: 'my.subdomain.com/cas',
},
async authCallback({ accessToken, providers }) {
const cas = purest({ provider: 'cas' }); const cas = purest({ provider: 'cas' });
return cas return cas
@ -302,7 +456,18 @@ const getInitialProviders = ({ purest }) => ({
}; };
}); });
}, },
async patreon({ accessToken }) { },
patreon: {
enabled: false,
icon: '',
grantConfig: {
key: '',
secret: '',
callback: `${baseURL}/patreon/callback`,
scope: ['identity', 'identity[email]'],
},
async authCallback({ accessToken }) {
const patreon = purest({ const patreon = purest({
provider: 'patreon', provider: 'patreon',
config: { config: {
@ -331,7 +496,18 @@ const getInitialProviders = ({ purest }) => ({
}; };
}); });
}, },
async keycloak({ accessToken, providers }) { },
keycloack: {
enabled: false,
icon: '',
grantConfig: {
key: '',
secret: '',
subdomain: 'myKeycloakProvider.com/realms/myrealm',
callback: `${baseURL}/keycloak/callback`,
scope: ['openid', 'email', 'profile'],
},
async authCallback({ accessToken, providers }) {
const keycloak = purest({ provider: 'keycloak' }); const keycloak = purest({ provider: 'keycloak' });
return keycloak return keycloak
@ -346,29 +522,43 @@ const getInitialProviders = ({ purest }) => ({
}; };
}); });
}, },
},
}); });
module.exports = () => { module.exports = () => {
const purest = require('purest'); const purest = require('purest');
const providersCallbacks = getInitialProviders({ purest }); const apiPrefix = strapi.config.get('api.rest.prefix');
const baseURL = urljoin(strapi.config.server.url, apiPrefix, 'auth');
const authProviders = initProviders({ baseURL, purest });
/**
* @public
*/
return { return {
register(providerName, provider) { getAll() {
assert(typeof providerName === 'string', 'Provider name must be a string'); return authProviders;
assert(typeof provider === 'function', 'Provider callback must be a function'); },
get(name) {
providersCallbacks[providerName] = provider({ purest }); return authProviders[name];
},
add(name, config) {
authProviders[name] = config;
},
remove(name) {
delete authProviders[name];
}, },
/**
* @internal
*/
async run({ provider, accessToken, query, providers }) { async run({ provider, accessToken, query, providers }) {
if (!providersCallbacks[provider]) { const authProvider = authProviders[provider];
throw new Error('Unknown provider.');
}
const providerCb = providersCallbacks[provider]; assert(authProvider, 'Unknown auth provider');
return providerCb({ accessToken, query, providers }); return authProvider.authCallback({ accessToken, query, providers, purest });
}, },
}; };
}; };

View File

@ -3,6 +3,7 @@ import * as user from '../services/user';
import * as role from '../services/role'; import * as role from '../services/role';
import * as jwt from '../services/jwt'; import * as jwt from '../services/jwt';
import * as providers from '../services/providers'; import * as providers from '../services/providers';
import * as providersRegistry from '../services/providers-registry';
import * as permission from '../services/permission'; import * as permission from '../services/permission';
type S = { type S = {
@ -11,7 +12,7 @@ type S = {
user: typeof user; user: typeof user;
jwt: typeof jwt; jwt: typeof jwt;
providers: typeof providers; providers: typeof providers;
['providers-registry']: typeof providers; ['providers-registry']: typeof providersRegistry;
permission: typeof permission; permission: typeof permission;
}; };