Skip to content

api_keys 0.5.1

Choose a tag to compare

@rameerez rameerez released this 18 Oct 22:13
· 6 commits to main since this release
Immutable release. Only release title and notes can be modified.
f604bfe

Security

  • Clearing every permission in the dashboard now removes the key's scopes. With every box unchecked the form submitted no scopes parameter, so an update kept the old scopes and a create applied the default scopes. Both checkbox groups now render an empty api_key[scopes][] sentinel: an empty selection means "no scopes" and the model's validations decide, while defaults still apply when the parameter is absent. The edit form changes only the scopes it offers (the owner's default scopes) and keeps every other scope a key holds, so a rename no longer strips a scope the form never showed. A failed create re-renders the boxes as submitted instead of re-checking all of them.
  • FormBuilderExtensions#api_key_scopes_checkboxes renders the same sentinel before the boxes when a block is given. Host forms will see one extra hidden input. By default it is rendered only when there is at least one box and the form's key enforces its scopes (new ApiKey#scopes_enforced?), because where scopes are not enforced an empty list still means "unrestricted". Per-owner default scopes are read through the key's owner, so a form with no object or on a bare ApiKey.new renders no sentinel unless you pass include_hidden: true; build the key with owner.api_keys.build. include_hidden: false turns it off.
  • ApiKey#revoke! no longer depends on unrelated validations. A key issued before its type's permission or restriction ceiling was narrowed kept authenticating but could not be revoked. Revocation keeps the revocability guard and the save callbacks, writes only revoked_at (and updated_at), and still validates any other unsaved edits on the same instance. The next request is refused as revoked_key, including through the token cache.
  • Malformed restriction values fail closed. Coercion dropped false, nil, and blank entries, so stored {"ips": false}, {"ips": [false]}, or {"ips": [null]} read as unrestricted and update!(allowed_ips: false) stored {}; the same applied to origins. Such values are now kept, rejected by validation, and refuse authentication with restriction_misconfigured when found in the database; a stored null or blank kind is refused too. Input keeps its 0.5.0 meaning where it was unambiguous: a kind given as nil or blank text to restrictions=, create_api_key!(restrictions:), or the allowed_origins= / allowed_ips= writers clears that list, so create_api_key!(restrictions: { ips: nil }) still creates an unrestricted key. false, wrong types, and null or blank members inside a list are rejected on input. A malformed restrictions: argument to create_api_key! now reaches validation instead of being normalized to {}. The dashboard shows a stored policy's malformed entries instead of empty fields and leaves an untouched field's list as stored, so a rename can no longer turn a malformed policy into an unrestricted one; the save is refused until the owner corrects it.
  • The authentication header is checked for encoding and size before it is parsed. An invalid UTF-8 header raised ArgumentError from the Bearer match; it now answers invalid_token, and a header longer than the token limit plus 64 bytes is refused without being scanned.
  • The one-time token handoff is now one-time across copies of the session. Retrieval deleted the payload only from the session it was given, so a copied pre-retrieval cookie, or two concurrent requests, could recover the token twice within its ten-minute lifetime. Each handoff now carries a random ID, and the first retrieval records it with an atomic Rails.cache.write(..., unless_exist: true); later retrievals get nil while the cache keeps that record, and a cache error withholds the token. The guarantee needs both a shared store with atomic unless_exist: and retention of the record for the ten-minute handoff lifetime: if the cache is flushed, restarted, or evicts the record within that window, a copied pre-retrieval cookie recovers the token again. Durable consumption state is planned for 0.6.0. With ActiveSupport::Cache::NullStore or no cache, the session deletion remains the only protection. With Solid Cache on PostgreSQL, two simultaneous first retrievals can both succeed; sequential replays are stopped. The handoff format is unchanged, so 0.5.0 and 0.5.1 processes can read each other's handoffs during a rolling deploy.
  • IP restrictions: the README, SECURITY.md, the generated initializer, and the add_restrictions generator now state that the default request.remote_ip resolver trusts X-Forwarded-For. Behind a proxy that is the only way in, keep it; when the Rails server is directly reachable, use ->(request) { request.remote_addr }. trusted_proxies alone is not a boundary. The suite covers both shapes through Rails' ActionDispatch::RemoteIp. The default is unchanged.

Fixed

  • The dashboard's security page no longer says publishable keys cannot be revoked individually; since 0.5.0 they can, unless their key type sets revocable: false.
  • The README's prefix table no longer says a key can be created without a type when key types are configured. Such a key takes default_key_type, or creation raises.