Skip to content

#2101 Wildcard in redirection URI support#2154

Open
anushasunkada wants to merge 4 commits intoasgardeo:mainfrom
anushasunkada:fix-2101
Open

#2101 Wildcard in redirection URI support#2154
anushasunkada wants to merge 4 commits intoasgardeo:mainfrom
anushasunkada:fix-2101

Conversation

@anushasunkada
Copy link
Copy Markdown

@anushasunkada anushasunkada commented Apr 3, 2026

Purpose

Wildcard support in the redirect_uri's both during registration and authorization calls.

Approach

Approch is documented here - feature-docs/wildcard-redirect-uri.md

Related Issues

  • N/A

Related PRs

  • N/A

Checklist

  • Followed the contribution guidelines.
  • Manual test round performed and verified.
  • Documentation provided. (Add links if there are any)
    • Ran Vale and fixed all errors and warnings
  • Tests provided. (Add links if there are any)
    • Unit Tests
    • Integration Tests
  • Breaking changes. (Fill if applicable)
    • Breaking changes section filled.
    • breaking change label added.

Security checks

  • Followed secure coding standards in WSO2 Secure Coding Guidelines
  • Confirmed that this PR doesn't commit any keys, passwords, tokens, usernames, or other secrets.

Summary by CodeRabbit

Release Notes

  • New Features

    • OAuth2 applications now support wildcard patterns in redirect URIs. Paths can use * (single segment) and ** (zero or more segments) wildcards, while scheme, domain, and query parameters require exact matching.
  • Documentation

    • Added comprehensive feature documentation covering wildcard redirect URI configuration and behavior.

Signed-off-by: anushasunkada <anushasunkada@gmail.com>
Signed-off-by: anushasunkada <anushasunkada@gmail.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 3, 2026

📝 Walkthrough

Walkthrough

This PR implements wildcard redirect URI support for OAuth2 authorization flows. It adds pattern matching logic to validate redirect URIs against registered patterns using * (single segment) and ** (multi-segment) wildcards, while maintaining exact-match requirements for scheme, host, and query components. Changes span backend validation, OAuth model updates, utilities, feature documentation, and E2E tests.

Changes

Cohort / File(s) Summary
Documentation Updates
AGENTS.md, feature-docs/wildcard-redirect-uri.md
Added feature documentation defining wildcard redirect URI matching rules (path-only with * and ** patterns, exact-match for scheme/host/query). Updated AGENTS.md with explicit relative links to Makefile, README, and docs/content directory.
Redirect URI Pattern Matching Utility
backend/internal/system/utils/http_util.go, backend/internal/system/utils/http_util_test.go
Implemented MatchRedirectURIPattern function with recursive segment-matching for * (single segment) and ** (multi-segment) wildcards, requiring exact scheme/host/query matches and enforcing 32-level recursion depth limit. Added 167 lines of comprehensive test coverage.
OAuth App Model Validation
backend/internal/application/model/oauth_app.go, backend/internal/application/model/oauth_app_test.go
Added matchAnyRedirectURIPattern helper to check redirect URIs against registered patterns with direct equality and wildcard matching. Updated validateRedirectURI to handle wildcard rejection for empty incoming URIs. Added test suite covering wildcard pattern matching semantics and precedence behavior.
Service Layer Validation
backend/internal/application/service.go, backend/internal/application/service_test.go
Enhanced validateRedirectURIs to reject wildcard characters in scheme, host, and query components while allowing * and ** in path only. Added containsRegexMetacharacter helper to block regex metacharacters in paths. Extended test coverage with 77 new assertion cases.
OAuth Authorization Validation Tests
backend/internal/oauth/oauth2/authz/validator_test.go
Added TestValidateInitialAuthorizationRequest_WildcardRedirectURI test covering pattern matching during authorization requests, including single/multi-segment wildcards, query mismatches, omitted redirect_uri, and first-match-wins evaluation order.
End-to-End Testing
tests/e2e/tests/oauth-app/wildcard-redirect-uri.spec.ts
Added Playwright API-only E2E test suite validating wildcard redirect URI behavior end-to-end, covering registration validation rules (rejecting invalid wildcard usage), authorization request matching semantics (single segment, multi-segment, exact URIs, failures), and first-match precedence.

Sequence Diagram(s)

