Conversation
There was a problem hiding this comment.
Pull request overview
This PR implements passkey authentication (WebAuthn) for the application, allowing users to sign in using biometric authentication, security keys, or platform authenticators instead of traditional passwords or email codes.
Changes:
- Adds a complete WebAuthn library implementation in
lib/action_pack/web_authn/including CBOR decoding, COSE key handling, and authenticator response validation - Implements
Identity::Credentialmodel for storing and managing user credentials with passkey registration and authentication methods - Adds UI for managing passkeys including registration, editing, and deletion of credentials
Reviewed changes
Copilot reviewed 51 out of 73 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| lib/action_pack/web_authn/*.rb | Core WebAuthn library implementing CBOR decoding, COSE key parsing, and authenticator response validation |
| app/models/identity/credential.rb | Model for managing WebAuthn credentials with registration and authentication logic |
| app/models/identity/credential/authenticator.rb | Registry for mapping AAGUIDs to authenticator metadata (icons, names) |
| app/controllers/users/credentials_controller.rb | Controller for credential management (CRUD operations) |
| app/controllers/sessions/passkeys_controller.rb | Controller for passkey-based authentication |
| app/javascript/controllers/*.js | Stimulus controllers for credential registration and conditional mediation |
| app/views/users/credentials/*.html.erb | Views for credential management UI |
| db/migrate/*.rb | Migrations for identity_credentials table |
| config/passkey_aaguids.yml | Configuration mapping AAGUIDs to authenticator details |
| test/lib/action_pack/web_authn/*.rb | Comprehensive test coverage for WebAuthn library |
| test/controllers/sessions/passkeys_controller_test.rb | Integration tests for passkey authentication |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # | ||
| # # Look up the credential by ID | ||
| # credential = user.credentials.find_by!( | ||
| # credentail_id: params[:id] |
There was a problem hiding this comment.
Typo in comment: "credentail_id" should be "credential_id".
| class Identity::Credential < ApplicationRecord | ||
| belongs_to :identity | ||
|
|
||
| serialize :transports, coder: JSON, type: Array, default: [] | ||
|
|
||
| class << self | ||
| def creation_options(identity:, display_name:) | ||
| ActionPack::WebAuthn::PublicKeyCredential::CreationOptions.new( | ||
| id: identity.id, | ||
| name: identity.email_address, | ||
| display_name: display_name, | ||
| resident_key: :required, | ||
| exclude_credentials: identity.credentials.map(&:to_public_key_credential) | ||
| ) | ||
| end | ||
|
|
||
| def request_options(credentials: []) | ||
| ActionPack::WebAuthn::PublicKeyCredential::RequestOptions.new(credentials: credentials.map(&:to_public_key_credential)) | ||
| end | ||
|
|
||
| def register(passkey:, challenge:, origin: ActionPack::WebAuthn::Current.origin, **attributes) | ||
| public_key_credential = ActionPack::WebAuthn::PublicKeyCredential.create( | ||
| client_data_json: passkey[:client_data_json], | ||
| attestation_object: Base64.urlsafe_decode64(passkey[:attestation_object]), | ||
| challenge: challenge, | ||
| origin: origin, | ||
| transports: Array(passkey[:transports]) | ||
| ) | ||
|
|
||
| create!( | ||
| **attributes, | ||
| name: attributes.fetch(:name, Authenticator.find_by_aaguid(public_key_credential.aaguid)&.name), | ||
| credential_id: public_key_credential.id, | ||
| public_key: public_key_credential.public_key.to_der, | ||
| sign_count: public_key_credential.sign_count, | ||
| aaguid: public_key_credential.aaguid, | ||
| backed_up: public_key_credential.backed_up, | ||
| transports: public_key_credential.transports | ||
| ) | ||
| end | ||
|
|
||
| def authenticate(passkey:, challenge:, origin: ActionPack::WebAuthn::Current.origin) | ||
| find_by(credential_id: passkey[:id])&.authenticate(passkey: passkey, challenge: challenge, origin: origin) | ||
| end | ||
| end | ||
|
|
||
| def authenticate(passkey:, challenge:, origin: ActionPack::WebAuthn::Current.origin) | ||
| pkc = to_public_key_credential | ||
| pkc.authenticate( | ||
| client_data_json: passkey[:client_data_json], | ||
| authenticator_data: Base64.urlsafe_decode64(passkey[:authenticator_data]), | ||
| signature: Base64.urlsafe_decode64(passkey[:signature]), | ||
| challenge: challenge, | ||
| origin: origin | ||
| ) | ||
| update!(sign_count: pkc.sign_count, backed_up: pkc.backed_up) | ||
| self | ||
| rescue ActionPack::WebAuthn::Authenticator::Response::InvalidResponseError | ||
| nil | ||
| end | ||
|
|
||
| def authenticator | ||
| Authenticator.find_by_aaguid(aaguid) | ||
| end | ||
|
|
||
| def to_public_key_credential | ||
| ActionPack::WebAuthn::PublicKeyCredential.new( | ||
| id: credential_id, | ||
| public_key: OpenSSL::PKey.read(public_key), | ||
| sign_count: sign_count, | ||
| transports: transports | ||
| ) | ||
| end | ||
| end |
There was a problem hiding this comment.
Missing model tests for Identity::Credential. The codebase has comprehensive test coverage for other models (as seen in test/models/), but there are no tests for the newly added Identity::Credential model. Consider adding tests to cover:
- Credential creation and registration
- Credential authentication
- The authenticator lookup functionality
- Public key credential conversion
- Edge cases and error handling
No description provided.