Skip to content

fix(auth): build CAS callbacks from a configured canonical origin - #310

Open
rlorenzo wants to merge 1 commit into
mainfrom
fix/cas-canonical-origin
Open

fix(auth): build CAS callbacks from a configured canonical origin#310
rlorenzo wants to merge 1 commit into
mainfrom
fix/cas-canonical-origin

Conversation

@rlorenzo

@rlorenzo rlorenzo commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Finding

CAS login, ticket validation and logout all built their service URL from HttpHelper.GetRootURL(), which derives the origin from Request.GetDisplayUrl(). AllowedHosts was "*" in every environment (no override in the Test or Production settings files). A Host header that got past IIS/F5/Cloudflare could therefore poison a CAS callback URL. A restrictive CAS service registry limits the impact, but the application should not depend on that external control.

Origins: GetRootURL() in db9745e (2023-05-05), switched to GetDisplayUrl() in 24417f5 (2023-06-23); the CAS login service in dc63022 (2023-05-22) and 47ecde9 (2023-08-29); AllowedHosts: "*" in cf06887 (2023-05-03).

Change

New web/Classes/PublicUrlService.cs

  • PublicUrlOptions binds Application:PublicBaseUrl.
  • IPublicUrlService.BaseUrl / BuildUrl(path) supply the canonical origin.
  • PublicUrlOptionsValidator runs via ValidateOnStart() and fails startup outside Development when the value is missing, relative, non-https, or carries user-info, a query string, or a fragment.
  • Development with no value configured falls back to the request, so the dynamic local port keeps working. Deployed environments never reach that path because startup validation would have failed.

Configuration

Environment Application:PublicBaseUrl AllowedHosts
Test https://secure-test.vetmed.ucdavis.edu/2 secure-test.vetmed.ucdavis.edu;localhost
Production https://viper.vetmed.ucdavis.edu/2 viper.vetmed.ucdavis.edu;localhost
Development unset (request-derived) * (unchanged)

Callers

  • HomeController injects IPublicUrlService; BuildRedirectUri, used by both the login redirect and the p3/serviceValidate call, and the logout service now come from configuration.
  • HttpHelper.GetRootURL() delegates to the same service, so the sitemap and the Directory emulation link stop being request-derived as well. It also now uses Request.PathBase instead of sniffing for a literal /2/ prefix, which fixes the edge case where a request to exactly /2 lost the base.
  • Login's returnURL.StartsWith("/api") guard now strips the PathBase first. The SPAs send ReturnUrl already prefixed (/2/api/...), so the guard never fired on TEST/PROD and an API caller got a CAS HTML redirect instead of a 401.

Forwarded headers were already limited to X-Forwarded-For and X-Forwarded-Proto with the F5 and Cloudflare CIDRs as known proxies; X-Forwarded-Host is still not trusted. No change needed there.

Tests

test/Classes/PublicUrlServiceTests.cs and test/Classes/HomeControllerCasUrlTests.cs, 26 new tests:

  • A configured origin wins over Host: attacker.example for BaseUrl, BuildUrl, the login service, and the logout service.
  • Startup validation rejects missing (outside Development), relative, http (outside Development), user-info, query, and fragment values; accepts the real TEST/PROD URLs; allows missing and http in Development.
  • Trailing-slash and whitespace normalisation.
  • Return URLs stay local and keep the /2 PathBase; an /api ReturnUrl returns 401 both with and without the PathBase prefix.

Full backend suite: 2741 passed.

