Harden OIDC token endpoints from security review
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
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
2defa26a87
commit
017dfdff0e
@@ -0,0 +1,8 @@
|
||||
class AddResourceIdentifiersToApplications < ActiveRecord::Migration[8.1]
|
||||
# The RFC 8707 resource identifier(s) this application serves as a resource
|
||||
# server. Used to authorize RFC 7662 introspection: a caller may only introspect
|
||||
# tokens bound to a resource it serves (or tokens issued to itself).
|
||||
def change
|
||||
add_column :applications, :resource_identifiers, :text
|
||||
end
|
||||
end
|
||||
Generated
+2
-1
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.1].define(version: 2026_07_19_000003) do
|
||||
ActiveRecord::Schema[8.1].define(version: 2026_07_19_000004) do
|
||||
create_table "active_storage_attachments", force: :cascade do |t|
|
||||
t.bigint "blob_id", null: false
|
||||
t.datetime "created_at", null: false
|
||||
@@ -96,6 +96,7 @@ ActiveRecord::Schema[8.1].define(version: 2026_07_19_000003) do
|
||||
t.text "redirect_uris"
|
||||
t.integer "refresh_token_ttl", default: 2592000
|
||||
t.boolean "require_pkce", default: true, null: false
|
||||
t.text "resource_identifiers"
|
||||
t.boolean "skip_consent", default: false, null: false
|
||||
t.string "slug", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
|
||||
+13
-4
@@ -34,18 +34,27 @@ 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
|
||||
active: true,
|
||||
resource_identifiers: [c2a2_resource].to_json
|
||||
)
|
||||
puts "Seeded 'c2a2-introspection' confidential client:"
|
||||
puts " client_id: c2a2-introspection"
|
||||
puts " client_secret: #{secret}"
|
||||
puts " Store these in c2a2 now — the secret is hashed and cannot be recovered."
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user