Skip to content

Commit 4ca105e

Browse files
Implement reset token validation and change password page
1 parent 3b915ff commit 4ca105e

File tree

12 files changed

+318
-21
lines changed

12 files changed

+318
-21
lines changed

default-views/account/register.hbs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,10 @@
5555

5656
<div class="col-md-10">
5757
<div>Already have an account?
58-
<a href="/login?returnToUrl={{{returnToUrl}}}">Log In</a>
58+
<a class="btn btn-xs btn-default"
59+
href="/login{{#if returnToUrl}}?returnToUrl={{{returnToUrl}}}{{/if}}">
60+
Log In
61+
</a>
5962
</div>
6063
</div>
6164
</div>
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1">
6+
<title>Change Password</title>
7+
<link rel="stylesheet" href="/common/css/bootstrap.min.css">
8+
</head>
9+
<body>
10+
<div class="container">
11+
<h4>Change Password</h4>
12+
</div>
13+
<div class="container">
14+
<form method="post" action="/account/password/change">
15+
{{#if error}}
16+
<div class="form-group">
17+
<div class="row">
18+
<div class="col-md-12">
19+
<p class="text-danger"><strong>{{error}}</strong></p>
20+
</div>
21+
</div>
22+
</div>
23+
{{/if}}
24+
25+
{{#if validToken}}
26+
<div class="form-group">
27+
<div class="row">
28+
<div class="col-md-12">
29+
<label for="newPassword">New Password:</label>
30+
31+
<input type="password" class="form-control" name="newPassword" />
32+
</div>
33+
</div>
34+
</div>
35+
36+
<div class="form-group">
37+
<div class="row">
38+
<div class="col-md-2">
39+
<button type="submit" class="btn btn-primary">Change Password</button>
40+
</div>
41+
</div>
42+
43+
<input type="hidden" name="returnToUrl" value="{{{returnToUrl}}}" />
44+
<input type="hidden" name="token" value="{{token}}" />
45+
</div>
46+
{{else}}
47+
<div class="form-group">
48+
<div class="row">
49+
<div class="col-md-12">
50+
<div>
51+
<strong>
52+
<a class="btn btn-xs btn-primary"
53+
href="/account/password/reset{{#if returnToUrl}}?returnToUrl={{{returnToUrl}}}{{/if}}">
54+
Email password reset link
55+
</a>
56+
</strong>
57+
</div>
58+
</div>
59+
</div>
60+
</div>
61+
{{/if}}
62+
</form>
63+
</div>
64+
</body>
65+
</html>

default-views/auth/login.hbs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,18 +37,24 @@
3737
<div class="form-group">
3838
<div class="row">
3939
<div class="col-md-2">
40-
<button type="submit" class="btn btn-primary" id="login">Login</button>
40+
<button type="submit" class="btn btn-primary" id="login">Log In</button>
4141
</div>
4242

4343
<div class="col-md-4">
4444
<div>Don't have an account?
45-
<a href="/register?returnToUrl={{{postRegisterUrl}}}">Register</a>
45+
<a class="btn btn-xs btn-default"
46+
href="/register{{#if returnToUrl}}?returnToUrl={{{returnToUrl}}}{{/if}}">
47+
Register
48+
</a>
4649
</div>
4750
</div>
4851

4952
<div class="col-md-6">
5053
<div>Forgot password?
51-
<a href="/account/password/reset?returnToUrl={{{postRegisterUrl}}}">Reset password</a>
54+
<a class="btn btn-xs btn-default"
55+
href="/account/password/reset{{#if returnToUrl}}?returnToUrl={{{returnToUrl}}}{{/if}}">
56+
Reset password
57+
</a>
5258
</div>
5359
</div>
5460
</div>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1">
6+
<title>Password Changed</title>
7+
<link rel="stylesheet" href="/common/css/bootstrap.min.css">
8+
</head>
9+
<body>
10+
<div class="container">
11+
<h4>Password Changed</h4>
12+
</div>
13+
<div class="container">
14+
<p>Your password has been changed.</p>
15+
16+
<p><strong>
17+
<a class="btn btn-default" href="/login{{#if returnToUrl}}?returnToUrl={{{returnToUrl}}}{{/if}}">
18+
Log in
19+
</a>
20+
</strong></p>
21+
</div>
22+
</body>
23+
</html>

default-views/auth/reset-password.hbs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@
4545

4646
<div class="col-md-4">
4747
<div>Don't have an account?
48-
<a href="/register?returnToUrl={{{returnToUrl}}}">Register</a>
48+
<a class="btn btn-xs btn-default"
49+
href="/register{{#if returnToUrl}}?returnToUrl={{{returnToUrl}}}{{/if}}">Register</a>
4950
</div>
5051
</div>
5152
</div>

lib/api/authn/webid-oidc.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const bodyParser = require('body-parser').urlencoded({ extended: false })
99
const { LoginByPasswordRequest } = require('../../requests/login-request')
1010

1111
const PasswordResetEmailRequest = require('../../requests/password-reset-email-request')
12+
const PasswordChangeRequest = require('../../requests/password-change-request')
1213

1314
const {
1415
AuthCallbackRequest,
@@ -38,6 +39,9 @@ function middleware (oidc) {
3839
router.get('/account/password/reset', PasswordResetEmailRequest.get)
3940
router.post('/account/password/reset', bodyParser, PasswordResetEmailRequest.post)
4041

42+
router.get('/account/password/change', PasswordChangeRequest.get)
43+
router.post('/account/password/change', bodyParser, PasswordChangeRequest.post)
44+
4145
router.get('/logout', LogoutRequest.handle)
4246

4347
router.get('/goodbye', (req, res) => { res.render('auth/goodbye') })

lib/models/account-manager.js

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const AccountTemplate = require('./account-template')
1111
const debug = require('./../debug').accounts
1212

1313
const DEFAULT_PROFILE_CONTENT_TYPE = 'text/turtle'
14+
const DEFAULT_ADMIN_USERNAME = 'admin'
1415

1516
/**
1617
* Manages account creation (determining whether accounts exist, creating
@@ -192,7 +193,8 @@ class AccountManager {
192193
* @param [accountName] {string}
193194
*
194195
* @throws {Error} via accountUriFor()
195-
* @return {string}
196+
*
197+
* @return {string|null}
196198
*/
197199
accountWebIdFor (accountName) {
198200
let accountUri = this.accountUriFor(accountName)
@@ -343,12 +345,32 @@ class AccountManager {
343345
username: userData.username,
344346
email: userData.email,
345347
name: userData.name,
346-
webId: userData.webid || this.accountWebIdFor(userData.username)
348+
webId: userData.webid || userData.webId ||
349+
this.accountWebIdFor(userData.username)
350+
}
351+
352+
if (userConfig.webId && !userConfig.username) {
353+
userConfig.username = this.usernameFromWebId(userConfig.webId)
354+
}
355+
356+
if (!userConfig.webId && !userConfig.username) {
357+
throw new Error('Username or web id is required')
347358
}
348359

349360
return UserAccount.from(userConfig)
350361
}
351362

363+
usernameFromWebId (webId) {
364+
if (!this.multiUser) {
365+
return DEFAULT_ADMIN_USERNAME
366+
}
367+
368+
let profileUrl = url.parse(webId)
369+
let hostname = profileUrl.hostname
370+
371+
return hostname.split('.')[0]
372+
}
373+
352374
/**
353375
* Creates a user account storage folder (from a default account template).
354376
*
@@ -382,6 +404,27 @@ class AccountManager {
382404
return this.tokenService.generate({ webId: userAccount.webId })
383405
}
384406

407+
/**
408+
* Validates that a token exists and is not expired, and returns the saved
409+
* token contents, or throws an error if invalid.
410+
* Does not consume / clear the token.
411+
*
412+
* @param token {string}
413+
*
414+
* @throws {Error} If missing or invalid token
415+
*
416+
* @return {Object} Saved token data object
417+
*/
418+
validateResetToken (token) {
419+
let tokenValue = this.tokenService.verify(token)
420+
421+
if (!tokenValue) {
422+
throw new Error('Invalid or expired reset token')
423+
}
424+
425+
return tokenValue
426+
}
427+
385428
/**
386429
* Returns a password reset URL (to be emailed to the user upon request)
387430
*
@@ -392,8 +435,11 @@ class AccountManager {
392435
*/
393436
passwordResetUrl (token, returnToUrl) {
394437
let resetUrl = url.resolve(this.host.serverUri,
395-
`/account/password/change?token=${token}` +
396-
`&returnToUrl=${returnToUrl}`)
438+
`/account/password/change?token=${token}`)
439+
440+
if (returnToUrl) {
441+
resetUrl += `&returnToUrl=${returnToUrl}`
442+
}
397443

398444
return resetUrl
399445
}

lib/requests/login-request.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ const url = require('url')
44
const validUrl = require('valid-url')
55

66
const debug = require('./../debug').authentication
7-
const UserAccount = require('../models/user-account')
87

98
/**
109
* Models a Login request, a POST submit from a Login form with a username and
@@ -221,7 +220,7 @@ class LoginByPasswordRequest {
221220

222221
if (validUrl.isUri(this.username)) {
223222
// A WebID URI was entered into the username field
224-
userOptions = { webid: this.username }
223+
userOptions = { webId: this.username }
225224
} else {
226225
// A regular username
227226
userOptions = { username: this.username }
@@ -246,14 +245,14 @@ class LoginByPasswordRequest {
246245
})
247246
.then(validUser => {
248247
if (!validUser) {
249-
error = new Error('User found but no password found')
248+
error = new Error('User found but no password match')
250249
error.statusCode = 400
251250
throw error
252251
}
253252

254253
debug('User found, password matches')
255254

256-
return UserAccount.from(validUser)
255+
return this.accountManager.userAccountFrom(validUser)
257256
})
258257
}
259258

0 commit comments

Comments
 (0)