Skip to content

Support per-request OAuth auth context - #10

Merged
akriaueno merged 7 commits into
developfrom
spike/auth-per-request-resolvers
Jun 13, 2026
Merged

akriaueno merged 7 commits into
developfrom
spike/auth-per-request-resolvers

Conversation

@akriaueno

@akriaueno akriaueno commented Jun 9, 2026

Copy link
Copy Markdown
Member

Summary

  • Pass the current request context through OAuth token validation and challenge generation.
  • Change Urchin.Auth.TokenValidator to validate/3 so validators can resolve tenant-specific issuers, JWKS, introspection endpoints, or policy from the request.
  • Allow authorization_servers to be a per-request resolver for realm-aware Protected Resource Metadata.

Notes

  • This is a breaking Auth API change: validator modules and functions now use validate/3 / 3-arity callbacks.
  • resource remains static, so RFC 8707 audience binding continues to use the configured resource.

Testing

  • devbox run -- mix format
  • devbox run -- mix test

Summary by CodeRabbit

  • New Features

    • Per-request resolution of authorization servers and resource-metadata URLs for tenant-aware discovery and 401 challenges.
    • Pluggable authorizer seam that receives request context and returns normalized claims or structured errors.
  • Refactor

    • Request context now threads through authorization and challenge generation; validator-style callbacks replaced by the authorizer pattern.
  • Bug Fixes

    • Protected Resource Metadata responses include Cache-Control: no-store.
    • Issuer URLs with query strings are rejected.
  • Documentation

    • Docs and examples updated for authorizer flow and dynamic per-request configuration; release bumped to 0.4.0.

@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@akriaueno, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 46 minutes and 34 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 6a28177c-8b00-472d-becf-936c20dad7dd

📥 Commits

Reviewing files that changed from the base of the PR and between d565e33 and a503771.

📒 Files selected for processing (8)
  • CHANGELOG.md
  • README.md
  • lib/urchin/auth.ex
  • lib/urchin/auth/authorizer.ex
  • lib/urchin/auth/claims.ex
  • lib/urchin/auth/plug.ex
  • test/urchin/auth/plug_test.exs
  • test/urchin/auth_test.exs
📝 Walkthrough

Walkthrough

This PR replaces the token-validator entrypoints with a request-aware authorizer (authorize/3), threads Plug.Conn through metadata/authorization-server resolution and challenge construction, adds the Urchin.Auth.Authorizer behaviour, updates examples, and converts tests to the new authorizer/conn-aware contracts.

Changes

Conn-aware Authorizer Migration

