1 | ;
|
2 |
|
3 | const audit = require('../helper/audit');
|
4 | const generateId = require('../helper/generateId');
|
5 |
|
6 | /**
|
7 | * Authenticator/passport strategy wrapper abstraction.
|
8 | *
|
9 | * I.e. it is the parent class of all auth classes.
|
10 | */
|
11 | class Auth
|
12 | {
|
13 |
|
14 | /**
|
15 | * @param {string} method
|
16 | * @param {object} options common options for all auth classes; see properties
|
17 | */
|
18 | constructor(method, options)
|
19 | {
|
20 |
|
21 | /**
|
22 | * Authentication method name. E.g. 'email' for Email based authentication.
|
23 | * This is used as an unique id for various things.
|
24 | * @type string
|
25 | */
|
26 | this.method = method;
|
27 |
|
28 | /**
|
29 | * This is a standard descriptor of this authentication mechanism that is publicly shared.
|
30 | * Clients should use this to figure out how to use a login auth from outside.
|
31 | *
|
32 | * Not directly configurable.
|
33 | *
|
34 | * @type object
|
35 | */
|
36 | this.description = {
|
37 | method: this.method
|
38 | };
|
39 |
|
40 | /**
|
41 | * Additional settings given to passport.authenticate.
|
42 | *
|
43 | * Not directly configurable.
|
44 | *
|
45 | * @type object
|
46 | */
|
47 | this.authenticateOptions = {
|
48 | failureMessage: 'login failed',
|
49 | badRequestMessage: 'XXX'
|
50 | };
|
51 |
|
52 | /**
|
53 | * Users collection.
|
54 | *
|
55 | * Note: various things all assume that a CachedCollection is being used.
|
56 | *
|
57 | * @type {CachedCollection}
|
58 | */
|
59 | this.users = options.users || options.collection;
|
60 |
|
61 | /**
|
62 | * Default roles new registered users should assume.
|
63 | *
|
64 | * @type {object}
|
65 | */
|
66 | this.defaultRoles = options.defaultRoles || {};
|
67 |
|
68 | /**
|
69 | * Custom fields
|
70 | *
|
71 | * @type {object}
|
72 | */
|
73 | this.custom = options.custom || {};
|
74 |
|
75 | //~ /**
|
76 | //~ * Instance of email sender class for sending emails.
|
77 | //~ *
|
78 | //~ * @type {EmailSender}
|
79 | //~ */
|
80 | //~ this.emailSender = options.emailSender;
|
81 |
|
82 |
|
83 | //~ this.defaultNotificationInterval = options.defaultNotificationInterval || -1;
|
84 |
|
85 | //~ if (options.recaptcha)
|
86 | //~ {
|
87 | //~ this.description.recaptcha = true;
|
88 | //~ }
|
89 | }
|
90 |
|
91 | /**
|
92 | * Must be overridden to provide implementation of said authentication method.
|
93 | * @param {ExpressApplication} app express application
|
94 | * @param {string} prefix all route prefix
|
95 | * @param {Passport} passport passport class
|
96 | * @abstract
|
97 | */
|
98 | install(app, prefix, passport)
|
99 | {
|
100 | throw new Error('TODO: ABSTRACT');
|
101 | }
|
102 |
|
103 | /**
|
104 | * Helper method that finds an user based on a credential.
|
105 | *
|
106 | * A credential is something like an email address or a facebook user id.
|
107 | *
|
108 | * This is something that uniquely identifies an account.
|
109 | *
|
110 | * @param {string} value
|
111 | * @return {User|false}
|
112 | */
|
113 | findUser(value)
|
114 | {
|
115 | const users = this.users.lookup;
|
116 |
|
117 | for (let userId in users)
|
118 | {
|
119 | let user = users[userId];
|
120 | let credentials = (user.credentials || [])
|
121 | .filter((credential) => credential.type === this.method && credential.value === value);
|
122 |
|
123 | if (credentials.length > 0)
|
124 | {
|
125 | return user;
|
126 | }
|
127 | }
|
128 |
|
129 | return false;
|
130 | }
|
131 |
|
132 | /**
|
133 | * Helper method for SSO type logins.
|
134 | *
|
135 | * This method finds existing or creates new accounts basen on profile
|
136 | * information returned from oauth partner.
|
137 | *
|
138 | * @param {string} username unique id
|
139 | * @param {Profile} profile unique id
|
140 | * @param {Function} done callback to call when our work is done
|
141 | * @param {Request} [req] request object
|
142 | */
|
143 | handleUserLoginByProfile(username, profile, done, req = undefined)
|
144 | {
|
145 | username = username || profile.id;
|
146 | // find an account
|
147 | let user = this.findUser(username);
|
148 |
|
149 | if (user) // if found, log in found account
|
150 | {
|
151 | done(null, user);
|
152 | }
|
153 | else // if not found, make a new user and log new user in
|
154 | {
|
155 | user = this.createUserFromProfile(profile);
|
156 | this.users.createRecord(user)
|
157 | .then((user) =>
|
158 | {
|
159 | req && req.audit(audit.ACCOUNT_CREATE, JSON.stringify({
|
160 | user,
|
161 | profile
|
162 | }));
|
163 | done(null, user);
|
164 | }, done);
|
165 | }
|
166 | }
|
167 |
|
168 | /**
|
169 | * Helper method that creates an User object from a Profile
|
170 | *
|
171 | * @param {Profile} profile
|
172 | * @return {User}
|
173 | */
|
174 | createUserFromProfile(profile)
|
175 | {
|
176 | let user = {
|
177 | // unique id
|
178 | // can't use id from profile as these might conflict across login providers
|
179 | id: generateId(),
|
180 | // login credentials
|
181 | credentials: [{
|
182 | type: this.method,
|
183 | value: profile.id
|
184 | }],
|
185 | // new user roles
|
186 | roles: this.defaultRoles,
|
187 | // profile bs
|
188 | displayName: profile.displayName,
|
189 | //~ name: profile.name,
|
190 | photos: profile.photos,
|
191 | //~ // notification settings
|
192 | //~ notifications: [],
|
193 | //~ notificationInterval: -1,
|
194 | //~ notificationLastSent: 0,
|
195 | //~ notificationSubscriptions: {},
|
196 | };
|
197 |
|
198 | for (let field in this.custom)
|
199 | {
|
200 | if (this.custom[field].derive)
|
201 | {
|
202 | let value = this.custom[field].derive(profile);
|
203 |
|
204 | if (value)
|
205 | {
|
206 | user[field] = value;
|
207 | }
|
208 | }
|
209 | }
|
210 |
|
211 | return user;
|
212 | }
|
213 |
|
214 | /**
|
215 | * Helper method that produces a middleware to handle successful logged in
|
216 | * case.
|
217 | *
|
218 | * @param {boolean} [redirect=false] redirect based login is used
|
219 | * @return {ExpressMiddleware}
|
220 | */
|
221 | loggedIn(redirect = false)
|
222 | {
|
223 | if (redirect && typeof redirect !== 'string')
|
224 | {
|
225 | redirect = '/';
|
226 | }
|
227 |
|
228 | return function (req, res)
|
229 | {
|
230 | // if not req.user.id then it is not a real use
|
231 | // i.e. we are forwarding error or custom payload
|
232 | if (!req.user.id)
|
233 | {
|
234 | res.error(req.user.error, audit.LOGIN_FAILURE);
|
235 | req.logout();
|
236 | }
|
237 | else
|
238 | {
|
239 | // otherwise, we make a fuss about logging in
|
240 | res.audit(audit.LOGIN, `Logged in via ${this.method}`, JSON.stringify({
|
241 | id: req.user.id,
|
242 | displayName: req.user.displayName,
|
243 | roles: req.user.roles
|
244 | }));
|
245 |
|
246 | // for redirect based login methods, we redirect back to some url
|
247 | if (redirect)
|
248 | {
|
249 | res.redirect(redirect);
|
250 | }
|
251 | else
|
252 | {
|
253 | // otherwise we return a login success message
|
254 | res.success(`Logged in via ${this.method}`, audit.LOGIN);
|
255 | }
|
256 | }
|
257 | }.bind(this);
|
258 | }
|
259 |
|
260 | }
|
261 |
|
262 | module.exports = Auth;
|