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 Turkish (`tr`)
### Fixed
- Fixed an issue in the biometric authentication related to matching passkeys
## 2.180.0 - 2025-07-08
### Added

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

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

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

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

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

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

Loading…
Cancel
Save