Skip to content

Add IndieAuth delegation and domain whitelist support#1

Open
Copilot wants to merge 13 commits intomasterfrom
copilot/add-domain-whitelist-auth-support
Open

Add IndieAuth delegation and domain whitelist support#1
Copilot wants to merge 13 commits intomasterfrom
copilot/add-domain-whitelist-auth-support

Conversation

Copy link
Copy Markdown

Copilot AI commented Feb 12, 2026

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

  • SSRF Protection: Block private/loopback/link-local IPs and non-HTTP(S) schemes
  • Link Header Parsing: Enhanced to handle multiple rel values and various quoting styles
  • HTML Rel Parsing: Now handles space-separated rel token lists
  • View Partials: Removed unused local assignments (partials use instance variables)
  • Test Structure: Moved tests inside main describe block with proper setup
  • Network Stubbing: Stubbed HTTP calls in tests to avoid real network requests
  • Config Restoration: Tests now restore original config to prevent state leakage
  • URL Validation: Validates URLs even when whitelist is empty
  • Normalized URLs: Controller stores normalized URLs instead of raw params

Security Enhancements

  • SSRF protection via IP range blocking (loopback, private, link-local)
  • Scheme validation (only http/https allowed)
  • ReDoS protection with input size limits
  • 5-second timeout on external HTTP requests
  • DNS resolution with fallback for IP checks

Files Changed

  • app/services/authorio/domain_validator.rb - SSRF protection, HTML/Link parsing improvements
  • app/controllers/authorio/auth_controller.rb - Store normalized URLs
  • app/views/authorio/auth/send_profile.json.jbuilder - Remove unused locals
  • app/views/authorio/auth/issue_token.json.jbuilder - Remove unused locals
  • spec/requests/auth_spec.rb - Fix test structure
  • spec/services/authorio/domain_validator_spec.rb - Add config restoration and network stubbing
Original 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.rb

  • Add allowed_domains attribute (array) to whitelist domains that can use this server
  • Add verify_delegation attribute (boolean) to optionally verify link rel tags
  • Initialize allowed_domains to empty array (allow all by default)
  • Initialize verify_delegation to false (backward compatible)

File: lib/generators/authorio/install/templates/authorio.rb

  • Add documentation and example configuration for allowed_domains
  • Add documentation for verify_delegation option
  • Include examples for single domain, multiple domains, and wildcard subdomain support

2. Domain Validator Service

Create new file: app/services/authorio/domain_validator.rb

