Skip to content

Move sms and email to adapters#675

Merged
markusahlstrand merged 3 commits intomainfrom
sms-email-adapters
Apr 1, 2026
Merged

Move sms and email to adapters#675
markusahlstrand merged 3 commits intomainfrom
sms-email-adapters

Conversation

@markusahlstrand
Copy link
Copy Markdown
Owner

@markusahlstrand markusahlstrand commented Mar 31, 2026

Summary by CodeRabbit

  • New Features

    • Moved email and SMS configuration from provider maps to adapter-based DataAdapters
    • Added passkey/WebAuthn as an MFA option and passkey enrollment flow (including guardian enrollment)
    • Support for tenant_id query parameter to select tenant in auth routes
  • Behavior / UX

    • Improved enrollment/ticket error messages and server error logging
    • Widget pages can include inline extra scripts for page-level WebAuthn handling
  • Documentation

    • Updated configuration guides for email and SMS adapter setup
  • Localization

    • Added passkey labels/descriptions across 8 languages

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 31, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
authero-docs Ready Ready Preview, Comment Apr 1, 2026 7:41am
authhero-react-admin Ready Ready Preview, Comment Apr 1, 2026 7:41am

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 31, 2026

📝 Walkthrough

Walkthrough

This PR centralizes email and SMS delivery behind new adapter interfaces exposed via DataAdapters, updates call sites to use .send(...), adds passkey enrollment and related UI/script injection, enables tenant_id via query string, adjusts login session expiry sourcing, and updates docs, tests, and locale strings.

Changes

Cohort / File(s) Summary
Adapter interfaces
packages/adapter-interfaces/src/adapters/EmailService.ts, packages/adapter-interfaces/src/adapters/SmsService.ts, packages/adapter-interfaces/src/adapters/index.ts
Add EmailServiceSendParams/EmailServiceAdapter and SmsServiceSendParams/SmsServiceAdapter; extend DataAdapters with optional emailService and smsService; re-export new adapters.
Email/SMS runtime migration
packages/authhero/src/emails/index.ts, packages/authhero/src/authentication-flows/mfa.ts, packages/authhero/src/types/Bindings.ts, packages/authhero/src/types/EmailService.ts, packages/authhero/src/types/SMSService.ts
Remove legacy provider maps/types; resolve services from ctx.env.data.emailService / ctx.env.data.smsService; change invocations to .send(...); remove old exported service types; update Bindings.
Docs & examples
apps/docs/customization/adapter-interfaces/index.md, apps/docs/customization/configuration.md, packages/authhero/README.md
Replace provider-based examples with adapter-based configuration and examples showing init({ dataAdapter }) and adapter imports.
Mocks & tests
packages/authhero/test/helpers/mock-email-service.ts, packages/authhero/test/helpers/mock-sms-service.ts, packages/authhero/test/helpers/test-server.ts, packages/authhero/test/middlewares/tenant.spec.ts, packages/authhero/test/i18n/locales.spec.ts
Mocks updated to implement adapter interfaces and inject via dataAdapter; test server wiring updated; tenant test adds mocked req.query; i18n test allows passkey loanword.
Universal login / widget & screens
packages/authhero/src/routes/universal-login/screens/types.ts, packages/authhero/src/routes/universal-login/u2-widget-page.tsx, packages/authhero/src/routes/universal-login/u2-routes.tsx, packages/authhero/src/routes/universal-login/error-handler.tsx
Add extraScript to screen results and widget props; propagate and SSR-inject inline script; enhance error mapping and server-error logging; refine guardian ticket handling and MFA routing.
Passkey MFA screens & flows
packages/authhero/src/routes/universal-login/screens/mfa-login-options.ts, packages/authhero/src/routes/universal-login/screens/passkey-enrollment.ts
Add passkey MFA option; include enrollment options when appropriate; implement passkey enrollment flow, WebAuthn page-level script via extraScript, guardian-enrollment conditional flow and success branch; adjust RP/origin handling.
Tenant middleware & management API
packages/authhero/src/middlewares/tenant.ts, packages/authhero/src/routes/management-api/guardian.ts
Add query-string tenant_id detection in tenant middleware; construct guardian ticket URL using URL/searchParams and append tenant_id when applicable.
Localization
packages/authhero/locales/{cs,da,en,fi,it,nb,pl,sv}.json
Add passkeyLabel, passkeyDescription, and enrollmentComplete translations; minor punctuation adjustments.
Login session creation
packages/kysely/src/loginSessions/create.ts
Use login.expires_at (if provided) to derive expires_at_ts, fallback to default 24h otherwise.
Changeset
.changeset/modern-lies-juggle.md
Add changeset to bump affected packages and note "Move email and sms to adapters".

