Skip to content

Commit 4055dcb

Browse files
committed
added owasp recommendations to IAM methods
1 parent d387a16 commit 4055dcb

File tree

15 files changed

+68
-45
lines changed

15 files changed

+68
-45
lines changed

src/lib/server/api/controllers/iam.controller.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,14 +90,16 @@ export class IamController implements Controller {
9090
});
9191
return c.json({ status: 'success' });
9292
})
93-
.post('/email/sendVerification', requireAuth, zValidator('json', updateEmailDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
93+
.patch('/email', requireAuth, zValidator('json', updateEmailDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
9494
const json = c.req.valid('json');
95-
await this.emailVerificationsService.dispatchEmailVerificationToken(c.var.user.id, json.email);
95+
await this.emailVerificationsService.dispatchEmailVerificationRequest(c.var.user.id, json.email);
9696
return c.json({ message: 'Verification email sent' });
9797
})
98-
.post('/email/verify', requireAuth, zValidator('json', verifyEmailDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
98+
// this could also be named to use custom methods, aka /email:verify
99+
// https://cloud.google.com/apis/design/custom_methods
100+
.post('/email/verification', requireAuth, zValidator('json', verifyEmailDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
99101
const json = c.req.valid('json');
100-
await this.emailVerificationsService.processEmailVerificationToken(c.var.user.id, json.token);
102+
await this.emailVerificationsService.processEmailVerificationRequest(c.var.user.id, json.token);
101103
return c.json({ message: 'Verified and updated' });
102104
});
103105
}

src/lib/server/api/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { container } from 'tsyringe';
66
import { validateAuthSession, verifyOrigin } from './middleware/auth.middleware';
77
import { IamController } from './controllers/iam.controller';
88
import { config } from './common/config';
9-
import { UsersController } from './controllers/users.controller';
109

1110
/* -------------------------------------------------------------------------- */
1211
/* Client Request */
File renamed without changes.

src/lib/server/api/infrastructure/database/migrations/meta/0000_snapshot.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"id": "2e0c1e11-ed33-45bf-8084-c3200d8f65a8",
2+
"id": "2fdb0575-b4b3-4ebb-9ca0-73a655a7fbe7",
33
"prevId": "00000000-0000-0000-0000-000000000000",
44
"version": "6",
55
"dialect": "postgresql",

src/lib/server/api/infrastructure/database/migrations/meta/_journal.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
{
66
"idx": 0,
77
"version": "6",
8-
"when": 1719436322147,
9-
"tag": "0000_sudden_human_fly",
8+
"when": 1719512747861,
9+
"tag": "0000_nostalgic_skrulls",
1010
"breakpoints": false
1111
}
1212
]
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<html lang='en'>
2+
<head>
3+
<meta http-equiv='X-UA-Compatible' content='IE=edge' />
4+
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
5+
<title>Email Change Request</title>
6+
</head>
7+
<body>
8+
<p class='title'>Email address change notice </p>
9+
<p>
10+
An update to your email address has been requested. If this is unexpected or you did not perform this action, please login and secure your account.</p>
11+
</body>
12+
<style>
13+
.title { font-size: 24px; font-weight: 700; } .token-text { font-size: 24px; font-weight: 700; margin-top: 8px; }
14+
.token-title { font-size: 18px; font-weight: 700; margin-bottom: 0px; }
15+
.center { display: flex; justify-content: center; align-items: center; flex-direction: column;}
16+
.token-subtext { font-size: 12px; margin-top: 0px; }
17+
</style>
18+
</html>

src/lib/server/api/infrastructure/email-templates/email-verification.handlebars renamed to src/lib/server/api/infrastructure/email-templates/email-verification-token.hbs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
Thanks for using example.com. We want to make sure it's really you. Please enter the following
1111
verification code when prompted. If you don't have an exmaple.com an account, you can ignore
1212
this message.</p>
13-
{{!-- <p>{{token}}</p> --}}
1413
<div class='center'>
1514
<p class="token-title">Verification Code</p>
1615
<p class='token-text'>{{token}}</p>
File renamed without changes.

src/lib/server/api/repositories/users.repository.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,20 @@ export type UpdateUser = Partial<CreateUser>;
2626

2727
@injectable()
2828
export class UsersRepository implements Repository {
29-
constructor(@inject(DatabaseProvider) private db: DatabaseProvider) {}
29+
constructor(@inject(DatabaseProvider) private db: DatabaseProvider) { }
3030

3131
async findOneById(id: string) {
3232
return this.db.query.usersTable.findFirst({
3333
where: eq(usersTable.id, id)
3434
});
3535
}
3636

37+
async findOneByIdOrThrow(id: string) {
38+
const user = await this.findOneById(id);
39+
if (!user) throw Error('User not found');
40+
return user;
41+
}
42+
3743
async findOneByEmail(email: string) {
3844
return this.db.query.usersTable.findFirst({
3945
where: eq(usersTable.email, email)

src/lib/server/api/services/email-verifications.service.ts

Lines changed: 15 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,6 @@ import { TokensService } from './tokens.service';
66
import { UsersRepository } from '../repositories/users.repository';
77
import { EmailVerificationsRepository } from '../repositories/email-verifications.repository';
88

9-
/* -------------------------------------------------------------------------- */
10-
/* Service */
11-
/* -------------------------------------------------------------------------- */
12-
/* -------------------------------------------------------------------------- */
13-
/* ---------------------------------- About --------------------------------- */
14-
/*
15-
Services are responsible for handling business logic and data manipulation.
16-
They genreally call on repositories or other services to complete a use-case.
17-
*/
18-
/* ---------------------------------- Notes --------------------------------- */
19-
/*
20-
Services should be kept as clean and simple as possible.
21-
22-
Create private functions to handle complex logic and keep the public methods as
23-
simple as possible. This makes the service easier to read, test and understand.
24-
*/
25-
/* -------------------------------------------------------------------------- */
26-
279
@injectable()
2810
export class EmailVerificationsService {
2911
constructor(
@@ -34,29 +16,36 @@ export class EmailVerificationsService {
3416
@inject(EmailVerificationsRepository) private readonly emailVerificationsRepository: EmailVerificationsRepository,
3517
) { }
3618

37-
38-
async dispatchEmailVerificationToken(userId: string, requestedEmail: string) {
19+
// These steps follow the process outlined in OWASP's "Changing A User's Email Address" guide.
20+
// https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html#changing-a-users-registered-email-address
21+
async dispatchEmailVerificationRequest(userId: string, requestedEmail: string) {
3922
// generate a token and expiry
4023
const { token, expiry, hashedToken } = await this.tokensService.generateTokenWithExpiryAndHash(15, 'm')
24+
const user = await this.usersRepository.findOneByIdOrThrow(userId)
4125

4226
// create a new email verification record
4327
await this.emailVerificationsRepository.create({ requestedEmail, userId, hashedToken, expiresAt: expiry })
4428

45-
// send the verification email - we don't need to await success and will opt for good-faith since we
46-
// will offer a way to resend the email if it fails
47-
this.mailerService.sendEmailVerification({
29+
// A confirmation-required email message to the proposed new address, instructing the user to
30+
// confirm the change and providing a link for unexpected situations
31+
this.mailerService.sendEmailVerificationToken({
4832
to: requestedEmail,
4933
props: {
5034
token
5135
}
5236
})
37+
38+
// A notification-only email message to the current address, alerting the user to the impending change and
39+
// providing a link for an unexpected situation.
40+
this.mailerService.sendEmailChangeNotification({
41+
to: user.email,
42+
props: null
43+
})
5344
}
5445

55-
async processEmailVerificationToken(userId: string, token: string) {
46+
async processEmailVerificationRequest(userId: string, token: string) {
5647
const validRecord = await this.findAndBurnEmailVerificationToken(userId, token)
5748
if (!validRecord) throw BadRequest('Invalid token');
58-
59-
// burn the token and update the user
6049
await this.usersRepository.update(userId, { email: validRecord.requestedEmail, verified: true });
6150
}
6251

0 commit comments

Comments
 (0)