@@ -9,11 +9,12 @@ import {
99 Patch ,
1010 Post ,
1111 Query ,
12+ Req ,
1213 Res ,
1314} from '@nestjs/common' ;
1415import { ApiParam , ApiTags } from '@nestjs/swagger' ;
1516import { FilterOptions , FilterSchema , SearchFilterOptions , SearchFilterSchema } from '~/_common/restools' ;
16- import { Response } from 'express' ;
17+ import { Request , Response } from 'express' ;
1718import { Types } from 'mongoose' ;
1819import { AbstractController } from '~/_common/abstracts/abstract.controller' ;
1920import { ApiCreateDecorator } from '~/_common/decorators/api-create.decorator' ;
@@ -144,26 +145,79 @@ export class AgentsController extends AbstractController {
144145 return Buffer . from ( `${ normalized } ${ pad } ` , 'base64' ) ;
145146 }
146147
147- private bufferToBase64url ( buf : Uint8Array | Buffer ) : string {
148+ private bufferToBase64url ( buf : Uint8Array | Buffer | string ) : string {
149+ if ( typeof buf === 'string' ) return `${ buf } ` . trim ( ) ;
148150 return Buffer . from ( buf ) . toString ( 'base64' ) . replace ( / \+ / g, '-' ) . replace ( / \/ / g, '_' ) . replace ( / = + $ / g, '' ) ;
149151 }
150152
151- private getWebAuthnExpectedOrigin ( ) : string {
152- const raw = ( process . env [ 'SESAME_WEBAUTHN_ORIGIN' ] || this . configService . get < string > ( 'frontPwd.url' ) || '' ) . trim ( ) ;
153- return raw . replace ( / \/ + $ / , '' ) ;
153+ private normalizeWebAuthnCredentialId ( value : string ) : string {
154+ const credentialId = `${ value || '' } ` . trim ( ) ;
155+ if ( ! credentialId ) return '' ;
156+
157+ try {
158+ const decoded = this . base64urlToBuffer ( credentialId ) . toString ( 'utf8' ) . trim ( ) ;
159+ const looksLikeNestedBase64url = / ^ [ A - Z a - z 0 - 9 _ - ] + $ / . test ( decoded ) && decoded . length >= 16 ;
160+ return looksLikeNestedBase64url ? decoded : credentialId ;
161+ } catch {
162+ return credentialId ;
163+ }
164+ }
165+
166+ private getWebAuthnExpectedOrigin ( req ?: Request ) : string {
167+ const explicitOrigin = ( process . env [ 'SESAME_WEBAUTHN_ORIGIN' ] || '' ) . trim ( ) ;
168+ if ( explicitOrigin ) return explicitOrigin . replace ( / \/ + $ / , '' ) ;
169+
170+ const requestOrigin = this . getRequestOrigin ( req ) ;
171+ if ( requestOrigin ) return requestOrigin ;
172+
173+ const fallbackOrigin = this . getForwardedOrigin ( req ) ;
174+ if ( fallbackOrigin ) return fallbackOrigin ;
175+
176+ const hostOrigin = this . getHostOrigin ( req ) ;
177+ if ( hostOrigin ) return hostOrigin ;
178+
179+ const configuredOrigin = ( this . configService . get < string > ( 'frontPwd.url' ) || '' ) . trim ( ) ;
180+ return configuredOrigin . replace ( / \/ + $ / , '' ) ;
154181 }
155182
156- private getWebAuthnRpId ( ) : string {
183+ private getWebAuthnRpId ( req ?: Request ) : string {
157184 const env = ( process . env [ 'SESAME_WEBAUTHN_RP_ID' ] || '' ) . trim ( ) ;
158185 if ( env ) return env ;
159- const origin = this . getWebAuthnExpectedOrigin ( ) ;
186+ const origin = this . getWebAuthnExpectedOrigin ( req ) ;
160187 try {
161188 return new URL ( origin ) . hostname ;
162189 } catch {
163190 return '' ;
164191 }
165192 }
166193
194+ private getRequestOrigin ( req ?: Request ) : string {
195+ const rawOrigin = `${ req ?. headers ?. origin || '' } ` . trim ( ) ;
196+ if ( ! rawOrigin ) return '' ;
197+ try {
198+ const url = new URL ( rawOrigin ) ;
199+ return `${ url . protocol } //${ url . host } ` ;
200+ } catch {
201+ return '' ;
202+ }
203+ }
204+
205+ private getForwardedOrigin ( req ?: Request ) : string {
206+ const forwardedHost = `${ req ?. headers ?. [ 'x-forwarded-host' ] || '' } ` . split ( ',' ) [ 0 ] ?. trim ( ) ;
207+ if ( ! forwardedHost ) return '' ;
208+
209+ const forwardedProto =
210+ `${ req ?. headers ?. [ 'x-forwarded-proto' ] || req ?. protocol || 'http' } ` . split ( ',' ) [ 0 ] ?. trim ( ) || 'http' ;
211+ return `${ forwardedProto } ://${ forwardedHost } ` . replace ( / \/ + $ / , '' ) ;
212+ }
213+
214+ private getHostOrigin ( req ?: Request ) : string {
215+ const host = `${ req ?. headers ?. host || '' } ` . trim ( ) ;
216+ if ( ! host ) return '' ;
217+ const protocol = req ?. protocol || 'http' ;
218+ return `${ protocol } ://${ host } ` . replace ( / \/ + $ / , '' ) ;
219+ }
220+
167221 private getWebAuthnRpName ( ) : string {
168222 return ( process . env [ 'SESAME_WEBAUTHN_RP_NAME' ] || 'Sesame' ) . trim ( ) || 'Sesame' ;
169223 }
@@ -512,11 +566,12 @@ export class AgentsController extends AbstractController {
512566 public async beginWebAuthnRegisterSelf (
513567 @ReqIdentity ( ) identity : AgentType ,
514568 @Body ( ) body : WebAuthnRegisterBeginDto ,
569+ @Req ( ) req : Request ,
515570 @Res ( ) res : Response ,
516571 ) : Promise < Response > {
517- const rpID = this . getWebAuthnRpId ( ) ;
572+ const rpID = this . getWebAuthnRpId ( req ) ;
518573 const rpName = this . getWebAuthnRpName ( ) ;
519- const expectedOrigin = this . getWebAuthnExpectedOrigin ( ) ;
574+ const expectedOrigin = this . getWebAuthnExpectedOrigin ( req ) ;
520575 if ( ! rpID || ! expectedOrigin ) {
521576 throw new BadRequestException ( 'WebAuthn non configuré (RP ID / origin manquants)' ) ;
522577 }
@@ -526,10 +581,10 @@ export class AgentsController extends AbstractController {
526581 ? ( currentAgent as any ) . security . u2fKey
527582 : [ ] ;
528583 const excludeCredentials = existing
529- . map ( ( k : any ) => `${ k ?. credentialId || '' } ` . trim ( ) )
584+ . map ( ( k : any ) => this . normalizeWebAuthnCredentialId ( `${ k ?. credentialId || '' } ` ) )
530585 . filter ( ( id : string ) => id . length > 0 )
531586 . map ( ( id : string ) => ( {
532- id : this . base64urlToBuffer ( id ) ,
587+ id,
533588 type : 'public-key' as const ,
534589 } ) ) ;
535590
@@ -610,7 +665,7 @@ export class AgentsController extends AbstractController {
610665 : { } ;
611666
612667 const existing = Array . isArray ( ( currentSecurity as any ) ?. u2fKey ) ? ( ( currentSecurity as any ) . u2fKey as any [ ] ) : [ ] ;
613- if ( existing . some ( ( k ) => `${ k ?. credentialId || '' } ` . trim ( ) === credentialIdB64u ) ) {
668+ if ( existing . some ( ( k ) => this . normalizeWebAuthnCredentialId ( `${ k ?. credentialId || '' } ` ) === credentialIdB64u ) ) {
614669 throw new BadRequestException ( 'Cette clé est déjà enregistrée' ) ;
615670 }
616671
@@ -645,6 +700,48 @@ export class AgentsController extends AbstractController {
645700 } ) ;
646701 }
647702
703+ @Delete ( 'me/mfa/webauthn/:credentialId' )
704+ @RequireMfa ( )
705+ public async deleteWebAuthnCredentialSelf (
706+ @ReqIdentity ( ) identity : AgentType ,
707+ @Param ( 'credentialId' ) credentialId : string ,
708+ @Res ( ) res : Response ,
709+ ) : Promise < Response > {
710+ const normalizedCredentialId = `${ credentialId || '' } ` . trim ( ) ;
711+ if ( ! normalizedCredentialId ) {
712+ throw new BadRequestException ( 'Identifiant de clé requis' ) ;
713+ }
714+
715+ const currentAgent = await this . _service . findById < Agents > ( identity . _id as Types . ObjectId ) ;
716+ const currentSecurity =
717+ currentAgent ?. security && typeof currentAgent . security === 'object'
718+ ? typeof ( currentAgent . security as any ) . toObject === 'function'
719+ ? ( currentAgent . security as any ) . toObject ( )
720+ : { ...( currentAgent . security as unknown as Record < string , unknown > ) }
721+ : { } ;
722+
723+ const existing = Array . isArray ( ( currentSecurity as any ) ?. u2fKey ) ? ( ( currentSecurity as any ) . u2fKey as any [ ] ) : [ ] ;
724+ const next = existing . filter ( ( key ) => `${ key ?. credentialId || '' } ` . trim ( ) !== normalizedCredentialId ) ;
725+ if ( next . length === existing . length ) {
726+ throw new BadRequestException ( 'Clé de sécurité introuvable' ) ;
727+ }
728+
729+ const data = await this . _service . update (
730+ identity . _id as Types . ObjectId ,
731+ {
732+ security : {
733+ ...currentSecurity ,
734+ u2fKey : next ,
735+ } ,
736+ } as AgentsUpdateDto ,
737+ ) ;
738+
739+ return res . status ( HttpStatus . OK ) . json ( {
740+ statusCode : HttpStatus . OK ,
741+ data : this . sanitizeAgentPayload ( data , { includeOtpKey : true } ) ,
742+ } ) ;
743+ }
744+
648745 /**
649746 * Met à jour un agent existant
650747 *
0 commit comments