Sequence Diagram(s)

sequenceDiagram
    participant Client as User/Client
    participant AuthService as Auth Service
    participant DataAdapter as Data Adapter (ctx.env.data)
    participant EmailAdapter as EmailServiceAdapter
    participant SmsAdapter as SmsServiceAdapter

    Client->>AuthService: Trigger action requiring delivery (email/SMS)
    AuthService->>DataAdapter: resolve emailService / smsService
    DataAdapter-->>AuthService: EmailServiceAdapter / SmsServiceAdapter instance

    alt Email path
        AuthService->>EmailAdapter: call .send(EmailServiceSendParams)
        EmailAdapter->>EmailAdapter: perform delivery logic
        EmailAdapter-->>AuthService: Promise<void> resolved
        AuthService->>Client: continue flow
    else SMS path
        AuthService->>SmsAdapter: call .send(SmsServiceSendParams)
        SmsAdapter->>SmsAdapter: perform delivery logic
        SmsAdapter-->>AuthService: Promise<void> resolved
        AuthService->>Client: continue flow
    end

    opt service missing
        AuthService->>AuthService: throw "<Service> service not configured"
        AuthService->>Client: error response
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

Poem

🐰
Adapters spring from burrows deep,
Emails, SMS no longer leap,
Passkeys shimmer, enroll with grace,
Tenant IDs hop into place,
A rabbit cheers the auth-stack's pace!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 68.75% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Move sms and email to adapters' accurately and concisely describes the primary change: migrating email and SMS services from provider-based configuration to adapter-based implementation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sms-email-adapters

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/authhero/src/routes/universal-login/u2-routes.tsx (1)

994-1018: ⚠️ Potential issue | 🟠 Major

Plumb extraScript through every HTML render path.

result.extraScript only reaches the GET default <WidgetPage> branch. The Liquid-template path and the POST re-render path still drop the screen result’s page script, so passkey enrollment won't auto-start for tenants with custom templates and any server-rendered retry/error page comes back without the WebAuthn bootstrap code.

Also applies to: 1023-1037, 1302-1343

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/authhero/src/routes/universal-login/u2-routes.tsx` around lines 994
- 1018, The screen result's extraScript (result.extraScript) is not being passed
into the Liquid-template render and POST re-render paths; ensure the page script
is plumbed through by passing extraScript into the HTML generation calls: update
the calls to generateWidgetContent (and generateFooterContent if it injects
scripts) to accept an extraScript parameter and include that script in the
returned widget/footer HTML, and then pass that same extraScript into
applyLiquidTemplate so the final renderedHtml contains the script before
returning ctx.html; make the same change in the POST re-render code paths that
rebuild widgetContent/footerContent and in any places that render
customTemplate.body so the WebAuthn/bootstrap script always flows through.
🧹 Nitpick comments (2)
packages/authhero/src/routes/universal-login/screens/types.ts (1)

90-91: Document extraScript as trusted-only content.

Since this field enables page-level script injection, add an explicit contract note that it must be server-generated trusted code (never user-derived strings) to reduce accidental misuse later.

📝 Suggested contract hardening
-  /** Optional inline script to inject at page level (e.g. WebAuthn ceremony) */
+  /** Optional inline script to inject at page level (e.g. WebAuthn ceremony).
+   *  Must be trusted server-generated JS; never interpolate unescaped user input.
+   */
   extraScript?: string;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/authhero/src/routes/universal-login/screens/types.ts` around lines
90 - 91, The JSDoc for the extraScript property is insufficiently
restrictive—document it explicitly as trusted-only server-generated script
content to prevent misuse: update the comment for the extraScript field (symbol:
extraScript in the types definition) to state it must be generated by the
server, never derived from user input, and only used for known-safe scripts
(e.g., WebAuthn ceremonies); include a short security note indicating that
callers must sanitize/validate sources and that the field is intended for
admin/trusted code injection only.
packages/authhero/test/middlewares/tenant.spec.ts (1)

22-22: Add a dedicated test for query-based tenant resolution.

Line 22 adds the query mock, but there’s still no explicit assertion for the new tenant_id query path. A focused test here will protect this new behavior from regressions.

✅ Suggested test addition
+  it("should set tenant_id from tenant_id query when no header/domain tenant is resolved", async () => {
+    mockHeaderFn.mockReturnValue(null);
+    mockCtx.req.query = vi.fn((key?: string) =>
+      key === "tenant_id" ? "tenant_from_query" : undefined,
+    );
+
+    await tenantMiddleware(mockCtx, mockNext);
+
+    expect(mockSet).toHaveBeenCalledWith("tenant_id", "tenant_from_query");
+    expect(mockNext).toHaveBeenCalled();
+  });