sequenceDiagram
    participant Client as OAuth Client
    participant AuthServer as Authorization Server
    participant AppRegistry as App Registry/Database
    participant PatternMatcher as Pattern Matcher Utility

    rect rgba(220, 100, 100, 0.5)
    Note over Client,AppRegistry: OAuth App Registration Flow

    Client->>AuthServer: POST /applications (with redirectUris: ["https://app.com/callback/*"])
    AuthServer->>AuthServer: validateRedirectURIs()
    AuthServer->>AuthServer: Check: wildcard not in scheme/host/query
    AuthServer->>AuthServer: Check: only * and ** in path (no regex chars)
    AuthServer->>AppRegistry: Store app with registered redirectUris
    AppRegistry-->>AuthServer: App created
    AuthServer-->>Client: 201 Created
    end

    rect rgba(100, 220, 100, 0.5)
    Note over Client,AppRegistry: OAuth Authorization Request Flow

    Client->>AuthServer: GET /oauth2/authorize?redirect_uri=https://app.com/callback/success
    AuthServer->>AppRegistry: Fetch app's registered redirectUris
    AppRegistry-->>AuthServer: Returns: ["https://app.com/callback/*"]
    AuthServer->>PatternMatcher: MatchRedirectURIPattern(pattern, incoming)
    PatternMatcher->>PatternMatcher: Parse URIs (scheme, host, path, query)
    PatternMatcher->>PatternMatcher: Compare scheme/host exactly
    PatternMatcher->>PatternMatcher: matchPathPattern("callback/*", "callback/success")
    PatternMatcher-->>AuthServer: true (match)
    AuthServer->>AuthServer: validateInitialAuthorizationRequest() passes
    AuthServer-->>Client: Redirect to login/consent
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

  • Wildcard Redirect URI Support #2101: Implements the wildcard redirect URI feature request with pattern matching logic, validation rules across multiple layers, comprehensive test coverage, and feature documentation.

Possibly related PRs

Suggested labels

Type/Improvement

Suggested reviewers

  • ThaminduDilshan
  • jeradrutnam
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding wildcard support in redirect URIs. It directly reflects the primary purpose of the PR.
Description check ✅ Passed The description includes the required Purpose and Approach sections with clear explanations. The checklist is substantially completed with most critical items checked (contribution guidelines, manual testing, documentation, unit/integration tests, security confirmation). Only the Vale linting step and breaking changes sections are unchecked, which are non-critical for this feature.
Docstring Coverage ✅ Passed Docstring coverage is 93.75% which is sufficient. The required threshold is 80.00%.

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

✨ 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 and usage tips.

@anushasunkada anushasunkada changed the title Fix 2101 #2101 Wildcard in redirection URI support Apr 3, 2026
@anushasunkada
Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 3, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@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: 5

Caution

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

⚠️ Outside diff range comments (3)
backend/internal/application/service.go (2)

829-875: ⚠️ Potential issue | 🟠 Major

Add end-user docs under docs/content/guides/ for wildcard redirect URIs.

This changes the OAuth client-registration and authorization-flow contract, but the PR only adds an internal design note under feature-docs/. Please update the OAuth/OIDC application guide in docs/content/guides/—or add a dedicated wildcard redirect URI guide there—to cover allowed wildcard syntax, deep-link limitations, and when callers must send a concrete redirect_uri.
As per coding guidelines, "This PR introduces a user-facing change (e.g., new/modified API endpoint, configuration option, authentication flow, or SDK behavior) but does not include corresponding documentation updates under docs/."

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

In `@backend/internal/application/service.go` around lines 829 - 875, Add a
user-facing guide under docs/content/guides that documents the new wildcard
redirect URI behavior introduced around redirectURI validation (the code paths
referencing sysutils.ParseURL, ErrorInvalidRedirectURI,
containsRegexMetacharacter and the checks for scheme/host/path/query/fragment).
The guide should state the exact allowed wildcard syntax in the path (* and **),
explain that wildcards are disallowed in scheme/host/query/fragment and that
fragments are forbidden, describe deep-linking limitations and examples of when
callers must supply a concrete redirect_uri during authorization/registration,
and include migration/compatibility notes and examples for client implementers.

869-892: ⚠️ Potential issue | 🟠 Major

Avoid blanket metacharacter rejection on redirect paths.

