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 _ = require('lodash');
const urljoin = require('url-join');
const { getService } = require('../utils');
const getGrantConfig = require('./grant-config');
const usersPermissionsActions = require('./users-permissions-actions');
const initGrant = async (pluginStore) => {
const apiPrefix = strapi.config.get('api.rest.prefix');
const baseURL = urljoin(strapi.config.server.url, apiPrefix, 'auth');
const allProviders = getService('providers-registry').getAll();
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' })) || {};
// store grant auth config to db
// when plugin_users-permissions_grant is not existed in db
// or we have added/deleted provider here.
if (!prevGrantConfig || !_.isEqual(_.keys(prevGrantConfig), _.keys(grantConfig))) {
if (!prevGrantConfig || !_.isEqual(prevGrantConfig, grantConfig)) {
// merge with the previous provider config.
_.keys(grantConfig).forEach((key) => {
if (key in prevGrantConfig) {

View File

@ -2,6 +2,7 @@
const { strict: assert } = require('assert');
const jwt = require('jsonwebtoken');
const urljoin = require('url-join');
const jwkToPem = require('jwk-to-pem');
const getCognitoPayload = async ({ idToken, jwksUrl, purest }) => {
@ -45,8 +46,22 @@ const getCognitoPayload = async ({ idToken, jwksUrl, purest }) => {
}
};
const getInitialProviders = ({ purest }) => ({
async discord({ accessToken }) {
const initProviders = ({ baseURL, purest }) => ({
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' });
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' });
return facebook
@ -84,7 +100,17 @@ const getInitialProviders = ({ purest }) => ({
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' });
return google
@ -97,7 +123,17 @@ const getInitialProviders = ({ purest }) => ({
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({
provider: 'github',
defaults: {
@ -126,7 +162,17 @@ const getInitialProviders = ({ purest }) => ({
: 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' });
return microsoft
@ -138,7 +184,17 @@ const getInitialProviders = ({ purest }) => ({
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({
provider: 'twitter',
defaults: {
@ -159,7 +215,17 @@ const getInitialProviders = ({ purest }) => ({
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' });
return instagram
@ -172,7 +238,17 @@ const getInitialProviders = ({ purest }) => ({
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' });
return vk
@ -185,7 +261,18 @@ const getInitialProviders = ({ purest }) => ({
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({
provider: 'twitch',
config: {
@ -211,7 +298,18 @@ const getInitialProviders = ({ purest }) => ({
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 {
body: { localizedFirstName },
@ -230,7 +328,39 @@ const getInitialProviders = ({ purest }) => ({
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({
provider: 'reddit',
config: {
@ -257,7 +387,19 @@ const getInitialProviders = ({ purest }) => ({
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' });
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' });
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({
provider: 'patreon',
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' });
return keycloak
@ -346,29 +522,43 @@ const getInitialProviders = ({ purest }) => ({
};
});
},
},
});
module.exports = () => {
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 {
register(providerName, provider) {
assert(typeof providerName === 'string', 'Provider name must be a string');
assert(typeof provider === 'function', 'Provider callback must be a function');
providersCallbacks[providerName] = provider({ purest });
getAll() {
return authProviders;
},
get(name) {
return authProviders[name];
},
add(name, config) {
authProviders[name] = config;
},
remove(name) {
delete authProviders[name];
},
/**
* @internal
*/
async run({ provider, accessToken, query, providers }) {
if (!providersCallbacks[provider]) {
throw new Error('Unknown provider.');
}
const authProvider = authProviders[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 jwt from '../services/jwt';
import * as providers from '../services/providers';
import * as providersRegistry from '../services/providers-registry';
import * as permission from '../services/permission';
type S = {
@ -11,7 +12,7 @@ type S = {
user: typeof user;
jwt: typeof jwt;
providers: typeof providers;
['providers-registry']: typeof providers;
['providers-registry']: typeof providersRegistry;
permission: typeof permission;
};