Based on learnings: Test all changes across multiple tenants to ensure proper data isolation and tenant-specific functionality.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/authhero/test/middlewares/tenant.spec.ts` at line 22, Add a focused
unit test in tenant.spec.ts that exercises the query-based tenant resolution
path: replace the existing query mock (query:
vi.fn().mockReturnValue(undefined)) with a mock that returns an object
containing tenant_id (e.g. query: vi.fn().mockReturnValue({ tenant_id:
'tenant-123' })), invoke the middleware/utility under test (the tenant
resolution function used in the spec), and assert the resolved tenant equals
'tenant-123'; also add a second case with a different tenant_id to verify
multi-tenant isolation. Ensure you reference and update the existing query:
vi.fn() mock and the test that calls the tenant middleware/resolver so the new
assertions cover the query-based flow.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/adapter-interfaces/src/adapters/index.ts`:
- Around line 85-86: The DataAdapters type currently marks emailService and
smsService as optional which lets storage-only adapter factories (e.g.
createAdapters in packages/aws and packages/kysely) satisfy init() while missing
delivery adapters; update the adapter types so the runtime bootstrap accepts a
stricter RuntimeAdapters shape (or make emailService: EmailServiceAdapter and
smsService: SmsServiceAdapter required on the runtime contract) and change the
init() signature to require RuntimeAdapters instead of the storage-only
DataAdapters; then update the adapter factory returns (createAdapters in
packages/aws and packages/kysely) or add a separate storage-only type so those
factories cannot be passed directly to init() without wiring real
EmailServiceAdapter/SmsServiceAdapter implementations.

In `@packages/authhero/locales/cs.json`:
- Around line 361-362: The new Czech strings use "Přístupový klíč" which is
inconsistent with the rest of the locale that uses the English term "passkey";
update the keys so the terminology is consistent—either change the values for
passkeyLabel and passkeyDescription to use the same term as the existing
passkeys.* strings (e.g., keep "passkey") or translate all other passkeys.*
entries to "Přístupový klíč"/the chosen Czech phrase; locate the keys
passkeyLabel and passkeyDescription in the locale and apply the same term across
all passkeys.* entries.

In `@packages/authhero/locales/fi.json`:
- Around line 361-362: The Finnish locale introduces an inconsistent term:
change the value of "passkeyLabel" (and optionally "passkeyDescription") to use
the same term as the existing passkeys.* entries (currently
"Tunnistautumisavain") so the selector and enrollment screens match; locate the
keys "passkeyLabel" and "passkeyDescription" in
packages/authhero/locales/fi.json and replace "Pääsyavain" (and any differing
phrasing) with "Tunnistautumisavain" (or update the other passkeys.* keys to
"Pääsyavain" if you prefer that term) to ensure consistent terminology across
passkeyLabel, passkeyDescription, and the passkeys.* strings.

In `@packages/authhero/locales/it.json`:
- Around line 361-362: The Italian translation is inconsistent: the new MFA key
uses "passkeyLabel" = "Chiave di accesso" while other keys in the passkey flow
still use the English term "passkey"; either make this key use the English term
to match the rest (set "passkeyLabel" back to "passkey") or translate all
related keys in the passkey flow for consistency (update every "passkeys.*" key
to use "Chiave di accesso" / corresponding Italian wording, and ensure
"passkeyDescription" and any other "passkeys.*" strings are updated
accordingly).

In `@packages/authhero/locales/nb.json`:
- Around line 361-362: The locale string for passkey is inconsistent: change the
value of the JSON key "passkeyLabel" from "Tilgangsnøkkel" to the same term used
by the other passkeys strings (e.g., "Passnøkkel") so it matches the existing
"passkeys.*" entries; update only the "passkeyLabel" value (and optionally scan
for other stray occurrences of "Tilgangsnøkkel") to ensure consistent use of
"passnøkkel" across the file.

In `@packages/authhero/src/middlewares/tenant.ts`:
- Around line 67-74: The middleware currently trusts ctx.req.query("tenant_id")
and sets ctx.var.tenant_id directly (tenantIdQuery), so restrict and validate
that value before setting it: only allow query-derived tenant_id on the specific
enrollment endpoint(s) (check the request path via ctx.req.path or ctx.req.url),
validate the tenantIdQuery format (e.g., UUID or expected tenant id pattern),
and verify it matches an enrollment ticket or other server-side artifact
(perform a lookup/verifyTicket(ticketId) or query the DB) tied to the request
before calling ctx.set("tenant_id", tenantIdQuery) and proceeding to await
next(); otherwise reject the query-supplied tenant_id and do not mutate
ctx.var.tenant_id.

