· 7 years ago · Dec 13, 2018, 08:22 PM
1import {Logger} from '../logger';
2import jwt from "jsonwebtoken";
3import ldap from "ldapjs";
4import _ from 'lodash';
5import Environment from "../env";
6
7/**
8 * User service exposes LDAP & JWT token based authentication mechanisms.
9 * @returns {Readonly<{}>}
10 */
11export const UserService = () => {
12 const envConfig = Environment().envConfig();
13 // TODO: Get available roles from redis maybe for the user
14 const {SecretKey, LDAP, AvailableRoles} = envConfig.Auth;
15 const {approvedGroups, groupDN} = LDAP;
16 const rolesAccumulated = (groups) => {
17 const availableRoles = [...AvailableRoles, ...groups];
18 return `roles=${availableRoles.reduce((acc, e) => `${acc},${e}`)}`;
19 };
20
21 const secretKey = () => SecretKey;
22 const logger = Logger().initLogger();
23
24 // token expiration settings
25 const exp = () => Math.floor(Date.now() / 1000) + (60 * 60);
26
27 // generates jwt token using DN for LDAP - if however a token is provided, just spits it as is.
28 const generateJWTToken = async (dn, tokenProvided) => {
29 const username = dn.split(',')[0].split("=")[1];
30 const groups = await getGroupsByUserName(username);
31 logger.log('info', `dn=${dn}, roles=${rolesAccumulated(Object.values(groups))}`);
32 return (!tokenProvided) ? jwt.sign({
33 exp: exp(),
34 data: `${dn},${rolesAccumulated(Object.values(groups))}`
35 }, secretKey()) : tokenProvided;
36 };
37
38 // JWT decode/verify.
39 const getDecodedJWT = (username, token) => {
40 const decoded = jwt.verify(token, secretKey());
41 logger.log('info', `decoded was : ${JSON.stringify(decoded)}`);
42 return decoded;
43 };
44
45 //verify JWT token by username
46 const verifyJWTTokenByUserName = (username, token) => {
47 try {
48 const decoded = getDecodedJWT(username, token);
49 const dnGiven = decoded.data;
50 const expiration = decoded.exp;
51 const expDate = new Date(expiration);
52 const todaysDate = new Date();
53 logger.log('info', `dnGiven was : ${dnGiven}, _bindDN(username) was : ${_bindDN(username)}, expDate.getTime() = ${expDate.getTime()}, todaysDate = ${todaysDate.getTime()}`);
54 return (dnGiven === `${_bindDN(username)},${rolesAccumulated}`);
55 } catch (error) {
56 logger.log('error', `failed due to : ${error.stack}`);
57 return false;
58 }
59 };
60 const _bindDN = (username) => `uid=${username},ou=People,dc=workday,dc=com`;
61 const {url, timeout: timeout = 5000, connectionTimeout: connectTimeout = 10000, groups} = LDAP;
62 const _initializeLDAPClient = () => ldap.createClient({url, timeout, connectTimeout});
63 const _opts = (username) => {
64 return {
65 filter: `uid=${username}`,
66 scope: 'sub'
67 };
68 };
69
70 const _groupOpts = (username) => {
71 return {
72 filter: `(&(objectClass=posixGroup)(memberUid=${username}))`,
73 scope: 'sub'
74 };
75 };
76 const ldapClientNextHandler = (extractFn, resolve, reject) => {
77 return (error, res) => {
78 res.on('searchEntry', entry => {
79 logger.log('info', `entry found was: ${extractFn(entry)}`);
80 resolve((extractFn(entry)));
81 });
82 res.on('error', err => reject(err));
83 };
84 };
85 const _ldapAuth = (username, password) => {
86 const client = _initializeLDAPClient();
87 const unbindErrFn = (error) => {
88 if (error) {
89 logger.log('error', error.message);
90 } else {
91 logger.log('info', 'client disconnected successfully.');
92 }
93 };
94 const unbind = (client) => client.unbind(unbindErrFn);
95 return new Promise((resolve, reject) => {
96 client.bind(_bindDN(username), password, (error) => {
97 if (error) {
98 logger.log('error', `###### failed to bind : ${error.message}`);
99 reject(error);
100 } else {
101 client.search(_bindDN(username), _opts(username), ldapClientNextHandler((entry) => entry.object.dn, resolve, reject));
102 }
103 unbind(client);
104 });
105 }).catch(err => {
106 logger.log('error', ` ${err}`);
107 unbind(client);
108 });
109 };
110
111 const _ldapAuthDelegate = async (next, res, username, password) => {
112 try {
113 const dn = await _ldapAuth(username, password);
114 const groups = await getGroupsByUserName(username);
115 if (groups) {
116 const groupsArr = Object.values(groups);
117 logger.log('info', `groups=${groupsArr}, dn=${dn}, filtered approved groups = ${approvedGroups.filter(e => groupsArr.includes(e))}`);
118 approvedGroups.filter(e => groupsArr.includes(e)).length > 0 ?
119 next(res, null, `${dn},${rolesAccumulated(groupsArr)}`, true) : next(res, error, null, false);
120 } else {
121 next(res, error, null, false);
122 }
123 } catch (error) {
124 logger.log('error', `failed due to : ${error}`);
125 next(res, error, null, false);
126 }
127 };
128
129 //Authenticates using username,password for LDAP or username+token if provided.
130 const authenticate = async ({username, password, token}, res, next) => {
131 logger.log('info', `token was : ${token}`);
132 if (token) {
133 if (verifyJWTTokenByUserName(username, token)) {
134 next(res, null, getDecodedJWT(username, token).data, false);
135 } else {
136 await _ldapAuthDelegate(next, res, username, password);
137 }
138 } else {
139 await _ldapAuthDelegate(next, res, username, password);
140 }
141 };
142 const getGroupsByUserName = (username) => {
143 const client = _initializeLDAPClient();
144 return new Promise((resolve, reject) => {
145 let entries = [];
146 client.search(groupDN, _groupOpts(username), (error, res) => {
147 res.on('searchEntry', entry => {
148 //logger.log('info', `>>> entry to be pushed was: ${entry.object.cn}`);
149 entries.push(entry.object.cn);
150 });
151 res.on('error', err => reject(err));
152 res.on('end', (err) => {
153 // logger.log('info', `end is nigh... ${JSON.stringify(entries)} error is: ${err.errorMessage}`);
154 if (err.errorMessage !== "") {
155 reject(new Error(error.errorMessage));
156 } else if (err.errorMessage === "" && entries.length !== 0) {
157 //logger.log('info', `resolving with ${entries}`);
158 resolve(entries);
159 } else {
160 reject(new Error("cannot find any groups this user belongs to"));
161 }
162 });
163 res.on('connectTimeout', e => reject(e));
164 res.on('timeout', e => reject(e));
165 res.on('idleTimeout', e => reject(e));
166 });
167 }).catch(err => {
168 logger.log('error', ` rejected ... ${err}`);
169 });
170 };
171
172 return Object.freeze({
173 getDecodedJWT: getDecodedJWT,
174 getLDAPGroups: getGroupsByUserName,
175 generateJWTToken: generateJWTToken,
176 verifyToken: verifyJWTTokenByUserName,
177 authenticate: authenticate,
178 });
179};