Add Sign in with passkey Button (#14577)
* Sign in with passkey (PoC) * 💄 Added "Login with Passkey" Button * refactor: Improve error response when WebAuthn challenge fails * signinResponse should be placed under the SigninWithPasskeyResponse object. * Frontend fix * Fix: Rate limiting key for passkey signin Use specific rate limiting key: 'signin-with-passkey' for passkey sign-in API to avoid collisions with signin rate-limit. * Refactor: enhance Passkey sign-in flow and error handling - Increased the rate limit for Passkey sign-in attempts to accommodate the two API calls needed per sign-in. - Improved error messages and handling in both the `WebAuthnService` and the `SigninWithPasskeyApiService`, providing more context and better usability. - Updated error messages to provide more specific and helpful details to the user. These changes aim to enhance the Passkey sign-in experience by providing more robust error handling, improving security by limiting API calls, and delivering a more user-friendly interface. * Refactor: Streamline 2FA flow and remove redundant Passkey button. - Separate the flow of 1FA and 2FA. - Remove duplicate passkey buttons * Fix: Add error messages to MkSignin * chore: Hide passkey button if the entered user does not use passkey login * Update CHANGELOG.md * Refactor: Rename functions and Add comments * Update locales/ja-JP.yml Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com> * Fix: Update translation - update index.d.ts - update ko-KR.yml, en-US.yml - Fix: Reflect Changed i18n key on MkSignin --------- Co-authored-by: Squarecat-meow <kw7551@gmail.com> Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
This commit is contained in:
@@ -164,6 +164,86 @@ export class WebAuthnService {
|
||||
return authenticationOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate Passkey Auth (Without specifying user)
|
||||
* @returns authenticationOptions
|
||||
*/
|
||||
@bindThis
|
||||
public async initiateSignInWithPasskeyAuthentication(context: string): Promise<PublicKeyCredentialRequestOptionsJSON> {
|
||||
const relyingParty = await this.getRelyingParty();
|
||||
|
||||
const authenticationOptions = await generateAuthenticationOptions({
|
||||
rpID: relyingParty.rpId,
|
||||
userVerification: 'preferred',
|
||||
});
|
||||
|
||||
await this.redisClient.setex(`webauthn:challenge:${context}`, 90, authenticationOptions.challenge);
|
||||
|
||||
return authenticationOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify Webauthn AuthenticationCredential
|
||||
* @throws IdentifiableError
|
||||
* @returns If the challenge is successful, return the user ID. Otherwise, return null.
|
||||
*/
|
||||
@bindThis
|
||||
public async verifySignInWithPasskeyAuthentication(context: string, response: AuthenticationResponseJSON): Promise<MiUser['id'] | null> {
|
||||
const challenge = await this.redisClient.get(`webauthn:challenge:${context}`);
|
||||
|
||||
if (!challenge) {
|
||||
throw new IdentifiableError('2d16e51c-007b-4edd-afd2-f7dd02c947f6', `challenge '${context}' not found`);
|
||||
}
|
||||
|
||||
await this.redisClient.del(`webauthn:challenge:${context}`);
|
||||
|
||||
const key = await this.userSecurityKeysRepository.findOneBy({
|
||||
id: response.id,
|
||||
});
|
||||
|
||||
if (!key) {
|
||||
throw new IdentifiableError('36b96a7d-b547-412d-aeed-2d611cdc8cdc', 'Unknown Webauthn key');
|
||||
}
|
||||
|
||||
const relyingParty = await this.getRelyingParty();
|
||||
|
||||
let verification;
|
||||
try {
|
||||
verification = await verifyAuthenticationResponse({
|
||||
response: response,
|
||||
expectedChallenge: challenge,
|
||||
expectedOrigin: relyingParty.origin,
|
||||
expectedRPID: relyingParty.rpId,
|
||||
authenticator: {
|
||||
credentialID: key.id,
|
||||
credentialPublicKey: Buffer.from(key.publicKey, 'base64url'),
|
||||
counter: key.counter,
|
||||
transports: key.transports ? key.transports as AuthenticatorTransportFuture[] : undefined,
|
||||
},
|
||||
requireUserVerification: true,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new IdentifiableError('b18c89a7-5b5e-4cec-bb5b-0419f332d430', `verification failed: ${error}`);
|
||||
}
|
||||
|
||||
const { verified, authenticationInfo } = verification;
|
||||
|
||||
if (!verified) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await this.userSecurityKeysRepository.update({
|
||||
id: response.id,
|
||||
}, {
|
||||
lastUsed: new Date(),
|
||||
counter: authenticationInfo.newCounter,
|
||||
credentialDeviceType: authenticationInfo.credentialDeviceType,
|
||||
credentialBackedUp: authenticationInfo.credentialBackedUp,
|
||||
});
|
||||
|
||||
return key.userId;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async verifyAuthentication(userId: MiUser['id'], response: AuthenticationResponseJSON): Promise<boolean> {
|
||||
const challenge = await this.redisClient.get(`webauthn:challenge:${userId}`);
|
||||
|
Reference in New Issue
Block a user