In `@packages/authhero/src/routes/management-api/guardian.ts`:
- Line 564: The ticketUrl is built via string interpolation which can break if
tenantId or other values contain reserved characters; instead construct it using
the URL API: create a URL with new URL('u2/guardian/enroll', issuer) (or new
URL(path, issuer)), then use url.searchParams.append('ticket', ticketId) and
url.searchParams.append('tenant_id', tenantId) before reading url.toString();
update the code that assigns ticketUrl (the variable defined from issuer,
ticketId, tenantId in guardian.ts) to use this approach so all query parameters
are properly encoded.

In `@packages/authhero/src/routes/universal-login/screens/passkey-enrollment.ts`:
- Around line 117-163: The try/catch around navigator.credentials.create in
extraScript only logs errors to the console, leaving the user stuck; update the
catch block to surface a user-facing error by finding the same form (the form
lookup used for cf and af), then either (a) set a hidden/visible error field
(e.g., querySelector('[name="webauthn-error"]') or create/insert an element with
id 'webauthn-error') and put a friendly message (use e.message or a generic
"Registration failed") into it, and set the action-field (af) or a status field
so the server/page knows an error occurred, or (b) if you prefer to auto-submit
to show a server-side error, set a hidden error input value and submit the form;
modify the catch in extraScript to perform one of these actions so users see a
retry/clear error instead of only a console.error.
- Around line 553-572: Replace the hardcoded English success copy in the
successScreen.description with a localized string from the existing localization
helper "m" (you already call m.title()); e.g. call an appropriate localization
method on m (such as m.passkeyEnrollmentSuccess() or a similarly named key you
maintain) and assign that to successScreen.description so the
guardian_enrollment success message is localized for non-English flows.
- Around line 96-105: The skip link's inline JS currently queries document for
the form and action field which fails when the form lives inside
authhero-widget's shadow root; update the href generation for the link with id
"skip" so the script first attempts document.querySelector('form') and, if not
found, locates document.querySelector('authhero-widget')?.shadowRoot and queries
that shadowRoot for the form and action field (or uses form.querySelector to
find the action field), then sets the action-field value to 'skip' and submits
the form—mirror the same fallback logic used elsewhere in this file to ensure
the skip action works inside the widget shadow root.
- Around line 96-105: The client-only removal of the "skip" link is
insufficient—add a server-side guard so POSTs with action=skip are honored only
for continuation flows; in the POST handler that processes this screen (where
createFrontChannelAuthResponse is called), check the request/session flag
(isContinuation or equivalent on the session/extra) before executing the skip
branch and return an error or re-render if false, and likewise ensure any other
handlers referenced around the other affected blocks (the POST handling code
sections that call createFrontChannelAuthResponse) have the same isContinuation
validation to prevent bypassing MFA.

In `@packages/authhero/src/routes/universal-login/u2-widget-page.tsx`:
- Around line 58-59: The injected extraScript currently embeds extra.optionsJSON
into a template unquoted after only escaping "<" (safeOptions), which can allow
JS syntax breakouts; modify the construction of extraScript in
u2-widget-page.tsx to serialize the options safely by using JSON.stringify on
the parsed object (or by injecting a quoted JSON string) before embedding, e.g.,
replace the safeOptions logic with a fully JSON-encoded value and assign it to
opts (reference symbols: extra.optionsJSON, safeOptions, extraScript, and the
dangerouslySetInnerHTML usage; related generator: generateRegistrationOptions in
passkey-enrollment.ts) so the assignment cannot be broken by quotes or braces in
the payload.

---

Outside diff comments:
In `@packages/authhero/src/routes/universal-login/u2-routes.tsx`:
- Around line 994-1018: The screen result's extraScript (result.extraScript) is
not being passed into the Liquid-template render and POST re-render paths;
ensure the page script is plumbed through by passing extraScript into the HTML
generation calls: update the calls to generateWidgetContent (and
generateFooterContent if it injects scripts) to accept an extraScript parameter
and include that script in the returned widget/footer HTML, and then pass that
same extraScript into applyLiquidTemplate so the final renderedHtml contains the
script before returning ctx.html; make the same change in the POST re-render
code paths that rebuild widgetContent/footerContent and in any places that
render customTemplate.body so the WebAuthn/bootstrap script always flows
through.

---

Nitpick comments:
In `@packages/authhero/src/routes/universal-login/screens/types.ts`:
- Around line 90-91: The JSDoc for the extraScript property is insufficiently
restrictive—document it explicitly as trusted-only server-generated script
content to prevent misuse: update the comment for the extraScript field (symbol:
extraScript in the types definition) to state it must be generated by the
server, never derived from user input, and only used for known-safe scripts
(e.g., WebAuthn ceremonies); include a short security note indicating that
callers must sanitize/validate sources and that the field is intended for
admin/trusted code injection only.

