Skip to content

feat(core): add OAuth2 consent flow support#584

Open
Jorgagu wants to merge 5 commits into
ory:mainfrom
Jorgagu:feat/oauth2-consent-flow
Open

feat(core): add OAuth2 consent flow support#584
Jorgagu wants to merge 5 commits into
ory:mainfrom
Jorgagu:feat/oauth2-consent-flow

Conversation

@Jorgagu

@Jorgagu Jorgagu commented Jan 5, 2026

Copy link
Copy Markdown
Contributor

Add complete OAuth2 consent flow support to @ory/nextjs and @ory/elements-react packages, enabling applications to handle OAuth2 authorization consent screens with Ory Hydra.

Related Issue or Design Document

Fixes #327

Features

  • Consent Flow Utilities (@ory/nextjs)

    • App router: getConsentFlow, acceptConsentRequest, rejectConsentRequest
    • Pages router: getServerSideConsentFlow (for getServerSideProps) and useSession
    • The consent request is always fetched server side, since reading it requires the Hydra admin API and the project API token
    • Accept and reject validate that the session identity matches the consent request subject, preventing consent hijacking with a stolen consent_challenge
  • OAuth2 Client Info Display (@ory/elements-react)

    • Consent card lists requested scopes as grant_scope checkboxes and shows the OAuth2 client name
    • The OAuth2 client logo is displayed on the consent card only (per review feedback, login and registration keep the application's own branding)
  • Example Implementations

    • App Router: consent page + API route
    • Pages Router: consent page + API route
    • Custom Components: ConsentFooter, custom scope checkbox with toggle switches
    • CSRF protection via @csrf-armor/nextjs middleware, plus an explicit double-submit comparison in the consent API routes (the library falls back to the csrf cookie when extracting the submitted token, so the cookie pair alone is not sufficient)
    • Consent pages only render when the session identity matches the consent request subject

Improvements

  • Optimize rewriteUrls to single-pass regex replacement
  • Add OAuth2 path exclusion in URL rewriting
  • Add null/undefined handling in rewriteJsonResponse

Tests

  • Unit tests for consent utilities and rewrite functions

Checklist

  • I have read the contributing guidelines and signed the CLA.
  • I have referenced an issue containing the design document if my change introduces a new feature.
  • I have read the security policy.
  • I confirm that this pull request does not address a security vulnerability.
    If this pull request addresses a security vulnerability,
    I confirm that I got approval (please contact security@ory.sh) from the maintainers to push the changes.
  • I have added tests that prove my fix is effective or that my feature works.
  • I have added the necessary documentation within the code base (if appropriate).

Further comments

This implementation follows the pattern established in kratos-selfservice-ui-node for handling OAuth2 flows. The OAuth2 client logo is displayed by overriding the project configuration's logo_light_url when an OAuth2 client logo is available, keeping the existing DefaultCardLogo component unchanged; the override is applied by the consent flow component only.

Summary by CodeRabbit

  • New Features
    • Added OAuth2 consent-flow support for Next.js App Router and Pages Router integrations.
    • Added APIs for retrieving, accepting, and rejecting consent requests, plus session access.
    • Consent screens now display the OAuth2 client name and logo.
    • Added customizable consent scope controls and footer actions.
  • Security
    • Added CSRF protection to consent endpoints and example applications.
    • Added validation to ensure the signed-in identity matches the consent request.
  • Bug Fixes
    • Improved URL rewriting for OAuth2/OIDC paths and edge cases.

@changeset-bot

changeset-bot Bot commented Jan 5, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 9fb70db

The changes in this PR will be included in the next version bump.

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Jan 5, 2026

Copy link
Copy Markdown

@Jorgagu is attempting to deploy a commit to the ory Team on Vercel.

A member of the Team first needs to authorize it.

@codecov

codecov Bot commented Jan 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 60.86957% with 45 lines in your changes missing coverage. Please review.
✅ Project coverage is 60.64%. Comparing base (f3fad4d) to head (9fb70db).
⚠️ Report is 355 commits behind head on main.

Additional details and impacted files
@@             Coverage Diff             @@
##             main     #584       +/-   ##
===========================================
+ Coverage   42.43%   60.64%   +18.21%     
===========================================
  Files         136      186       +50     
  Lines        2008     3781     +1773     
  Branches      288      623      +335     
===========================================
+ Hits          852     2293     +1441     
- Misses       1149     1364      +215     
- Partials        7      124      +117     
Components Coverage Δ
@ory/elements-react 60.65% <ø> (+23.86%) ⬆️
@ory/nextjs 60.59% <ø> (-5.39%) ⬇️
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@Jorgagu Jorgagu changed the title feat(oauth2): add OAuth2 consent flow support feat(core): add OAuth2 consent flow support Jan 5, 2026
@Jorgagu

Jorgagu commented Jan 7, 2026

Copy link
Copy Markdown
Contributor Author

@vinckr @jonas-jonas @aeneasr Happy New Year ! 🎉 Could you please review this one ?

@jonas-jonas

Copy link
Copy Markdown
Member

hi @Jorgagu, thank you very much for this contribution, and happy new year!

We'll take a look at this in the coming weeks. We do have some code for this already; it just wasn't ready to be published, so we might need to do some merging with that.

And just a heads-up, we're quite busy ramping up after the holidays again, so it might take a couple days longer for us to get to this.

@Jorgagu
Jorgagu force-pushed the feat/oauth2-consent-flow branch 3 times, most recently from 70b3e84 to 73020fc Compare February 3, 2026 12:43
@alamchrstn

Copy link
Copy Markdown

hi thanks for this contrib @Jorgagu ! this really helps our team to understand more context on the Ory FE stack here
wondering if we're looking to have this into main anytime soon @jonas-jonas ?

@jonas-jonas jonas-jonas left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thank you for this contribution! Overall it looks solid. I do have one note about the missing CSRF protection. Happy to discuss this in more detail, if needed!

consentChallenge={consentRequest}
session={session}
config={config}
csrfToken=""

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The endpoints used in this component are susceptible to CSRF attacks. This is also the reason the consent screen is not yet part of the elements examples.

Since the <Consent /> component does not yet support server actions, the CSRF protection must be done through middleware. This is kind of awkward to implement, and there aren't many libraries out there that support this.

Ideally, we'd move the Consent screen to a server actions-based form submission because they're CSRF protected by default, but that would make support for the pages router impossible.

https://www.npmjs.com/package/@edge-csrf/nextjs?activeTab=readme is the best library for this (even though it is unmaintained, and vulnerable to subdomain attacks). Could you integrate that into this change?

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.

Hey @jonas-jonas, thanks for the review!

A few things worth noting about the current state:

  • The consent form submits via fetch() with Content-Type: application/json (line 161 of useOryFormSubmit.ts), so cross-origin CSRF is blocked by CORS preflight.
  • I've added server-side session identity validation on both routers so the server checks session.identity.id matches
    consentRequest.subject before processing.
  • The csrfToken="" is not validated anywhere server-side though, which is a gap if the submission mechanism ever changes.

Regarding @edge-csrf/nextjs, it uses the Naive Double-Submit Cookie Pattern (vulnerable to subdomain attacks) and the maintainer is looking to hand off the project (last stable release: October 2024).

Here are the options I see:

Approach Trade-offs
@edge-csrf/nextjs Subdomain vulnerability, uncertain maintenance
Signed Double Submit Cookie (custom) No dependency, works with both routers, no subdomain vulnerability (OWASP recommended)
Custom request header Simplest with fetch(), breaks if we ever switch to HTML form POST
Server Actions CSRF-protected by default, but kills Pages Router support

I'd lean toward the Signed Double Submit Cookie. What's your preference?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for the investigation.

Signed double submit seems like the best solution. However, we generally try not to reinvent the wheel (unless necessary!), and CSRF seems like a well-solved issue, so ideally we use a library to solve this. "Don't roll your own security" applies here.

However, when I last looked into this, there were few libraries that actually solved this for Next.js specifically. Do you know of any?

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.

I looked into Next.js CSRF libraries that implement the Signed Double Submit Cookie pattern. Here's what I found:

Package Pattern Version Latest Release Monthly Downloads App Router Pages Router Status
@edge-csrf/nextjs Naive Double Submit v2.5.3 Nov 2024 117,278 Yes Yes Maintainer stepping down, subdomain vulnerability
next-csrf Synchronizer Token (stateful) v0.2.1 Apr 2022 23,608 No Yes Abandoned
@csrf-armor/nextjs Signed Double Submit (HMAC) v1.4.1 Feb 2026 694 Yes Yes Active
@simple-csrf/next Signed Double Submit v0.1.1 Apr 2025 176 Yes No Requires React 19

@csrf-armor/nextjs is the only library that checks all the boxes: Signed Double Submit (HMAC), both routers, Edge
Runtime compatible, zero dependencies, TypeScript, and CodeQL analysis. The adoption is low, but the code is auditable
and the core package (@csrf-armor/core) is framework-agnostic, which
could allow building a Nuxt adapter on top of it for the Vue/Nuxt side of ory-elements in the future.

What do you think about going with @csrf-armor/nextjs?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for the compilation. I think that's right; let's use @csrf-armor/nextjs here. Would you mind adjusting the examples to use the package?

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.

@jonas-jonas its done, could you please try it and give me review on it ?

@Jorgagu
Jorgagu force-pushed the feat/oauth2-consent-flow branch 3 times, most recently from 0afc868 to a4ba0fd Compare March 9, 2026 20:57
@alexanderNthl

Copy link
Copy Markdown

hey @Jorgagu, thanks a lot for your work on this, it really helps!
I noticed that the Consent flow is not fully compatible with internationalization (i18n) yet:

  • The UINodes in oauth2.ts have conflicting IDs (4x 9999111) and are not part of the known messages yet.
  • The footer contains hard coded messages.

Do you think it make sense to include i18n for the consent flow in this PR or should this be part of a new issue/PR? Thanks in advance for your answer.

@Jorgagu

Jorgagu commented Mar 14, 2026

Copy link
Copy Markdown
Contributor Author

Good catch @alexanderNthl, you're right the consent flow doesn't have proper i18n yet.

I looked into it and the 9999111 ID reused across all nodes in oauth2.ts was just a placeholder, and the footer strings are hardcoded in English.

Kratos already has numeric ID ranges per flow (1010xxx for Login, 1040xxx for Registration, up to 1080xxx Verification). I checked and the 1090xxx range is completely free, so I'd use that for consent:

  • 1090001: "Accept"
  • 1090002: "Reject"
  • 1090003: "Remember my decision"
  • 1090004: "Make sure you trust {client}"
  • 1090005: "You may be sharing sensitive information with this site or application."
  • 1090006: "Authorizing will redirect to {client}"

The existing string keys (consent.title, consent.subtitle, consent.scope.*) are already fine.

@jonas-jonas do you think I should include this in the current PR or handle it separately?

@jonas-jonas jonas-jonas left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One more comment; otherwise, this looks good! Could you resolve the merge conflicts once more?

Comment on lines +63 to +66
const configWithLogo = getConfigWithOAuth2Logo(
config,
flow.oauth2_login_request?.client?.logo_uri,
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't think this is correct. The login screen is for the main application (think Google, Facebook, etc.), whereas with this change, it would render as the client logo (e.g., My ACME app). This would be very confusing to end users ("Am I logging in with my ACME app account or my google account?").

@jonas-jonas

Copy link
Copy Markdown
Member

@Jorgagu The message IDs issue has to be addressed; it's on our list internally, but it's out of scope of this PR, as the component already existed before.

Jorgagu added 3 commits July 20, 2026 23:38
Add getConsentFlow, acceptConsentRequest and rejectConsentRequest for
the app router, and getServerSideConsentFlow plus useSession for the
pages router. The consent request is always fetched server side since
reading it requires the Hydra admin API and the project API token.

Accepting and rejecting validate that the session identity matches the
consent request subject to prevent consent hijacking with a stolen
consent_challenge. URL rewriting is consent-aware in rewriteUrls.
The consent card lists the requested scopes as grant_scope checkboxes
and shows the OAuth2 client name. The client logo override only applies
to the consent screen: per review feedback, login and registration keep
the application's own branding.
Add consent pages and API routes to the app router, custom components,
and pages router examples. CSRF protection uses the @csrf-armor/nextjs
middleware plus an explicit double-submit comparison in the consent
routes, since the library falls back to the csrf cookie when extracting
the submitted token. Middleware path matching is aligned with the
@ory/nextjs proxy match list, and the consent pages only render when
the session identity matches the consent request subject.
@Jorgagu
Jorgagu force-pushed the feat/oauth2-consent-flow branch from a4ba0fd to 7ec7ef5 Compare July 20, 2026 21:43
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds OAuth2 consent APIs for Next.js App and Pages Routers, consent-specific Elements rendering, CSRF-protected consent examples, client branding support, and OAuth2-aware URL rewriting.

Changes

OAuth2 consent flow

Layer / File(s) Summary
Next.js consent APIs
packages/nextjs/src/app/..., packages/nextjs/src/pages/..., packages/nextjs/api-report/...
Adds consent retrieval, accept/reject handlers, server and client OAuth2 clients, session loading, public exports, tests, and API declarations.
Consent UI rendering
packages/elements-react/src/components/..., packages/elements-react/src/theme/default/...
Separates footer nodes, renders consent inputs, applies OAuth2 client logos, and adds helper tests.
App Router consent example
examples/nextjs-app-router/...
Adds consent pages, API processing, session and subject checks, CSRF validation, and middleware routing.
Custom consent components
examples/nextjs-app-router-custom-components/...
Adds custom scope controls, consent footer rendering, component overrides, and CSRF-protected submission handling.
Pages Router consent example
examples/nextjs-pages-router/...
Adds server-side consent retrieval, session validation, API handling, CSRF middleware, and consent rendering.
OAuth2 URL rewrite handling
packages/nextjs/src/utils/..., .gitignore
Preserves OAuth2/OIDC URLs during rewriting, handles nullish JSON inputs, expands tests, and ignores coverage output.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant ConsentPage
  participant NextjsAPI
  participant OAuth2Service
  Browser->>ConsentPage: request consent challenge
  ConsentPage->>NextjsAPI: load consent request and session
  NextjsAPI->>OAuth2Service: fetch consent request
  OAuth2Service-->>NextjsAPI: consent request
  ConsentPage-->>Browser: render consent form
  Browser->>NextjsAPI: submit CSRF-protected decision
  NextjsAPI->>OAuth2Service: accept or reject consent
  OAuth2Service-->>Browser: redirect_to
Loading

Suggested labels: contribution

Suggested reviewers: jonas-jonas

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR covers consent-flow support, but #327 also requests login features and no login implementation is shown. Add the missing login-flow implementation or narrow the issue scope to consent-only work; the current changes do not satisfy the full linked request.
Out of Scope Changes check ⚠️ Warning The PR includes URL-rewriting and null/undefined handling changes that are unrelated to the linked Hydra consent-flow request. Remove or justify the rewrite-related changes, since they are outside the consent/login scope of #327.
Docstring Coverage ⚠️ Warning Docstring coverage is 39.39% 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
Title check ✅ Passed The title is concise and accurately summarizes the main change: OAuth2 consent flow support.
Description check ✅ Passed The description mostly matches the template, with a clear summary, linked issue, checklist, and extra implementation notes.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@Jorgagu

Jorgagu commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

@jonas-jonas the branch is rebased on the current main and the merge conflicts are resolved.

Changes since your last review:

  • The OAuth2 client logo override no longer applies to the login and registration screens, it is now used by the consent flow component only. I also removed the client-name subtitles from constructCardHeader.ts for the same reason, so login and registration keep the application's own branding entirely.
  • The pages router client-side hook useConsentFlow was replaced by a server-side getServerSideConsentFlow for getServerSideProps: fetching the consent request requires the Hydra admin API and the project API token, so it must never run in the browser.
  • CSRF: the examples use @csrf-armor/nextjs as agreed. One note: in the current release (1.4.4), the token extraction falls back to the csrf cookie itself when neither the header nor the body carries it, which would make the middleware comparison self-referential for the consent form submit. The example consent API routes therefore add an explicit double-submit comparison between the submitted csrf_token field and the cookie. This looks worth an upstream issue.
  • The consent pages only render the consent request when the session identity matches the request subject, mirroring the existing accept/reject validation.
  • Added a changeset (minor for @ory/elements-react and @ory/nextjs).

The PR description is updated accordingly.

@Jorgagu
Jorgagu requested a review from jonas-jonas July 20, 2026 21:54

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (8)
packages/nextjs/src/pages/client.ts (1)

27-29: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Remove redundant and unsafe window.location.origin access.

guessPotentiallyProxiedOrySdkUrl already safely defaults to window.location.origin when running on the client. Accessing window.location.origin unconditionally here bypasses that safety and will throw a ReferenceError if this factory is ever accidentally invoked during Server-Side Rendering (SSR).

♻️ Proposed refactor
-      basePath: guessPotentiallyProxiedOrySdkUrl({
-        knownProxiedUrl: window.location.origin,
-      }),
+      basePath: guessPotentiallyProxiedOrySdkUrl(),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/nextjs/src/pages/client.ts` around lines 27 - 29, Remove the
explicit knownProxiedUrl value from the guessPotentiallyProxiedOrySdkUrl call in
the client factory, allowing the helper to apply its safe client-side default
without unconditionally accessing window during SSR.
packages/nextjs/src/pages/consent.ts (1)

76-79: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider logging the error before swallowing it.

Returning null on failure handles invalid challenges gracefully, but silently swallowing all errors (such as 500s or misconfigured API keys) can make debugging difficult. Consider logging the error to help developers identify configuration or network issues.

♻️ Proposed refactor
   return serverSideOAuth2Client()
     .getOAuth2ConsentRequest({ consentChallenge })
-    .catch(() => null)
+    .catch((error) => {
+      console.error("Failed to fetch OAuth2 consent request:", error)
+      return null
+    })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/nextjs/src/pages/consent.ts` around lines 76 - 79, Update the OAuth2
consent request flow in serverSideOAuth2Client().getOAuth2ConsentRequest to log
the caught error before returning null. Preserve the existing graceful null
fallback while ensuring failures, including configuration and network errors,
include useful error details in the established logging mechanism.
packages/nextjs/src/pages/session.ts (1)

47-55: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Simplify state updates with finally.

You can use a .finally() block to handle setting the loading state, reducing duplication across the success and error paths.

♻️ Proposed refactor
       .then((session) => {
         setSession(session)
-        setLoading(false)
-        return
       })
       .catch((err: unknown) => {
         setError(err instanceof Error ? err : new Error(String(err)))
-        setLoading(false)
+      })
+      .finally(() => {
+        setLoading(false)
       })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/nextjs/src/pages/session.ts` around lines 47 - 55, Refactor the
promise chain in the session-loading flow to move setLoading(false) from both
the success and error handlers into a shared finally block. Keep setSession in
the success handler and setError in the catch handler, preserving their existing
behavior.
packages/nextjs/src/app/consent.test.ts (1)

82-161: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Add tests for the identity-match/mismatch branch.

No test in either describe block passes identityId, so the "Forbidden: Session identity does not match..." guard (the security-critical part of both functions) is never exercised. The mocks here also don't stub getOAuth2ConsentRequest, so this branch would currently throw if invoked.

Add cases for: (1) matching identityId proceeds to accept/reject, (2) mismatched identityId throws.

Also applies to: 163-208

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/nextjs/src/app/consent.test.ts` around lines 82 - 161, Extend both
consent test describe blocks covering acceptConsentRequest and
rejectConsentRequest with identityId cases: stub getOAuth2ConsentRequest with a
request identity for each test, verify matching identityId proceeds to the
underlying accept/reject call, and verify mismatched identityId throws the
forbidden error without proceeding. Keep assertions focused on the security
guard and ensure the mocks support the lookup path.
packages/elements-react/src/theme/default/utils/oauth2-config.ts (1)

4-4: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use a relative import for internal types.

Importing from the package's own public entry point (@ory/elements-react) from within its internal source code can cause module resolution or circular dependency issues during the build step. Please use a relative path instead.

♻️ Proposed fix
-import { OryClientConfiguration } from "`@ory/elements-react`"
+import { OryClientConfiguration } from "../../../util/clientConfiguration"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/elements-react/src/theme/default/utils/oauth2-config.ts` at line 4,
Update the OryClientConfiguration import in oauth2-config.ts to use the
appropriate relative internal source path instead of the package public entry
point, preserving the existing type usage.
packages/nextjs/src/utils/rewrite.ts (1)

82-85: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Handle top-level arrays safely to prevent type conversion issues.

If obj is an Array (e.g., a top-level JSON array response), allowing it to fall through to Object.fromEntries(Object.entries(obj)...) will incorrectly convert it into an Object with stringified numeric keys (e.g., {"0": item1, "1": item2}).

Consider adding early handling for arrays to prevent accidental data corruption.

♻️ Proposed refactor
   // Handle null/undefined input to prevent runtime errors
   if (!obj) {
     return obj
   }
+  if (Array.isArray(obj)) {
+    return obj
+      .map((item) =>
+        typeof item === "object" && item !== null
+          ? rewriteJsonResponse(item, proxyUrl)
+          : typeof item === "string" && proxyUrl
+            ? item.replaceAll(orySdkUrl(), proxyUrl)
+            : item,
+      )
+      .filter((item) => item !== undefined) as unknown as T
+  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/nextjs/src/utils/rewrite.ts` around lines 82 - 85, Update the
null/undefined guard in the object-rewrite logic to detect top-level arrays and
return them unchanged before the Object.fromEntries(Object.entries(...))
conversion. Preserve the existing handling for non-array objects and nullish
inputs.
examples/nextjs-app-router/middleware.ts (1)

34-51: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Add export const config = { matcher: [...] } to scope middleware execution.

Neither middleware file defines a matcher, so per Next.js defaults this middleware (CSRF processing + Ory path routing) runs on every request, including static assets, images, and favicon — unlike the @csrf-armor/nextjs documented setup, which explicitly excludes _next/static, _next/image, and favicon.ico.

  • examples/nextjs-app-router/middleware.ts#L34-L51: add a matcher export excluding static asset paths.
  • examples/nextjs-app-router-custom-components/middleware.ts#L34-L51: apply the same addition.
♻️ Proposed addition
   const response = NextResponse.next()
   const result = await csrfProtect(request, response)
   if (!result.success) {
     return NextResponse.json(
       { error: "CSRF validation failed" },
       { status: 403 },
     )
   }
   return response
 }
+
+export const config = {
+  matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/nextjs-app-router/middleware.ts` around lines 34 - 51, Add an
exported Next.js middleware config with a matcher that excludes _next/static,
_next/image, and favicon.ico from execution. Apply the same configuration
alongside middleware in examples/nextjs-app-router/middleware.ts and
examples/nextjs-app-router-custom-components/middleware.ts; leave the middleware
request handling unchanged.
examples/nextjs-app-router/app/api/consent/route.ts (1)

82-114: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Validate action explicitly instead of defaulting unknown values to reject.

Any body.action other than exactly "accept" (including missing/malformed values) silently falls through to rejectConsentRequest. This fails safe (denies rather than grants), but it hides malformed-request bugs as spurious consent denials instead of a clear 400.

♻️ Proposed validation
+  if (action !== "accept" && action !== "reject") {
+    return NextResponse.json(
+      { error: "invalid_request", error_description: "Invalid or missing action" },
+      { status: 400 },
+    )
+  }
+
   if (!consentChallenge) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/nextjs-app-router/app/api/consent/route.ts` around lines 82 - 114,
Validate body.action explicitly before the consent operation: accept only
“accept” or “reject”, and return a 400 invalid_request response for missing or
any other value. Update the branching around acceptConsentRequest and
rejectConsentRequest so rejectConsentRequest is called only when action is
exactly “reject”.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@examples/nextjs-app-router/middleware.ts`:
- Around line 42-50: Return the protected response from csrfProtect instead of
the original response. Update the success path in
examples/nextjs-app-router/middleware.ts (lines 42-50) and
examples/nextjs-app-router-custom-components/middleware.ts (lines 42-50) to
return result.response, while preserving the existing CSRF failure response.

In `@examples/nextjs-pages-router/pages/auth/consent.tsx`:
- Around line 12-51: Update getServerSideProps to read and decode the CSRF token
from the request cookie, include it in ConsentPageProps, and pass it through the
returned props. Update ConsentPage to use the prop instead of
getCsrfTokenFromCookie, then remove that client-side helper.

In `@packages/nextjs/src/app/consent.ts`:
- Around line 112-156: Make identity validation mandatory in
acceptConsentRequest by requiring identityId in its options and always
retrieving the consent request to verify its subject matches identityId before
accepting it. Apply the same requirement and validation to rejectConsentRequest,
preferably by extracting and reusing an assertConsentSubject helper so both
consent flows enforce identical checks.

In `@packages/nextjs/src/utils/rewrite.ts`:
- Around line 43-63: Update the URL regex in the rewrite logic around the regex
construction to require a valid domain boundary after baseUrlNormalized,
preventing matches inside longer hostnames. In the callback’s OAuth2 and UI
mapping checks, replace raw path.startsWith checks with boundary-aware matching
so only the exact path or a path beginning with the configured segment followed
by a separator is rewritten.

---

Nitpick comments:
In `@examples/nextjs-app-router/app/api/consent/route.ts`:
- Around line 82-114: Validate body.action explicitly before the consent
operation: accept only “accept” or “reject”, and return a 400 invalid_request
response for missing or any other value. Update the branching around
acceptConsentRequest and rejectConsentRequest so rejectConsentRequest is called
only when action is exactly “reject”.

In `@examples/nextjs-app-router/middleware.ts`:
- Around line 34-51: Add an exported Next.js middleware config with a matcher
that excludes _next/static, _next/image, and favicon.ico from execution. Apply
the same configuration alongside middleware in
examples/nextjs-app-router/middleware.ts and
examples/nextjs-app-router-custom-components/middleware.ts; leave the middleware
request handling unchanged.

In `@packages/elements-react/src/theme/default/utils/oauth2-config.ts`:
- Line 4: Update the OryClientConfiguration import in oauth2-config.ts to use
the appropriate relative internal source path instead of the package public
entry point, preserving the existing type usage.

In `@packages/nextjs/src/app/consent.test.ts`:
- Around line 82-161: Extend both consent test describe blocks covering
acceptConsentRequest and rejectConsentRequest with identityId cases: stub
getOAuth2ConsentRequest with a request identity for each test, verify matching
identityId proceeds to the underlying accept/reject call, and verify mismatched
identityId throws the forbidden error without proceeding. Keep assertions
focused on the security guard and ensure the mocks support the lookup path.

In `@packages/nextjs/src/pages/client.ts`:
- Around line 27-29: Remove the explicit knownProxiedUrl value from the
guessPotentiallyProxiedOrySdkUrl call in the client factory, allowing the helper
to apply its safe client-side default without unconditionally accessing window
during SSR.

In `@packages/nextjs/src/pages/consent.ts`:
- Around line 76-79: Update the OAuth2 consent request flow in
serverSideOAuth2Client().getOAuth2ConsentRequest to log the caught error before
returning null. Preserve the existing graceful null fallback while ensuring
failures, including configuration and network errors, include useful error
details in the established logging mechanism.

In `@packages/nextjs/src/pages/session.ts`:
- Around line 47-55: Refactor the promise chain in the session-loading flow to
move setLoading(false) from both the success and error handlers into a shared
finally block. Keep setSession in the success handler and setError in the catch
handler, preserving their existing behavior.

In `@packages/nextjs/src/utils/rewrite.ts`:
- Around line 82-85: Update the null/undefined guard in the object-rewrite logic
to detect top-level arrays and return them unchanged before the
Object.fromEntries(Object.entries(...)) conversion. Preserve the existing
handling for non-array objects and nullish inputs.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 225bf684-e938-4b95-b2fa-4640b5eced41

📥 Commits

Reviewing files that changed from the base of the PR and between 11d7877 and 7ec7ef5.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (42)
  • .changeset/oauth2-consent-flow.md
  • .gitignore
  • examples/nextjs-app-router-custom-components/.env
  • examples/nextjs-app-router-custom-components/app/api/consent/route.ts
  • examples/nextjs-app-router-custom-components/app/auth/consent/consent-form.tsx
  • examples/nextjs-app-router-custom-components/app/auth/consent/page.tsx
  • examples/nextjs-app-router-custom-components/components/consent-utils.ts
  • examples/nextjs-app-router-custom-components/components/custom-consent-scope-checkbox.tsx
  • examples/nextjs-app-router-custom-components/components/custom-footer.tsx
  • examples/nextjs-app-router-custom-components/components/index.tsx
  • examples/nextjs-app-router-custom-components/middleware.ts
  • examples/nextjs-app-router-custom-components/package.json
  • examples/nextjs-app-router/.env
  • examples/nextjs-app-router/app/api/consent/route.ts
  • examples/nextjs-app-router/app/auth/consent/consent-form.tsx
  • examples/nextjs-app-router/app/auth/consent/page.tsx
  • examples/nextjs-app-router/middleware.ts
  • examples/nextjs-app-router/package.json
  • examples/nextjs-pages-router/.env
  • examples/nextjs-pages-router/middleware.ts
  • examples/nextjs-pages-router/package.json
  • examples/nextjs-pages-router/pages/api/consent.ts
  • examples/nextjs-pages-router/pages/auth/consent.tsx
  • packages/elements-react/src/components/card/card-consent.test.ts
  • packages/elements-react/src/components/card/card-consent.tsx
  • packages/elements-react/src/components/form/nodes/input.tsx
  • packages/elements-react/src/components/form/nodes/node-button.tsx
  • packages/elements-react/src/theme/default/flows/consent.tsx
  • packages/elements-react/src/theme/default/utils/oauth2-config.ts
  • packages/elements-react/src/theme/default/utils/oauth2.ts
  • packages/nextjs/api-report/nextjs-client.api.json
  • packages/nextjs/api-report/nextjs.api.md
  • packages/nextjs/src/app/client.ts
  • packages/nextjs/src/app/consent.test.ts
  • packages/nextjs/src/app/consent.ts
  • packages/nextjs/src/app/index.ts
  • packages/nextjs/src/pages/client.ts
  • packages/nextjs/src/pages/consent.ts
  • packages/nextjs/src/pages/index.ts
  • packages/nextjs/src/pages/session.ts
  • packages/nextjs/src/utils/rewrite.test.ts
  • packages/nextjs/src/utils/rewrite.ts
💤 Files with no reviewable changes (1)
  • packages/elements-react/src/components/form/nodes/node-button.tsx

Comment thread examples/nextjs-app-router/middleware.ts Outdated
Comment thread examples/nextjs-pages-router/pages/auth/consent.tsx
Comment thread packages/nextjs/src/app/consent.ts
Comment thread packages/nextjs/src/utils/rewrite.ts
Jorgagu added 2 commits July 21, 2026 00:18
Make identityId mandatory in acceptConsentRequest and
rejectConsentRequest so the subject check cannot be skipped by omission,
via a shared assertConsentSubject helper.

Harden rewriteUrls against prefix collisions: a lookahead enforces a
domain boundary so hostnames merely sharing the base URL prefix are left
untouched, and UI path overrides only match whole path segments so
"/login" no longer captures "/login-methods".
Return result.response from csrfProtect instead of the pre-protection
response. The current adapter mutates the passed response in place, but
the processed response is the documented contract and stays correct if
that implementation detail changes.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/nextjs/src/utils/rewrite.ts`:
- Around line 56-60: In the uiPathMappings loop, replace the
String.prototype.replace call with string concatenation using path.slice so the
configUrl value is never interpreted for replacement patterns. Preserve the
existing matched uiPath prefix and append the unchanged suffix after uiPath.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: fcd8702f-078b-4c16-a113-6bb5e54a802e

📥 Commits

Reviewing files that changed from the base of the PR and between 7ec7ef5 and 9fb70db.

📒 Files selected for processing (9)
  • examples/nextjs-app-router-custom-components/middleware.ts
  • examples/nextjs-app-router/middleware.ts
  • examples/nextjs-pages-router/middleware.ts
  • packages/nextjs/api-report/nextjs-client.api.json
  • packages/nextjs/api-report/nextjs.api.md
  • packages/nextjs/src/app/consent.test.ts
  • packages/nextjs/src/app/consent.ts
  • packages/nextjs/src/utils/rewrite.test.ts
  • packages/nextjs/src/utils/rewrite.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • examples/nextjs-pages-router/middleware.ts
  • packages/nextjs/src/utils/rewrite.test.ts
  • packages/nextjs/api-report/nextjs.api.md
  • examples/nextjs-app-router/middleware.ts
  • examples/nextjs-app-router-custom-components/middleware.ts
  • packages/nextjs/src/app/consent.test.ts

Comment on lines +56 to +60
for (const [uiPath, configUrl] of Object.entries(uiPathMappings)) {
if (path && configUrl && pathStartsWithSegment(path, uiPath)) {
return path.replace(uiPath, new URL(configUrl, selfUrl).toString())
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Avoid pattern interpolation in String.prototype.replace.

When using String.prototype.replace(searchValue, replaceValue), the replaceValue string is parsed for special replacement patterns like $&, $', or $n. Since configUrl is populated from the project configuration, if it contains any of these sequences (for example, within a ?redirect=$& query parameter), they will be unintentionally evaluated and replaced with the matched string.

Use string concatenation with path.slice instead, which is safer and avoids pattern interpolation entirely.

🛠️ Proposed fix
     for (const [uiPath, configUrl] of Object.entries(uiPathMappings)) {
       if (path && configUrl && pathStartsWithSegment(path, uiPath)) {
-        return path.replace(uiPath, new URL(configUrl, selfUrl).toString())
+        return new URL(configUrl, selfUrl).toString() + path.slice(uiPath.length)
       }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (const [uiPath, configUrl] of Object.entries(uiPathMappings)) {
if (path && configUrl && pathStartsWithSegment(path, uiPath)) {
return path.replace(uiPath, new URL(configUrl, selfUrl).toString())
}
}
for (const [uiPath, configUrl] of Object.entries(uiPathMappings)) {
if (path && configUrl && pathStartsWithSegment(path, uiPath)) {
return new URL(configUrl, selfUrl).toString() + path.slice(uiPath.length)
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/nextjs/src/utils/rewrite.ts` around lines 56 - 60, In the
uiPathMappings loop, replace the String.prototype.replace call with string
concatenation using path.slice so the configUrl value is never interpreted for
replacement patterns. Preserve the existing matched uiPath prefix and append the
unchanged suffix after uiPath.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Ory Hydra login and consent features to Next.js examples and Next.js package

5 participants