Implement a service class with the following functionality:

  • initialize(me_url, request_env = nil) - constructor that takes the me URL and optional request environment
  • valid? - main validation method that checks both whitelist and delegation
  • domain_whitelisted? - checks if the domain is in the allowed_domains list
    • Support exact domain matching (e.g., 'example.com')
    • Support wildcard subdomains (e.g., '*.example.com' matches 'blog.example.com', 'www.example.com')
    • Return true if allowed_domains is empty (no restriction)
  • delegation_verified? - fetches the me URL and verifies it has proper link rel tags
    • Check for <link rel="authorization_endpoint"> in HTML
    • Check for Link: HTTP headers
    • Verify the authorization_endpoint URL points to this authorio server
    • Handle redirects (follow up to 5 redirects)
    • Return true if verify_delegation config is false
  • Include proper error handling and logging
  • Add URL normalization helper (auto-add https:// if no scheme)
  • Add HTTP fetching with timeout (5 seconds)
  • Parse both HTML link tags and HTTP Link headers

3. Database Migration

Create migration: db/migrate/XXXXXX_add_me_to_authorio_requests.rb

  • Add me column (string) to authorio_requests table
  • Add index on me column

4. Model Updates

File: app/models/authorio/request.rb

  • Add me attribute
  • Add validation: validate :me_domain_allowed, if: -> { me.present? }
  • Implement me_domain_allowed method that uses DomainValidator to check if domain is whitelisted
  • Add error message if domain is not allowed

5. Controller Updates

File: app/controllers/authorio/auth_controller.rb

In authorization_interface method:

  • Add validation check for me parameter using DomainValidator
  • If validation fails, render oauth_error with message 'The domain is not authorized to use this authentication server'
  • Update session to include :me in the slice: session.update auth_interface_params.slice(:state, :client_id, :code_challenge, :redirect_uri, :me)

In create_auth_request method:

  • Add me: session[:me] to the Request.create parameters

6. View Updates

File: app/views/authorio/users/_profile.json.jbuilder

  • Change the json.me line to: json.me @auth_request.me.presence || profile_url(@auth_request.authorio_user)
  • This returns the delegated me URL if present, otherwise falls back to the server's profile URL

File: app/views/authorio/auth/send_profile.json.jbuilder

  • Ensure it uses the @request variable (should already be calling the profile partial)

7. Service Directory

Ensure the app/services/authorio/ directory exists (create if needed).

Requirements

  • Require 'net/http' and 'uri' in the DomainValidator
  • All changes should maintain backward compatibility
  • Empty allowed_domains array means no restrictions (allow all domains)
  • verify_delegation defaults to false
  • Proper error messages in English
  • Use Rails logger for error logging
  • Handle URI parsing errors gracefully
  • HTTP timeouts set to 5 seconds for external requests

Testing Considerations

The changes should support these use cases:

  1. No restrictions (default): allowed_domains = [] - works as before
  2. Single domain: allowed_domains = ['example.com'] - only example.com can authenticate
  3. Multiple domains: allowed_domains = ['example.com', 'mysite.org']
  4. Wildcard subdomains: allowed_domains = ['*.example.com'] - matches blog.example.com, www.example.com, etc.
  5. With verification: verify_delegation = true - fetches me URL and verifies link rel tags
  6. Without verification: verify_delegation = false - only checks whitelist

Example Configuration

Users should be able to configure in config/initializers/authorio.rb:

Authorio.configure do |config|
  config.allowed_domains = ['example.com']
  config.verify_delegation = true
end

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.

Copilot AI and others added 5 commits February 12, 2026 11:04
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>
Copilot AI changed the title [WIP] Add domain whitelist and delegated authentication support Add IndieAuth delegation and domain whitelist support Feb 12, 2026
Copilot AI requested a review from GrassBlock1 February 12, 2026 11:12
@GrassBlock1 GrassBlock1 marked this pull request as ready for review February 12, 2026 11:17
Copilot AI review requested due to automatic review settings February 12, 2026 11:17
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

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_delegation configuration and initializer template docs.
  • Introduce Authorio::DomainValidator to whitelist domains and (optionally) verify authorization_endpoint delegation via HTTP/HTML.
  • Persist me on authorio_requests and 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.

Comment on lines +94 to +105
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)
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@copilot apply changes based on this feedback

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.

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

Comment on lines +159 to +164
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
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@copilot apply changes based on this feedback

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.

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.

Comment on lines +135 to +140
end

describe 'domain whitelist and delegation' do
context 'with domain whitelist enabled' do
before do
Authorio.configuration.allowed_domains = ['example.com']
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

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

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@copilot apply changes based on this feedback

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.

Fixed in commit 9da1e3d. Tests are now inside the main RSpec.describe block and inherit shared examples/helpers properly.

Comment on lines +73 to +81
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
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@copilot apply changes based on this feedback

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.

Fixed in commit 9da1e3d. Network calls are now stubbed using allow(validator).to receive(:fetch_authorization_endpoint) to avoid real HTTP requests in tests.

Comment on lines +7 to +10
context 'when allowed_domains is empty' do
before do
Authorio.configuration.allowed_domains = []
end
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@copilot apply changes based on this feedback

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.

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.

Comment on lines +22 to 31
# 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
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@copilot apply changes based on this feedback

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.

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>
GrassBlock1 and others added 6 commits February 12, 2026 19:30
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>
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.

3 participants