containsRegexMetacharacter(parsedURI.Path) will reject legitimate redirect URIs like /callback.html, because . is a normal literal path character. That turns valid exact redirect URIs into registration failures. If the goal is to constrain wildcard usage, validate */** placement separately from URI path validity instead of rejecting every regex-like character up front. Based on learnings, ensure all identity-related code aligns with relevant RFC specifications.

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

In `@backend/internal/application/service.go` around lines 869 - 892, The current
containsRegexMetacharacter(parsedURI.Path) used in the redirect URI validation
(see containsRegexMetacharacter and its call in the redirect URI loop)
incorrectly rejects valid path characters like '.'; change the validation so it
no longer treats normal literal path characters as invalid: remove '.' from the
banned metacharacter set and/or eliminate this blanket rejection, and instead
implement a focused check that only enforces allowed wildcard usage (validate
'*' and '**' placement and that no other regex-exclusive tokens like
[](){}+?^$|\ are present) while leaving ordinary path punctuation (e.g., '.')
permitted; update containsRegexMetacharacter (or replace it with a more specific
function) and adjust the redirect URI validation logic to call the new check so
exact URIs pass but improper regex tokens or misplaced wildcards are still
rejected.
backend/internal/application/model/oauth_app.go (1)

216-274: ⚠️ Potential issue | 🟠 Major

Add published docs for wildcard redirectUris before merge.

This changes the registration contract for redirectUris and the authorization-time redirect_uri validation rules, but the PR only references internal feature docs. Please add customer-facing docs under docs/—at minimum an API note in docs/content/apis.mdx and a guide/update under docs/content/guides/ covering */** path semantics, invalid placements in scheme/host/query, and the requirement to send an explicit redirect_uri when a wildcard URI is registered.

As per coding guidelines, "This PR introduces a user-facing change (e.g., new/modified API endpoint, configuration option, authentication flow, or SDK behavior) but does not include corresponding documentation updates under docs/."

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

In `@backend/internal/application/model/oauth_app.go` around lines 216 - 274, The
PR adds wildcard support for redirectUris (see matchAnyRedirectURIPattern and
utils.MatchRedirectURIPattern) and changes validation rules but lacks
user-facing docs; add an API note in docs/content/apis.mdx describing the new
redirectUris registration contract and authorization-time behavior (explicit
redirect_uri required when a wildcard is registered), and add a guide under
docs/content/guides/ explaining the wildcard path semantics for `*` vs `**`,
where wildcards are invalid (scheme, host, query, fragment), examples of
valid/invalid registrations and authorization requests, and a note about
fragment prohibition and error messages returned by validation functions (e.g.,
the errors returned by matchAnyRedirectURIPattern and the redirect URI
parsing/validation paths).
🧹 Nitpick comments (3)
backend/internal/system/utils/http_util_test.go (1)

891-1055: Add one mixed-case scheme/host case to lock the matcher contract.

The table says a lot about path semantics, but every URI here uses lowercase scheme and host. One case like HTTPS://Example.com/callback vs https://example.com/callback would prevent a regression to raw string comparison. Based on learnings, ensure all identity-related code aligns with relevant RFC specifications.

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

In `@backend/internal/system/utils/http_util_test.go` around lines 891 - 1055, Add
a test case in the TestMatchRedirectURIPattern table to verify scheme/host
comparison is case-insensitive (per RFC): call out the MatchRedirectURIPattern
function and add an entry where the pattern is "https://example.com/callback"
and the incoming URI uses mixed-case scheme/host like
"HTTPS://Example.com/callback" (wantMatch: true), so the matcher is exercised
for case-normalization rather than raw string equality; place this new case
alongside the other positive matches in the tests slice.
backend/internal/application/model/oauth_app.go (1)

230-235: Make the matcher error contract explicit.

matchAnyRedirectURIPattern() always swallows utils.MatchRedirectURIPattern() errors with continue, so the error branch at Lines 232-235 is currently unreachable. Either propagate matcher errors or collapse the helper to a plain bool return so malformed-pattern handling is explicit.

Also applies to: 257-273

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

In `@backend/internal/application/model/oauth_app.go` around lines 230 - 235, The
matcher helper matchAnyRedirectURIPattern currently swallows errors from
utils.MatchRedirectURIPattern (making the caller error branch unreachable);
update the contract so malformed pattern errors are explicit: either change
matchAnyRedirectURIPattern to return (bool, error) and propagate any
utils.MatchRedirectURIPattern errors up to callers (and update both call sites
used around the other block at the second usage) or collapse it into a plain
bool and validate/parse patterns separately before matching so callers never
expect an error; adjust the callers (the two places invoking
matchAnyRedirectURIPattern) to handle the new return type accordingly.
tests/e2e/tests/oauth-app/wildcard-redirect-uri.spec.ts (1)

228-386: Use unique app names in the registration suite too.

This block still uses fixed names like e2e-wc-ac04, while the authz suite already adds a Date.now() suffix. If a previous run leaks an app or the suite runs against a reused tenant, these cases can fail for reasons unrelated to wildcard validation.

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