In `@packages/authhero/test/middlewares/tenant.spec.ts`:
- Line 22: Add a focused unit test in tenant.spec.ts that exercises the
query-based tenant resolution path: replace the existing query mock (query:
vi.fn().mockReturnValue(undefined)) with a mock that returns an object
containing tenant_id (e.g. query: vi.fn().mockReturnValue({ tenant_id:
'tenant-123' })), invoke the middleware/utility under test (the tenant
resolution function used in the spec), and assert the resolved tenant equals
'tenant-123'; also add a second case with a different tenant_id to verify
multi-tenant isolation. Ensure you reference and update the existing query:
vi.fn() mock and the test that calls the tenant middleware/resolver so the new
assertions cover the query-based flow.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3764f31a-9b89-42fd-b0b7-4b942075093a

📥 Commits

Reviewing files that changed from the base of the PR and between 9eac183 and 022f12f.

📒 Files selected for processing (33)
  • .changeset/modern-lies-juggle.md
  • apps/docs/customization/adapter-interfaces/index.md
  • apps/docs/customization/configuration.md
  • packages/adapter-interfaces/src/adapters/EmailService.ts
  • packages/adapter-interfaces/src/adapters/SmsService.ts
  • packages/adapter-interfaces/src/adapters/index.ts
  • packages/authhero/README.md
  • packages/authhero/locales/cs.json
  • packages/authhero/locales/da.json
  • packages/authhero/locales/en.json
  • packages/authhero/locales/fi.json
  • packages/authhero/locales/it.json
  • packages/authhero/locales/nb.json
  • packages/authhero/locales/pl.json
  • packages/authhero/locales/sv.json
  • packages/authhero/src/authentication-flows/mfa.ts
  • packages/authhero/src/emails/index.ts
  • packages/authhero/src/middlewares/tenant.ts
  • packages/authhero/src/routes/management-api/guardian.ts
  • packages/authhero/src/routes/universal-login/error-handler.tsx
  • packages/authhero/src/routes/universal-login/screens/mfa-login-options.ts
  • packages/authhero/src/routes/universal-login/screens/passkey-enrollment.ts
  • packages/authhero/src/routes/universal-login/screens/types.ts
  • packages/authhero/src/routes/universal-login/u2-routes.tsx
  • packages/authhero/src/routes/universal-login/u2-widget-page.tsx
  • packages/authhero/src/types/Bindings.ts
  • packages/authhero/src/types/EmailService.ts
  • packages/authhero/src/types/SMSService.ts
  • packages/authhero/test/helpers/mock-email-service.ts
  • packages/authhero/test/helpers/mock-sms-service.ts
  • packages/authhero/test/helpers/test-server.ts
  • packages/authhero/test/middlewares/tenant.spec.ts
  • packages/kysely/src/loginSessions/create.ts
💤 Files with no reviewable changes (3)
  • packages/authhero/src/types/SMSService.ts
  • packages/authhero/src/types/Bindings.ts
  • packages/authhero/src/types/EmailService.ts

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/authhero/test/i18n/locales.spec.ts (1)

67-83: ⚠️ Potential issue | 🟠 Major

"passkey" in the global loanword allowlist hides real untranslated labels

Line 71 makes "Passkey" auto-exempt from untranslated detection. That suppresses real gaps like packages/authhero/locales/cs.json and packages/authhero/locales/it.json where passkeyLabel is still identical to English, while other passkey strings are translated. Please avoid allowlisting passkey globally.

