Browse Source

Bugfix/biometric authentication issue related to matching passkeys (#5137)

* Fix for biometric authentication issue related to matching passkeys

* Update changelog
pull/5153/head
Attila Cseh 2 weeks ago
committed by GitHub
parent
commit
c98bd0ed52
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      CHANGELOG.md
  2. 14
      apps/api/src/app/auth/auth.controller.ts
  3. 59
      apps/api/src/app/auth/web-auth.service.ts
  4. 4
      apps/client/src/app/services/web-authn.service.ts

4
CHANGELOG.md

@ -15,6 +15,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Improved the language localization for Spanish (`es`) - Improved the language localization for Spanish (`es`)
- Improved the language localization for Turkish (`tr`) - Improved the language localization for Turkish (`tr`)
### Fixed
- Fixed an issue in the biometric authentication related to matching passkeys
## 2.180.0 - 2025-07-08 ## 2.180.0 - 2025-07-08
### Added ### Added

14
apps/api/src/app/auth/auth.controller.ts

@ -133,17 +133,19 @@ export class AuthController {
return this.webAuthService.verifyAttestation(body.credential); return this.webAuthService.verifyAttestation(body.credential);
} }
@Post('webauthn/generate-assertion-options') @Post('webauthn/generate-authentication-options')
public async generateAssertionOptions(@Body() body: { deviceId: string }) { public async generateAuthenticationOptions(
return this.webAuthService.generateAssertionOptions(body.deviceId); @Body() body: { deviceId: string }
) {
return this.webAuthService.generateAuthenticationOptions(body.deviceId);
} }
@Post('webauthn/verify-assertion') @Post('webauthn/verify-authentication')
public async verifyAssertion( public async verifyAuthentication(
@Body() body: { deviceId: string; credential: AssertionCredentialJSON } @Body() body: { deviceId: string; credential: AssertionCredentialJSON }
) { ) {
try { try {
const authToken = await this.webAuthService.verifyAssertion( const authToken = await this.webAuthService.verifyAuthentication(
body.deviceId, body.deviceId,
body.credential body.credential
); );

59
apps/api/src/app/auth/web-auth.service.ts

@ -25,6 +25,7 @@ import {
VerifyRegistrationResponseOpts VerifyRegistrationResponseOpts
} from '@simplewebauthn/server'; } from '@simplewebauthn/server';
import { isoBase64URL, isoUint8Array } from '@simplewebauthn/server/helpers'; import { isoBase64URL, isoUint8Array } from '@simplewebauthn/server/helpers';
import ms from 'ms';
import { import {
AssertionCredentialJSON, AssertionCredentialJSON,
@ -41,42 +42,42 @@ export class WebAuthService {
@Inject(REQUEST) private readonly request: RequestWithUser @Inject(REQUEST) private readonly request: RequestWithUser
) {} ) {}
get rpID() { private get expectedOrigin() {
return new URL(this.configurationService.get('ROOT_URL')).hostname; return this.configurationService.get('ROOT_URL');
} }
get expectedOrigin() { private get rpID() {
return this.configurationService.get('ROOT_URL'); return new URL(this.configurationService.get('ROOT_URL')).hostname;
} }
public async generateRegistrationOptions() { public async generateRegistrationOptions() {
const user = this.request.user; const user = this.request.user;
const opts: GenerateRegistrationOptionsOpts = { const opts: GenerateRegistrationOptionsOpts = {
rpName: 'Ghostfolio',
rpID: this.rpID,
userID: isoUint8Array.fromUTF8String(user.id),
userName: '',
timeout: 60000,
authenticatorSelection: { authenticatorSelection: {
authenticatorAttachment: 'platform', authenticatorAttachment: 'platform',
requireResidentKey: false, residentKey: 'required',
userVerification: 'required' userVerification: 'preferred'
} },
rpID: this.rpID,
rpName: 'Ghostfolio',
timeout: ms('60 seconds'),
userID: isoUint8Array.fromUTF8String(user.id),
userName: ''
}; };
const options = await generateRegistrationOptions(opts); const registrationOptions = await generateRegistrationOptions(opts);
await this.userService.updateUser({ await this.userService.updateUser({
data: { data: {
authChallenge: options.challenge authChallenge: registrationOptions.challenge
}, },
where: { where: {
id: user.id id: user.id
} }
}); });
return options; return registrationOptions;
} }
public async verifyAttestation( public async verifyAttestation(
@ -84,13 +85,14 @@ export class WebAuthService {
): Promise<AuthDeviceDto> { ): Promise<AuthDeviceDto> {
const user = this.request.user; const user = this.request.user;
const expectedChallenge = user.authChallenge; const expectedChallenge = user.authChallenge;
let verification: VerifiedRegistrationResponse; let verification: VerifiedRegistrationResponse;
try { try {
const opts: VerifyRegistrationResponseOpts = { const opts: VerifyRegistrationResponseOpts = {
expectedChallenge, expectedChallenge,
expectedOrigin: this.expectedOrigin, expectedOrigin: this.expectedOrigin,
expectedRPID: this.rpID, expectedRPID: this.rpID,
requireUserVerification: false,
response: { response: {
clientExtensionResults: credential.clientExtensionResults, clientExtensionResults: credential.clientExtensionResults,
id: credential.id, id: credential.id,
@ -99,6 +101,7 @@ export class WebAuthService {
type: 'public-key' type: 'public-key'
} }
}; };
verification = await verifyRegistrationResponse(opts); verification = await verifyRegistrationResponse(opts);
} catch (error) { } catch (error) {
Logger.error(error, 'WebAuthService'); Logger.error(error, 'WebAuthService');
@ -144,7 +147,7 @@ export class WebAuthService {
throw new InternalServerErrorException('An unknown error occurred'); throw new InternalServerErrorException('An unknown error occurred');
} }
public async generateAssertionOptions(deviceId: string) { public async generateAuthenticationOptions(deviceId: string) {
const device = await this.deviceService.authDevice({ id: deviceId }); const device = await this.deviceService.authDevice({ id: deviceId });
if (!device) { if (!device) {
@ -152,32 +155,27 @@ export class WebAuthService {
} }
const opts: GenerateAuthenticationOptionsOpts = { const opts: GenerateAuthenticationOptionsOpts = {
allowCredentials: [ allowCredentials: [],
{
id: isoBase64URL.fromBuffer(device.credentialId),
transports: ['internal']
}
],
rpID: this.rpID, rpID: this.rpID,
timeout: 60000, timeout: ms('60 seconds'),
userVerification: 'preferred' userVerification: 'preferred'
}; };
const options = await generateAuthenticationOptions(opts); const authenticationOptions = await generateAuthenticationOptions(opts);
await this.userService.updateUser({ await this.userService.updateUser({
data: { data: {
authChallenge: options.challenge authChallenge: authenticationOptions.challenge
}, },
where: { where: {
id: device.userId id: device.userId
} }
}); });
return options; return authenticationOptions;
} }
public async verifyAssertion( public async verifyAuthentication(
deviceId: string, deviceId: string,
credential: AssertionCredentialJSON credential: AssertionCredentialJSON
) { ) {
@ -190,6 +188,7 @@ export class WebAuthService {
const user = await this.userService.user({ id: device.userId }); const user = await this.userService.user({ id: device.userId });
let verification: VerifiedAuthenticationResponse; let verification: VerifiedAuthenticationResponse;
try { try {
const opts: VerifyAuthenticationResponseOpts = { const opts: VerifyAuthenticationResponseOpts = {
credential: { credential: {
@ -200,6 +199,7 @@ export class WebAuthService {
expectedChallenge: `${user.authChallenge}`, expectedChallenge: `${user.authChallenge}`,
expectedOrigin: this.expectedOrigin, expectedOrigin: this.expectedOrigin,
expectedRPID: this.rpID, expectedRPID: this.rpID,
requireUserVerification: false,
response: { response: {
clientExtensionResults: credential.clientExtensionResults, clientExtensionResults: credential.clientExtensionResults,
id: credential.id, id: credential.id,
@ -208,13 +208,14 @@ export class WebAuthService {
type: 'public-key' type: 'public-key'
} }
}; };
verification = await verifyAuthenticationResponse(opts); verification = await verifyAuthenticationResponse(opts);
} catch (error) { } catch (error) {
Logger.error(error, 'WebAuthService'); Logger.error(error, 'WebAuthService');
throw new InternalServerErrorException({ error: error.message }); throw new InternalServerErrorException({ error: error.message });
} }
const { verified, authenticationInfo } = verification; const { authenticationInfo, verified } = verification;
if (verified) { if (verified) {
device.counter = authenticationInfo.newCounter; device.counter = authenticationInfo.newCounter;

4
apps/client/src/app/services/web-authn.service.ts

@ -85,7 +85,7 @@ export class WebAuthnService {
return this.http return this.http
.post<PublicKeyCredentialRequestOptionsJSON>( .post<PublicKeyCredentialRequestOptionsJSON>(
`/api/v1/auth/webauthn/generate-assertion-options`, '/api/v1/auth/webauthn/generate-authentication-options',
{ deviceId } { deviceId }
) )
.pipe( .pipe(
@ -94,7 +94,7 @@ export class WebAuthnService {
}), }),
switchMap((credential) => { switchMap((credential) => {
return this.http.post<{ authToken: string }>( return this.http.post<{ authToken: string }>(
`/api/v1/auth/webauthn/verify-assertion`, '/api/v1/auth/webauthn/verify-authentication',
{ {
credential, credential,
deviceId deviceId

Loading…
Cancel
Save