Deployment

  1. Set Application:PublicBaseUrl in TEST and Production before deploying this code. It is in the checked-in appsettings.Test.json / appsettings.Production.json, so no SSM change is required, but confirm nothing overrides it. Startup fails with a clear message if it is missing.
  2. Deploy to TEST and exercise login, logout, expired-session login, and deep-link return.
  3. Confirm the CAS service registry accepts the exact canonical callback (https://secure-test.vetmed.ucdavis.edu/2/CasLogin).
  4. Watch for 400s from host filtering. AllowedHosts is now restrictive. If an F5 or IIS health probe reaches the app with a Host other than the canonical hostname or localhost (an IP or machine name, for example), it will be rejected. This is the one item I could not verify from the repo. If it shows up in the TEST logs, add that host to the list.
  5. Deploy to Production.

Notes

  • EmailSettings:BaseUrl already carries the same two URLs. I left it alone rather than widening this PR, but the two settings should probably converge later.

@codecov-commenter

Copy link
Copy Markdown

Bundle Report

Bundle size has no change ✅

@codecov-commenter

codecov-commenter commented Aug 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 84.21053% with 12 lines in your changes missing coverage. Please review.
✅ Project coverage is 41.88%. Comparing base (21551ff) to head (8c584e6).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
web/Classes/PublicUrlService.cs 85.18% 7 Missing and 1 partial ⚠️
web/Classes/HttpHelper.cs 57.14% 3 Missing ⚠️
web/Controllers/HomeController.cs 93.33% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #310      +/-   ##
==========================================
+ Coverage   41.74%   41.88%   +0.13%     
==========================================
  Files         992      993       +1     
  Lines       49697    49751      +54     
  Branches     5854     5866      +12     
==========================================
+ Hits        20748    20839      +91     
+ Misses      28038    27998      -40     
- Partials      911      914       +3     
Flag Coverage Δ
backend 39.95% <84.21%> (+0.15%) ⬆️
frontend 58.15% <ø> (ø)

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

Files with missing lines Coverage Δ
web/Controllers/HomeController.cs 24.26% <93.33%> (+24.26%) ⬆️
web/Classes/HttpHelper.cs 28.57% <57.14%> (+5.13%) ⬆️
web/Classes/PublicUrlService.cs 85.18% <85.18%> (ø)

... and 2 files with indirect coverage changes

Comment thread web/Classes/HttpHelper.cs Fixed
Comment thread web/Classes/HttpHelper.cs Fixed
Comment thread web/Classes/HttpHelper.cs Fixed
Comment thread web/Classes/HttpHelper.cs Fixed
Comment thread test/Classes/PublicUrlServiceTests.cs Fixed
Comment thread web/Classes/HttpHelper.cs Fixed

Copilot AI 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.

Pull request overview

This PR hardens CAS login/validation/logout callback URL generation by introducing a configured canonical public origin (instead of deriving the origin from the incoming request), reducing exposure to Host-header poisoning and aligning outbound URLs across the app.

Changes:

  • Added IPublicUrlService + startup validation for Application:PublicBaseUrl (fail-fast outside Development).
  • Updated CAS login/logout and HttpHelper.GetRootURL() callers to use the canonical origin; fixed /2 PathBase handling and strengthened /api ReturnUrl guarding.
  • Tightened AllowedHosts in Test/Production and added unit tests covering canonical-origin behavior and validation rules.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
web/Program.cs Registers/validates PublicUrlOptions, wires IPublicUrlService into HttpHelper.
web/Controllers/HomeController.cs Uses canonical base URL for CAS service/logout URLs; adjusts ReturnUrl handling and /api guard.
web/Classes/PublicUrlService.cs New canonical-origin service + options + validator enforcing safe/expected base URL shapes.
web/Classes/HttpHelper.cs Delegates GetRootURL() to IPublicUrlService (or request-derived fallback in Development).
web/appsettings.Test.json Sets restrictive AllowedHosts and Application:PublicBaseUrl for Test.
web/appsettings.Production.json Sets restrictive AllowedHosts and Application:PublicBaseUrl for Production.
test/Classes/PublicUrlServiceTests.cs Unit tests for normalization, request fallback, and startup validation rules.
test/Classes/HomeControllerCasUrlTests.cs Unit tests ensuring CAS URLs are built from configured origin and /api ReturnUrl yields 401.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread web/Controllers/HomeController.cs
Comment thread web/Program.cs Outdated
@rlorenzo
rlorenzo force-pushed the fix/cas-canonical-origin branch from bee6b85 to 98e21f0 Compare August 18, 2026 03:45
CAS login, ticket validation and logout derived their service URL from
HttpHelper.GetRootURL(), which reads the request Host, and AllowedHosts
was "*" in every environment. A Host header that got past the proxies
could therefore poison a CAS callback.

- Application:PublicBaseUrl per environment, validated on start so a
  deployed environment fails fast rather than falling back to the request
- AllowedHosts narrowed to the real TEST/PROD hostnames plus localhost
- GetRootURL() returns the canonical origin when configured, so the
  sitemap and emulation links stop being request-derived too
- Login's /api guard strips the PathBase, so an API ReturnUrl gets a 401
  instead of a CAS HTML redirect under the deployed /2 sub-app

Copilot AI 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.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.

@rlorenzo

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@coderabbitai

coderabbitai Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a7227d64-90d8-44d8-957b-b575abdc1fcd

📥 Commits

Reviewing files that changed from the base of the PR and between ee1cfed and 8c584e6.

📒 Files selected for processing (8)
  • test/Classes/HomeControllerCasUrlTests.cs
  • test/Classes/PublicUrlServiceTests.cs
  • web/Classes/HttpHelper.cs
  • web/Classes/PublicUrlService.cs
  • web/Controllers/HomeController.cs
  • web/Program.cs
  • web/appsettings.Production.json
  • web/appsettings.Test.json

Included review availability: Your plan includes up to 1 review per rolling hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

Added a validated canonical public URL service. Integrated it with HttpHelper and HomeController for request URL generation and CAS redirects. Added environment configuration and tests for URL normalization, fallback behavior, path bases, API responses, and forged Host headers.

Changes

Public URL integration

Layer / File(s) Summary
Public URL contracts and resolution
web/Classes/PublicUrlService.cs, test/Classes/PublicUrlServiceTests.cs
Added public URL options, normalization, absolute URL construction, Development request fallback, and environment-specific validation.
Application registration and URL configuration
web/Program.cs, web/appsettings.Production.json, web/appsettings.Test.json, web/Classes/HttpHelper.cs
Registered and validated IPublicUrlService, configured public origins and allowed hosts, and passed the service to HttpHelper.
CAS redirect URL handling
web/Controllers/HomeController.cs, test/Classes/HomeControllerCasUrlTests.cs
Updated login and logout redirects to use the public URL service. Preserved PathBase and added API path detection. Tests cover redirects, API responses, and Host-header handling.

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

Merge Risk: ⚪ Minimal · up to 8c584

The change builds CAS and related public URLs from validated canonical origins and preserves local development behavior. No actionable merge-blocking risk remains beyond normal checks and review.

Sequence Diagram(s)

sequenceDiagram
  participant HomeController
  participant IPublicUrlService
  participant HttpHelper
  HomeController->>IPublicUrlService: BuildUrl(relative redirect path)
  IPublicUrlService-->>HomeController: return absolute public URL
  HttpHelper->>IPublicUrlService: read BaseUrl or derive request URL
  IPublicUrlService-->>HttpHelper: return application root URL
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% 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
Title check ✅ Passed The title clearly and concisely summarizes the main change: using a configured canonical origin for CAS callbacks.
Description check ✅ Passed The description directly explains the canonical URL change, configuration, affected callers, tests, and deployment requirements.
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.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/cas-canonical-origin

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.

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.

4 participants