mirror of
https://hk.gh-proxy.com/https://github.com/NodeBB/nodebb-plugin-sso-oauth2-multiple.git
synced 2025-10-04 04:32:03 +08:00
feat: additional adjustment toggles for syncing full name, picture, and email verification trusting
This commit is contained in:
parent
8eda9de31e
commit
f774bf5622
5 changed files with 88 additions and 15 deletions
|
@ -1,6 +1,7 @@
|
|||
# NodeBB OAuth2 Multiple Client SSO
|
||||
|
||||
This NodeBB plugin allows you to configure logins to multiple configurable OAuth2 endpoints, via the admin backend.
|
||||
Use this plugin if you have a separate database of users and you'd like to allow access to the forum to those users via that database.
|
||||
|
||||
## Caveat
|
||||
|
||||
|
@ -14,6 +15,11 @@ Support for other OAuth2 providers is _explicitly not guaranteed_.
|
|||
If you'd like to help this plugin play nice with other providers, please
|
||||
[open an issue](https://github.com/NodeBB/nodebb-plugin-sso-oauth2-multiple/issues).
|
||||
|
||||
## Profile Updates
|
||||
|
||||
v1.4.0 of this plugin introduces the ability to update a user's full name and picture with data supplied by the remote userinfo endpoint.
|
||||
The functionality (including which fields to sync/ignore) can be configured on a per-strategy basis, under the "Adjustments" menu when editing a strategy.
|
||||
|
||||
## Role-Based Access Control
|
||||
|
||||
This plugin is able to sort users into specific user groups based on user roles.
|
||||
|
|
|
@ -73,6 +73,11 @@ Controllers.editStrategy = async (req, res) => {
|
|||
|
||||
payload.enabled = !!req.body.enabled;
|
||||
|
||||
const checkboxes = ['usernameViaEmail', 'trustEmailVerified', 'syncFullname', 'syncPicture'];
|
||||
checkboxes.forEach((prop) => {
|
||||
payload[prop] = payload.hasOwnProperty(prop) && payload[prop] === 'on' ? 1 : 0;
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
db.sortedSetAdd('oauth2-multiple:strategies', Date.now(), name),
|
||||
db.setObject(`oauth2-multiple:strategies:${name}`, payload),
|
||||
|
|
66
library.js
66
library.js
|
@ -89,7 +89,7 @@ OAuth.loadStrategies = async (strategies) => {
|
|||
callbackURL,
|
||||
passReqToCallback: true,
|
||||
}, async (req, token, secret, profile, done) => {
|
||||
const { id, displayName, email } = profile;
|
||||
const { id, displayName, email, email_verified } = profile;
|
||||
if (![id, displayName, email].every(Boolean)) {
|
||||
return done(new Error('insufficient-scope'));
|
||||
}
|
||||
|
@ -99,10 +99,12 @@ OAuth.loadStrategies = async (strategies) => {
|
|||
oAuthid: id,
|
||||
handle: displayName,
|
||||
email,
|
||||
email_verified,
|
||||
});
|
||||
winston.verbose(`[plugin/sso-oauth2-multiple] Successful login to uid ${user.uid} via ${name} (remote id ${id})`);
|
||||
await authenticationController.onSuccessfulLogin(req, user.uid);
|
||||
await OAuth.assignGroups({ provider: name, user, profile });
|
||||
await OAuth.updateProfile(user.uid, profile);
|
||||
done(null, user);
|
||||
|
||||
plugins.hooks.fire('action:oauth2.login', { name, user, profile });
|
||||
|
@ -159,17 +161,27 @@ OAuth.getUserProfile = function (name, userRoute, accessToken, done) {
|
|||
|
||||
OAuth.parseUserReturn = async (provider, profile) => {
|
||||
const {
|
||||
id, sub, name, nickname, preferred_username, picture,
|
||||
roles, email, /* , email_verified */
|
||||
id, sub,
|
||||
name, nickname, preferred_username,
|
||||
given_name, middle_name, family_name,
|
||||
picture, roles, email, email_verified,
|
||||
} = profile;
|
||||
const { usernameViaEmail, idKey } = await OAuth.getStrategy(provider);
|
||||
|
||||
const displayName = nickname || preferred_username || name;
|
||||
|
||||
const combinedFullName = [given_name, middle_name, family_name].filter(Boolean).join(' ');
|
||||
const fullname = name || combinedFullName;
|
||||
|
||||
const normalized = {
|
||||
provider,
|
||||
id: profile[idKey] || id || sub,
|
||||
displayName: nickname || preferred_username || name,
|
||||
displayName,
|
||||
fullname,
|
||||
picture,
|
||||
roles,
|
||||
email,
|
||||
email_verified,
|
||||
};
|
||||
|
||||
if (!normalized.displayName && email && usernameViaEmail === 'on') {
|
||||
|
@ -204,19 +216,32 @@ OAuth.login = async (payload) => {
|
|||
return ({ uid });
|
||||
}
|
||||
|
||||
// Check for user via email fallback
|
||||
uid = await user.getUidByEmail(payload.email);
|
||||
if (!uid) {
|
||||
const { email } = payload;
|
||||
const { trustEmailVerified } = await OAuth.getStrategy(payload.name);
|
||||
const { email } = payload;
|
||||
const email_verified =
|
||||
parseInt(trustEmailVerified, 10) &&
|
||||
(payload.email_verified || payload.email_verified === undefined);
|
||||
|
||||
|
||||
// Check for user via email fallback
|
||||
if (email && email_verified) {
|
||||
uid = await user.getUidByEmail(payload.email);
|
||||
}
|
||||
|
||||
if (!uid) {
|
||||
// New user
|
||||
uid = await user.create({
|
||||
username: payload.handle,
|
||||
});
|
||||
|
||||
// Automatically confirm user email
|
||||
await user.setUserField(uid, 'email', email);
|
||||
await user.email.confirmByUid(uid);
|
||||
if (email) {
|
||||
await user.setUserField(uid, 'email', email);
|
||||
|
||||
if (email_verified) {
|
||||
await user.email.confirmByUid(uid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save provider-specific information to the user
|
||||
|
@ -250,6 +275,27 @@ OAuth.assignGroups = async ({ user, profile }) => {
|
|||
winston.verbose(`[plugins/sso-auth0] uid ${uid} now a part of ${toJoin.length} these user groups: ${toJoin.join(', ')}`);
|
||||
};
|
||||
|
||||
OAuth.updateProfile = async (uid, profile) => {
|
||||
const fields = ['fullname', 'picture'];
|
||||
const strategy = await OAuth.getStrategy(profile.provider);
|
||||
const allowList = [];
|
||||
|
||||
const payload = fields.reduce((memo, field) => {
|
||||
const setting = `sync${field[0].toUpperCase()}${field.slice(1)}`;
|
||||
if (strategy[setting] && parseInt(strategy[setting], 10)) {
|
||||
memo[field] = profile[field];
|
||||
if (field === 'picture') {
|
||||
allowList.push('picture');
|
||||
}
|
||||
}
|
||||
|
||||
return memo;
|
||||
}, {});
|
||||
payload.uid = uid;
|
||||
|
||||
await user.updateProfile(uid, payload, allowList);
|
||||
};
|
||||
|
||||
OAuth.getUidByOAuthid = async (name, oAuthid) => db.getObjectField(`${name}Id:uid`, oAuthid);
|
||||
|
||||
OAuth.deleteUserData = async (data) => {
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
<td>
|
||||
{{{ if ./enabled }}}✓{{{ else }}}✗{{{ end }}}
|
||||
</td>
|
||||
<td class="text-break">{./callbackUrl}</td>
|
||||
<td class="text-break user-select-all">{./callbackUrl}</td>
|
||||
<td class="text-end">
|
||||
<a href="#" data-action="edit">Edit</a>
|
||||
|
||||
|
|
|
@ -107,11 +107,15 @@
|
|||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="form-check form-switch mb-3">
|
||||
<input type="checkbox" class="form-check-input" id="usernameViaEmail" name="usernameViaEmail" {{{ if (./usernameViaEmail == "on") }}}checked{{{ end }}}>
|
||||
<label for="usernameViaEmail" class="form-check-label">Fall back to email as username if no username available (e.g. <code><strong>username</strong>@example.org</code>)</label>
|
||||
<input type="checkbox" class="form-check-input" id="usernameViaEmail" name="usernameViaEmail" {{{ if (./usernameViaEmail == "1") }}}checked{{{ end }}}>
|
||||
<label for="usernameViaEmail" class="form-check-label">Fall back to email as username if no username available (e.g. <code><strong>username</strong>@example.org</code>).</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
|
||||
<div class="form-check form-switch mb-3">
|
||||
<input type="checkbox" class="form-check-input" id="trustEmailVerified" name="trustEmailVerified" {{{ if (./trustEmailVerified == "1") }}}checked{{{ end }}}>
|
||||
<label for="trustEmailVerified" class="form-check-label">Automatically confirm emails when <code>email_verified</code> is true.</code></label>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="idKey">Alternative <code>id</code> key</label>
|
||||
<input type="text" id="idKey" name="idKey" title="Alternative id key" class="form-control" placeholder="e.g. auth0Id" value="{./idKey}">
|
||||
|
@ -122,6 +126,18 @@
|
|||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<label class="form-label mb-2">Synchronize profile data</label>
|
||||
<div class="form-check form-switch mb-3">
|
||||
<input type="checkbox" class="form-check-input" id="syncFullname" name="syncFullname" {{{ if (./syncFullname == "1") }}}checked{{{ end }}}>
|
||||
<label for="syncFullname" class="form-check-label">Full Name</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check form-switch mb-3">
|
||||
<input type="checkbox" class="form-check-input" id="syncPicture" name="syncPicture" {{{ if (./syncPicture == "1") }}}checked{{{ end }}}>
|
||||
<label for="syncPicture" class="form-check-label">Picture</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue