Class: Otto::Security::Authentication::Strategies::APIKeyStrategy
- Inherits:
-
AuthStrategy
- Object
- AuthStrategy
- Otto::Security::Authentication::Strategies::APIKeyStrategy
- Defined in:
- lib/otto/security/authentication/strategies/api_key_strategy.rb
Overview
API key authentication strategy.
Accepts exactly one key source: a static list (api_keys:), a
callable (resolver:), or a block. A resolver receives the presented
key (a non-empty String, never env) and returns the account behind it,
or nil/false when the key is unknown. Any other return value, including
an empty container, counts as a match: an ORM relation from where,
an empty Array from select, or {} from a cache miss is truthy and
authenticates the caller. Return exactly one record (find_by,
first) or nil. The strategy never branches on mode: a static list is
wrapped in a resolver internally.
Fails closed: a strategy with no source, or a static list with no non-empty key, is a misconfiguration and raises ArgumentError at construction rather than authenticating every caller. Only a truthy resolver return (for a static list: a constant-time match) grants success.
A credential that was presented and rejected, including a non-String credential such as an array query parameter, fails TERMINALLY, so a bad key halts the strategy chain instead of falling through to a later anonymous-capable strategy. A missing credential fails non-terminally. Blank and non-String credentials are rejected before the resolver runs.
Exceptions raised by a resolver propagate. A database outage must surface as an error, not as a silent 401 and never as success.
Timing: the static list is compared in constant time against every configured key. Both sides are reduced to fixed-width SHA-256 digests first, so the comparison never short-circuits on a length mismatch and the lengths of configured keys are not observable. A black-box lookup cannot be made constant-time by the strategy; the documented pattern is to store SHA-256 digests and look up by APIKeyStrategy.digest, which is constant-time by construction and keeps raw keys out of the database.
The strategy never places the raw key in the result itself: the only
strategy-generated field derived from the key is a short SHA-256
fingerprint. With a resolver, user is whatever the resolver returns,
verbatim; the result is stored in env and
exposed to handlers, so anything the application serializes or logs
from it carries user. It is the resolver's responsibility not to
return an object that holds the raw key: return the account, not the
ApiKey row that stores the key, and store digests. A resolver that
returns the presented key String itself as the user raises
ArgumentError.
The query/form parameter path is opt-in (param_name:), because keys in
URLs are recorded by access logs, proxies, and browser history.
Scope: this is a small static-allowlist authenticator shipped as a low-dependency convenience and reference implementation. It has no native support for runtime addition or revocation, expiration, roles or scopes, key metadata, quotas, a management API, audit history, or hashed verifier storage. The resolver form delegates those concerns to the application's key store; see docs/guides/authentication.md.
Class Method Summary collapse
-
.digest(key) ⇒ String
Full SHA-256 hex digest of a key.
Instance Method Summary collapse
- #authenticate(env, _requirement) ⇒ Object
-
#initialize(api_keys: nil, resolver: nil, header_name: 'X-API-Key', param_name: nil) {|presented_key| ... } ⇒ APIKeyStrategy
constructor
A new instance of APIKeyStrategy.
Methods inherited from AuthStrategy
#authorization_failure, #failure, #success
Constructor Details
#initialize(api_keys: nil, resolver: nil, header_name: 'X-API-Key', param_name: nil) {|presented_key| ... } ⇒ APIKeyStrategy
Returns a new instance of APIKeyStrategy.
103 104 105 106 107 108 |
# File 'lib/otto/security/authentication/strategies/api_key_strategy.rb', line 103 def initialize(api_keys: nil, resolver: nil, header_name: 'X-API-Key', param_name: nil, &block) super() @resolver = build_resolver(api_keys, resolver, block) @header_name = header_name @param_name = param_name end |
Class Method Details
.digest(key) ⇒ String
Full SHA-256 hex digest of a key. Store this instead of the raw key and look up presented keys by their digest.
86 87 88 |
# File 'lib/otto/security/authentication/strategies/api_key_strategy.rb', line 86 def self.digest(key) Digest::SHA256.hexdigest(key) end |
Instance Method Details
#authenticate(env, _requirement) ⇒ Object
110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 |
# File 'lib/otto/security/authentication/strategies/api_key_strategy.rb', line 110 def authenticate(env, _requirement) # Header first; the parameter path is consulted only when opted in. api_key = env["HTTP_#{@header_name.upcase.tr('-', '_')}"] if api_key.nil? && @param_name request = Otto::Request.new(env) api_key = request.params[@param_name] end # '' is truthy in Ruby; treat it, and a whitespace-only value, as a # missing credential. The static list already refuses blank keys, so # the resolver must never be asked about one either. return failure('No API key provided') if api_key.nil? || (api_key.is_a?(String) && api_key.strip.empty?) # A non-String credential (e.g. `?api_key[]=k` yields an Array) was # still presented, so reject it terminally rather than handing it to # the resolver. Credentials were explicitly presented and rejected: # fail closed. return failure('Invalid API key', terminal: true) unless api_key.is_a?(String) # Resolver exceptions propagate deliberately (see class docs). user = @resolver.call(api_key) return failure('Invalid API key', terminal: true) unless user # The most likely naive misuse: `->(k) { k if keys.include?(k) }`. # Fail loud before the key can reach the result and env. if user.is_a?(String) && constant_time_equal?(user, api_key) raise ArgumentError, 'APIKeyStrategy resolver returned the presented key as the user; ' \ 'return the account behind the key, not the key' end # Identify the credential by a non-reversible fingerprint. The strategy # itself never places the raw key in the result; `user` is the # resolver's return value, verbatim (see class docs). success(user: user, auth_method: 'api_key', api_key_fingerprint: key_fingerprint(api_key)) end |