@@ -18,8 +18,8 @@ import {
18
18
import { ResourcesAPIBase , ServiceBase , FilterValueType } from '@restorecommerce/resource-base-interface' ;
19
19
import { Logger } from 'winston' ;
20
20
import {
21
- ACSAuthZ ,
22
- AuthZAction ,
21
+ ACSAuthZ , authZ ,
22
+ AuthZAction , cfg ,
23
23
DecisionResponse ,
24
24
HierarchicalScope ,
25
25
Operation ,
@@ -63,6 +63,9 @@ import {
63
63
ExchangeTOTPRequest ,
64
64
SetupTOTPRequest ,
65
65
SetupTOTPResponse ,
66
+ CreateBackupTOTPCodesRequest ,
67
+ CreateBackupTOTPCodesResponse ,
68
+ ResetTOTPRequest ,
66
69
} from '@restorecommerce/rc-grpc-clients/dist/generated-server/io/restorecommerce/user.js' ;
67
70
import {
68
71
Role ,
@@ -91,35 +94,37 @@ import {
91
94
Subject ,
92
95
Tokens
93
96
} from '@restorecommerce/rc-grpc-clients/dist/generated-server/io/restorecommerce/auth.js' ;
94
- import { zxcvbnOptions , zxcvbnAsync , ZxcvbnResult } from '@zxcvbn-ts/core' ;
97
+ import { zxcvbnOptions , zxcvbnAsync , ZxcvbnResult , Matcher , Match , MatchEstimated , MatchExtended } from '@zxcvbn-ts/core' ;
95
98
import * as zxcvbnCommonPackage from '@zxcvbn-ts/language-common' ;
96
99
import * as zxcvbnEnPackage from '@zxcvbn-ts/language-en' ;
97
100
import * as zxcvbnDePackage from '@zxcvbn-ts/language-de' ;
98
101
import { matcherPwnedFactory } from '@zxcvbn-ts/matcher-pwned' ;
99
- import {
100
- MatchEstimated ,
101
- MatchExtended ,
102
- Matcher ,
103
- Match ,
104
- } from '@zxcvbn-ts/core/dist/types.js' ;
105
102
import fetch from 'node-fetch' ;
106
103
107
104
import { authenticator , totp } from 'otplib' ;
108
105
import * as jose from 'jose' ;
106
+ import crypto from 'node:crypto' ;
109
107
110
108
export const DELETE_USERS_WITH_EXPIRED_ACTIVATION = 'delete-users-with-expired-activation-job' ;
111
109
112
110
export class UserService extends ServiceBase < UserListResponse , UserList > implements UserServiceImplementation {
113
111
db : Arango ;
114
112
topics : any ;
115
113
cfg : any ;
116
- registrationSubjectTpl : string ;
117
- changePWEmailSubjectTpl : string ;
114
+
118
115
layoutTpl : string ;
116
+ registrationSubjectTpl : string ;
119
117
registrationBodyTpl : string ;
118
+
119
+ changePWEmailSubjectTpl : string ;
120
120
changePWEmailBodyTpl : string ;
121
+
121
122
invitationSubjectTpl : string ;
122
123
invitationBodyTpl : string ;
124
+
125
+ resetTotpSubjectTpl : string ;
126
+ resetTotpBodyTpl : string ;
127
+
123
128
emailEnabled : boolean ;
124
129
emailStyle : string ;
125
130
roleService : RoleService ;
@@ -762,7 +767,7 @@ export class UserService extends ServiceBase<UserListResponse, UserList> impleme
762
767
// the user role associations if not skip validation
763
768
let acsResponse : DecisionResponse | PolicySetRQResponse ;
764
769
try {
765
- const ctx = { subject, resources : [ ] } ;
770
+ const ctx = { subject, resources : [ ] as any [ ] } ;
766
771
acsResponse = await checkAccessRequest ( ctx , [ { resource : 'user' } ] , AuthZAction . MODIFY , Operation . whatIsAllowed ) ;
767
772
} catch ( err : any ) {
768
773
this . logger . error ( 'Error making whatIsAllowedACS request for verifying role associations' , { code : err . code , message : err . message , stack : err . stack } ) ;
@@ -2659,6 +2664,12 @@ export class UserService extends ServiceBase<UserListResponse, UserList> impleme
2659
2664
response = await fetch ( hbsTemplates . layoutTpl , { headers } ) ;
2660
2665
this . layoutTpl = await response . text ( ) ;
2661
2666
2667
+ response = await fetch ( hbsTemplates . resetTotpSubjectTpl , { headers } ) ;
2668
+ this . resetTotpSubjectTpl = await response . text ( ) ;
2669
+
2670
+ response = await fetch ( hbsTemplates . resetTotpBodyTpl , { headers } ) ;
2671
+ this . resetTotpBodyTpl = await response . text ( ) ;
2672
+
2662
2673
response = await fetch ( hbsTemplates . resourcesTpl , { headers } ) ;
2663
2674
if ( response . status == 200 ) {
2664
2675
const externalRrc = JSON . parse ( await response . text ( ) ) ;
@@ -3225,7 +3236,7 @@ export class UserService extends ServiceBase<UserListResponse, UserList> impleme
3225
3236
this . logger . debug ( 'user does not exist' , { identifier : subject . id } ) ;
3226
3237
return returnStatus ( 404 , 'user does not exist' ) ;
3227
3238
} else if ( users . total_count > 1 ) {
3228
- return returnStatus ( 400 , `Invalid identifier provided for totp setup , multiple users found for identifier ${ subject . id } ` ) ;
3239
+ return returnStatus ( 400 , `Invalid identifier provided for totp exchange , multiple users found for identifier ${ subject . id } ` ) ;
3229
3240
}
3230
3241
3231
3242
const user = users . items [ 0 ] . payload ;
@@ -3241,11 +3252,22 @@ export class UserService extends ServiceBase<UserListResponse, UserList> impleme
3241
3252
return returnStatus ( 400 , 'Invalid TOTP session token' ) ;
3242
3253
}
3243
3254
3244
- if ( ! totp . check ( request . code , user . totp_secret_processing ) ) {
3245
- return returnStatus ( 400 , 'Invalid TOTP code' ) ;
3255
+ if ( totp . check ( request . code , user . totp_secret_processing ) ) {
3256
+ return { payload : user , status : { code : 200 , message : 'success' } } ;
3246
3257
}
3247
3258
3248
- return { payload : user , status : { code : 200 , message : 'success' } } ;
3259
+ const backupCode = user . totp_recovery_codes . indexOf ( request . code ) ;
3260
+ if ( backupCode >= 0 ) {
3261
+ user . totp_recovery_codes . splice ( backupCode , 1 ) ;
3262
+
3263
+ const updateStatus = await super . update ( UserList . fromPartial ( {
3264
+ items : [ user ]
3265
+ } ) , context ) ;
3266
+
3267
+ return { payload : updateStatus . items [ 0 ] . payload , status : { code : 200 , message : 'success' } } ;
3268
+ }
3269
+
3270
+ return returnStatus ( 400 , 'Invalid TOTP code' ) ;
3249
3271
}
3250
3272
3251
3273
async getUnauthenticatedSubjectTokenForTenant ( request : TenantRequest , context : any ) : Promise < DeepPartial < TenantResponse > > {
@@ -3293,6 +3315,111 @@ export class UserService extends ServiceBase<UserListResponse, UserList> impleme
3293
3315
token : token ?. token
3294
3316
} ) ) ;
3295
3317
}
3318
+
3319
+ async createBackupTOTPCodes ( request : CreateBackupTOTPCodesRequest , context : any ) : Promise < DeepPartial < CreateBackupTOTPCodesResponse > > {
3320
+ const subject = request . subject ;
3321
+ const users = await super . read ( ReadRequest . fromPartial ( {
3322
+ filters : [ {
3323
+ filters : [ {
3324
+ field : 'id' ,
3325
+ operation : Filter_Operation . eq ,
3326
+ value : subject ?. id
3327
+ } ]
3328
+ } ]
3329
+ } ) , context ) ;
3330
+
3331
+ if ( ! users || users . total_count === 0 ) {
3332
+ this . logger . debug ( 'user does not exist' , { identifier : subject . id } ) ;
3333
+ return returnOperationStatus ( 404 , 'user does not exist' ) ;
3334
+ } else if ( users . total_count > 1 ) {
3335
+ return returnOperationStatus ( 400 , `Invalid identifier provided for backup totp code setup, multiple users found for identifier ${ subject . id } ` ) ;
3336
+ }
3337
+
3338
+ const recovery_code_count = 12 ;
3339
+ const totp_recovery_codes : string [ ] = [ ] ;
3340
+ for ( let i = 0 ; i < recovery_code_count ; i ++ ) {
3341
+ totp_recovery_codes [ i ] = crypto . randomBytes ( 16 ) . toString ( 'base64url' )
3342
+ }
3343
+
3344
+ const user = users . items [ 0 ] . payload ;
3345
+ let acsResponse : DecisionResponse ;
3346
+ try {
3347
+ acsResponse = await checkAccessRequest ( {
3348
+ ...context ,
3349
+ subject,
3350
+ resources : { id : user . id , totp_recovery_codes, meta : user . meta }
3351
+ } , [ { resource : 'user' , id : user . id , property : [ 'totp_recovery_codes' ] } ] , AuthZAction . MODIFY , Operation . isAllowed ) ;
3352
+ } catch ( err : any ) {
3353
+ this . logger . error ( 'Error occurred requesting access-control-srv for createBackupTOTPCodes' , { code : err . code , message : err . message , stack : err . stack } ) ;
3354
+ return returnOperationStatus ( err . code , err . message ) ;
3355
+ }
3356
+
3357
+ if ( acsResponse . decision != Response_Decision . PERMIT ) {
3358
+ return { operation_status : acsResponse . operation_status } ;
3359
+ }
3360
+
3361
+ user . totp_recovery_codes = totp_recovery_codes ;
3362
+
3363
+ const updateStatus = await super . update ( UserList . fromPartial ( {
3364
+ items : [ user ]
3365
+ } ) , context ) ;
3366
+ return {
3367
+ backup_codes : totp_recovery_codes ,
3368
+ operation_status : updateStatus ?. items [ 0 ] ?. status
3369
+ } ;
3370
+ }
3371
+
3372
+ async resetTOTP ( request : ResetTOTPRequest , context : any ) : Promise < DeepPartial < OperationStatusObj > > {
3373
+ const subject = request . subject ;
3374
+ const users = await super . read ( ReadRequest . fromPartial ( {
3375
+ filters : [ {
3376
+ filters : [ {
3377
+ field : 'id' ,
3378
+ operation : Filter_Operation . eq ,
3379
+ value : subject ?. id
3380
+ } ]
3381
+ } ]
3382
+ } ) , context ) ;
3383
+
3384
+ if ( ! users || users . total_count === 0 ) {
3385
+ this . logger . debug ( 'user does not exist' , { identifier : subject . id } ) ;
3386
+ return returnOperationStatus ( 404 , 'user does not exist' ) ;
3387
+ } else if ( users . total_count > 1 ) {
3388
+ return returnOperationStatus ( 400 , `Invalid identifier provided for backup totp code setup, multiple users found for identifier ${ subject . id } ` ) ;
3389
+ }
3390
+
3391
+ const totpCode = crypto . randomBytes ( 16 ) . toString ( 'base64url' ) ;
3392
+
3393
+ const user = users . items [ 0 ] . payload ;
3394
+ user . totp_recovery_codes . push ( totpCode )
3395
+
3396
+ const updateStatus = await super . update ( UserList . fromPartial ( {
3397
+ items : [ user ]
3398
+ } ) , context ) ;
3399
+
3400
+ if ( this . emailEnabled ) {
3401
+ await this . fetchHbsTemplates ( ) ;
3402
+ const renderRequest = this . makeTOTPResetData ( user , totpCode ) ;
3403
+ await this . topics . rendering . emit ( 'renderRequest' , renderRequest ) ;
3404
+ }
3405
+
3406
+ return {
3407
+ operation_status : updateStatus ?. items [ 0 ] ?. status
3408
+ } ;
3409
+ }
3410
+
3411
+ private makeTOTPResetData ( user : DeepPartial < User > , totpCode : string ) : any {
3412
+ const emailBody = this . resetTotpBodyTpl ;
3413
+ const emailSubject = this . resetTotpSubjectTpl ;
3414
+
3415
+ const dataBody = {
3416
+ firstName : user . first_name ,
3417
+ lastName : user . last_name ,
3418
+ totpCode,
3419
+ } ;
3420
+ return this . makeRenderRequestMsg ( user , emailSubject , emailBody ,
3421
+ dataBody , { } , user . email ) ;
3422
+ }
3296
3423
}
3297
3424
3298
3425
export class RoleService extends ServiceBase < RoleListResponse , RoleList > implements RoleServiceImplementation {
@@ -3316,7 +3443,7 @@ export class RoleService extends ServiceBase<RoleListResponse, RoleList> impleme
3316
3443
}
3317
3444
}
3318
3445
}
3319
- super ( 'role' , roleTopic , logger , new ResourcesAPIBase ( db , 'roles' , resourceFieldConfig ) , isEventsEnabled ) ;
3446
+ super ( 'role' , roleTopic as any , logger , new ResourcesAPIBase ( db , 'roles' , resourceFieldConfig ) , isEventsEnabled ) ;
3320
3447
const redisConfig = cfg . get ( 'redis' ) ;
3321
3448
redisConfig . database = cfg . get ( 'redis:db-indexes:db-subject' ) ;
3322
3449
this . redisClient = createClient ( redisConfig ) ;
0 commit comments