feat: additional adjustment toggles for syncing full name, picture, and email verification trusting

This commit is contained in:
Julian Lam 2023-11-28 14:54:12 -05:00
parent 8eda9de31e
commit f774bf5622
5 changed files with 88 additions and 15 deletions

View file

@ -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.

View file

@ -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),

View file

@ -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) => {

View file

@ -32,7 +32,7 @@
<td>
{{{ if ./enabled }}}&check;{{{ else }}}&cross;{{{ 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>
&nbsp;&nbsp;&nbsp;

View file

@ -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>