In `@tests/e2e/tests/oauth-app/wildcard-redirect-uri.spec.ts` around lines 228 -
386, The tests use fixed application names (e.g., the "name" field values like
"e2e-wc-ac01", "e2e-wc-ac04", "e2e-wc-ac13", etc.) which can collide across
runs; update each POST payload in these tests to generate a unique name (append
a timestamp or random suffix such as Date.now() or a UUID) when setting the name
before calling the API (keep createdAppIds.push(body.id) unchanged). Ensure all
tests in this file that set name values (AC-01 through AC-05, AC-13, AC-14) use
the same uniqueness pattern so runs against reused tenants won’t conflict.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/internal/system/utils/http_util.go`:
- Around line 103-159: The recursive matcher currently uses a global depth cap
which rejects valid deep paths; replace this with memoization of (i,j) to bound
work. In matchPathPattern, create a memo map (e.g., map[[2]int]bool or
map[string]bool) and pass it into matchSegs; in matchSegs (and remove the depth
parameter), check the memo for the current (i,j) and return false if already
visited, otherwise mark (i,j) visited before exploring recursion; keep all
existing segment logic (*, **) but eliminate the depth > 32 checks so
deep-but-valid patterns like /** succeed. Ensure you update signatures:
matchPathPattern -> matchSegs to accept the memo and remove depth usage.
- Around line 70-159: The code introduces user-visible wildcard redirect_uri
semantics via MatchRedirectURIPattern and helpers matchPathPattern/matchSegs;
add public documentation before merging by updating the OAuth/application API
reference and guides to describe the new '*' (single segment) and '**'
(zero-or-more segments) wildcards, the restriction that wildcards are only
allowed in the path, and the exact matching rules for scheme, host
(case-insensitive per RFC 3986), and query; include examples of valid/invalid
pattern+incoming URIs, note the fragment is disallowed, and link to the internal
feature-docs for implementation details so users can understand behavior and
migrate safely.
- Around line 97-109: The matcher is using percent-decoded URL.Path which allows
encoded slashes to be treated as real separators; update the call that invokes
matchPathPattern to pass the escaped paths instead (use EscapedPath() for both
pattern and incoming URLs) so matchPathPattern and its matchSegs logic operate
on percent-encoded segments and `%2F` cannot be interpreted as a path separator.

In `@feature-docs/wildcard-redirect-uri.md`:
- Line 16: The current bullet "At least one redirection uri MUST be registrated
per client." is too broad; change it to require redirect URIs only for
authorization-based clients by rewording it to something like "Clients that use
the authorization_code grant MUST register at least one redirection URI." Ensure
the phrasing references the authorization_code grant (or "authorization-based
clients") so the documented requirement matches the validator logic.

In `@tests/e2e/tests/oauth-app/wildcard-redirect-uri.spec.ts`:
- Around line 406-430: Initialize the created app variables (singleStarApp,
doubleStarApp, exactApp, queryApp, multiApp) to null before test.beforeAll and
make test.afterAll resilient: check each variable for truthiness before calling
deleteApp (e.g. if (singleStarApp) await deleteApp(ctx, adminToken,
singleStarApp.appId)), wrap each deleteApp call in its own try/catch so one
failing delete doesn't stop the rest, and ensure ctx.dispose() is called in a
finally-like manner (guarding if ctx exists) so resources are always released
even if beforeAll partially fails; update references to createApp/deleteApp
accordingly.

---

Outside diff comments:
In `@backend/internal/application/model/oauth_app.go`:
- Around line 216-274: The PR adds wildcard support for redirectUris (see
matchAnyRedirectURIPattern and utils.MatchRedirectURIPattern) and changes
validation rules but lacks user-facing docs; add an API note in
docs/content/apis.mdx describing the new redirectUris registration contract and
authorization-time behavior (explicit redirect_uri required when a wildcard is
registered), and add a guide under docs/content/guides/ explaining the wildcard
path semantics for `*` vs `**`, where wildcards are invalid (scheme, host,
query, fragment), examples of valid/invalid registrations and authorization
requests, and a note about fragment prohibition and error messages returned by
validation functions (e.g., the errors returned by matchAnyRedirectURIPattern
and the redirect URI parsing/validation paths).

In `@backend/internal/application/service.go`:
- Around line 829-875: Add a user-facing guide under docs/content/guides that
documents the new wildcard redirect URI behavior introduced around redirectURI
validation (the code paths referencing sysutils.ParseURL,
ErrorInvalidRedirectURI, containsRegexMetacharacter and the checks for
scheme/host/path/query/fragment). The guide should state the exact allowed
wildcard syntax in the path (* and **), explain that wildcards are disallowed in
scheme/host/query/fragment and that fragments are forbidden, describe
deep-linking limitations and examples of when callers must supply a concrete
redirect_uri during authorization/registration, and include
migration/compatibility notes and examples for client implementers.
- Around line 869-892: The current containsRegexMetacharacter(parsedURI.Path)
used in the redirect URI validation (see containsRegexMetacharacter and its call
in the redirect URI loop) incorrectly rejects valid path characters like '.';
change the validation so it no longer treats normal literal path characters as
invalid: remove '.' from the banned metacharacter set and/or eliminate this
blanket rejection, and instead implement a focused check that only enforces
allowed wildcard usage (validate '*' and '**' placement and that no other
regex-exclusive tokens like [](){}+?^$|\ are present) while leaving ordinary
path punctuation (e.g., '.') permitted; update containsRegexMetacharacter (or
replace it with a more specific function) and adjust the redirect URI validation
logic to call the new check so exact URIs pass but improper regex tokens or
misplaced wildcards are still rejected.

---

Nitpick comments:
In `@backend/internal/application/model/oauth_app.go`:
- Around line 230-235: The matcher helper matchAnyRedirectURIPattern currently
swallows errors from utils.MatchRedirectURIPattern (making the caller error
branch unreachable); update the contract so malformed pattern errors are
explicit: either change matchAnyRedirectURIPattern to return (bool, error) and
propagate any utils.MatchRedirectURIPattern errors up to callers (and update
both call sites used around the other block at the second usage) or collapse it
into a plain bool and validate/parse patterns separately before matching so
callers never expect an error; adjust the callers (the two places invoking
matchAnyRedirectURIPattern) to handle the new return type accordingly.

In `@backend/internal/system/utils/http_util_test.go`:
- Around line 891-1055: Add a test case in the TestMatchRedirectURIPattern table
to verify scheme/host comparison is case-insensitive (per RFC): call out the
MatchRedirectURIPattern function and add an entry where the pattern is
"https://example.com/callback" and the incoming URI uses mixed-case scheme/host
like "HTTPS://Example.com/callback" (wantMatch: true), so the matcher is
exercised for case-normalization rather than raw string equality; place this new
case alongside the other positive matches in the tests slice.

In `@tests/e2e/tests/oauth-app/wildcard-redirect-uri.spec.ts`:
- Around line 228-386: The tests use fixed application names (e.g., the "name"
field values like "e2e-wc-ac01", "e2e-wc-ac04", "e2e-wc-ac13", etc.) which can
collide across runs; update each POST payload in these tests to generate a
unique name (append a timestamp or random suffix such as Date.now() or a UUID)
when setting the name before calling the API (keep createdAppIds.push(body.id)
unchanged). Ensure all tests in this file that set name values (AC-01 through
AC-05, AC-13, AC-14) use the same uniqueness pattern so runs against reused
tenants won’t conflict.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f1218cdb-42c1-41de-a08e-2be3e27e3d1e

📥 Commits

Reviewing files that changed from the base of the PR and between bb606f8 and a2998df.

📒 Files selected for processing (10)
  • AGENTS.md
  • backend/internal/application/model/oauth_app.go
  • backend/internal/application/model/oauth_app_test.go
  • backend/internal/application/service.go
  • backend/internal/application/service_test.go
  • backend/internal/oauth/oauth2/authz/validator_test.go
  • backend/internal/system/utils/http_util.go
  • backend/internal/system/utils/http_util_test.go
  • feature-docs/wildcard-redirect-uri.md
  • tests/e2e/tests/oauth-app/wildcard-redirect-uri.spec.ts

Comment on lines +70 to +159
// MatchRedirectURIPattern reports whether incoming matches pattern.
// pattern may contain * (exactly one path segment) or ** (zero or more path segments)
// only in the path component. Scheme, host, and query must match exactly (case-insensitive
// scheme and host per RFC 3986). Returns (false, error) for malformed inputs,
// (false, nil) for no match, (true, nil) for a match.
func MatchRedirectURIPattern(pattern, incoming string) (bool, error) {
patternURL, err := url.Parse(pattern)
if err != nil || patternURL.Scheme == "" || patternURL.Host == "" {
return false, errors.New("invalid pattern URI: missing scheme or host")
}
incomingURL, err := url.Parse(incoming)
if err != nil || incomingURL.Scheme == "" || incomingURL.Host == "" {
return false, errors.New("invalid incoming URI: missing scheme or host")
}

if !strings.EqualFold(patternURL.Scheme, incomingURL.Scheme) {
return false, nil
}
if !strings.EqualFold(patternURL.Host, incomingURL.Host) {
return false, nil
}
if patternURL.RawQuery != incomingURL.RawQuery {
return false, nil
}
if incomingURL.Fragment != "" {
return false, nil
}
return matchPathPattern(patternURL.Path, incomingURL.Path, 0), nil
}

// matchPathPattern reports whether incomingPath matches patternPath.
// Wildcards * (one segment) and ** (zero or more segments) are supported in patternPath.
// depth guards against excessive recursion.
func matchPathPattern(patternPath, incomingPath string, depth int) bool {
if depth > 32 {
return false
}
patSegs := strings.Split(patternPath, "/")
incSegs := strings.Split(incomingPath, "/")
return matchSegs(patSegs, incSegs, 0, 0, depth)
}

func matchSegs(patSegs, incSegs []string, i, j, depth int) bool {
if depth > 32 {
return false
}
// Both exhausted.
if i == len(patSegs) && j == len(incSegs) {
return true
}
// Pattern exhausted but incoming still has segments.
if i == len(patSegs) {
return false
}
// Incoming exhausted but pattern still has segments:
// only true if all remaining pattern segments are "**".
if j == len(incSegs) {
for k := i; k < len(patSegs); k++ {
if patSegs[k] != "**" {
return false
}
}
return true
}

pSeg := patSegs[i]

if pSeg == "**" {
// Try consuming zero incoming segments (advance pattern only).
if matchSegs(patSegs, incSegs, i+1, j, depth+1) {
return true
}
// Try consuming one incoming segment (keep pattern position).
return matchSegs(patSegs, incSegs, i, j+1, depth+1)
}

if pSeg == "*" {
// Must match exactly one non-empty segment.
if incSegs[j] == "" {
return false
}
return matchSegs(patSegs, incSegs, i+1, j+1, depth+1)
}

// Literal segment: must match exactly.
if pSeg != incSegs[j] {
return false
}
return matchSegs(patSegs, incSegs, i+1, j+1, depth+1)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🔴 Documentation Required.

This adds a user-visible OAuth behavior change: clients can now register and authorize with wildcard redirect_uri patterns. I only see internal notes under feature-docs/; please also update public docs under docs/—at minimum the OAuth/application API reference in docs/content/apis.mdx and the relevant guide in docs/content/guides/ (or a new wildcard redirect URI guide) to document */**, the path-only restriction, and the scheme/host/query rules before merge.

As per coding guidelines, "🔴 Documentation Required: This PR introduces a user-facing change (e.g., new/modified API endpoint, configuration option, authentication flow, or SDK behavior) but does not include corresponding documentation updates under docs/. Please update the relevant documentation before merging."

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

In `@backend/internal/system/utils/http_util.go` around lines 70 - 159, The code
introduces user-visible wildcard redirect_uri semantics via
MatchRedirectURIPattern and helpers matchPathPattern/matchSegs; add public
documentation before merging by updating the OAuth/application API reference and
guides to describe the new '*' (single segment) and '**' (zero-or-more segments)
wildcards, the restriction that wildcards are only allowed in the path, and the
exact matching rules for scheme, host (case-insensitive per RFC 3986), and
query; include examples of valid/invalid pattern+incoming URIs, note the
fragment is disallowed, and link to the internal feature-docs for implementation
details so users can understand behavior and migrate safely.

Comment on lines +103 to +159
func matchPathPattern(patternPath, incomingPath string, depth int) bool {
if depth > 32 {
return false
}
patSegs := strings.Split(patternPath, "/")
incSegs := strings.Split(incomingPath, "/")
return matchSegs(patSegs, incSegs, 0, 0, depth)
}

func matchSegs(patSegs, incSegs []string, i, j, depth int) bool {
if depth > 32 {
return false
}
// Both exhausted.
if i == len(patSegs) && j == len(incSegs) {
return true
}
// Pattern exhausted but incoming still has segments.
if i == len(patSegs) {
return false
}
// Incoming exhausted but pattern still has segments:
// only true if all remaining pattern segments are "**".
if j == len(incSegs) {
for k := i; k < len(patSegs); k++ {
if patSegs[k] != "**" {
return false
}
}
return true
}

pSeg := patSegs[i]

if pSeg == "**" {
// Try consuming zero incoming segments (advance pattern only).
if matchSegs(patSegs, incSegs, i+1, j, depth+1) {
return true
}
// Try consuming one incoming segment (keep pattern position).
return matchSegs(patSegs, incSegs, i, j+1, depth+1)
}

if pSeg == "*" {
// Must match exactly one non-empty segment.
if incSegs[j] == "" {
return false
}
return matchSegs(patSegs, incSegs, i+1, j+1, depth+1)
}

// Literal segment: must match exactly.
if pSeg != incSegs[j] {
return false
}
return matchSegs(patSegs, incSegs, i+1, j+1, depth+1)
}
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Apr 3, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

The 32-step cap rejects valid deep paths.

Line 104 and Line 113 increment depth on every recursive step, not just on wildcard expansion. That means a legitimate match with more than 32 path steps can fail even when the pattern is compatible, including simple /** cases. A memoized (i, j) matcher would keep the work bounded without turning deep-but-valid redirect URIs into false negatives.

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

In `@backend/internal/system/utils/http_util.go` around lines 103 - 159, The
recursive matcher currently uses a global depth cap which rejects valid deep
paths; replace this with memoization of (i,j) to bound work. In
matchPathPattern, create a memo map (e.g., map[[2]int]bool or map[string]bool)
and pass it into matchSegs; in matchSegs (and remove the depth parameter), check
the memo for the current (i,j) and return false if already visited, otherwise
mark (i,j) visited before exploring recursion; keep all existing segment logic
(*, **) but eliminate the depth > 32 checks so deep-but-valid patterns like /**
succeed. Ensure you update signatures: matchPathPattern -> matchSegs to accept
the memo and remove depth usage.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

We have intentionally capped it to 32-step depth during the path match.

- The redirect_uri in token requests is still validated with exact match logic per RFC 6749 §4.1.3; pattern matching is not applied there.
- Invalid redirect_uri should inform the resource owner of the error and MUST NOT automatically redirect the user-agent to the invalid redirection URI
- Support deeplinks for the mobile app redirections. wildcards are not allowed in the scheme part of the deeplink.
- At least one redirection uri MUST be registrated per client.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Don't require redirect URIs for every OAuth client.

Line 16 says every client must register at least one redirect URI, but the validator only enforces that when authorization_code is enabled. Please scope this bullet to authorization-based clients so the feature contract matches the implementation.

📝 Suggested edit
-- At least one redirection uri MUST be registrated per client.
+- Clients using the `authorization_code` grant MUST register at least one redirect URI.
📝 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
- At least one redirection uri MUST be registrated per client.
- Clients using the `authorization_code` grant MUST register at least one redirect URI.
🧰 Tools
🪛 LanguageTool

[grammar] ~16-~16: Ensure spelling is correct
Context: ... - At least one redirection uri MUST be registrated per client. ## Non-Goals - Query para...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

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

In `@feature-docs/wildcard-redirect-uri.md` at line 16, The current bullet "At
least one redirection uri MUST be registrated per client." is too broad; change
it to require redirect URIs only for authorization-based clients by rewording it
to something like "Clients that use the authorization_code grant MUST register
at least one redirection URI." Ensure the phrasing references the
authorization_code grant (or "authorization-based clients") so the documented
requirement matches the validator logic.

Comment on lines +406 to +430
test.beforeAll(async () => {
ctx = await playwrightRequest.newContext({ ignoreHTTPSErrors: true });
adminToken = getAdminTokenFromStoredSession();

const currentTimestamp = Date.now();
// Create apps sequentially to avoid SQLite database lock contention
singleStarApp = await createApp(ctx, adminToken, `e2e-wc-authz-single-${currentTimestamp}`, ["https://client.example.com/cb/*"]);
doubleStarApp = await createApp(ctx, adminToken, `e2e-wc-authz-double-${currentTimestamp}`, ["https://client.example.com/app/**/cb"]);
exactApp = await createApp(ctx, adminToken, `e2e-wc-authz-exact-${currentTimestamp}`, ["https://client.example.com/callback"]);
queryApp = await createApp(ctx, adminToken, `e2e-wc-authz-query-${currentTimestamp}`, ["https://client.example.com/cb?foo=bar"]);
multiApp = await createApp(ctx, adminToken, `e2e-wc-authz-multi-${currentTimestamp}`, [
"https://client.example.com/a/*",
"https://client.example.com/b/*",
]);
});

test.afterAll(async () => {
// Delete apps sequentially to avoid SQLite database lock contention
await deleteApp(ctx, adminToken, singleStarApp.appId);
await deleteApp(ctx, adminToken, doubleStarApp.appId);
await deleteApp(ctx, adminToken, exactApp.appId);
await deleteApp(ctx, adminToken, queryApp.appId);
await deleteApp(ctx, adminToken, multiApp.appId);
await ctx.dispose();
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Guard cleanup against partial beforeAll failures.

If one createApp() call fails, the remaining singleStarApp / doubleStarApp / ... vars stay uninitialized and Lines 424-428 will throw before cleanup finishes. That can mask the original failure and leak the apps that were already created.

Suggested hardening
 test.describe("Wildcard Redirect URI — Authorization Request Matching", () => {
   let ctx: APIRequestContext;
   let adminToken: string;
+  const createdApps: CreatedApp[] = [];
 ...
-    singleStarApp = await createApp(ctx, adminToken, `e2e-wc-authz-single-${currentTimestamp}`, ["https://client.example.com/cb/*"]);
-    doubleStarApp = await createApp(ctx, adminToken, `e2e-wc-authz-double-${currentTimestamp}`, ["https://client.example.com/app/**/cb"]);
-    exactApp = await createApp(ctx, adminToken, `e2e-wc-authz-exact-${currentTimestamp}`, ["https://client.example.com/callback"]);
-    queryApp = await createApp(ctx, adminToken, `e2e-wc-authz-query-${currentTimestamp}`, ["https://client.example.com/cb?foo=bar"]);
-    multiApp = await createApp(ctx, adminToken, `e2e-wc-authz-multi-${currentTimestamp}`, [
+    singleStarApp = await createApp(ctx, adminToken, `e2e-wc-authz-single-${currentTimestamp}`, ["https://client.example.com/cb/*"]);
+    createdApps.push(singleStarApp);
+    doubleStarApp = await createApp(ctx, adminToken, `e2e-wc-authz-double-${currentTimestamp}`, ["https://client.example.com/app/**/cb"]);
+    createdApps.push(doubleStarApp);
+    exactApp = await createApp(ctx, adminToken, `e2e-wc-authz-exact-${currentTimestamp}`, ["https://client.example.com/callback"]);
+    createdApps.push(exactApp);
+    queryApp = await createApp(ctx, adminToken, `e2e-wc-authz-query-${currentTimestamp}`, ["https://client.example.com/cb?foo=bar"]);
+    createdApps.push(queryApp);
+    multiApp = await createApp(ctx, adminToken, `e2e-wc-authz-multi-${currentTimestamp}`, [
       "https://client.example.com/a/*",
       "https://client.example.com/b/*",
     ]);
+    createdApps.push(multiApp);
   });
 
   test.afterAll(async () => {
-    await deleteApp(ctx, adminToken, singleStarApp.appId);
-    await deleteApp(ctx, adminToken, doubleStarApp.appId);
-    await deleteApp(ctx, adminToken, exactApp.appId);
-    await deleteApp(ctx, adminToken, queryApp.appId);
-    await deleteApp(ctx, adminToken, multiApp.appId);
+    for (const app of createdApps) {
+      await deleteApp(ctx, adminToken, app.appId);
+    }
     await ctx.dispose();
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/e2e/tests/oauth-app/wildcard-redirect-uri.spec.ts` around lines 406 -
430, Initialize the created app variables (singleStarApp, doubleStarApp,
exactApp, queryApp, multiApp) to null before test.beforeAll and make
test.afterAll resilient: check each variable for truthiness before calling
deleteApp (e.g. if (singleStarApp) await deleteApp(ctx, adminToken,
singleStarApp.appId)), wrap each deleteApp call in its own try/catch so one
failing delete doesn't stop the rest, and ensure ctx.dispose() is called in a
finally-like manner (guarding if ctx exists) so resources are always released
even if beforeAll partially fails; update references to createApp/deleteApp
accordingly.

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 6, 2026

Codecov Report

❌ Patch coverage is 92.04545% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.84%. Comparing base (7ad7b59) to head (2d9c3fc).
⚠️ Report is 38 commits behind head on main.

Files with missing lines Patch % Lines
backend/internal/application/model/oauth_app.go 73.68% 3 Missing and 2 partials ⚠️
backend/internal/system/utils/http_util.go 95.65% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2154      +/-   ##
==========================================
- Coverage   89.84%   89.84%   -0.01%     
==========================================
  Files         898      898              
  Lines       58951    59038      +87     
==========================================
+ Hits        52967    53044      +77     
- Misses       4418     4425       +7     
- Partials     1566     1569       +3     
Flag Coverage Δ
backend-integration-postgres 53.82% <13.63%> (-0.09%) ⬇️
backend-integration-sqlite 53.78% <13.63%> (-0.09%) ⬇️
backend-unit 84.61% <92.04%> (+<0.01%) ⬆️
frontend-apps-console-unit 90.41% <ø> (+<0.01%) ⬆️
frontend-apps-gate-unit 97.48% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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