UNPKG

13.5 kBJavaScriptView Raw
1'use strict'
2
3const AuthRequest = require('./auth-request')
4const WebIdTlsCertificate = require('../models/webid-tls-certificate')
5const debug = require('../debug').accounts
6const blacklistService = require('../services/blacklist-service')
7const { isValidUsername } = require('../common/user-utils')
8
9/**
10 * Represents a 'create new user account' http request (either a POST to the
11 * `/accounts/api/new` endpoint, or a GET to `/register`).
12 *
13 * Intended just for browser-based requests; to create new user accounts from
14 * a command line, use the `AccountManager` class directly.
15 *
16 * This is an abstract class, subclasses are created (for example
17 * `CreateOidcAccountRequest`) depending on which Authentication mode the server
18 * is running in.
19 *
20 * @class CreateAccountRequest
21 */
22class CreateAccountRequest extends AuthRequest {
23 /**
24 * @param [options={}] {Object}
25 * @param [options.accountManager] {AccountManager}
26 * @param [options.userAccount] {UserAccount}
27 * @param [options.session] {Session} e.g. req.session
28 * @param [options.response] {HttpResponse}
29 * @param [options.returnToUrl] {string} If present, redirect the agent to
30 * this url on successful account creation
31 * @param [options.enforceToc] {boolean} Whether or not to enforce the service provider's T&C
32 * @param [options.tocUri] {string} URI to the service provider's T&C
33 * @param [options.acceptToc] {boolean} Whether or not user has accepted T&C
34 */
35 constructor (options) {
36 super(options)
37
38 this.username = options.username
39 this.userAccount = options.userAccount
40 this.acceptToc = options.acceptToc
41 this.disablePasswordChecks = options.disablePasswordChecks
42 }
43
44 /**
45 * Factory method, creates an appropriate CreateAccountRequest subclass from
46 * an HTTP request (browser form submit), depending on the authn method.
47 *
48 * @param req
49 * @param res
50 *
51 * @throws {Error} If required parameters are missing (via
52 * `userAccountFrom()`), or it encounters an unsupported authentication
53 * scheme.
54 *
55 * @return {CreateOidcAccountRequest|CreateTlsAccountRequest}
56 */
57 static fromParams (req, res) {
58 let options = AuthRequest.requestOptions(req, res)
59
60 let locals = req.app.locals
61 let authMethod = locals.authMethod
62 let accountManager = locals.accountManager
63
64 let body = req.body || {}
65
66 if (body.username) {
67 options.username = body.username.toLowerCase()
68 options.userAccount = accountManager.userAccountFrom(body)
69 }
70
71 options.enforceToc = locals.enforceToc
72 options.tocUri = locals.tocUri
73 options.disablePasswordChecks = locals.disablePasswordChecks
74
75 switch (authMethod) {
76 case 'oidc':
77 options.password = body.password
78 return new CreateOidcAccountRequest(options)
79 case 'tls':
80 options.spkac = body.spkac
81 return new CreateTlsAccountRequest(options)
82 default:
83 throw new TypeError('Unsupported authentication scheme')
84 }
85 }
86
87 static async post (req, res) {
88 let request = CreateAccountRequest.fromParams(req, res)
89
90 try {
91 request.validate()
92 await request.createAccount()
93 } catch (error) {
94 request.error(error, req.body)
95 }
96 }
97
98 static get (req, res) {
99 let request = CreateAccountRequest.fromParams(req, res)
100
101 return Promise.resolve()
102 .then(() => request.renderForm())
103 .catch(error => request.error(error))
104 }
105
106 /**
107 * Renders the Register form
108 */
109 renderForm (error, data = {}) {
110 let authMethod = this.accountManager.authMethod
111
112 let params = Object.assign({}, this.authQueryParams, {
113 enforceToc: this.enforceToc,
114 loginUrl: this.loginUrl(),
115 multiuser: this.accountManager.multiuser,
116 registerDisabled: authMethod === 'tls',
117 returnToUrl: this.returnToUrl,
118 tocUri: this.tocUri,
119 disablePasswordChecks: this.disablePasswordChecks,
120 username: data.username,
121 name: data.name,
122 email: data.email,
123 externalWebId: data.externalWebId,
124 acceptToc: data.acceptToc,
125 connectExternalWebId: data.connectExternalWebId
126 })
127
128 if (error) {
129 params.error = error.message
130 this.response.status(error.statusCode)
131 }
132
133 this.response.render('account/register', params)
134 }
135
136 /**
137 * Creates an account for a given user (from a POST to `/api/accounts/new`)
138 *
139 * @throws {Error} If errors were encountering while validating the username.
140 *
141 * @return {Promise<UserAccount>} Resolves with newly created account instance
142 */
143 async createAccount () {
144 let userAccount = this.userAccount
145 let accountManager = this.accountManager
146
147 this.cancelIfUsernameInvalid(userAccount)
148 this.cancelIfBlacklistedUsername(userAccount)
149 await this.cancelIfAccountExists(userAccount)
150 await this.createAccountStorage(userAccount)
151 await this.saveCredentialsFor(userAccount)
152 await this.sendResponse(userAccount)
153
154 // 'return' not used deliberately, no need to block and wait for email
155 if (userAccount && userAccount.email) {
156 debug('Sending Welcome email')
157 accountManager.sendWelcomeEmail(userAccount)
158 }
159
160 return userAccount
161 }
162
163 /**
164 * Rejects with an error if an account already exists, otherwise simply
165 * resolves with the account.
166 *
167 * @param userAccount {UserAccount} Instance of the account to be created
168 *
169 * @return {Promise<UserAccount>} Chainable
170 */
171 cancelIfAccountExists (userAccount) {
172 let accountManager = this.accountManager
173
174 return accountManager.accountExists(userAccount.username)
175 .then(exists => {
176 if (exists) {
177 debug(`Canceling account creation, ${userAccount.webId} already exists`)
178 let error = new Error('Account already exists')
179 error.status = 400
180 throw error
181 }
182 // Account does not exist, proceed
183 return userAccount
184 })
185 }
186
187 /**
188 * Creates the root storage folder, initializes default containers and
189 * resources for the new account.
190 *
191 * @param userAccount {UserAccount} Instance of the account to be created
192 *
193 * @throws {Error} If errors were encountering while creating new account
194 * resources.
195 *
196 * @return {Promise<UserAccount>} Chainable
197 */
198 createAccountStorage (userAccount) {
199 return this.accountManager.createAccountFor(userAccount)
200 .catch(error => {
201 error.message = 'Error creating account storage: ' + error.message
202 throw error
203 })
204 .then(() => {
205 debug('Account storage resources created')
206 return userAccount
207 })
208 }
209
210 /**
211 * Check if a username is a valid slug.
212 *
213 * @param userAccount {UserAccount} Instance of the account to be created
214 *
215 * @throws {Error} If errors were encountering while validating the
216 * username.
217 *
218 * @return {UserAccount} Chainable
219 */
220 cancelIfUsernameInvalid (userAccount) {
221 if (!userAccount.username || !isValidUsername(userAccount.username)) {
222 debug('Invalid username ' + userAccount.username)
223 const error = new Error('Invalid username (contains invalid characters)')
224 error.status = 400
225 throw error
226 }
227
228 return userAccount
229 }
230
231 /**
232 * Check if a username is a valid slug.
233 *
234 * @param userAccount {UserAccount} Instance of the account to be created
235 *
236 * @throws {Error} If username is blacklisted
237 *
238 * @return {UserAccount} Chainable
239 */
240 cancelIfBlacklistedUsername (userAccount) {
241 const validUsername = blacklistService.validate(userAccount.username)
242 if (!validUsername) {
243 debug('Invalid username ' + userAccount.username)
244 const error = new Error('Invalid username (username is blacklisted)')
245 error.status = 400
246 throw error
247 }
248
249 return userAccount
250 }
251}
252
253/**
254 * Models a Create Account request for a server using WebID-OIDC (OpenID Connect)
255 * as a primary authentication mode. Handles saving user credentials to the
256 * `UserStore`, etc.
257 *
258 * @class CreateOidcAccountRequest
259 * @extends CreateAccountRequest
260 */
261class CreateOidcAccountRequest extends CreateAccountRequest {
262 /**
263 * @constructor
264 *
265 * @param [options={}] {Object} See `CreateAccountRequest` constructor docstring
266 * @param [options.password] {string} Password, as entered by the user at signup
267 * @param [options.acceptToc] {boolean} Whether or not user has accepted T&C
268 */
269 constructor (options) {
270 super(options)
271
272 this.password = options.password
273 }
274
275 /**
276 * Validates the Login request (makes sure required parameters are present),
277 * and throws an error if not.
278 *
279 * @throws {Error} If missing required params
280 */
281 validate () {
282 let error
283
284 if (!this.username) {
285 error = new Error('Username required')
286 error.statusCode = 400
287 throw error
288 }
289
290 if (!this.password) {
291 error = new Error('Password required')
292 error.statusCode = 400
293 throw error
294 }
295
296 if (this.enforceToc && !this.acceptToc) {
297 error = new Error('Accepting Terms & Conditions is required for this service')
298 error.statusCode = 400
299 throw error
300 }
301 }
302
303 /**
304 * Generate salted password hash, etc.
305 *
306 * @param userAccount {UserAccount}
307 *
308 * @return {Promise<null|Graph>}
309 */
310 saveCredentialsFor (userAccount) {
311 return this.userStore.createUser(userAccount, this.password)
312 .then(() => {
313 debug('User credentials stored')
314 return userAccount
315 })
316 }
317
318 /**
319 * Generate the response for the account creation
320 *
321 * @param userAccount {UserAccount}
322 *
323 * @return {UserAccount}
324 */
325 sendResponse (userAccount) {
326 let redirectUrl = this.returnToUrl || userAccount.podUri
327 this.response.redirect(redirectUrl)
328
329 return userAccount
330 }
331}
332
333/**
334 * Models a Create Account request for a server using WebID-TLS as primary
335 * authentication mode. Handles generating and saving a TLS certificate, etc.
336 *
337 * @class CreateTlsAccountRequest
338 * @extends CreateAccountRequest
339 */
340class CreateTlsAccountRequest extends CreateAccountRequest {
341 /**
342 * @constructor
343 *
344 * @param [options={}] {Object} See `CreateAccountRequest` constructor docstring
345 * @param [options.spkac] {string}
346 * @param [options.acceptToc] {boolean} Whether or not user has accepted T&C
347 */
348 constructor (options) {
349 super(options)
350
351 this.spkac = options.spkac
352 this.certificate = null
353 }
354
355 /**
356 * Validates the Signup request (makes sure required parameters are present),
357 * and throws an error if not.
358 *
359 * @throws {Error} If missing required params
360 */
361 validate () {
362 let error
363
364 if (!this.username) {
365 error = new Error('Username required')
366 error.statusCode = 400
367 throw error
368 }
369
370 if (this.enforceToc && !this.acceptToc) {
371 error = new Error('Accepting Terms & Conditions is required for this service')
372 error.statusCode = 400
373 throw error
374 }
375 }
376
377 /**
378 * Generates a new X.509v3 RSA certificate (if `spkac` was passed in) and
379 * adds it to the user account. Used for storage in an agent's WebID
380 * Profile, for WebID-TLS authentication.
381 *
382 * @param userAccount {UserAccount}
383 * @param userAccount.webId {string} An agent's WebID URI
384 *
385 * @throws {Error} HTTP 400 error if errors were encountering during
386 * certificate generation.
387 *
388 * @return {Promise<UserAccount>} Chainable
389 */
390 generateTlsCertificate (userAccount) {
391 if (!this.spkac) {
392 debug('Missing spkac param, not generating cert during account creation')
393 return Promise.resolve(userAccount)
394 }
395
396 return Promise.resolve()
397 .then(() => {
398 let host = this.accountManager.host
399 return WebIdTlsCertificate.fromSpkacPost(this.spkac, userAccount, host)
400 .generateCertificate()
401 })
402 .catch(err => {
403 err.status = 400
404 err.message = 'Error generating a certificate: ' + err.message
405 throw err
406 })
407 .then(certificate => {
408 debug('Generated a WebID-TLS certificate as part of account creation')
409 this.certificate = certificate
410 return userAccount
411 })
412 }
413
414 /**
415 * Generates a WebID-TLS certificate and saves it to the user's profile
416 * graph.
417 *
418 * @param userAccount {UserAccount}
419 *
420 * @return {Promise<UserAccount>} Chainable
421 */
422 saveCredentialsFor (userAccount) {
423 return this.generateTlsCertificate(userAccount)
424 .then(userAccount => {
425 if (this.certificate) {
426 return this.accountManager
427 .addCertKeyToProfile(this.certificate, userAccount)
428 .then(() => {
429 debug('Saved generated WebID-TLS certificate to profile')
430 })
431 } else {
432 debug('No certificate generated, no need to save to profile')
433 }
434 })
435 .then(() => {
436 return userAccount
437 })
438 }
439
440 /**
441 * Writes the generated TLS certificate to the http Response object.
442 *
443 * @param userAccount {UserAccount}
444 *
445 * @return {UserAccount} Chainable
446 */
447 sendResponse (userAccount) {
448 let res = this.response
449 res.set('User', userAccount.webId)
450 res.status(200)
451
452 if (this.certificate) {
453 res.set('Content-Type', 'application/x-x509-user-cert')
454 res.send(this.certificate.toDER())
455 } else {
456 res.end()
457 }
458
459 return userAccount
460 }
461}
462
463module.exports = CreateAccountRequest
464module.exports.CreateAccountRequest = CreateAccountRequest
465module.exports.CreateTlsAccountRequest = CreateTlsAccountRequest