UNPKG

15.6 kBJavaScriptView Raw
1/**
2* vim:set sw=2 ts=2 sts=2 ft=javascript expandtab:
3*
4* # Authentification Module
5*
6* ## License
7*
8* Licensed to the Apache Software Foundation (ASF) under one
9* or more contributor license agreements. See the NOTICE file
10* distributed with this work for additional information
11* regarding copyright ownership. The ASF licenses this file
12* to you under the Apache License, Version 2.0 (the
13* "License"); you may not use this file except in compliance
14* with the License. You may obtain a copy of the License at
15*
16* http://www.apache.org/licenses/LICENSE-2.0
17*
18* Unless required by applicable law or agreed to in writing,
19* software distributed under the License is distributed on an
20* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
21* KIND, either express or implied. See the License for the
22* specific language governing permissions and limitations
23* under the License.
24*
25* ## Description
26*
27* This module contains all functions about authentification of MyPads users.
28* It's mainly based upon the excellent *passport* and *jsonwebtoken* Node
29* libraries.
30*
31* TODO: abstract passport.authenticate to allow more easily custom errors
32*/
33
34// External dependencies
35var ld = require('lodash');
36var jwt = require('jsonwebtoken');
37var ExtractJwt = require('passport-jwt').ExtractJwt;
38var LdapAuth = require('ldapauth-fork');
39var CasAuth = require('simple-cas-interface');
40var settings;
41try {
42 // Normal case : when installed as a plugin
43 settings = require('ep_etherpad-lite/node/utils/Settings');
44}
45catch (e) {
46 if (process.env.TEST_LDAP) {
47 settings = {
48 'users': {
49 'admin': {
50 'password': 'admin',
51 'is_admin': true
52 },
53 'parker': {
54 'password': 'lovesKubiak',
55 'is_admin': false
56 }
57 },
58 'ep_mypads': {
59 'ldap': {
60 'url': 'ldap://rroemhild-test-openldap',
61 'bindDN': 'cn=admin,dc=planetexpress,dc=com',
62 'bindCredentials': 'GoodNewsEveryone',
63 'searchBase': 'ou=people,dc=planetexpress,dc=com',
64 'searchFilter': '(uid={{username}})',
65 'properties': {
66 'login': 'uid',
67 'email': 'mail',
68 'firstname': 'givenName',
69 'lastname': 'sn'
70 },
71 'defaultLang': 'fr'
72 }
73 }
74 };
75 } else {
76 // Testing case : we need to mock the express dependency
77 settings = {
78 users: {
79 admin: { password: 'admin', is_admin: true },
80 grace: { password: 'admin', is_admin: true },
81 parker: { password: 'lovesKubiak', is_admin: false }
82 }
83 };
84 }
85}
86var passport = require('passport');
87var JWTStrategy = require('passport-jwt').Strategy;
88var cuid = require('cuid');
89
90// Local dependencies
91var common = require('./model/common.js');
92var user = require('./model/user.js');
93var conf = require('./configuration.js');
94
95var NOT_INTERNAL_AUTH_PWD = 'soooooo_useless';
96
97module.exports = (function () {
98 'use strict';
99
100 /**
101 * ## Authentification
102 *
103 * - `tokens` holds all actives tokens with user logins as key and user data
104 * as value, plus a special `key` attribute to check validity
105 * - `secret` is the temporary random string key. It will be reinitialized
106 * after server relaunch, invaliditing all active users
107 */
108
109 var auth = {
110 tokens: {},
111 adminTokens: {},
112 secret: cuid() // secret per etherpad session
113 };
114
115 /**
116 * ## Internal functions
117 *
118 * These functions are not private like with closures, for testing purposes,
119 * but they are expected be used only internally by other MyPads functions.
120 */
121
122 auth.fn = {};
123
124 /**
125 * ### getUser
126 *
127 * `getUser` is a synchronous function that checks if the given encrypted
128 * `token` is valid, ie if login has been already found in local cache and if
129 * the given key is the same as the generated one.
130 * It returns the *user* object in case of success and *false* otherwise.
131 *
132 * TODO: unit test missing
133 */
134
135 auth.fn.getUser = function (token) {
136 var jwt_payload = jwt.decode(token, auth.secret);
137 if (!jwt_payload) { return false; }
138 var login = jwt_payload.login;
139 var userAuth = (login && auth.tokens[login]);
140 if (userAuth && (auth.tokens[login].key === jwt_payload.key)) {
141 return auth.tokens[login];
142 } else {
143 return false;
144 }
145 };
146
147 /**
148 * ### local
149 *
150 * `local` is a synchronous function used to set up JWT strategy.
151 */
152
153 auth.fn.local = function () {
154 var opts = {
155 secretOrKey: auth.secret,
156 passReqToCallback: true,
157 jwtFromRequest: ExtractJwt.fromExtractors([
158 ExtractJwt.fromUrlQueryParameter('auth_token'),
159 ExtractJwt.fromBodyField('auth_token'),
160 ExtractJwt.fromAuthHeaderWithScheme('JWT')
161 ])
162 };
163 passport.use(new JWTStrategy(opts,
164 function (req, jwt_payload, callback) {
165 var isFS = function (s) { return (ld.isString(s) && !ld.isEmpty(s)); };
166 if (!isFS(jwt_payload.login)) {
167 throw new TypeError('BACKEND.ERROR.TYPE.LOGIN_STR');
168 }
169 if (!ld.isFunction(callback)) {
170 throw new TypeError('BACKEND.ERROR.TYPE.CALLBACK_FN');
171 }
172 auth.fn.JWTFn(req, jwt_payload, false, callback);
173 }
174 ));
175 };
176
177 /**
178 * ### checkMyPadsUser
179 *
180 * `checkMyPadsUser` checks user existence from given `login` and `pass`. It
181 * uses the last argument, `callback` function, to return an *error* or *null*
182 * and the *user* object.
183 */
184
185 auth.fn.checkMyPadsUser = function (login, pass, callback) {
186 switch (conf.get('authMethod')) {
187 case 'ldap':
188 // ld.cloneDeep because LdapAuth would otherwise modify authLdapSettings conf
189 var ldapConf = ld.cloneDeep(conf.get('authLdapSettings'));
190 var lauth = new LdapAuth(ldapConf);
191 lauth.authenticate(login, pass, function(err, ldapuser) {
192 lauth.close(function(error) {
193 if (error) { console.error(error); }
194 });
195 if (err) {
196 var emsg = err;
197 // openldap error message || active directory error message
198 if (ld.isString(err.lde_message) &&
199 (err.lde_message === 'Invalid Credentials' ||
200 err.lde_message.match(/data 52e,/))
201 ) {
202 emsg = 'BACKEND.ERROR.AUTHENTICATION.PASSWORD_INCORRECT';
203 } else if (
204 (ld.isString(err) && err.match(/no such user/)) ||
205 (ld.isString(err.lde_message) &&
206 (err.lde_message.match(/no such user/) ||
207 err.lde_message.match(/data 525,/)))
208 ) {
209 emsg = 'BACKEND.ERROR.USER.NOT_FOUND';
210 } else {
211 console.error('LdapAuth error: ', err);
212 }
213 return callback(new Error(emsg), false);
214 }
215 user.get(login, function(err, u) {
216 if (err) {
217 // We have to create the user in mypads database
218 var mail;
219 var props = ldapConf.properties;
220 ldapConf = conf.get('authLdapSettings');
221 if (Array.isArray(ldapuser[props.email])) {
222 mail = ldapuser[props.email][0];
223 } else {
224 mail = ldapuser[props.email];
225 }
226 user.set({
227 login: ldapuser[props.login],
228 password: NOT_INTERNAL_AUTH_PWD,
229 firstname: ldapuser[props.firstname],
230 lastname: ldapuser[props.lastname],
231 email: mail,
232 lang: ldapConf.defaultLang || 'en'
233 }, callback);
234 } else {
235 return callback(null, u);
236 }
237 });
238 });
239 break;
240 case 'cas':
241 user.get(login.login, function(err, u) {
242 // If the user does not exist, we create the user
243 if (err) {
244 user.set(login, callback);
245 } else {
246 return callback(null, u);
247 }
248 });
249 break;
250 default:
251 /* Prevents to use default external auth password if configuration has been changed
252 * and now use internal authentification
253 */
254 if (pass === NOT_INTERNAL_AUTH_PWD) {
255 var emsg = 'BACKEND.ERROR.AUTHENTICATION.PLEASE_CHANGE_YOUR_PASSWORD';
256 return callback(new Error(emsg), false);
257 }
258 user.get(login, function (err, u) {
259 if (err) { return callback(err); }
260 auth.fn.isPasswordValid(u, pass, function (err, isValid) {
261 if (err) { return callback(err); }
262 if (!isValid) {
263 var emsg = 'BACKEND.ERROR.AUTHENTICATION.PASSWORD_INCORRECT';
264 return callback(new Error(emsg), false);
265 }
266 return callback(null, u);
267 });
268 });
269 }
270 };
271
272 /**
273 * ### checkAdminUser
274 *
275 * `checkAdminUser` checks admin existence from given `login` and `pass`. It
276 * uses the last argument, `callback` function, to return an *error* or *null*
277 * and the *user* object.
278 */
279
280 auth.fn.checkAdminUser = function (login, pass, callback) {
281 if (!settings || !settings.users || !settings.users[login]) {
282 return callback(new Error('BACKEND.ERROR.USER.NOT_FOUND'), null);
283 }
284 var u = settings.users[login];
285 u.login = login;
286 var emsg;
287 if (pass !== u.password) {
288 emsg = 'BACKEND.ERROR.AUTHENTICATION.PASSWORD_INCORRECT';
289 return callback(new Error(emsg, false));
290 }
291 if (!u.is_admin) {
292 emsg = 'BACKEND.ERROR.AUTHENTICATION.ADMIN';
293 return callback(new Error(emsg, false));
294 }
295 return callback(null, u);
296 };
297
298 /*
299 * ### casAuth
300 *
301 * `casAuth` checks that the given CAS ticket is valid. It uses the last
302 * argument, `callback` function, to return an *error* or *null* and the *user*
303 * object.
304 */
305 var rgx = new RegExp('api/auth/login/cas$');
306 auth.fn.casAuth = function (req, res, callback) {
307 var ticket = req.body.ticket;
308 if (ticket) {
309 var acs = ld.cloneDeep(conf.get('authCasSettings'));
310 var props = acs.properties;
311 var defaultLang = acs.defaultLang;
312 delete acs.properties;
313 delete acs.defaultLang;
314 acs.serviceUrl = req.protocol+'://'+req.host+req.path.replace(rgx, '?/login');
315 var cas = new CasAuth(acs);
316 cas.validateServiceTicket(ticket)
317 .then(function(info) {
318 return callback(null, {
319 login: info.attributes[props.login] || info[props.login],
320 password: NOT_INTERNAL_AUTH_PWD,
321 firstname: info.attributes[props.firstname],
322 lastname: info.attributes[props.lastname],
323 email: info.attributes[props.email],
324 lang: defaultLang || 'en'
325 });
326 })
327 .catch(function(err) {
328 console.error(err);
329 return callback(new Error(err));
330 });
331 } else {
332 callback(new Error('BACKEND.ERROR.AUTHENTICATION.CAS_NO_TICKET'));
333 }
334 };
335
336 /**
337 * ### JWTFn
338 *
339 * `JWTFn` is the function used by JWT strategy for verifying if used
340 * encrypted jwt token is correct. It takes:
341 *
342 * - the `req` Express request object, automatically populated from the
343 * strategy
344 * - the JWT decoded token `jwt_payload` object
345 * - a `admin` boolean
346 * - a `callback` function, returning
347 * - *Error* if there is a problem
348 * - *null*, *false* and an object for auth error
349 * - *null* and the *user* or *token* object for auth success
350 *
351 * TODO: expiration handling?
352 */
353
354 auth.fn.JWTFn = function (req, jwt_payload, admin, callback) {
355 var checkFn = (admin ? auth.fn.checkAdminUser : auth.fn.checkMyPadsUser);
356 var ns = (admin ? 'adminTokens' : 'tokens');
357 var login = jwt_payload.login;
358 var token = auth[ns][login];
359 if (!token || !jwt_payload.key) {
360 var pass = jwt_payload.password;
361 if (!ld.isString(pass)) {
362 return callback(new Error('BACKEND.ERROR.TYPE.PASSWORD_STR'));
363 }
364 if (conf.get('authMethod') === 'cas' && !admin) {
365 login = jwt_payload;
366 }
367 checkFn(login, pass, function (err, u) {
368 if (err) { return callback(err, u); }
369 auth[ns][u.login] = u;
370 auth[ns][u.login].key = cuid();
371 return callback(null, u);
372 });
373 } else {
374 if (token.key !== jwt_payload.key) {
375 var emsg = 'BACKEND.ERROR.AUTHENTICATION.NOT_AUTH';
376 return callback(new Error(emsg), false);
377 }
378 req.mypadsLogin = login;
379 callback(null, token);
380 }
381 };
382
383 /**
384 * ### localFn
385 *
386 * `localFn` is a function that checks if login and password are correct. It
387 * takes :
388 *
389 * - a `login` string
390 * - a `password` string
391 * - a `callback` function, returning
392 * - *Error* if there is a problem
393 * - *null*, *false* and an object for auth error
394 * - *null* and the *user* object for auth success
395 */
396
397 auth.fn.localFn = function (login, password, callback) {
398 user.get(login, function (err, u) {
399 if (err) { return callback(err); }
400 auth.fn.isPasswordValid(u, password, function (err, isValid) {
401 if (err) { return callback(err); }
402 if (!isValid) {
403 var emsg = 'BACKEND.ERROR.AUTHENTICATION.PASSWORD_INCORRECT';
404 callback(new Error(emsg), false);
405 } else {
406 callback(null, u);
407 }
408 });
409 });
410 };
411
412 /**
413 * ### isPasswordValid
414 *
415 * `isPasswordValid` compares the hash of given password and saved salt with
416 * the one saved in database.
417 * It takes :
418 *
419 * - the `u` user object
420 * - the `password` string
421 * - a `callback` function, returning *Error* or *null* and a boolean
422 */
423
424 auth.fn.isPasswordValid = function (u, password, callback) {
425 if (!ld.isString(password)) {
426 return callback(new TypeError('BACKEND.ERROR.TYPE.PASSWORD_MISSING'));
427 }
428 // if u.visibility is defined, u is a group, which we shouldn't authenticate against LDAP
429 if (conf.get('authMethod') === 'ldap' && ld.isUndefined(u.visibility)) {
430 // ld.cloneDeep because LdapAuth would otherwise modify authLdapSettings conf
431 var lauth = new LdapAuth(ld.cloneDeep(conf.get('authLdapSettings')));
432 if (ld.isUndefined(u) || ld.isNull(u) || ld.isUndefined(u.login) || ld.isNull(u.login)) {
433 return callback(null, false);
434 } else {
435 lauth.authenticate(u.login, password, function(err) {
436 lauth.close(function(error) {
437 if (error) { console.error(error); }
438 });
439 if (err) {
440 if (err.lde_message === 'Invalid Credentials') {
441 err = 'BACKEND.ERROR.AUTHENTICATION.PASSWORD_INCORRECT';
442 } else if (err.match(/no such user/)) {
443 err = 'BACKEND.ERROR.USER.NOT_FOUND';
444 }
445 console.error('LdapAuth error: ', err);
446 return callback(null, false);
447 }
448 return callback(null, true);
449 });
450 }
451 } else {
452 common.hashPassword(u.password.salt, password, function (err, res) {
453 if (err) { return callback(err); }
454 callback(null, (res.hash === u.password.hash));
455 });
456 }
457 };
458
459 /**
460 * ## Public functions
461 *
462 * ### init
463 *
464 * `init` is a synchronous function used to set up authentification. It :
465 *
466 * - initializes local strategy by default
467 * - uses of passport middlwares for express
468 * - launch session middleware bundled with express, using secret phrase saved
469 * in database
470 * - mocks admin behavior if in testingMode
471 */
472
473 auth.init = function (app) {
474 app.use(passport.initialize());
475 auth.fn.local();
476 };
477
478 return auth;
479
480}).call(this);