Layer / File(s) Summary
Authorizer behaviour and validator contract
lib/urchin/auth/authorizer.ex, lib/urchin/auth/token_validator.ex, lib/urchin/auth/claims.ex
Adds Urchin.Auth.Authorizer behaviour (authorize/3) and updates token-validator docs/callback to validate/3 (conn-aware).
Core Auth refactor: types, construction, request helpers
lib/urchin/auth.ex
Replace token-validator wiring with authorizer; make authorization_servers/2, resource_metadata_url/2, metadata_document/2, and well_known_paths/2 accept conn; add authorize/3 entrypoint and update challenge/5/www-authenticate builders to include dynamic resource_metadata and stronger issuer validation.
Metadata & Plug updates
lib/urchin/auth/metadata.ex, lib/urchin/auth/plug.ex
Metadata endpoint and detection use conn-aware helpers and set Cache-Control: no-store; plug now calls Auth.authorize(auth, token, conn) and invokes Auth.challenge with conn.
Examples: authenticated_server & Keycloak
examples/authenticated_server.exs, examples/keycloak/server.exs, examples/keycloak/README.md
Replace token-validator examples with authorizer modules implementing authorize/3, update wiring to authorizer: ..., and document audience/scope checks and per-request metadata hooks.
Tests & test support
test/support/auth_authorizers.ex, test/support/auth_validators.ex, test/urchin/*
Add test Authorizer implementations and update test validators/helpers to 3-arity; update tests to pass conn into metadata generation, authorization calls, and challenge assertions; add tests for per-request authorization_servers and resource_metadata_url.
Docs & changelog
CHANGELOG.md, README.md, SECURITY.md, mix.exs
Document new authorizer seam, conn-aware configuration hooks (fn conn -> ... end for authorization_servers and resource_metadata_url), changed deployment guidance to require an authorizer enforcing signature/audience/scopes/tenant policy, and bump version to 0.4.0.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • urth-inc/urchin#8: Introduces Keycloak introspection example and overlaps with the Keycloak example rewritten here.
  • urth-inc/urchin#1: Earlier work on the Auth/Plug integration and token validator seam that this PR replaces with an authorizer/conn-aware flow.

Poem

🐰 I threaded Conn through each OAuth gate,
authorizers now choose each tenant’s fate,
metadata whispers per-request and realm,
tests hop in line, examples at the helm,
a little rabbit cheers — authorize, not wait!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 53.66% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Support per-request OAuth auth context' accurately and concisely captures the main objective of this PR: enabling per-request resolution of OAuth auth configuration (authorization servers, metadata URLs, and authorizer decisions) by threading the Plug.Conn through the auth stack.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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


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

@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.

🧹 Nitpick comments (1)
lib/urchin/auth.ex (1)

211-215: 💤 Low value

Runtime validation of dynamic resource_metadata_url may raise in request path.

When resource_metadata_url is a function, validate_resource_metadata_url!/1 is called at request time (line 214). If the function returns an invalid URL (non-absolute, non-http(s), or with a fragment), this will raise an ArgumentError during challenge generation, potentially causing an unhandled 500 instead of a graceful error response.

Consider whether this should be a softer validation that logs and falls back, or if raising is the intended behavior for misconfigured resolvers.

🤖 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 `@lib/urchin/auth.ex` around lines 211 - 215, The current
resource_metadata_url/2 calls validate_resource_metadata_url!/1 at request time
which can raise an ArgumentError if a dynamic resolver returns an invalid URL;
change this to safe validation by either calling a non-bang validator (e.g.,
validate_resource_metadata_url/1) or wrap the fun.(conn) + validation in a
try/rescue to catch ArgumentError, log the invalid value, and return a safe
fallback (nil or a configured static URL) instead of allowing the exception to
bubble; update references to resource_metadata_url/1 and
validate_resource_metadata_url!/1 accordingly so challenge generation never
raises on malformed resolver output.
🤖 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.

Nitpick comments:
In `@lib/urchin/auth.ex`:
- Around line 211-215: The current resource_metadata_url/2 calls
validate_resource_metadata_url!/1 at request time which can raise an
ArgumentError if a dynamic resolver returns an invalid URL; change this to safe
validation by either calling a non-bang validator (e.g.,
validate_resource_metadata_url/1) or wrap the fun.(conn) + validation in a
try/rescue to catch ArgumentError, log the invalid value, and return a safe
fallback (nil or a configured static URL) instead of allowing the exception to
bubble; update references to resource_metadata_url/1 and
validate_resource_metadata_url!/1 accordingly so challenge generation never
raises on malformed resolver output.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 4104a4ae-5f92-4adf-b8db-9d9ddb30111a

📥 Commits

Reviewing files that changed from the base of the PR and between ec78ea3 and 583c315.

📒 Files selected for processing (19)
  • CHANGELOG.md
  • README.md
  • SECURITY.md
  • examples/authenticated_server.exs
  • examples/keycloak/README.md
  • examples/keycloak/server.exs
  • lib/urchin/auth.ex
  • lib/urchin/auth/authorizer.ex
  • lib/urchin/auth/claims.ex
  • lib/urchin/auth/metadata.ex
  • lib/urchin/auth/plug.ex
  • lib/urchin/auth/token_validator.ex
  • test/support/auth_authorizers.ex
  • test/support/auth_validators.ex
  • test/urchin/auth/metadata_test.exs
  • test/urchin/auth/plug_test.exs
  • test/urchin/auth_test.exs
  • test/urchin/endpoint_auth_test.exs
  • test/urchin/transport/streamable_http_auth_test.exs
💤 Files with no reviewable changes (2)
  • lib/urchin/auth/token_validator.ex
  • test/support/auth_validators.ex
✅ Files skipped from review due to trivial changes (6)
  • SECURITY.md
  • examples/keycloak/README.md
  • lib/urchin/auth/authorizer.ex
  • CHANGELOG.md
  • lib/urchin/auth/claims.ex
  • README.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/urchin/auth/metadata.ex

akriaueno

This comment was marked as resolved.

akriaueno and others added 5 commits June 11, 2026 18:08
from_map/1 now reads both string- and atom-keyed maps so an authorizer that hand-builds atom-keyed claims is not silently dropped, and covers_resource?/2 returns false for a non-list audience instead of raising.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A missing or blank bearer token is now resolved by Urchin.Auth to a 401 :missing challenge before the authorizer runs, so the unauthenticated discovery bootstrap always gets the spec challenge instead of a 500 from an authorizer without a nil clause.

challenge/4 resolves the scope hint and resource_metadata URL itself; a per-request resolver that raises while the WWW-Authenticate header is built degrades to a valid challenge without the failed hint rather than escalating the 401/403 to a 500 with no header, and the scope hint is resolved only when a challenge is built. A foreign struct returned in {:ok, ...} is reported as a server error instead of crashing the pipeline.

Also dedupes the shared http(s)-URI validation and the challenge-kind type.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A :resource_metadata_url resolver preserves tenant context in the challenge (e.g. a query parameter), but Urchin serves the metadata document only at the static well-known paths derived from :resource; a resolver that points at a different path must be served by your own route or an external host.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@akriaueno
akriaueno merged commit 8307645 into develop Jun 13, 2026
6 checks passed
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