Fixes from review of the device flow / introspection / DCR work:
Introspection over-disclosure (RFC 7662):
- Restrict introspection to authorized callers — the client a token was issued
to, or a resource server registered (resource_identifiers) to serve the token's
bound RFC 8707 audience. Unauthorized callers get {active:false}, disclosing
nothing, instead of any confidential client reading any token.
- Scope-gate disclosed claims: username only with the email scope, groups only
with the groups scope (mirrors userinfo). ADR 0005.
Tokens minted for revoked users:
- Re-check application.user_allowed?(user) at mint time in the device, refresh,
and authorization-code grants (covers app-active, user-active, group
membership). A user deactivated or removed from the allowed group between
approval and the token request is refused with access_denied. The refresh
check runs before rotation so a denied refresh has no side effects.
Device authorization hardening:
- Require confidential clients to authenticate, and require PKCE (code_challenge)
up front for clients that require it, so an intercepted device_code plus a
known public client_id cannot redeem tokens.
- Merge (not overwrite) the shared consent record on device approval.
Tests: new oidc_introspection_test and oidc_mint_authorization_test; existing
PKCE/claims tests updated to grant app access (they created ungrouped apps and
relied on the token endpoint not checking authorization). Full suite green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F7cwhwDJp3MJJDoNPVE6zq
61 lines
2.6 KiB
Ruby
61 lines
2.6 KiB
Ruby
# This file should ensure the existence of records required to run the application in every environment (production,
|
|
# development, test). The code here should be idempotent so that it can be executed at any point in every environment.
|
|
# The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup).
|
|
#
|
|
# Example:
|
|
#
|
|
# ["Action", "Comedy", "Drama", "Horror"].each do |genre_name|
|
|
# MovieGenre.find_or_create_by!(name: genre_name)
|
|
# end
|
|
|
|
# --- OAuth clients for CLI / agent access and token introspection ---------------
|
|
#
|
|
# These support the Device Authorization Grant (RFC 8628) and RFC 7662 token
|
|
# introspection. See docs/decisions/0002-device-authorization-grant.md.
|
|
|
|
admins = Group.find_by(admin: true)
|
|
|
|
# Public client (no secret, PKCE) used by CLIs and agents via the device flow.
|
|
# Ships with a well-known client_id so tools can hard-code it.
|
|
cli = Application.find_or_create_by!(client_id: "clinch-cli") do |app|
|
|
app.name = "Clinch CLI"
|
|
app.slug = "clinch-cli"
|
|
app.app_type = "oidc"
|
|
app.is_public_client = true
|
|
app.active = true
|
|
end
|
|
|
|
# Grant the CLI to the admins group by default (device flow enforces
|
|
# Application#user_allowed?). Adjust to taste.
|
|
if admins && cli.allowed_groups.exclude?(admins)
|
|
cli.allowed_groups << admins
|
|
puts "Seeded 'clinch-cli' public client (allowed group: #{admins.name})."
|
|
end
|
|
|
|
# Confidential client that resource servers (e.g. c2a2) use to authenticate to
|
|
# the introspection endpoint. The secret is only shown once, on creation.
|
|
#
|
|
# resource_identifiers declares the RFC 8707 resource URI(s) this server answers
|
|
# for. Introspection is authorized against it: c2a2 may only introspect tokens
|
|
# whose bound audience is one of these. The CLI/agent must therefore request its
|
|
# token with resource=<C2A2_RESOURCE>. Set C2A2_RESOURCE to c2a2's real URL.
|
|
unless Application.exists?(client_id: "c2a2-introspection")
|
|
secret = SecureRandom.urlsafe_base64(48)
|
|
c2a2_resource = ENV["C2A2_RESOURCE"].presence || "https://c2a2.example.com"
|
|
Application.create!(
|
|
name: "c2a2 (introspection caller)",
|
|
slug: "c2a2-introspection",
|
|
client_id: "c2a2-introspection",
|
|
client_secret: secret,
|
|
app_type: "oidc",
|
|
active: true,
|
|
resource_identifiers: [c2a2_resource].to_json
|
|
)
|
|
puts "Seeded 'c2a2-introspection' confidential client:"
|
|
puts " client_id: c2a2-introspection"
|
|
puts " client_secret: #{secret}"
|
|
puts " resource_identifier: #{c2a2_resource}"
|
|
puts " Store the secret in c2a2 now — it is hashed and cannot be recovered."
|
|
puts " The CLI must request tokens with resource=#{c2a2_resource} for c2a2 to introspect them."
|
|
end
|