MVP: Add WebAuthn passkey authentication support#1108
Draft
heyvaleria wants to merge 5 commits intomainfrom
Draft
MVP: Add WebAuthn passkey authentication support#1108heyvaleria wants to merge 5 commits intomainfrom
heyvaleria wants to merge 5 commits intomainfrom
Conversation
Introduces opt-in passkey (WebAuthn) support to Clearance via a new generator and model. Add the `webauthn ~> 3.0` gem dependency. Add `Clearance::Passkey` (lib/clearance/passkey.rb), an ActiveRecord model with: - `belongs_to :user, class_name: "::User", optional: false` - presence validations on `label`, `external_id`, and `public_key` - uniqueness validation on `external_id` Add the `clearance:passkeys` generator (lib/generators/clearance/passkeys/) which produces two migrations: - `add_webauthn_id_to_users` — adds a unique `webauthn_id:string` column to the users table (skipped if column already exists) - `create_passkeys` — creates the passkeys table with `user_id`, `label`, `external_id`, `public_key`, `sign_count` (not null, default 0), timestamps, and a unique index on `external_id` (skipped if table already exists) Wire up the dummy app with the corresponding migrations, an updated schema, and a `Passkey < Clearance::Passkey` model. Add a `:passkey` FactoryBot factory and specs covering the generator and the model (belongs_to, validations, db columns/indexes, and uniqueness behaviour including after destroy).
Before this change, the clearance:passkeys generator set up the necessary database structure — a passkeys table and a webauthn_id column on users — but there was no controller layer. There was no way for a user to register a passkey or sign in with one. The goal was to wire up both WebAuthn ceremonies using the webauthn gem's credential API, following the same patterns as Clearance's existing session and password controllers. PasskeysController handles registration: GET /passkeys/new generates WebAuthn credential creation options and stores the challenge in the session; POST /passkeys verifies the browser's response and persists the new passkey against the signed-in user. The webauthn_id on the user is set lazily on first registration. PasskeyAuthenticationsController handles sign-in: GET /passkey_authentication/new returns assertion options, and POST /passkey_authentication verifies the credential, updates the stored sign count, and delegates to Clearance's sign_in so the full guard stack still runs. Both controllers render JSON, since the WebAuthn API is JavaScript-driven. Routes for both are added inside the existing routes_enabled? block. The generator was also updated to inject has_many :passkeys into the host app's User model, and to print a README after running with instructions for configuring the webauthn gem. That configuration (origin and rp_name) is deliberately left to the host app via WebAuthn.configure rather than routing it through Clearance.configuration — the webauthn gem already provides a good home for those settings and wrapping them would add abstraction without benefit.
inject_into_class inserts at the top of the class body, which placed has_many :passkeys before include Clearance::User. Switched to inject_into_file with after: "include Clearance::User\n" so the association is always declared after the module inclusion. Also tightened the spec to assert ordering rather than just presence.
Replace deprecated config.origin= with config.allowed_origins=, and expand the frontend setup instructions to show the importmap install path and working JS snippets for both the registration and authentication flows.
Add the missing instruction to pin the app's passkeys JS file in config/importmap.rb, without which the module specifier cannot be resolved and the WebAuthn flows silently fail.
Member
|
@heyvaleria looks good! I'll do some testing during upcoming investment time. Shall we release this as a new beta version so you can add more features to make it production ready? |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Before this PR, Clearance's only authentication path was email and password.
There was no built-in support for
WebAuthnpasskeys, so apps that wanted passkey sign-in had to build the entire ceremony layer themselves, managing credential options, challenge storage, assertion verification, and session sign-in completely outside of Clearance.The goal of this code change was to add passkey support as an opt-in feature: run a generator to prepare the database and
Usermodel, configure thewebauthngem for the host app that uses clearance, and get a working JSON backend for both the registration and authentication ceremonies, integrated with Clearance's existingsign_inguard stack.Clearance::Passkeyis a newActiveRecordmodel that belongs to a user and stores the credential'sexternal_id(the ID issued by the authenticator),public_key,sign_count, and a human-readablelabel.external_idis validated for uniqueness so the same credential can't be registered twice.The
clearance:passkeysgenerator sets up everything the host app needs: it creates anadd_webauthn_id_to_usersmigration (skipped if the column already exists) and acreate_passkeysmigration (skipped if the table already exists), injectshas_many :passkeysinto theUsermodel, and prints aREADMEwith instructions for configuring thewebauthngem.PasskeysControllerhandles the registration ceremony.GET /passkeys/newreturns WebAuthn credential creation options as JSON and stores the challenge in the session; the user'swebauthn_idis generated lazily at this point if not already set.POST /passkeysverifies the credential response and persists the new passkey against the current user.Registration requires the user to be signed in.
PasskeyAuthenticationsControllerhandles the authentication ceremony.GET /passkey_authentication/newreturns assertion options as JSON.POST /passkey_authenticationverifies the assertion, updates the stored sign count, and delegates to Clearance's sign_in so the full guard stack still runs.On success it responds with JSON containing a
redirect_tokey pointing toClearance.configuration.redirect_url.Both controllers render JSON, since the
WebAuthnAPI is driven by browser-side JavaScript.Routes for both are added inside the existing
routes_enabled?block.In a flexible thoughbot approach, I decided that the
WebAuthnconfiguration (origin, rp_name) is deliberately left to the host app viaWebAuthn.configurerather than threading it throughClearance.configuration— thewebauthngem already provides the right home for those settings.Setup instructions
1) Install
bundle add clearance rails generate clearance:install # skip if already using Clearance rails generate clearance:passkeys rails db:migrateBy running
bundle add clearance,webauthn ~> 3.0comes as a dependency.2) Configure WebAuthn
Create
config/initializers/webauthn.rb3) Endpoints
4) Registration flow (user must be signed in)
5) Authentication flow
The WebAuthn browser API expects specific object shapes — for a production app you'd typically reach for a library like
@github/webauthn-jsonto handle the Base64url encoding/decoding between the JSON and the native credential objects.