Skip to content

feat: add configurable challenge store for WebAuthn challenges - #150

Merged
RenzoMinelli merged 7 commits into
masterfrom
rm--challenge-store
Oct 7, 2026
Merged

RenzoMinelli merged 7 commits into
masterfrom
rm--challenge-store

Conversation

@RenzoMinelli

@RenzoMinelli RenzoMinelli commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

What: Adds Devise::Webauthn.challenge_store so WebAuthn challenges can be kept somewhere other than the session. The default session store keeps today's keys and behaviour.

Why: This is the first step toward passkey sign-in from API clients such as React Native with devise-jwt, which have no cookie session. A cache-backed store follows in the next PR.

How to test: bundle exec rspec. passkey_authentication_spec.rb signs in through MemoryChallengeStore, a store that doesn't use the session.

RenzoMinelli added a commit that referenced this pull request Sep 25, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RenzoMinelli added a commit that referenced this pull request Sep 25, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@RenzoMinelli
RenzoMinelli marked this pull request as draft September 25, 2026 15:29
RenzoMinelli added a commit that referenced this pull request Sep 25, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@RenzoMinelli
RenzoMinelli changed the base branch from master to rm--pin-json-for-rails-7 September 25, 2026 18:33
RenzoMinelli added a commit that referenced this pull request Sep 25, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@RenzoMinelli
RenzoMinelli changed the base branch from rm--pin-json-for-rails-7 to rm--retry-detached-node-errors September 25, 2026 19:06
RenzoMinelli added a commit that referenced this pull request Sep 25, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RenzoMinelli added a commit that referenced this pull request Sep 25, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Base automatically changed from rm--retry-detached-node-errors to master September 25, 2026 20:31
RenzoMinelli and others added 4 commits September 25, 2026 17:32
Controllers and strategies now read and write challenges through
`Devise::Webauthn.challenge_store`. The default session store keeps
the existing session keys, so behaviour is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Controllers and strategies include `Devise::Webauthn::ChallengeStoreAccess`
instead of calling `Devise::Webauthn.challenge_store_for(request)`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The challenge is consumed inside `verify_and_save_*`, which is skipped when
parsing the credential raises or when an app overrides it. The `ensure`
restores the previous guarantee that the challenge is always cleared.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@RenzoMinelli
RenzoMinelli marked this pull request as ready for review September 25, 2026 20:34
@RenzoMinelli
RenzoMinelli added this pull request to stack #156 October 2, 2026 20:17

@santiagorodriguez96 santiagorodriguez96 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking really good!!!

Comment thread lib/devise/strategies/webauthn_two_factor_authenticatable.rb Outdated
@session[KEYS.fetch(purpose)] = challenge
end

def pending?(purpose, _credential_json)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the _credential_json param if we are not going to use it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The store interface needs it. ChallengeStores::Cache in #152 has no session, so it reads the challenge from the credential's clientDataJSON to build the key. I renamed it to _credential to match #152.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmmm I see...

In that case I think I would hold off these changes and instead include them in #152 as I think they can be confusing here. I think that would give clarity about what we are including those params in this methods. What's more, I'd be able to explore different approaches and discuss them in that PR to avoid having to do that.

What do you think?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Removed in 46aaa6c; the argument comes back in #152 with the Cache store.

Comment thread spec/requests/devise/custom_challenge_store_spec.rb Outdated
Comment thread spec/requests/devise/custom_challenge_store_spec.rb Outdated
RenzoMinelli and others added 2 commits October 2, 2026 19:00
Rename the verified challenge to expected_challenge and the unused store
argument to _credential. Move the custom store specs into the passkey
authentication spec and extract MemoryChallengeStore to spec/support.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@session[KEYS.fetch(purpose)] = challenge
end

def pending?(purpose, _credential_json)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmmm I see...

In that case I think I would hold off these changes and instead include them in #152 as I think they can be confusing here. I think that would give clarity about what we are including those params in this methods. What's more, I'd be able to explore different approaches and discuss them in that PR to avoid having to do that.

What do you think?

end
end

describe "sign-in with passkeys through a custom challenge store" do

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been thinking about the new sign-in with passkeys through a custom challenge store block, and I wonder if we could fold it into the existing specs 🤔

The existing specs still pass, but only because :session is the default. They read session directly, so they test the session store's internals, not the challenge store interface.

What if we merge the new block with the existing examples, like this?

  • Set the specs to use MemoryChallengeStore explicitly, so nothing passes just because of the default store.
  • Check challenges through challenge_store instead of the session

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 46aaa6c. The passkey authentication examples now run against MemoryChallengeStore and read challenges through the store.

…face

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

@santiagorodriguez96 santiagorodriguez96 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall looks good to me!

Let's keep iterating over this 🙂

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this a model spec? Should we move it to our models/ folder?

Now that I think about it tho, shouldn't it be placed in spec/models? Not to be done as part of this PR but just to spark the discussion :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These aren't models, they're plain Ruby classes. Specs here mirror lib/ (spec/devise/webauthn/ ↔ lib/devise/webauthn/, spec/devise/models/ ↔ lib/devise/models/), so I'd leave them. Happy to discuss a different layout in a separate PR.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These aren't models, they're plain Ruby classes. Specs here mirror lib/ (spec/devise/webauthn/ ↔ lib/devise/webauthn/, spec/devise/models/ ↔ lib/devise/models/), so I'd leave them. Happy to discuss a different layout in a separate PR.

@RenzoMinelli
RenzoMinelli merged commit 796cf2b into master Oct 7, 2026
31 checks passed
@RenzoMinelli
RenzoMinelli deleted the rm--challenge-store branch October 7, 2026 21:15
RenzoMinelli added a commit that referenced this pull request Oct 7, 2026
…request

Stores already receive the request, so callers no longer pass the credential and the store interface stays as merged in #150.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants