Add IndieAuth delegation and domain whitelist support#1
Add IndieAuth delegation and domain whitelist support#1
Conversation
Co-authored-by: GrassBlock1 <46253950+GrassBlock1@users.noreply.github.com>
Co-authored-by: GrassBlock1 <46253950+GrassBlock1@users.noreply.github.com>
…ation Co-authored-by: GrassBlock1 <46253950+GrassBlock1@users.noreply.github.com>
Co-authored-by: GrassBlock1 <46253950+GrassBlock1@users.noreply.github.com>
Co-authored-by: GrassBlock1 <46253950+GrassBlock1@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds IndieAuth “delegated me” support so clients can authenticate using user-owned domains against this Authorio server, with optional domain whitelisting and delegation verification.
Changes:
- Add
allowed_domains+verify_delegationconfiguration and initializer template docs. - Introduce
Authorio::DomainValidatorto whitelist domains and (optionally) verifyauthorization_endpointdelegation via HTTP/HTML. - Persist
meonauthorio_requestsand return it from profile JSON when present.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
lib/authorio/configuration.rb |
Adds configuration knobs for whitelisting + delegation verification. |
lib/generators/authorio/install/templates/authorio.rb |
Documents new configuration options in the install template. |
app/services/authorio/domain_validator.rb |
Implements whitelist matching + optional delegation verification. |
app/controllers/authorio/auth_controller.rb |
Validates me, stores it in session, and persists it on requests. |
app/models/authorio/request.rb |
Adds model-level whitelist validation for persisted me. |
db/migrate/20260212110310_add_me_to_authorio_requests.rb |
Adds me column + index to requests table. |
app/views/authorio/users/_profile.json.jbuilder |
Returns delegated me when present, otherwise server profile URL. |
app/views/authorio/auth/send_profile.json.jbuilder |
Adjusts partial invocation for profile JSON rendering. |
app/views/authorio/auth/issue_token.json.jbuilder |
Adjusts partial invocation for profile JSON rendering. |
spec/services/authorio/domain_validator_spec.rb |
Adds unit coverage for whitelist + delegation verification toggles. |
spec/requests/auth_spec.rb |
Adds request coverage for whitelist behavior and me persistence. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| def fetch_authorization_endpoint(url, redirect_count = 0) | ||
| return nil if redirect_count > MAX_REDIRECTS | ||
|
|
||
| uri = URI.parse(url) | ||
|
|
||
| http = Net::HTTP.new(uri.host, uri.port) | ||
| http.use_ssl = (uri.scheme == 'https') | ||
| http.open_timeout = HTTP_TIMEOUT | ||
| http.read_timeout = HTTP_TIMEOUT | ||
|
|
||
| request = Net::HTTP::Get.new(uri.request_uri) | ||
| response = http.request(request) |
There was a problem hiding this comment.
SSRF risk: when verify_delegation is enabled and allowed_domains is empty ("allow all"), this will fetch a user-controlled URL. Consider blocking private/loopback/link-local IP ranges, restricting schemes to http/https, and validating redirect targets before following them to avoid internal network access.
There was a problem hiding this comment.
@copilot apply changes based on this feedback
There was a problem hiding this comment.
Applied SSRF protection in commit 9da1e3d. The implementation now:
- Blocks private/loopback/link-local IP ranges (127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, IPv6 equivalents)
- Restricts schemes to http/https only
- Validates redirect targets before following them
- Uses DNS resolution with Resolv to check IP addresses against blocked ranges
| link_tags.each do |tag| | ||
| # Check if it has rel="authorization_endpoint" and extract href | ||
| if tag =~ /rel\s*=\s*["']authorization_endpoint["']/i | ||
| href_match = tag.match(/href\s*=\s*["']([^"']+)["']/i) | ||
| return href_match[1] if href_match | ||
| end |
There was a problem hiding this comment.
HTML parsing only matches rel="authorization_endpoint" as a single value. In HTML, rel can be a space-separated list (e.g., rel="authorization_endpoint something"), which would incorrectly fail verification. Consider checking rel as a token list and matching authorization_endpoint as one of the values.
There was a problem hiding this comment.
@copilot apply changes based on this feedback
There was a problem hiding this comment.
Fixed in commit 9da1e3d. HTML rel parsing now handles space-separated token lists by splitting on whitespace and checking if 'authorization_endpoint' is one of the tokens.
spec/requests/auth_spec.rb
Outdated
| end | ||
|
|
||
| describe 'domain whitelist and delegation' do | ||
| context 'with domain whitelist enabled' do | ||
| before do | ||
| Authorio.configuration.allowed_domains = ['example.com'] |
There was a problem hiding this comment.
The new describe 'domain whitelist and delegation' block is placed after the closing end of the existing RSpec.describe block, so it won't inherit the shared examples/helpers (params, post_params, etc.) and may fail with undefined variables. Move this block inside the existing RSpec.describe 'Requests', type: :request do ... end (or wrap it in its own RSpec.describe ... with the needed setup).
There was a problem hiding this comment.
@copilot apply changes based on this feedback
There was a problem hiding this comment.
Fixed in commit 9da1e3d. Tests are now inside the main RSpec.describe block and inherit shared examples/helpers properly.
| context 'when verify_delegation is true' do | ||
| before do | ||
| Authorio.configuration.verify_delegation = true | ||
| end | ||
|
|
||
| it 'returns false when endpoint cannot be fetched' do | ||
| validator = described_class.new('https://nonexistent-domain-12345.com') | ||
| expect(validator.delegation_verified?).to be false | ||
| end |
There was a problem hiding this comment.
This example makes a real network call to a non-existent domain. With no WebMock/VCR configured in this repo, it can add ~5s timeouts and cause flaky test runs. Stub Net::HTTP (or fetch_authorization_endpoint) and assert behavior without external I/O.
There was a problem hiding this comment.
@copilot apply changes based on this feedback
There was a problem hiding this comment.
Fixed in commit 9da1e3d. Network calls are now stubbed using allow(validator).to receive(:fetch_authorization_endpoint) to avoid real HTTP requests in tests.
| context 'when allowed_domains is empty' do | ||
| before do | ||
| Authorio.configuration.allowed_domains = [] | ||
| end |
There was a problem hiding this comment.
These specs mutate global Authorio.configuration but never restore prior values, which can leak state into other specs depending on run order. Capture existing allowed_domains/verify_delegation in an around block (or after) and restore them after each example.
There was a problem hiding this comment.
@copilot apply changes based on this feedback
There was a problem hiding this comment.
Fixed in commit 9da1e3d. Added around(:each) block to save and restore configuration values (allowed_domains, verify_delegation) after each test to prevent state leakage.
| # Validate me parameter if present | ||
| if params[:me].present? | ||
| validator = DomainValidator.new(params[:me], request.env) | ||
| unless validator.valid? | ||
| render oauth_error('invalid_request', 'The domain is not authorized to use this authentication server') and return | ||
| end | ||
| end | ||
|
|
||
| session.update auth_interface_params.slice(:state, :client_id, :code_challenge, :redirect_uri, :me) | ||
| rescue ActionController::ParameterMissing, ActionController::UnpermittedParameters => e |
There was a problem hiding this comment.
The controller validates a normalized me via DomainValidator, but it stores the original params[:me] value in session and persists it to Authorio::Request. This means inputs without a scheme won't get the documented https:// normalization, and the me echoed back may differ from what was validated. Store the normalized URL (e.g., via a normalized_me_url accessor on the validator) instead of the raw param.
There was a problem hiding this comment.
@copilot apply changes based on this feedback
There was a problem hiding this comment.
Fixed in commit 9da1e3d. Controller now stores the normalized URL via validator.normalized_me_url instead of the raw parameter, ensuring consistent HTTPS scheme normalization.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…ixes, and view cleanup Co-authored-by: GrassBlock1 <46253950+GrassBlock1@users.noreply.github.com>
Implementation Complete: IndieAuth Delegation and Domain Whitelist
✅ All tasks completed and review feedback addressed
Summary
Successfully implemented support for IndieAuth delegation and domain whitelisting with comprehensive security hardening based on PR review feedback.
Review Feedback Addressed
Security Enhancements
Files Changed
app/services/authorio/domain_validator.rb- SSRF protection, HTML/Link parsing improvementsapp/controllers/authorio/auth_controller.rb- Store normalized URLsapp/views/authorio/auth/send_profile.json.jbuilder- Remove unused localsapp/views/authorio/auth/issue_token.json.jbuilder- Remove unused localsspec/requests/auth_spec.rb- Fix test structurespec/services/authorio/domain_validator_spec.rb- Add config restoration and network stubbingOriginal prompt
Add Domain Whitelist and Delegated Authentication Support
Implement support for IndieAuth delegation and domain whitelisting to allow users to authenticate with their own domains while using this authorio server as the authorization endpoint.
Changes Required
1. Configuration Updates
File:
lib/authorio/configuration.rballowed_domainsattribute (array) to whitelist domains that can use this serververify_delegationattribute (boolean) to optionally verify link rel tagsallowed_domainsto empty array (allow all by default)verify_delegationto false (backward compatible)File:
lib/generators/authorio/install/templates/authorio.rballowed_domainsverify_delegationoption2. Domain Validator Service
Create new file:
app/services/authorio/domain_validator.rbImplement a service class with the following functionality:
initialize(me_url, request_env = nil)- constructor that takes the me URL and optional request environmentvalid?- main validation method that checks both whitelist and delegationdomain_whitelisted?- checks if the domain is in the allowed_domains listdelegation_verified?- fetches the me URL and verifies it has proper link rel tags<link rel="authorization_endpoint">in HTMLLink:HTTP headers3. Database Migration
Create migration:
db/migrate/XXXXXX_add_me_to_authorio_requests.rbmecolumn (string) toauthorio_requeststablemecolumn4. Model Updates
File:
app/models/authorio/request.rbmeattributevalidate :me_domain_allowed, if: -> { me.present? }me_domain_allowedmethod that uses DomainValidator to check if domain is whitelisted5. Controller Updates
File:
app/controllers/authorio/auth_controller.rbIn
authorization_interfacemethod:meparameter using DomainValidator:mein the slice:session.update auth_interface_params.slice(:state, :client_id, :code_challenge, :redirect_uri, :me)In
create_auth_requestmethod:me: session[:me]to the Request.create parameters6. View Updates
File:
app/views/authorio/users/_profile.json.jbuilderjson.meline to:json.me @auth_request.me.presence || profile_url(@auth_request.authorio_user)File:
app/views/authorio/auth/send_profile.json.jbuilder@requestvariable (should already be calling the profile partial)7. Service Directory
Ensure the
app/services/authorio/directory exists (create if needed).Requirements
allowed_domainsarray means no restrictions (allow all domains)verify_delegationdefaults to falseTesting Considerations
The changes should support these use cases:
allowed_domains = []- works as beforeallowed_domains = ['example.com']- only example.com can authenticateallowed_domains = ['example.com', 'mysite.org']allowed_domains = ['*.example.com']- matches blog.example.com, www.example.com, etc.verify_delegation = true- fetches me URL and verifies link rel tagsverify_delegation = false- only checks whitelistExample Configuration
Users should be able to configure in
config/initializers/authorio.rb:HTML Tags on User's Domain
When configure...
This pull request was created from Copilot chat.
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.