11'use strict' ;
22
3- /*
4- Welcome to the SSO OAuth plugin! If you're inspecting this code, you're probably looking to
5- hook up NodeBB with your existing OAuth endpoint.
6-
7- Step 1: Fill in the "constants" section below with the requisite informaton. Either the "oauth"
8- or "oauth2" section needs to be filled, depending on what you set "type" to.
9-
10- Step 2: Give it a whirl. If you see the congrats message, you're doing well so far!
11-
12- Step 3: Customise the `parseUserReturn` method to normalise your user route's data return into
13- a format accepted by NodeBB. Instructions are provided there. (Line 146)
14-
15- Step 4: If all goes well, you'll be able to login/register via your OAuth endpoint credentials.
16- */
17-
183const User = require . main . require ( './src/user' ) ;
194const Groups = require . main . require ( './src/groups' ) ;
205const db = require . main . require ( './src/database' ) ;
216const authenticationController = require . main . require ( './src/controllers/authentication' ) ;
227const routeHelpers = require . main . require ( './src/routes/helpers' ) ;
238
24- const async = require ( 'async' ) ;
25-
269const passport = module . parent . require ( 'passport' ) ;
2710const nconf = module . parent . require ( 'nconf' ) ;
2811const winston = module . parent . require ( 'winston' ) ;
2912
30- /**
31- * REMEMBER
32- * Never save your OAuth Key/Secret or OAuth2 ID/Secret pair in code! It could be published and leaked accidentally.
33- * Save it into your config.json file instead:
34- *
35- * {
36- * ...
37- * "oauth": {
38- * "id": "someoauthid",
39- * "secret": "youroauthsecret"
40- * }
41- * ...
42- * }
43- *
44- * ... or use environment variables instead:
45- *
46- * `OAUTH__ID=someoauthid OAUTH__SECRET=youroauthsecret node app.js`
47- */
48-
49- const constants = Object . freeze ( {
50- type : '' , // Either 'oauth' or 'oauth2'
51- name : '' , // Something unique to your OAuth provider in lowercase, like "github", or "nodebb"
52- oauth : {
53- requestTokenURL : '' ,
54- accessTokenURL : '' ,
55- userAuthorizationURL : '' ,
56- consumerKey : nconf . get ( 'oauth:key' ) , // don't change this line
57- consumerSecret : nconf . get ( 'oauth:secret' ) , // don't change this line
58- } ,
59- oauth2 : {
60- authorizationURL : '' ,
61- tokenURL : '' ,
62- clientID : nconf . get ( 'oauth:id' ) , // don't change this line
63- clientSecret : nconf . get ( 'oauth:secret' ) , // don't change this line
64- } ,
65- userRoute : '' , // This is the address to your app's "user profile" API endpoint (expects JSON)
66- } ) ;
67-
6813const OAuth = module . exports ;
69- let configOk = false ;
70- let passportOAuth ;
71- let opts ;
72-
73- if ( ! constants . name ) {
74- winston . error ( '[sso-oauth] Please specify a name for your OAuth provider (library.js:32)' ) ;
75- } else if ( ! constants . type || ( constants . type !== 'oauth' && constants . type !== 'oauth2' ) ) {
76- winston . error ( '[sso-oauth] Please specify an OAuth strategy to utilise (library.js:31)' ) ;
77- } else if ( ! constants . userRoute ) {
78- winston . error ( '[sso-oauth] User Route required (library.js:31)' ) ;
79- } else {
80- configOk = true ;
81- }
8214
8315OAuth . init = async ( params ) => {
8416 const { router /* , middleware , controllers */ } = params ;
@@ -111,9 +43,9 @@ OAuth.addAdminNavigation = (header) => {
11143 return header ;
11244} ;
11345
114- OAuth . listStrategies = async ( ) => {
46+ OAuth . listStrategies = async ( full ) => {
11547 const names = await db . getSortedSetMembers ( 'oauth2-multiple:strategies' ) ;
116- const strategies = await db . getObjects ( names . map ( name => `oauth2-multiple:strategies:${ name } ` ) , [ 'enabled' ] ) ;
48+ const strategies = await db . getObjects ( names . map ( name => `oauth2-multiple:strategies:${ name } ` ) , full ? undefined : [ 'enabled' ] ) ;
11749 strategies . forEach ( ( strategy , idx ) => {
11850 strategy . name = names [ idx ] ;
11951 strategy . enabled = strategy . enabled === 'true' ;
@@ -123,184 +55,134 @@ OAuth.listStrategies = async () => {
12355 return strategies ;
12456} ;
12557
126- OAuth . getStrategy = function ( strategies , callback ) {
127- if ( configOk ) {
128- passportOAuth = require ( 'passport-oauth' ) [ constants . type === 'oauth' ? 'OAuthStrategy' : 'OAuth2Strategy' ] ;
129-
130- if ( constants . type === 'oauth' ) {
131- // OAuth options
132- opts = constants . oauth ;
133- opts . callbackURL = `${ nconf . get ( 'url' ) } /auth/${ constants . name } /callback` ;
134-
135- passportOAuth . Strategy . prototype . userProfile = function ( token , secret , params , done ) {
136- // If your OAuth provider requires the access token to be sent in the query parameters
137- // instead of the request headers, comment out the next line:
138- this . _oauth . _useAuthorizationHeaderForGET = true ;
139-
140- this . _oauth . get ( constants . userRoute , token , secret , ( err , body /* , res */ ) => {
141- if ( err ) {
142- return done ( err ) ;
143- }
144-
145- try {
146- const json = JSON . parse ( body ) ;
147- OAuth . parseUserReturn ( json , ( err , profile ) => {
148- if ( err ) return done ( err ) ;
149- profile . provider = constants . name ;
150-
151- done ( null , profile ) ;
152- } ) ;
153- } catch ( e ) {
154- done ( e ) ;
155- }
156- } ) ;
157- } ;
158- } else if ( constants . type === 'oauth2' ) {
159- // OAuth 2 options
160- opts = constants . oauth2 ;
161- opts . callbackURL = `${ nconf . get ( 'url' ) } /auth/${ constants . name } /callback` ;
162-
163- passportOAuth . Strategy . prototype . userProfile = function ( accessToken , done ) {
164- // If your OAuth provider requires the access token to be sent in the query parameters
165- // instead of the request headers, comment out the next line:
166- this . _oauth2 . _useAuthorizationHeaderForGET = true ;
167-
168- this . _oauth2 . get ( constants . userRoute , accessToken , ( err , body /* , res */ ) => {
169- if ( err ) {
170- return done ( err ) ;
171- }
172-
173- try {
174- const json = JSON . parse ( body ) ;
175- OAuth . parseUserReturn ( json , ( err , profile ) => {
176- if ( err ) return done ( err ) ;
177- profile . provider = constants . name ;
178-
179- done ( null , profile ) ;
180- } ) ;
181- } catch ( e ) {
182- done ( e ) ;
183- }
184- } ) ;
185- } ;
186- }
187-
188- opts . passReqToCallback = true ;
189-
190- passport . use ( constants . name , new passportOAuth ( opts , async ( req , token , secret , profile , done ) => {
191- const user = await OAuth . login ( {
192- oAuthid : profile . id ,
193- handle : profile . displayName ,
194- email : profile . emails [ 0 ] . value ,
195- isAdmin : profile . isAdmin ,
196- } ) ;
197-
198- authenticationController . onSuccessfulLogin ( req , user . uid ) ;
199- done ( null , user ) ;
200- } ) ) ;
201-
202- strategies . push ( {
203- name : constants . name ,
204- url : `/auth/${ constants . name } ` ,
205- callbackURL : `/auth/${ constants . name } /callback` ,
206- icon : 'fa-check-square' ,
207- scope : ( constants . scope || '' ) . split ( ',' ) ,
58+ OAuth . loadStrategies = async ( strategies ) => {
59+ const passportOAuth = require ( 'passport-oauth' ) . OAuth2Strategy ;
60+
61+ let configured = await OAuth . listStrategies ( true ) ;
62+ configured = configured . filter ( obj => obj . enabled ) ;
63+
64+ const configs = configured . map ( ( {
65+ name,
66+ authUrl : authorizationURL ,
67+ tokenUrl : tokenURL ,
68+ id : clientID ,
69+ secret : clientSecret ,
70+ callbackUrl : callbackURL ,
71+ } ) => new passportOAuth ( {
72+ authorizationURL,
73+ tokenURL,
74+ clientID,
75+ clientSecret,
76+ callbackURL,
77+ passReqToCallback : true ,
78+ } , async ( req , token , secret , { id, displayName, email } , done ) => {
79+ const user = await OAuth . login ( {
80+ name,
81+ oAuthid : id ,
82+ handle : displayName ,
83+ email,
20884 } ) ;
20985
210- callback ( null , strategies ) ;
211- } else {
212- callback ( new Error ( 'OAuth Configuration is invalid' ) ) ;
213- }
214- } ;
86+ authenticationController . onSuccessfulLogin ( req , user . uid ) ;
87+ done ( null , user ) ;
88+ } ) ) ;
89+
90+ configs . forEach ( ( strategy , idx ) => {
91+ strategy . userProfile = OAuth . getUserProfile . bind ( strategy , configured [ idx ] . name , configured [ idx ] . userRoute ) ;
92+ passport . use ( configured [ idx ] . name , strategy ) ;
93+ } ) ;
21594
216- OAuth . parseUserReturn = function ( data , callback ) {
217- // Alter this section to include whatever data is necessary
218- // NodeBB *requires* the following: id, displayName, emails.
219- // Everything else is optional.
95+ strategies . push ( ...configured . map ( ( { name } ) => ( {
96+ name,
97+ url : `/auth/${ name } ` ,
98+ callbackURL : `/auth/${ name } /callback` ,
99+ icon : 'fa-check-square' ,
100+ scope : 'openid email profile' ,
101+ } ) ) ) ;
220102
221- // Find out what is available by uncommenting this line:
222- // console.log(data) ;
103+ return strategies ;
104+ } ;
223105
224- const profile = { } ;
225- profile . id = data . id ;
226- profile . displayName = data . name ;
227- profile . emails = [ { value : data . email } ] ;
106+ OAuth . getUserProfile = function ( name , userRoute , accessToken , done ) {
107+ // If your OAuth provider requires the access token to be sent in the query parameters
108+ // instead of the request headers, comment out the next line:
109+ this . _oauth2 . _useAuthorizationHeaderForGET = true ;
228110
229- // Do you want to automatically make somebody an admin? This line might help you do that...
230- // profile.isAdmin = data.isAdmin ? true : false;
111+ this . _oauth2 . get ( userRoute , accessToken , ( err , body /* , res */ ) => {
112+ if ( err ) {
113+ return done ( err ) ;
114+ }
115+
116+ try {
117+ const json = JSON . parse ( body ) ;
118+ const profile = OAuth . parseUserReturn ( json ) ;
119+ profile . provider = name ;
120+ done ( null , profile ) ;
121+ } catch ( e ) {
122+ done ( e ) ;
123+ }
124+ } ) ;
125+ } ;
231126
232- // Delete or comment out the next TWO (2) lines when you are ready to proceed
233- process . stdout . write ( '===\nAt this point, you\'ll need to customise the above section to id, displayName, and emails into the "profile" object.\n===' ) ;
234- return callback ( new Error ( 'Congrats! So far so good -- please see server log for details' ) ) ;
127+ OAuth . parseUserReturn = ( { sub, nickname, picture, email/* , email_verified */ } ) => {
128+ const profile = { } ;
129+ profile . id = sub ;
130+ profile . displayName = nickname ;
131+ profile . picture = picture ;
132+ profile . email = email ;
235133
236- // eslint-disable-next-line
237- callback ( null , profile ) ;
134+ return profile ;
238135} ;
239136
240137OAuth . login = async ( payload ) => {
241- let uid = await OAuth . getUidByOAuthid ( payload . oAuthid ) ;
138+ let uid = await OAuth . getUidByOAuthid ( payload . name , payload . oAuthid ) ;
242139 if ( uid !== null ) {
243140 // Existing User
244- return ( {
245- uid : uid ,
246- } ) ;
141+ return ( { uid } ) ;
247142 }
248143
249144 // Check for user via email fallback
250145 uid = await User . getUidByEmail ( payload . email ) ;
251146 if ( ! uid ) {
252- /**
253- * The email retrieved from the user profile might not be trusted.
254- * Only you would know — it's up to you to decide whether or not to:
255- * - Send the welcome email which prompts for verification (default)
256- * - Bypass the welcome email and automatically verify the email (commented out, below)
257- */
258147 const { email } = payload ;
259148
260149 // New user
261150 uid = await User . create ( {
262151 username : payload . handle ,
263- email, // if you uncomment the block below, comment this line out
264152 } ) ;
265153
266154 // Automatically confirm user email
267- // await User.setUserField(uid, 'email', email);
268- // await UserEmail .confirmByUid(uid);
155+ await User . setUserField ( uid , 'email' , email ) ;
156+ await User . email . confirmByUid ( uid ) ;
269157 }
270158
271159 // Save provider-specific information to the user
272- await User . setUserField ( uid , `${ constants . name } Id` , payload . oAuthid ) ;
273- await db . setObjectField ( `${ constants . name } Id:uid` , payload . oAuthid , uid ) ;
160+ await User . setUserField ( uid , `${ payload . name } Id` , payload . oAuthid ) ;
161+ await db . setObjectField ( `${ payload . name } Id:uid` , payload . oAuthid , uid ) ;
274162
275- if ( payload . isAdmin ) {
276- await Groups . join ( 'administrators' , uid ) ;
277- }
278-
279- return {
280- uid : uid ,
281- } ;
163+ return { uid } ;
282164} ;
283165
284- OAuth . getUidByOAuthid = async oAuthid => db . getObjectField ( `${ constants . name } Id:uid` , oAuthid ) ;
166+ OAuth . getUidByOAuthid = async ( name , oAuthid ) => db . getObjectField ( `${ name } Id:uid` , oAuthid ) ;
285167
286- OAuth . deleteUserData = function ( data , callback ) {
287- async . waterfall ( [
288- async . apply ( User . getUserField , data . uid , `${ constants . name } Id` ) ,
289- function ( oAuthIdToDelete , next ) {
290- db . deleteObjectField ( `${ constants . name } Id:uid` , oAuthIdToDelete , next ) ;
291- } ,
292- ] , ( err ) => {
293- if ( err ) {
294- winston . error ( `[sso-oauth] Could not remove OAuthId data for uid ${ data . uid } . Error: ${ err } ` ) ;
295- return callback ( err ) ;
168+ OAuth . deleteUserData = async ( data ) => {
169+ const names = await db . getSortedSetMembers ( 'oauth2-multiple:strategies' ) ;
170+ const oAuthIds = await User . getUserFields ( data . uid , names . map ( name => `${ name } Id` ) ) ;
171+
172+ await Promise . all ( oAuthIds . map ( async ( oAuthIdToDelete , idx ) => {
173+ if ( ! oAuthIdToDelete ) {
174+ return ;
296175 }
297176
298- callback ( null , data ) ;
299- } ) ;
177+ const name = names [ idx ] ;
178+ await db . deleteObjectField ( `${ name } Id:uid` , oAuthIdToDelete ) ;
179+ } ) ) ;
300180} ;
301181
302182// If this filter is not there, the deleteUserData function will fail when getting the oauthId for deletion.
303- OAuth . whitelistFields = function ( params , callback ) {
304- params . whitelist . push ( `${ constants . name } Id` ) ;
305- callback ( null , params ) ;
183+ OAuth . whitelistFields = async ( params ) => {
184+ const names = await db . getSortedSetMembers ( 'oauth2-multiple:strategies' ) ;
185+ params . whitelist . push ( ...names . map ( name => `${ name } Id` ) ) ;
186+
187+ return params ;
306188} ;
0 commit comments