@@ -10,6 +10,8 @@ import { LoginVerificationEmail } from '../emails/login-verification.email';
10
10
import { DatabaseProvider } from '../providers/database.provider' ;
11
11
import { BadRequest } from '../common/exceptions' ;
12
12
import { WelcomeEmail } from '../emails/welcome.email' ;
13
+ import { EmailVerificationsRepository } from '../repositories/email-verifications.repository' ;
14
+ import { EmailChangeNoticeEmail } from '../emails/email-change-notice.email' ;
13
15
14
16
@injectable ( )
15
17
export class IamService {
@@ -20,7 +22,7 @@ export class IamService {
20
22
@inject ( MailerService ) private readonly mailerService : MailerService ,
21
23
@inject ( UsersRepository ) private readonly usersRepository : UsersRepository ,
22
24
@inject ( LoginRequestsRepository ) private readonly loginRequestsRepository : LoginRequestsRepository ,
23
-
25
+ @ inject ( EmailVerificationsRepository ) private readonly emailVerificationsRepository : EmailVerificationsRepository ,
24
26
) { }
25
27
26
28
async createLoginRequest ( data : RegisterEmailDto ) {
@@ -46,6 +48,41 @@ export class IamService {
46
48
return this . lucia . createSession ( existingUser . id , { } ) ;
47
49
}
48
50
51
+ // These steps follow the process outlined in OWASP's "Changing A User's Email Address" guide.
52
+ // https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html#changing-a-users-registered-email-address
53
+ async dispatchEmailVerificationRequest ( userId : string , requestedEmail : string ) {
54
+ // generate a token and expiry
55
+ const { token, expiry, hashedToken } = await this . tokensService . generateTokenWithExpiryAndHash ( 15 , 'm' )
56
+ const user = await this . usersRepository . findOneByIdOrThrow ( userId )
57
+
58
+ // create a new email verification record
59
+ await this . emailVerificationsRepository . create ( { requestedEmail, userId, hashedToken, expiresAt : expiry } )
60
+
61
+ // A confirmation-required email message to the proposed new address, instructing the user to
62
+ // confirm the change and providing a link for unexpected situations
63
+ this . mailerService . send ( {
64
+ to : requestedEmail ,
65
+ email : new LoginVerificationEmail ( token )
66
+ } )
67
+
68
+ // A notification-only email message to the current address, alerting the user to the impending change and
69
+ // providing a link for an unexpected situation.
70
+ this . mailerService . send ( {
71
+ to : user . email ,
72
+ email : new EmailChangeNoticeEmail ( )
73
+ } )
74
+ }
75
+
76
+ async processEmailVerificationRequest ( userId : string , token : string ) {
77
+ const validRecord = await this . findAndBurnEmailVerificationToken ( userId , token )
78
+ if ( ! validRecord ) throw BadRequest ( 'Invalid token' ) ;
79
+ await this . usersRepository . update ( userId , { email : validRecord . requestedEmail , verified : true } ) ;
80
+ }
81
+
82
+ async logout ( sessionId : string ) {
83
+ return this . lucia . invalidateSession ( sessionId ) ;
84
+ }
85
+
49
86
// Create a new user and send a welcome email - or other onboarding process
50
87
private async handleNewUserRegistration ( email : string ) {
51
88
const newUser = await this . usersRepository . create ( { email, verified : true } )
@@ -71,7 +108,21 @@ export class IamService {
71
108
} )
72
109
}
73
110
74
- async logout ( sessionId : string ) {
75
- return this . lucia . invalidateSession ( sessionId ) ;
111
+ private async findAndBurnEmailVerificationToken ( userId : string , token : string ) {
112
+ return this . db . transaction ( async ( trx ) => {
113
+ // find a valid record
114
+ const emailVerificationRecord = await this . emailVerificationsRepository . trxHost ( trx ) . findValidRecord ( userId ) ;
115
+ if ( ! emailVerificationRecord ) return null ;
116
+
117
+ // check if the token is valid
118
+ const isValidRecord = await this . tokensService . verifyHashedToken ( emailVerificationRecord . hashedToken , token ) ;
119
+ if ( ! isValidRecord ) return null
120
+
121
+ // burn the token if it is valid
122
+ await this . emailVerificationsRepository . trxHost ( trx ) . deleteById ( emailVerificationRecord . id )
123
+ return emailVerificationRecord
124
+ } )
76
125
}
126
+
127
+
77
128
}
0 commit comments