Skip to content

feat: add WebAuthn/Passkey Phoenix components#718

Open
raul-gracia wants to merge 5 commits intoteam-alembic:mainfrom
raul-gracia:webauthn-components
Open

feat: add WebAuthn/Passkey Phoenix components#718
raul-gracia wants to merge 5 commits intoteam-alembic:mainfrom
raul-gracia:webauthn-components

Conversation

@raul-gracia
Copy link
Copy Markdown

@raul-gracia raul-gracia commented Mar 29, 2026

Add Phoenix LiveView components for WebAuthn/Passkey authentication.

Components

  • Components.WebAuthn - container with register/sign-in toggle
  • Components.WebAuthn.RegistrationForm - passkey registration form
  • Components.WebAuthn.AuthenticationForm - passkey sign-in form
  • Components.WebAuthn.Input - identity field + submit button with WebAuthn JS hooks
  • Components.WebAuthn.ManageCredentials - list, rename, delete, add security keys
  • Components.WebAuthn.Support - browser capability check

Overrides

Default and DaisyUI themes cover all WebAuthn components, same structure as Password and TOTP.

JS Hooks

Bundles priv/static/webauthn_hooks.js -- handles navigator.credentials.create() / .get(), base64url encoding, and LiveView event bridging. Import and register the hooks in your app.js.

Depends on

Test plan

Adds LiveView components for the WebAuthn authentication strategy:
- WebAuthn container component with register/sign-in toggle
- RegistrationForm and AuthenticationForm components
- Input component with identity field and submit button
- ManageCredentials component for listing, renaming, deleting keys
- Support component for browser capability detection
- Overrides for Default and DaisyUI themes
- Ships bundled JS hooks in priv/static/webauthn_hooks.js

Depends on ash_authentication webauthn-strategy branch.
@raul-gracia
Copy link
Copy Markdown
Author

Dependency Note

This PR depends on team-alembic/ash_authentication#1144 which adds the AshAuthentication.Strategy.WebAuthn struct and related modules.

The ash_authentication dependency currently points to a local path (../ash_authentication) because the WebAuthn strategy is not yet available in the published hex package. CI compile/audit failures are expected until the upstream PR is merged and a new rc is published.

Once that happens, the dependency in mix.exs should be reverted back to:

{:ash_authentication, "~> 5.0.0-rc"}

There's a TODO comment in mix.exs marking this.