Suggested minimal fix
     const loanwords = new Set([
       "email",
       "status",
       "password",
-      "passkey",
       "ok",
       "invitation",
     ]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/authhero/test/i18n/locales.spec.ts` around lines 67 - 83, The
current loanwords allowlist in the test (the loanwords Set used by
canBeIdentical) includes "passkey", which masks untranslated real labels; remove
"passkey" from the loanwords Set so canBeIdentical will no longer auto-exempt
that token and the untranslated detection will catch identical passkey labels
(adjust any test expectations if they assumed the exemption). Ensure the change
is made where the loanwords Set and the canBeIdentical function are defined.
🧹 Nitpick comments (2)
packages/authhero/src/routes/universal-login/screens/passkey-enrollment.ts (1)

384-399: Consider consolidating state_data parsing in POST handler.

The loginSession.state_data is parsed multiple times (lines 385, 489, 572) within the POST handler. While functionally correct, consolidating this into a single parse near the top of the handler would improve clarity and avoid redundant JSON parsing.

💡 Example consolidation
       // At top of post handler after fetching loginSession
       const loginSession = await ctx.env.data.loginSessions.get(...);
+      const stateData = loginSession?.state_data
+        ? JSON.parse(loginSession.state_data)
+        : {};
       if (!loginSession || !loginSession.user_id) { ... }

       // Allow passkey enrollment from continuation flow or MFA enrollment flow
-      const postStateData = loginSession.state_data
-        ? JSON.parse(loginSession.state_data)
-        : {};
-      const isGuardianEnrollment = postStateData.guardian_enrollment === true;
+      const isGuardianEnrollment = stateData.guardian_enrollment === true;

Also applies to: 489-491, 572-574

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/authhero/src/routes/universal-login/screens/passkey-enrollment.ts`
around lines 384 - 399, The POST handler parses loginSession.state_data multiple
times; consolidate by parsing it once into a single variable (e.g.,
postStateData) at the top of the handler and reuse that variable everywhere
instead of repeating JSON.parse(loginSession.state_data). Update all occurrences
that re-parse state_data (the checks around postStateData.guardian_enrollment,
the later references at lines you flagged, and any other
JSON.parse(loginSession.state_data) in this handler) to use the single parsed
variable, and guard the initial parse with a safe fallback (try/catch or
conditional) so postStateData defaults to {} on missing/invalid JSON; keep
existing logic that uses hasValidContinuationScope, passkeyEnrollmentScreen, and
LoginSessionState.AWAITING_MFA unchanged.
packages/authhero/src/routes/universal-login/u2-routes.tsx (1)

1928-1946: Consider documenting supported guardian enrollment factors.

The routing logic handles OTP, SMS, and WebAuthn factors but omits email, push_notification, recovery_code, and duo factors that exist in the tenant schema. If this is intentional (e.g., guardian enrollment only supports a subset of MFA methods), a brief comment would clarify this design decision for future maintainers.

💡 Suggested documentation
       const state = encodeURIComponent(loginSession.id);

+      // Guardian enrollment currently supports OTP, SMS, and WebAuthn factors.
+      // Email, push_notification, recovery_code, and duo are handled elsewhere.
       const hasOtp = factors?.otp === true;
       const hasSms = factors?.sms === true;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/authhero/src/routes/universal-login/u2-routes.tsx` around lines 1928
- 1946, The routing block that computes hasOtp, hasSms, hasWebauthn and
factorCount and then redirects (the conditional that returns ctx.redirect(...)
for login-options, totp-enrollment, phone-enrollment, passkey/enrollment) only
accounts for otp, sms and webauthn factors; either expand the logic to include
the other tenant factors (email, push_notification, recovery_code, duo) with
appropriate redirects/handlers, or explicitly add a concise inline comment above
the hasOtp/hasSms/hasWebauthn and factorCount logic stating that guardian
enrollment intentionally supports only otp/sms/webauthn and why, so future
maintainers understand the design decision. Ensure you reference the same
symbols (hasOtp, hasSms, hasWebauthn, factorCount, and the ctx.redirect
destinations) when updating the code or comment.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/authhero/src/routes/universal-login/screens/passkey-enrollment.ts`:
- Around line 181-195: Update getRpId and getExpectedOrigin to prefer the
x-forwarded-host header before falling back to host (and then "localhost") so
WebAuthn origin/rpId reflect the client-facing domain behind proxies;
specifically modify getRpId(ctx) and getExpectedOrigin(ctx) to read
ctx.req.header("x-forwarded-host") || ctx.req.header("host") || "localhost",
keep the existing port-stripping in getRpId and the localhost->http protocol
logic in getExpectedOrigin, and ensure the functions continue to return the rpId
(host without port) and the full origin string respectively.

---

Outside diff comments:
In `@packages/authhero/test/i18n/locales.spec.ts`:
- Around line 67-83: The current loanwords allowlist in the test (the loanwords
Set used by canBeIdentical) includes "passkey", which masks untranslated real
labels; remove "passkey" from the loanwords Set so canBeIdentical will no longer
auto-exempt that token and the untranslated detection will catch identical
passkey labels (adjust any test expectations if they assumed the exemption).
Ensure the change is made where the loanwords Set and the canBeIdentical
function are defined.

---

Nitpick comments:
In `@packages/authhero/src/routes/universal-login/screens/passkey-enrollment.ts`:
- Around line 384-399: The POST handler parses loginSession.state_data multiple
times; consolidate by parsing it once into a single variable (e.g.,
postStateData) at the top of the handler and reuse that variable everywhere
instead of repeating JSON.parse(loginSession.state_data). Update all occurrences
that re-parse state_data (the checks around postStateData.guardian_enrollment,
the later references at lines you flagged, and any other
JSON.parse(loginSession.state_data) in this handler) to use the single parsed
variable, and guard the initial parse with a safe fallback (try/catch or
conditional) so postStateData defaults to {} on missing/invalid JSON; keep
existing logic that uses hasValidContinuationScope, passkeyEnrollmentScreen, and
LoginSessionState.AWAITING_MFA unchanged.

In `@packages/authhero/src/routes/universal-login/u2-routes.tsx`:
- Around line 1928-1946: The routing block that computes hasOtp, hasSms,
hasWebauthn and factorCount and then redirects (the conditional that returns
ctx.redirect(...) for login-options, totp-enrollment, phone-enrollment,
passkey/enrollment) only accounts for otp, sms and webauthn factors; either
expand the logic to include the other tenant factors (email, push_notification,
recovery_code, duo) with appropriate redirects/handlers, or explicitly add a
concise inline comment above the hasOtp/hasSms/hasWebauthn and factorCount logic
stating that guardian enrollment intentionally supports only otp/sms/webauthn
and why, so future maintainers understand the design decision. Ensure you
reference the same symbols (hasOtp, hasSms, hasWebauthn, factorCount, and the
ctx.redirect destinations) when updating the code or comment.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cbb443c3-3a0f-4afb-bbbc-310aa6f650ae

📥 Commits

Reviewing files that changed from the base of the PR and between 022f12f and 639c76b.

📒 Files selected for processing (12)
  • packages/authhero/locales/cs.json
  • packages/authhero/locales/da.json
  • packages/authhero/locales/en.json
  • packages/authhero/locales/fi.json
  • packages/authhero/locales/it.json
  • packages/authhero/locales/nb.json
  • packages/authhero/locales/pl.json
  • packages/authhero/locales/sv.json
  • packages/authhero/src/routes/management-api/guardian.ts
  • packages/authhero/src/routes/universal-login/screens/passkey-enrollment.ts
  • packages/authhero/src/routes/universal-login/u2-routes.tsx
  • packages/authhero/test/i18n/locales.spec.ts
✅ Files skipped from review due to trivial changes (2)
  • packages/authhero/locales/en.json
  • packages/authhero/locales/da.json
🚧 Files skipped from review as they are similar to previous changes (7)
  • packages/authhero/locales/nb.json
  • packages/authhero/src/routes/management-api/guardian.ts
  • packages/authhero/locales/fi.json
  • packages/authhero/locales/cs.json
  • packages/authhero/locales/sv.json
  • packages/authhero/locales/it.json
  • packages/authhero/locales/pl.json

Comment on lines 181 to 195
function getRpId(ctx: any): string {
const host = ctx.var.custom_domain || ctx.req.header("host") || "localhost";
const host = ctx.req.header("host") || "localhost";
// Strip port if present
return host.split(":")[0];
}

/**
* Get the origin URL for WebAuthn verification
* Get the origin URL for WebAuthn verification.
* Must use the actual request host to match the browser's origin.
*/
function getExpectedOrigin(ctx: any): string {
const host = ctx.var.custom_domain || ctx.req.header("host") || "localhost";
const host = ctx.req.header("host") || "localhost";
const protocol = host.includes("localhost") ? "http" : "https";
return `${protocol}://${host}`;
}
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Mar 31, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if X-Forwarded-Host is used elsewhere in the codebase for similar patterns
rg -n "X-Forwarded-Host" --type=ts
# Check for any proxy-aware host resolution utilities
rg -n "getHost|get.*[Hh]ost" --type=ts -C2 | head -50

Repository: markusahlstrand/authhero

Length of output: 3400


🏁 Script executed:

# Get context around the X-Forwarded-Host usage in tenant.ts
cat -n packages/authhero/src/middlewares/tenant.ts | sed -n '25,40p'

Repository: markusahlstrand/authhero

Length of output: 721


🏁 Script executed:

# Check if there are other host resolution functions in the codebase
rg -n "req\.header.*host" --type=ts -i -C3

Repository: markusahlstrand/authhero

Length of output: 8351


🏁 Script executed:

# Look at the full context of the functions in question
cat -n packages/authhero/src/routes/universal-login/screens/passkey-enrollment.ts | sed -n '175,200p'

Repository: markusahlstrand/authhero

Length of output: 1107


Use proxy-aware host resolution for WebAuthn.

These functions should check x-forwarded-host before falling back to the host header to match how the codebase handles proxied requests elsewhere (e.g., tenant.ts middleware). Behind a reverse proxy, the host header reflects the proxy's internal address, not the client-facing domain that the browser uses for origin verification.

Consider:

function getRpId(ctx: any): string {
  const host = ctx.req.header("x-forwarded-host") || ctx.req.header("host") || "localhost";
  return host.split(":")[0];
}

function getExpectedOrigin(ctx: any): string {
  const host = ctx.req.header("x-forwarded-host") || ctx.req.header("host") || "localhost";
  const protocol = host.includes("localhost") ? "http" : "https";
  return `${protocol}://${host}`;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/authhero/src/routes/universal-login/screens/passkey-enrollment.ts`
around lines 181 - 195, Update getRpId and getExpectedOrigin to prefer the
x-forwarded-host header before falling back to host (and then "localhost") so
WebAuthn origin/rpId reflect the client-facing domain behind proxies;
specifically modify getRpId(ctx) and getExpectedOrigin(ctx) to read
ctx.req.header("x-forwarded-host") || ctx.req.header("host") || "localhost",
keep the existing port-stripping in getRpId and the localhost->http protocol
logic in getExpectedOrigin, and ensure the functions continue to return the rpId
(host without port) and the full origin string respectively.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Already fixed. ctx.var.host is set in the tenant middleware at tenant.ts:18-20 to xForwardedHost || hostHeader || ..., which already prefers x-forwarded-host before host. The getRpId and getExpectedOrigin functions use ctx.var.host, so they already get the correct proxy-aware host. No change needed.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

cat -n packages/authhero/src/middlewares/tenant.ts | sed -n '1,35p'

Length of output: 1207


@markusahlstrand, confirmed — tenant.ts lines 16–21 clearly set ctx.var.host as:

xForwardedHost || hostHeader || new URL(getIssuer(ctx.env)).host

You're right that getRpId and getExpectedOrigin already get the correct proxy-aware host via ctx.var.host. My concern was invalid. Apologies for the noise!


✏️ Learnings added
Learnt from: markusahlstrand
Repo: markusahlstrand/authhero PR: 675
File: packages/authhero/src/routes/universal-login/screens/passkey-enrollment.ts:181-195
Timestamp: 2026-04-01T08:10:49.309Z
Learning: In `packages/authhero/src/middlewares/tenant.ts` (lines 16-21), `ctx.var.host` is set early in the tenant middleware as `x-forwarded-host || host header || issuer host`, making it proxy-aware. Functions that resolve the browser-facing host (e.g., `getRpId`, `getExpectedOrigin` in `passkey-enrollment.ts`) should use `ctx.var.host` directly — do not flag these as missing x-forwarded-host handling.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: markusahlstrand
Repo: markusahlstrand/authhero PR: 646
File: packages/authhero/src/routes/universal-login/screens/mfa-phone-challenge.ts:176-178
Timestamp: 2026-03-23T06:59:11.333Z
Learning: In `packages/authhero/src/routes/universal-login/screens/`, ensure that the universal-login post handler return type uses the discriminated union shape: `{ screen: ScreenResult } | { redirect: string; cookies?: string[] } | { error: string; screen: ScreenResult } | { response: Response }`, where `ScreenResult` is `{ screen: UiScreen; branding?: ScreenBranding }`. Therefore, returning `{ screen: await someScreen(context) }` is correct (it intentionally wraps a `ScreenResult` under the outer `screen` key). Do not flag this as incorrect nesting.

Learnt from: CR
Repo: markusahlstrand/authhero PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-25T13:20:21.406Z
Learning: Applies to src/{index,App,TenantsApp,*Tenant*,*Context*}.{ts,tsx} : When extracting tenant ID from URL path, ensure it is properly parsed and passed to all tenant-scoped operations

Learnt from: markusahlstrand
Repo: markusahlstrand/authhero PR: 6
File: packages/authhero/src/authentication-flows/authorization-code.ts:47-52
Timestamp: 2024-12-08T17:06:51.637Z
Learning: In the `authorizationCodeGrant` function within `packages/authhero/src/authentication-flows/authorization-code.ts`, revoked keys are removed in the data-adapter before being used. However, adding an additional check for `key.revoked` and verifying the `key.algorithm` can enhance safety.

Learnt from: markusahlstrand
Repo: markusahlstrand/authhero PR: 13
File: packages/authhero/src/routes/auth-api/dbconnections.ts:185-191
Timestamp: 2024-12-16T08:03:19.741Z
Learning: In the `change_password` route in `packages/authhero/src/routes/auth-api/dbconnections.ts`, the IP address is already included in the session data via `getClientInfo`.

Learnt from: CR
Repo: markusahlstrand/authhero PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-25T13:20:21.406Z
Learning: Applies to src/dataProvider.ts,src/**/*{api,service,provider}*.{ts,tsx} : Always include tenant ID in API request headers for data scoping to specified tenant

Learnt from: markusahlstrand
Repo: markusahlstrand/authhero PR: 173
File: apps/react-admin/src/components/users/edit.tsx:552-566
Timestamp: 2025-08-14T07:49:13.288Z
Learning: The user markusahlstrand prefers to validate API endpoint changes in separate PRs before implementing suggested fixes, especially for user permission management endpoints.

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.

1 participant