Skip to content

api_keys 0.8.0: config.require_bearer_scheme - #78

Merged
rameerez merged 1 commit into
mainfrom
replybrain-slice-0
Oct 25, 2026
Merged

rameerez merged 1 commit into
mainfrom
replybrain-slice-0

Conversation

@rameerez

Copy link
Copy Markdown
Owner

config.require_bearer_scheme makes the authentication header accept one credential shape only: Bearer <token>. One opt-in setting, no migration, no generator. The default keeps accepting a bare token exactly as every earlier version did.

Why

Since 0.1.0 the header has accepted two shapes: Authorization: Bearer ak_… and, as a fallback, the bare Authorization: ak_…. That fallback is convenient for curl, but a public API should parse one credential shape only. Two shapes mean two things to document, to test and to support forever, and a header such as Basic … or Token … was handed to the key lookup as if it were a token. ReplyBrain's public API asked for the strict shape; any app with a public API wants the same.

What changed

Added

  • config.require_bearer_scheme (Boolean, default false).

    ApiKeys.configure do |config|
      config.require_bearer_scheme = true
    end

    When true, the header named by config.header is accepted only when it matches Bearer <token>: the scheme in any case (bearer, BEARER), followed by one or more spaces or tabs, then the token. Everything else is refused:

    Header value false (default) true
    Bearer ak_… (any case, spaces or tabs) token token
    ak_… (bare token) token 401 invalid_token
    Basic …, Token … looked up as a token, fails 401 invalid_token, never looked up
    Bearer alone looked up as a token, fails 401 invalid_token, never looked up
    no header, config.query_param set query parameter query parameter

    A refused header is answered exactly like any wrong key: the ordinary 401 invalid_token through render_unauthorized, with no hint that the shape was the problem. It is never looked up, so it costs no database query or bcrypt comparison. The header was present, so a refused header never falls back to config.query_param. The setting applies to whichever header config.header names, not only Authorization.

    Like the other boolean settings, the writer accepts only true or false and raises ArgumentError otherwise.

  • Initializer template: rails g api_keys:install writes the commented setting under config.header, recommended for public APIs.

  • README: a new section, "Require the Bearer scheme".

Changed

  • README: the query parameter section says the gem expects keys in the Authorization header, instead of "exclusively as HTTP Authentication Bearer tokens", which the bare-token fallback contradicted.

Security

  • With the setting on, a credential in another scheme can no longer be mistaken for an API key, and the header is never parsed as a raw token. The refusal reuses the internal MALFORMED_CREDENTIAL sentinel that 0.7.0 introduced for oversized and invalid UTF-8 headers, so it takes the same constant-shape failure path and adds no new answer an attacker could distinguish.
  • No refusal statistics are stamped for a refused header: no key was identified.

Compatibility

  • Default false: nothing changes for any app that does not set it.
  • Turning it on breaks clients that send the bare token. Check your logs, or your SDKs and documentation, before you turn it on. With config.debug_logging = true the gem logs "Refused a header without the Bearer scheme." for each refusal.

Upgrade notes

No migration and no generator. Update the gem:

# Gemfile
gem "api_keys", "~> 0.8"
bundle update api_keys

To require the scheme, add one line to config/initializers/api_keys.rb:

config.require_bearer_scheme = true

The 0.7.0 upgrade notes still apply to apps coming from 0.6.x or earlier.

Tests

  • test/services/authenticator_test.rb: a bare token authenticates by default; with the setting, a bare token is an invalid token and is never looked up; the Bearer scheme still authenticates in any case and spacing; a refused header does not fall back to the query parameter; the setting applies to a custom config.header.
  • test/controllers/authentication_test.rb: through a controller, a bare token gets the ordinary 401 invalid_token answer.
  • Locally: 774 runs, 3424 assertions, 0 failures, 0 errors, 0 skips.

🤖 Generated with Claude Code

https://claude.ai/code/session_01TFgkk7hfVmggDZz6BvW815

When true, the authentication header is accepted only as "Bearer <token>".
A bare token, another scheme or a lone "Bearer" gets the ordinary 401
invalid_token, is never looked up, and never falls back to the query
parameter. It applies to whichever header config.header names. The
default, false, keeps accepting a bare token as before.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TFgkk7hfVmggDZz6BvW815
@rameerez
rameerez merged commit 2bfbf5e into main Oct 25, 2026
19 checks passed
@rameerez
rameerez deleted the replybrain-slice-0 branch October 25, 2026 19:07
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.

1 participant