Adds SPDX copyright/license headers to all new WebAuthn files for REUSE
compliance. The ash_authentication dependency uses a local path override
because the WebAuthn strategy is not yet in the published hex package
(pending team-alembic/ash_authentication#1144).
@raul-gracia raul-gracia force-pushed the webauthn-components branch from 7d0e879 to 1ce2ed3 Compare March 31, 2026 22:27
Copy link
Copy Markdown
Collaborator

@jimsynz jimsynz left a comment

Choose a reason for hiding this comment

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

Hey @raul-gracia — thanks again for the work on this companion PR. The component hierarchy is well-structured (WebAuthn → RegistrationForm / AuthenticationForm / ManageCredentials / Support / Input), the override integration for both Default and DaisyUI themes is thorough, and the JS hooks handle the ceremony lifecycle cleanly. Having the credential management panel built-in is a real bonus.

Following on from my review on #1144, here are the items I found on the Phoenix side that need addressing before we can ship.

Security items

ManageCredentials bypasses Actions.add_credential (Medium)

ManageCredentials.handle_event("registration-attestation", ...) creates credentials directly via Ash.Changeset.for_create(:create, attrs, ash_opts) rather than going through WebAuthn.Actions.add_credential/3. This means any future validation, hooks, or security checks added to add_credential won't apply to credentials added through the management panel. Should route through the Actions module for consistency.

SVG icon override rendered with Phoenix.HTML.raw() (Low)

The register_button_icon and sign_in_button_icon overrides are rendered via Phoenix.HTML.raw(icon). The docs note these must be trusted static SVG, which is fine for normal usage. Just flagging it — if anyone builds dynamic overrides from user input (unlikely but possible), it's an XSS vector.

Functionality items

Missing component_for_strategy clause in SignIn

The diff adds strategy_style(%AshAuthentication.Strategy.WebAuthn{}) returning :form, but I don't see a corresponding component_for_strategy clause mapping WebAuthn to Components.WebAuthn. Without this, the auto-discovery flow in the SignIn component won't know which component to render for WebAuthn strategies. Needs a clause like:

defp component_for_strategy(%{strategy_module: AshAuthentication.Strategy.WebAuthn}),
  do: Components.WebAuthn

(or whatever the matching pattern is for Custom strategies — worth checking how the existing ones dispatch)

mix.exs local path override

The mix.exs has {:ash_authentication, path: "../ash_authentication", override: true} — this needs to be removed before merge. Should point at the hex package (with a version constraint that includes WebAuthn once #1144 lands), or at a git ref if we want to merge this before the hex release.

Test helper references Example.Accounts.User

WebAuthnHelpers.mock_webauthn_strategy/1 sets resource: Example.Accounts.User, but the test support in ash_authentication uses Example.UserWithWebAuthn. This may cause issues depending on what's available in the Phoenix repo's test support.

No end-to-end documentation guide

The module docs are solid, but a Getting Started guide covering the full setup — backend strategy + Phoenix components + JS hook registration — would help a lot with adoption. WebAuthn has more moving parts than most strategies (credential resource, JS hooks, origin configuration), so a step-by-step walkthrough would be valuable. This could be a HexDocs guide page or a section in the README.

JS hook registration is manual

Users need to import and register WebAuthnRegistrationHook, WebAuthnAuthenticationHook, and WebAuthnSupportHook in their app.js. It's documented in comments in the hooks file, but easy to miss. An Igniter generator (mentioned in the #1144 review) could automate this.


Same offer as on #1144 — these are all tractable items and I'm happy to pick up whatever you'd rather not tackle yourself. This is a really solid foundation and I'd love to get it shipped. Let me know how you'd like to split the remaining work.

- ManageCredentials now routes new credential creation through
  WebAuthn.Actions.add_credential/3 instead of constructing an Ash
  changeset directly. This keeps policies, hooks, and future validation
  on the credential resource in play.

- Adds end-to-end WebAuthn tutorial covering backend strategy, router,
  JS hook registration, rp_id pitfalls, credential management, and
  troubleshooting. Registered in the docs extras list.

- Documents the WebAuthnHelpers mock resource choice and its scope
  (gated UI-only tests vs real ceremony tests in the upstream repo).
Explicitly document that rp_id is a hostname only — never include scheme
or port — with a comparison table covering dev/staging/prod. Adds the
localhost:4000 gotcha to both the rp_id section and troubleshooting.
@raul-gracia
Copy link
Copy Markdown
Author

raul-gracia commented Apr 14, 2026

Thanks for the review @jimsynz. Pushed 58c70dc and fffadc3 addressing the items below. Using your section structure so each point is easy to track.

Security items

ManageCredentials bypasses Actions.add_credential (Medium): fixed

The registration-attestation handler in ManageCredentials now routes through WebAuthn.Actions.add_credential(strategy, params, challenge:, user:, tenant:) instead of building an Ash.Changeset directly.
Dropped the inline Wax.register/3 call since the action does the verification internally.
Policies, hooks, and any future validations on the credential resource now apply whether a credential is added at sign-up or through the management panel.

SVG icon override rendered with Phoenix.HTML.raw() (Low): not changed

Leaving as-is. The override docs already note that icons must be trusted static SVG, which matches the documented usage. If you want a stronger inline warning in the moduledoc, I can add one in a follow-up.

Functionality items

Missing component_for_strategy clause in SignIn: not changed (checked the fallback)

After tracing it, the existing fallback already resolves WebAuthn correctly. For %AshAuthentication.Strategy.WebAuthn{}:

  1. strategy.__struct__AshAuthentication.Strategy.WebAuthn
  2. Module.split/1["AshAuthentication", "Strategy", "WebAuthn"]
  3. List.replace_at(_, 1, "Components")["AshAuthentication", "Components", "WebAuthn"]
  4. List.insert_at(_, 1, "Phoenix")["AshAuthentication", "Phoenix", "Components", "WebAuthn"]
  5. Module.concat/1AshAuthentication.Phoenix.Components.WebAuthn

The Strategy.Apple clause exists because Apple is an AshAuthentication.Strategy.OAuth2 struct identified by its :strategy_module field, so the fallback would resolve it to AshAuthentication.Phoenix.Components.OAuth2, which is the wrong module. WebAuthn is its own struct type like Strategy.Password and Strategy.MagicLink, both of which rely on the fallback successfully. I can add an explicit clause anyway if you prefer it for discoverability.

mix.exs local path override: not changed (blocked on #1144)

Still pointing at ../ash_authentication with a TODO comment marking the revert. As mentioned in the previous comment, this PR depends on #1144 for the AshAuthentication.Strategy.WebAuthn struct and WebAuthn.Actions module, neither of which exist in the published hex package yet.

I would still prefer to wait until #1144 merges and a new rc is published, then switch this PR's dependency back to "~> 5.0.0-rc" in a single final commit before merge.
I can add a git ref pin if you want CI green sooner.

Test helper references Example.Accounts.User: clarified (not replaced)

Added a @moduledoc to WebAuthnHelpers explaining the scope.
The helper is used only by rendering tests gated behind @moduletag :webauthn_strategy_required.
The component only needs the resource for subject-name derivation via Info, which Example.Accounts.User satisfies.
Real WebAuthn ceremony tests (credential creation, sign-in, sign-count verification) belong in the upstream ash_authentication repo alongside Example.UserWithWebAuthn, which isn't available in this repo's test support. If you want a dedicated Example.Accounts.UserWithWebAuthn fixture here too I can add one, though it felt like duplication given the upstream coverage.

No end-to-end documentation guide: added

Added documentation/tutorials/webauthn.md and registered it in the docs extras list in mix.exs. It covers:

  • Overview of the ceremony flow, so readers understand why WebAuthn has more moving parts than other strategies.
  • Prerequisites with minimal backend strategy config and a pointer to the upstream guide.
  • Router setup (auto-discovery handles the WebAuthn component without extra config).
  • JavaScript hooks: the three hooks that need to be imported and registered in app.js, with a one-liner per hook explaining what each one drives.
  • Origin and rp_id configuration: dev/staging/prod comparison table, explicit callout that rp_id is a hostname only (no scheme, no port), and why "localhost:4000" is a common mistake. rp_id is a domain string, not an origin, so port and scheme are validated separately by the browser. Also covers the HTTP-over-localhost exception.
  • Credential management: usage snippet for ManageCredentials with a note that deletion of the last credential is prevented.
  • Customization: override example with a link to the UI Overrides guide.
  • Troubleshooting: NotAllowedError, SecurityError from bad rp_id, the "prompt never appears on non-localhost HTTP" trap, credentials missing after deploy, hook registration, and failed registration.

JS hook registration is manual: leaving to you

Same as on #1144, I'll leave the Igniter generator part to you. Each repo has its own Igniter callbacks so it needs to be handled per-repo, and you're better placed to wire it into the existing generator conventions here.


Let me know which of the open items you'd like to pick up yourself and which I should tackle, and whether any of the "not changed" items above you would still want to change anyway.

# Conflicts:
#	lib/ash_authentication_phoenix/overrides/list.ex
#	mix.exs
#	mix.lock
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