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: init 🎉
This commit is contained in:
commit
d0753a5ff3
17 changed files with 2696 additions and 0 deletions
3
.eslintrc
Normal file
3
.eslintrc
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": "nodebb"
|
||||
}
|
22
.gitattributes
vendored
Normal file
22
.gitattributes
vendored
Normal file
|
@ -0,0 +1,22 @@
|
|||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
|
||||
# Custom for Visual Studio
|
||||
*.cs diff=csharp
|
||||
*.sln merge=union
|
||||
*.csproj merge=union
|
||||
*.vbproj merge=union
|
||||
*.fsproj merge=union
|
||||
*.dbproj merge=union
|
||||
|
||||
# Standard to msysgit
|
||||
*.doc diff=astextplain
|
||||
*.DOC diff=astextplain
|
||||
*.docx diff=astextplain
|
||||
*.DOCX diff=astextplain
|
||||
*.dot diff=astextplain
|
||||
*.DOT diff=astextplain
|
||||
*.pdf diff=astextplain
|
||||
*.PDF diff=astextplain
|
||||
*.rtf diff=astextplain
|
||||
*.RTF diff=astextplain
|
224
.gitignore
vendored
Normal file
224
.gitignore
vendored
Normal file
|
@ -0,0 +1,224 @@
|
|||
#################
|
||||
## Eclipse
|
||||
#################
|
||||
|
||||
*.pydevproject
|
||||
.project
|
||||
.metadata
|
||||
bin/
|
||||
tmp/
|
||||
*.tmp
|
||||
*.bak
|
||||
*.swp
|
||||
*~.nib
|
||||
local.properties
|
||||
.classpath
|
||||
.settings/
|
||||
.loadpath
|
||||
|
||||
# External tool builders
|
||||
.externalToolBuilders/
|
||||
|
||||
# Locally stored "Eclipse launch configurations"
|
||||
*.launch
|
||||
|
||||
# CDT-specific
|
||||
.cproject
|
||||
|
||||
# PDT-specific
|
||||
.buildpath
|
||||
|
||||
|
||||
#################
|
||||
## Visual Studio
|
||||
#################
|
||||
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
|
||||
# User-specific files
|
||||
*.suo
|
||||
*.user
|
||||
*.sln.docstates
|
||||
|
||||
# Build results
|
||||
|
||||
[Dd]ebug/
|
||||
[Rr]elease/
|
||||
x64/
|
||||
build/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
*_i.c
|
||||
*_p.c
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*.log
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.log
|
||||
*.scc
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# NCrunch
|
||||
*.ncrunch*
|
||||
.*crunch*.local.xml
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.Publish.xml
|
||||
*.pubxml
|
||||
|
||||
# NuGet Packages Directory
|
||||
## TODO: If you have NuGet Package Restore enabled, uncomment the next line
|
||||
#packages/
|
||||
|
||||
# Windows Azure Build Output
|
||||
csx
|
||||
*.build.csdef
|
||||
|
||||
# Windows Store app package directory
|
||||
AppPackages/
|
||||
|
||||
# Others
|
||||
sql/
|
||||
*.Cache
|
||||
ClientBin/
|
||||
[Ss]tyle[Cc]op.*
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.[Pp]ublish.xml
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file to a newer
|
||||
# Visual Studio version. Backup files are not needed, because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
|
||||
# SQL Server files
|
||||
App_Data/*.mdf
|
||||
App_Data/*.ldf
|
||||
|
||||
#############
|
||||
## Windows detritus
|
||||
#############
|
||||
|
||||
# Windows image file caches
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
|
||||
# Folder config file
|
||||
Desktop.ini
|
||||
|
||||
# Recycle Bin used on file shares
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# Mac crap
|
||||
.DS_Store
|
||||
|
||||
|
||||
#############
|
||||
## Python
|
||||
#############
|
||||
|
||||
*.py[co]
|
||||
|
||||
# Packages
|
||||
*.egg
|
||||
*.egg-info
|
||||
dist/
|
||||
build/
|
||||
eggs/
|
||||
parts/
|
||||
var/
|
||||
sdist/
|
||||
develop-eggs/
|
||||
.installed.cfg
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
.coverage
|
||||
.tox
|
||||
|
||||
#Translations
|
||||
*.mo
|
||||
|
||||
#Mr Developer
|
||||
.mr.developer.cfg
|
||||
|
||||
sftp-config.json
|
||||
node_modules/
|
||||
|
||||
####################
|
||||
# JetBrains
|
||||
####################
|
||||
|
||||
.idea
|
2
.npmignore
Normal file
2
.npmignore
Normal file
|
@ -0,0 +1,2 @@
|
|||
sftp-config.json
|
||||
node_modules/
|
8
LICENSE
Normal file
8
LICENSE
Normal file
|
@ -0,0 +1,8 @@
|
|||
Copyright (c) 2013-2014, psychobunny <psycho.bunny@hotmail.com>
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
3
README.md
Normal file
3
README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# NodeBB OAuth2 Multiple Client SSO
|
||||
|
||||
This NodeBB plugin allows you to configure logins to multiple configurable OAuth2 endpoints, via the admin backend.
|
3
commitlint.config.js
Normal file
3
commitlint.config.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = { extends: ['@commitlint/config-angular'] };
|
59
lib/controllers.js
Normal file
59
lib/controllers.js
Normal file
|
@ -0,0 +1,59 @@
|
|||
'use strict';
|
||||
|
||||
const db = require.main.require('./src/database');
|
||||
const slugify = require.main.require('./src/slugify');
|
||||
const helpers = require.main.require('./src/controllers/helpers');
|
||||
|
||||
const main = require('../library');
|
||||
|
||||
const Controllers = module.exports;
|
||||
|
||||
Controllers.renderAdminPage = async (req, res) => {
|
||||
const strategies = await main.listStrategies();
|
||||
res.render('admin/plugins/sso-oauth2-multiple', {
|
||||
title: 'Multiple OAuth2',
|
||||
strategies,
|
||||
});
|
||||
};
|
||||
|
||||
Controllers.getStrategy = async (req, res) => {
|
||||
const name = slugify(req.params.name);
|
||||
|
||||
const strategy = await db.getObject(`oauth2-multiple:strategies:${name}`);
|
||||
strategy.name = name;
|
||||
|
||||
helpers.formatApiResponse(200, res, { strategy });
|
||||
};
|
||||
|
||||
Controllers.editStrategy = async (req, res) => {
|
||||
const name = slugify(req.params.name || req.body.name);
|
||||
const payload = { ...req.body };
|
||||
delete payload.name;
|
||||
|
||||
const valuesOk = ['authUrl', 'tokenUrl', 'id', 'secret'].every(prop => payload.hasOwnProperty(prop) && payload[prop]);
|
||||
if (!name || !valuesOk) {
|
||||
throw new Error('[[error:invalid-data]]');
|
||||
}
|
||||
|
||||
payload.enabled = !!req.body.enabled;
|
||||
|
||||
await Promise.all([
|
||||
db.sortedSetAdd('oauth2-multiple:strategies', Date.now(), name),
|
||||
db.setObject(`oauth2-multiple:strategies:${name}`, payload),
|
||||
]);
|
||||
|
||||
const strategies = await main.listStrategies();
|
||||
helpers.formatApiResponse(200, res, { strategies });
|
||||
};
|
||||
|
||||
Controllers.deleteStrategy = async (req, res) => {
|
||||
const name = slugify(req.params.name);
|
||||
|
||||
await Promise.all([
|
||||
db.sortedSetRemove('oauth2-multiple:strategies', name),
|
||||
db.delete(`oauth2-multiple:strategies:${name}`),
|
||||
]);
|
||||
|
||||
const strategies = await main.listStrategies();
|
||||
helpers.formatApiResponse(200, res, { strategies });
|
||||
};
|
303
library.js
Normal file
303
library.js
Normal file
|
@ -0,0 +1,303 @@
|
|||
'use strict';
|
||||
|
||||
/*
|
||||
Welcome to the SSO OAuth plugin! If you're inspecting this code, you're probably looking to
|
||||
hook up NodeBB with your existing OAuth endpoint.
|
||||
|
||||
Step 1: Fill in the "constants" section below with the requisite informaton. Either the "oauth"
|
||||
or "oauth2" section needs to be filled, depending on what you set "type" to.
|
||||
|
||||
Step 2: Give it a whirl. If you see the congrats message, you're doing well so far!
|
||||
|
||||
Step 3: Customise the `parseUserReturn` method to normalise your user route's data return into
|
||||
a format accepted by NodeBB. Instructions are provided there. (Line 146)
|
||||
|
||||
Step 4: If all goes well, you'll be able to login/register via your OAuth endpoint credentials.
|
||||
*/
|
||||
|
||||
const User = require.main.require('./src/user');
|
||||
const Groups = require.main.require('./src/groups');
|
||||
const db = require.main.require('./src/database');
|
||||
const authenticationController = require.main.require('./src/controllers/authentication');
|
||||
const routeHelpers = require.main.require('./src/routes/helpers');
|
||||
|
||||
const async = require('async');
|
||||
|
||||
const passport = module.parent.require('passport');
|
||||
const nconf = module.parent.require('nconf');
|
||||
const winston = module.parent.require('winston');
|
||||
|
||||
/**
|
||||
* REMEMBER
|
||||
* Never save your OAuth Key/Secret or OAuth2 ID/Secret pair in code! It could be published and leaked accidentally.
|
||||
* Save it into your config.json file instead:
|
||||
*
|
||||
* {
|
||||
* ...
|
||||
* "oauth": {
|
||||
* "id": "someoauthid",
|
||||
* "secret": "youroauthsecret"
|
||||
* }
|
||||
* ...
|
||||
* }
|
||||
*
|
||||
* ... or use environment variables instead:
|
||||
*
|
||||
* `OAUTH__ID=someoauthid OAUTH__SECRET=youroauthsecret node app.js`
|
||||
*/
|
||||
|
||||
const constants = Object.freeze({
|
||||
type: '', // Either 'oauth' or 'oauth2'
|
||||
name: '', // Something unique to your OAuth provider in lowercase, like "github", or "nodebb"
|
||||
oauth: {
|
||||
requestTokenURL: '',
|
||||
accessTokenURL: '',
|
||||
userAuthorizationURL: '',
|
||||
consumerKey: nconf.get('oauth:key'), // don't change this line
|
||||
consumerSecret: nconf.get('oauth:secret'), // don't change this line
|
||||
},
|
||||
oauth2: {
|
||||
authorizationURL: '',
|
||||
tokenURL: '',
|
||||
clientID: nconf.get('oauth:id'), // don't change this line
|
||||
clientSecret: nconf.get('oauth:secret'), // don't change this line
|
||||
},
|
||||
userRoute: '', // This is the address to your app's "user profile" API endpoint (expects JSON)
|
||||
});
|
||||
|
||||
const OAuth = module.exports;
|
||||
let configOk = false;
|
||||
let passportOAuth;
|
||||
let opts;
|
||||
|
||||
if (!constants.name) {
|
||||
winston.error('[sso-oauth] Please specify a name for your OAuth provider (library.js:32)');
|
||||
} else if (!constants.type || (constants.type !== 'oauth' && constants.type !== 'oauth2')) {
|
||||
winston.error('[sso-oauth] Please specify an OAuth strategy to utilise (library.js:31)');
|
||||
} else if (!constants.userRoute) {
|
||||
winston.error('[sso-oauth] User Route required (library.js:31)');
|
||||
} else {
|
||||
configOk = true;
|
||||
}
|
||||
|
||||
OAuth.init = async (params) => {
|
||||
const { router /* , middleware , controllers */ } = params;
|
||||
const controllers = require('./lib/controllers');
|
||||
|
||||
routeHelpers.setupAdminPageRoute(router, '/admin/plugins/sso-oauth2-multiple', controllers.renderAdminPage);
|
||||
};
|
||||
|
||||
OAuth.addRoutes = async ({ router, middleware }) => {
|
||||
const controllers = require('./lib/controllers');
|
||||
const middlewares = [
|
||||
middleware.ensureLoggedIn,
|
||||
middleware.admin.checkPrivileges,
|
||||
];
|
||||
|
||||
routeHelpers.setupApiRoute(router, 'post', '/oauth2-multiple/strategies', middlewares, controllers.editStrategy);
|
||||
routeHelpers.setupApiRoute(router, 'get', '/oauth2-multiple/strategies/:name', middlewares, controllers.getStrategy);
|
||||
routeHelpers.setupApiRoute(router, 'delete', '/oauth2-multiple/strategies/:name', middlewares, controllers.deleteStrategy);
|
||||
};
|
||||
|
||||
OAuth.addAdminNavigation = (header) => {
|
||||
header.authentication.push({
|
||||
route: '/plugins/sso-oauth2-multiple',
|
||||
icon: 'fa-tint',
|
||||
name: 'Multiple OAuth2',
|
||||
});
|
||||
|
||||
return header;
|
||||
};
|
||||
|
||||
OAuth.listStrategies = async () => {
|
||||
const names = await db.getSortedSetMembers('oauth2-multiple:strategies');
|
||||
const strategies = await db.getObjects(names.map(name => `oauth2-multiple:strategies:${name}`), ['enabled']);
|
||||
strategies.forEach((strategy, idx) => {
|
||||
strategy.name = names[idx];
|
||||
strategy.enabled = strategy.enabled === 'true';
|
||||
});
|
||||
|
||||
return strategies;
|
||||
};
|
||||
|
||||
OAuth.getStrategy = function (strategies, callback) {
|
||||
if (configOk) {
|
||||
passportOAuth = require('passport-oauth')[constants.type === 'oauth' ? 'OAuthStrategy' : 'OAuth2Strategy'];
|
||||
|
||||
if (constants.type === 'oauth') {
|
||||
// OAuth options
|
||||
opts = constants.oauth;
|
||||
opts.callbackURL = `${nconf.get('url')}/auth/${constants.name}/callback`;
|
||||
|
||||
passportOAuth.Strategy.prototype.userProfile = function (token, secret, params, done) {
|
||||
// If your OAuth provider requires the access token to be sent in the query parameters
|
||||
// instead of the request headers, comment out the next line:
|
||||
this._oauth._useAuthorizationHeaderForGET = true;
|
||||
|
||||
this._oauth.get(constants.userRoute, token, secret, (err, body/* , res */) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
try {
|
||||
const json = JSON.parse(body);
|
||||
OAuth.parseUserReturn(json, (err, profile) => {
|
||||
if (err) return done(err);
|
||||
profile.provider = constants.name;
|
||||
|
||||
done(null, profile);
|
||||
});
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
};
|
||||
} else if (constants.type === 'oauth2') {
|
||||
// OAuth 2 options
|
||||
opts = constants.oauth2;
|
||||
opts.callbackURL = `${nconf.get('url')}/auth/${constants.name}/callback`;
|
||||
|
||||
passportOAuth.Strategy.prototype.userProfile = function (accessToken, done) {
|
||||
// If your OAuth provider requires the access token to be sent in the query parameters
|
||||
// instead of the request headers, comment out the next line:
|
||||
this._oauth2._useAuthorizationHeaderForGET = true;
|
||||
|
||||
this._oauth2.get(constants.userRoute, accessToken, (err, body/* , res */) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
try {
|
||||
const json = JSON.parse(body);
|
||||
OAuth.parseUserReturn(json, (err, profile) => {
|
||||
if (err) return done(err);
|
||||
profile.provider = constants.name;
|
||||
|
||||
done(null, profile);
|
||||
});
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
opts.passReqToCallback = true;
|
||||
|
||||
passport.use(constants.name, new passportOAuth(opts, async (req, token, secret, profile, done) => {
|
||||
const user = await OAuth.login({
|
||||
oAuthid: profile.id,
|
||||
handle: profile.displayName,
|
||||
email: profile.emails[0].value,
|
||||
isAdmin: profile.isAdmin,
|
||||
});
|
||||
|
||||
authenticationController.onSuccessfulLogin(req, user.uid);
|
||||
done(null, user);
|
||||
}));
|
||||
|
||||
strategies.push({
|
||||
name: constants.name,
|
||||
url: `/auth/${constants.name}`,
|
||||
callbackURL: `/auth/${constants.name}/callback`,
|
||||
icon: 'fa-check-square',
|
||||
scope: (constants.scope || '').split(','),
|
||||
});
|
||||
|
||||
callback(null, strategies);
|
||||
} else {
|
||||
callback(new Error('OAuth Configuration is invalid'));
|
||||
}
|
||||
};
|
||||
|
||||
OAuth.parseUserReturn = function (data, callback) {
|
||||
// Alter this section to include whatever data is necessary
|
||||
// NodeBB *requires* the following: id, displayName, emails.
|
||||
// Everything else is optional.
|
||||
|
||||
// Find out what is available by uncommenting this line:
|
||||
// console.log(data);
|
||||
|
||||
const profile = {};
|
||||
profile.id = data.id;
|
||||
profile.displayName = data.name;
|
||||
profile.emails = [{ value: data.email }];
|
||||
|
||||
// Do you want to automatically make somebody an admin? This line might help you do that...
|
||||
// profile.isAdmin = data.isAdmin ? true : false;
|
||||
|
||||
// Delete or comment out the next TWO (2) lines when you are ready to proceed
|
||||
process.stdout.write('===\nAt this point, you\'ll need to customise the above section to id, displayName, and emails into the "profile" object.\n===');
|
||||
return callback(new Error('Congrats! So far so good -- please see server log for details'));
|
||||
|
||||
// eslint-disable-next-line
|
||||
callback(null, profile);
|
||||
};
|
||||
|
||||
OAuth.login = async (payload) => {
|
||||
let uid = await OAuth.getUidByOAuthid(payload.oAuthid);
|
||||
if (uid !== null) {
|
||||
// Existing User
|
||||
return ({
|
||||
uid: uid,
|
||||
});
|
||||
}
|
||||
|
||||
// Check for user via email fallback
|
||||
uid = await User.getUidByEmail(payload.email);
|
||||
if (!uid) {
|
||||
/**
|
||||
* The email retrieved from the user profile might not be trusted.
|
||||
* Only you would know — it's up to you to decide whether or not to:
|
||||
* - Send the welcome email which prompts for verification (default)
|
||||
* - Bypass the welcome email and automatically verify the email (commented out, below)
|
||||
*/
|
||||
const { email } = payload;
|
||||
|
||||
// New user
|
||||
uid = await User.create({
|
||||
username: payload.handle,
|
||||
email, // if you uncomment the block below, comment this line out
|
||||
});
|
||||
|
||||
// Automatically confirm user email
|
||||
// await User.setUserField(uid, 'email', email);
|
||||
// await UserEmail.confirmByUid(uid);
|
||||
}
|
||||
|
||||
// Save provider-specific information to the user
|
||||
await User.setUserField(uid, `${constants.name}Id`, payload.oAuthid);
|
||||
await db.setObjectField(`${constants.name}Id:uid`, payload.oAuthid, uid);
|
||||
|
||||
if (payload.isAdmin) {
|
||||
await Groups.join('administrators', uid);
|
||||
}
|
||||
|
||||
return {
|
||||
uid: uid,
|
||||
};
|
||||
};
|
||||
|
||||
OAuth.getUidByOAuthid = async oAuthid => db.getObjectField(`${constants.name}Id:uid`, oAuthid);
|
||||
|
||||
OAuth.deleteUserData = function (data, callback) {
|
||||
async.waterfall([
|
||||
async.apply(User.getUserField, data.uid, `${constants.name}Id`),
|
||||
function (oAuthIdToDelete, next) {
|
||||
db.deleteObjectField(`${constants.name}Id:uid`, oAuthIdToDelete, next);
|
||||
},
|
||||
], (err) => {
|
||||
if (err) {
|
||||
winston.error(`[sso-oauth] Could not remove OAuthId data for uid ${data.uid}. Error: ${err}`);
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
callback(null, data);
|
||||
});
|
||||
};
|
||||
|
||||
// If this filter is not there, the deleteUserData function will fail when getting the oauthId for deletion.
|
||||
OAuth.whitelistFields = function (params, callback) {
|
||||
params.whitelist.push(`${constants.name}Id`);
|
||||
callback(null, params);
|
||||
};
|
59
package.json
Normal file
59
package.json
Normal file
|
@ -0,0 +1,59 @@
|
|||
{
|
||||
"name": "nodebb-plugin-sso-oauth2-multiple",
|
||||
"version": "0.5.0",
|
||||
"description": "NodeBB Multiple OAuth2 SSO",
|
||||
"main": "library.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/julianlam/nodebb-plugin-sso-oauth"
|
||||
},
|
||||
"keywords": [
|
||||
"nodebb",
|
||||
"plugin",
|
||||
"oauth",
|
||||
"oauth2",
|
||||
"sso",
|
||||
"single sign on",
|
||||
"login",
|
||||
"registration"
|
||||
],
|
||||
"author": {
|
||||
"name": "Julian Lam",
|
||||
"email": "julian@nodebb.org"
|
||||
},
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/julianlam/nodebb-plugin-sso-oauth/issues"
|
||||
},
|
||||
"readme": "",
|
||||
"readmeFilename": "README.md",
|
||||
"dependencies": {
|
||||
"async": "^3.2.0",
|
||||
"eslint": "8.x",
|
||||
"passport-oauth": "~1.0.0"
|
||||
},
|
||||
"nbbpm": {
|
||||
"compatibility": "^1.0.1",
|
||||
"index": false
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/config-angular": "15.0.0",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
"eslint-config-nodebb": "0.2.1",
|
||||
"eslint-plugin-import": "2.x",
|
||||
"husky": "8.0.3",
|
||||
"lint-staged": "13.2.3"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged",
|
||||
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.js": [
|
||||
"eslint --fix",
|
||||
"git add"
|
||||
]
|
||||
}
|
||||
}
|
19
plugin.json
Normal file
19
plugin.json
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"id": "nodebb-plugin-sso-oauth2-multiple",
|
||||
"name": "NodeBB Multiple OAuth2 SSO",
|
||||
"description": "NodeBB Plugin that configures multiple OAuth2 login endpoints",
|
||||
"url": "https://github.com/nodebb/nodebb-plugin-sso-oauth2-multiple",
|
||||
"library": "./library.js",
|
||||
"hooks": [
|
||||
{ "hook": "static:app.load", "method": "init" },
|
||||
{ "hook": "static:api.routes", "method": "addRoutes" },
|
||||
{ "hook": "filter:admin.header.build", "method": "addAdminNavigation" },
|
||||
{ "hook": "static:user.delete", "method": "deleteUserData" },
|
||||
{ "hook": "filter:user.whitelistFields", "method": "whitelistFields" },
|
||||
{ "hook": "filter:auth.init", "method": "getStrategy" }
|
||||
],
|
||||
"modules": {
|
||||
"../admin/plugins/sso-oauth2-multiple.js": "./static/lib/admin.js"
|
||||
},
|
||||
"templates": "static/templates"
|
||||
}
|
5
renovate.json
Normal file
5
renovate.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"extends": [
|
||||
"config:base"
|
||||
]
|
||||
}
|
3
static/.eslintrc
Normal file
3
static/.eslintrc
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": "nodebb/public"
|
||||
}
|
104
static/lib/admin.js
Normal file
104
static/lib/admin.js
Normal file
|
@ -0,0 +1,104 @@
|
|||
'use strict';
|
||||
|
||||
// import * as settings from 'settings';
|
||||
import { confirm } from 'bootbox';
|
||||
import { get, post, del } from 'api';
|
||||
import { error } from 'alerts';
|
||||
import { render } from 'benchpress';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export function init() {
|
||||
// settings.load('sso-oauth2-multiple', $('.sso-oauth2-multiple-settings'));
|
||||
// $('#save').on('click', saveSettings);
|
||||
|
||||
const formEl = document.querySelector('.sso-oauth2-multiple-settings');
|
||||
formEl.addEventListener('click', async (e) => {
|
||||
const subselector = e.target.closest('[data-action]');
|
||||
if (subselector) {
|
||||
const action = subselector.getAttribute('data-action');
|
||||
|
||||
switch (action) {
|
||||
case 'new': {
|
||||
const title = 'New OAuth2 Strategy';
|
||||
const message = await app.parseAndTranslate('partials/edit-oauth2-strategy', {});
|
||||
confirm({
|
||||
title,
|
||||
message,
|
||||
callback: handleEditStrategy,
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'edit': {
|
||||
const name = subselector.closest('[data-name]').getAttribute('data-name');
|
||||
const { strategy } = await get(`/plugins/oauth2-multiple/strategies/${name}`);
|
||||
const title = 'Edit OAuth2 Strategy';
|
||||
const message = await app.parseAndTranslate('partials/edit-oauth2-strategy', { ...strategy });
|
||||
confirm({
|
||||
title,
|
||||
message,
|
||||
callback: handleEditStrategy,
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'delete': {
|
||||
const name = subselector.closest('[data-name]').getAttribute('data-name');
|
||||
|
||||
if (!name) {
|
||||
break;
|
||||
}
|
||||
|
||||
confirm({
|
||||
title: 'Delete Strategy',
|
||||
message: `Are you sure you wish to delete the OAuth2 strategy named <strong>${name}</strong>?`,
|
||||
callback: function (ok) {
|
||||
handleDeleteStrategy.call(this, ok, name);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleEditStrategy(ok) {
|
||||
if (!ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
const $modal = this;
|
||||
const modalEl = this.get(0);
|
||||
const formEl = modalEl.querySelector('form');
|
||||
const data = new FormData(formEl);
|
||||
|
||||
post('/plugins/oauth2-multiple/strategies', data).then(async ({ strategies }) => {
|
||||
const html = await render('admin/plugins/sso-oauth2-multiple', { strategies }, 'strategies');
|
||||
const tbodyEl = document.querySelector('#strategies tbody');
|
||||
tbodyEl.innerHTML = html;
|
||||
$modal.modal('hide');
|
||||
}).catch(error);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function handleDeleteStrategy(ok, name) {
|
||||
if (!ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
const $modal = this;
|
||||
|
||||
del(`/plugins/oauth2-multiple/strategies/${name}`).then(async ({ strategies }) => {
|
||||
const html = await render('admin/plugins/sso-oauth2-multiple', { strategies }, 'strategies');
|
||||
const tbodyEl = document.querySelector('#strategies tbody');
|
||||
tbodyEl.innerHTML = html;
|
||||
$modal.modal('hide');
|
||||
}).catch(error);
|
||||
}
|
||||
|
||||
// function saveSettings() {
|
||||
// settings.save('sso-oauth2-multiple', $('.sso-oauth2-multiple-settings'));
|
||||
// }
|
56
static/templates/admin/plugins/sso-oauth2-multiple.tpl
Normal file
56
static/templates/admin/plugins/sso-oauth2-multiple.tpl
Normal file
|
@ -0,0 +1,56 @@
|
|||
<div class="acp-page-container">
|
||||
<!-- IMPORT admin/partials/settings/header.tpl -->
|
||||
|
||||
<div class="row m-0">
|
||||
<div id="spy-container" class="col-12 col-md-8 px-0 mb-4" tabindex="0">
|
||||
<form role="form" class="sso-oauth2-multiple-settings">
|
||||
<div class="mb-4">
|
||||
<h5 class="fw-bold tracking-tight settings-header">Connections</h5>
|
||||
|
||||
<p class="lead">
|
||||
The following OAuth2 endpoints have been configured.
|
||||
</p>
|
||||
|
||||
<table class="table small" id="strategies">
|
||||
<thead>
|
||||
<th>Name</th>
|
||||
<th>Enabled</th>
|
||||
<th><span class="visually-hidden">Actions</span></th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{{ if !strategies.length }}}
|
||||
<tr>
|
||||
<td colspan="3">
|
||||
<div class="alert alert-info text-center mb-0"><em>No OAuth2 endpoints configured.</em></div>
|
||||
</td>
|
||||
</tr>
|
||||
{{{ end }}}
|
||||
{{{ each strategies }}}
|
||||
<tr data-name="{./name}">
|
||||
<td>{./name}</td>
|
||||
<td>
|
||||
{{{ if ./enabled }}}✓{{{ else }}}✗{{{ end }}}
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<a href="#" data-action="edit">Edit</a>
|
||||
|
||||
<a href="#" data-action="delete" class="text-danger">Delete</a>
|
||||
</td>
|
||||
</tr>
|
||||
{{{ end }}}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="3">
|
||||
<button type="button" class="btn btn-success btn-sm pull-right" data-action="new"><i class="fa fa-plus"></i> New Endpoint</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- IMPORT admin/partials/settings/toc.tpl -->
|
||||
</div>
|
||||
</div>
|
44
static/templates/partials/edit-oauth2-strategy.tpl
Normal file
44
static/templates/partials/edit-oauth2-strategy.tpl
Normal file
|
@ -0,0 +1,44 @@
|
|||
<form role="form">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" class="form-check-input" id="enabled" name="enabled" {{{ if (./enabled == "true") }}}checked{{{ end }}}>
|
||||
<label for="enabled" class="form-check-label">Enabled</label>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="name">Name</label>
|
||||
<input type="text" id="name" name="name" title="Name" class="form-control" placeholder="Name" value="{./name}">
|
||||
<p class="form-text">
|
||||
Enter something unique to your OAuth provider in lowercase, like <code>github</code>, or <code>nodebb</code>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="authUrl">Authorization URL</label>
|
||||
<input type="text" id="authUrl" name="authUrl" title="Authorization URL" class="form-control" placeholder="https://..." value="{./authUrl}">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="tokenUrl">Token URL</label>
|
||||
<input type="text" id="tokenUrl" name="tokenUrl" title="Token URL" class="form-control" placeholder="https://..." value="{./tokenUrl}">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="id">Client ID</label>
|
||||
<input type="text" id="id" name="id" title="Client ID" class="form-control" value="{./id}">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="secret">Client Secret</label>
|
||||
<input type="text" id="secret" name="secret" title="Client Secret" class="form-control" value="{./secret}">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="userRoute">User Info URL</label>
|
||||
<input type="text" id="userRoute" name="userRoute" title="User Info URL" class="form-control" placeholder="/userinfo" value="{./userRoute}">
|
||||
<p class="form-text">
|
||||
If a relative path is specified here, we will assume the hostname from the authorization URL.
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
Loading…
Add table
Add a link
Reference in a new issue