Device flow: replay-revocation, user_code CSPRNG, /device rate limit
More device-flow security-review fixes. Replay-revocation on device_code redemption (was: destroy on use): - Redemption now marks the code redeemed_at and links the issued access/refresh tokens back to it (new oidc_device_code_id FKs, on_delete: :nullify) instead of destroying the row. A replayed redeemed code is detected as reuse — revoking every descended token and returning a distinguishable "already been used" invalid_grant rather than the generic "Invalid device_code" — mirroring the authorization-code reuse semantics (RFC 6749 §4.1.2). The device_code FK is carried forward across refresh rotation so revocation reaches the whole chain. Redeemed codes are reaped by the existing expiry cleanup sweep. user_code CSPRNG (RFC 8628 §6.1): - The 8-char user_code is a credential (type it + Approve mints tokens), so draw it from SecureRandom instead of Ruby's global Mersenne Twister (Array#sample). Rate-limit the /device verification endpoint (RFC 8628 §5.1): - Add the app's standard 10/min limit on show + verify so a signed-in user can't brute-force the short code space to deny or hijack a pending authorization. Also carried in this commit (other agent's working-tree change): - Give each OidcController rate_limit a distinct name: so frequent device polling on the token endpoint no longer shares one counter with (and 429s) unrelated token/refresh/revoke/introspect calls. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Q4ATZHoMCWqvSpE2yYoie
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
64410a0c50
commit
71ee301dd2
@@ -0,0 +1,20 @@
|
||||
class AddDeviceCodeReplayTracking < ActiveRecord::Migration[8.1]
|
||||
# Replay-revocation for the device grant, mirroring the authorization-code path.
|
||||
#
|
||||
# The device flow previously destroy!ed the code on redemption, so a replayed
|
||||
# redeemed code was indistinguishable from an unknown one and the tokens it
|
||||
# minted could not be revoked. Instead we now mark the code redeemed_at and link
|
||||
# the issued tokens back to it, so a second redemption is detected as reuse and
|
||||
# every descended token is revoked (RFC 6749 §4.1.2 reuse semantics).
|
||||
#
|
||||
# on_delete: :nullify matches the oidc_authorization_code FK: the cleanup job can
|
||||
# still delete expired device codes without orphaning or destroying live tokens.
|
||||
def change
|
||||
add_column :oidc_device_codes, :redeemed_at, :datetime
|
||||
|
||||
add_reference :oidc_access_tokens, :oidc_device_code, null: true,
|
||||
foreign_key: {on_delete: :nullify}
|
||||
add_reference :oidc_refresh_tokens, :oidc_device_code, null: true,
|
||||
foreign_key: {on_delete: :nullify}
|
||||
end
|
||||
end
|
||||
Generated
+8
-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_000004) do
|
||||
ActiveRecord::Schema[8.1].define(version: 2026_07_19_000005) do
|
||||
create_table "active_storage_attachments", force: :cascade do |t|
|
||||
t.bigint "blob_id", null: false
|
||||
t.datetime "created_at", null: false
|
||||
@@ -124,6 +124,7 @@ ActiveRecord::Schema[8.1].define(version: 2026_07_19_000004) do
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "expires_at", null: false
|
||||
t.integer "oidc_authorization_code_id"
|
||||
t.integer "oidc_device_code_id"
|
||||
t.string "resource"
|
||||
t.datetime "revoked_at"
|
||||
t.string "scope"
|
||||
@@ -134,6 +135,7 @@ ActiveRecord::Schema[8.1].define(version: 2026_07_19_000004) do
|
||||
t.index ["application_id"], name: "index_oidc_access_tokens_on_application_id"
|
||||
t.index ["expires_at"], name: "index_oidc_access_tokens_on_expires_at"
|
||||
t.index ["oidc_authorization_code_id"], name: "index_oidc_access_tokens_on_oidc_authorization_code_id"
|
||||
t.index ["oidc_device_code_id"], name: "index_oidc_access_tokens_on_oidc_device_code_id"
|
||||
t.index ["revoked_at"], name: "index_oidc_access_tokens_on_revoked_at"
|
||||
t.index ["token_hmac"], name: "index_oidc_access_tokens_on_token_hmac", unique: true
|
||||
t.index ["user_id"], name: "index_oidc_access_tokens_on_user_id"
|
||||
@@ -176,6 +178,7 @@ ActiveRecord::Schema[8.1].define(version: 2026_07_19_000004) do
|
||||
t.integer "interval", default: 5, null: false
|
||||
t.datetime "last_polled_at"
|
||||
t.string "nonce"
|
||||
t.datetime "redeemed_at"
|
||||
t.string "resource"
|
||||
t.string "scope"
|
||||
t.string "status", default: "pending", null: false
|
||||
@@ -197,6 +200,7 @@ ActiveRecord::Schema[8.1].define(version: 2026_07_19_000004) do
|
||||
t.datetime "expires_at", null: false
|
||||
t.integer "oidc_access_token_id", null: false
|
||||
t.integer "oidc_authorization_code_id"
|
||||
t.integer "oidc_device_code_id"
|
||||
t.string "resource"
|
||||
t.datetime "revoked_at"
|
||||
t.string "scope"
|
||||
@@ -209,6 +213,7 @@ ActiveRecord::Schema[8.1].define(version: 2026_07_19_000004) do
|
||||
t.index ["expires_at"], name: "index_oidc_refresh_tokens_on_expires_at"
|
||||
t.index ["oidc_access_token_id"], name: "index_oidc_refresh_tokens_on_oidc_access_token_id"
|
||||
t.index ["oidc_authorization_code_id"], name: "index_oidc_refresh_tokens_on_oidc_authorization_code_id"
|
||||
t.index ["oidc_device_code_id"], name: "index_oidc_refresh_tokens_on_oidc_device_code_id"
|
||||
t.index ["revoked_at"], name: "index_oidc_refresh_tokens_on_revoked_at"
|
||||
t.index ["token_family_id"], name: "index_oidc_refresh_tokens_on_token_family_id"
|
||||
t.index ["token_hmac"], name: "index_oidc_refresh_tokens_on_token_hmac", unique: true
|
||||
@@ -320,6 +325,7 @@ ActiveRecord::Schema[8.1].define(version: 2026_07_19_000004) do
|
||||
add_foreign_key "application_user_claims", "users", on_delete: :cascade
|
||||
add_foreign_key "oidc_access_tokens", "applications"
|
||||
add_foreign_key "oidc_access_tokens", "oidc_authorization_codes", on_delete: :nullify
|
||||
add_foreign_key "oidc_access_tokens", "oidc_device_codes", on_delete: :nullify
|
||||
add_foreign_key "oidc_access_tokens", "users"
|
||||
add_foreign_key "oidc_authorization_codes", "applications"
|
||||
add_foreign_key "oidc_authorization_codes", "users"
|
||||
@@ -328,6 +334,7 @@ ActiveRecord::Schema[8.1].define(version: 2026_07_19_000004) do
|
||||
add_foreign_key "oidc_refresh_tokens", "applications"
|
||||
add_foreign_key "oidc_refresh_tokens", "oidc_access_tokens"
|
||||
add_foreign_key "oidc_refresh_tokens", "oidc_authorization_codes", on_delete: :nullify
|
||||
add_foreign_key "oidc_refresh_tokens", "oidc_device_codes", on_delete: :nullify
|
||||
add_foreign_key "oidc_refresh_tokens", "users"
|
||||
add_foreign_key "oidc_user_consents", "applications"
|
||||
add_foreign_key "oidc_user_consents", "users"
|
||||
|
||||
Reference in New Issue
Block a user