class

Authly::Handler::Bearer

Inherits HTTP::Handler < Reference < Object

Protects the routes behind it with OAuth 2.0 bearer tokens (RFC 6750). It is a plain HTTP::Handler: it needs no web framework, so it goes into any handler chain.

It protects every request unless told otherwise. List what is public with except_exact (just that path) or except (that path and everything below it, case-sensitively: except: ["/health"] also exempts /health/anything). Prefer except_exact for a health check:

verifier = Authly::StrategyTokenVerifier.new(audience: "https://api.example.com")
bearer = Authly::Handler::Bearer.new(verifier, realm: "api", except_exact: ["/health"])
HTTP::Server.new([bearer, Authly::Handler::RequireScope.all("read:users"), app])

prefixes: turns that around into an allowlist (only those paths are protected, every other path is open), which fails open for any route you forget to list: prefer except. See PathFilter for how paths are compared.

Set an audience on the verifier (and config.access_token_audience on the issuer): without it a token minted for one API is accepted by every other API that shares the key.

A verified token's claims are available downstream as context.authly_claims. Anything else ends the request here: no credentials 401, a malformed header 400 invalid_request, a bad, expired, revoked or wrongly addressed token 401 invalid_token, and a token store that cannot be reached 503 with Retry-After (not 401, which would make clients re-authenticate for nothing). The token is read from the Authorization header only, unless allow_query_token is set (RFC 6750 section 2.3 discourages ?access_token=: URLs end up in logs, referrers and browser history; such responses get Cache-Control: private and Referrer-Policy: no-referrer), and it is never logged.

Put a CORS handler before this one, or browsers cannot read the 401. A CORS preflight (OPTIONS with Access-Control-Request-Method) carries no credentials; pass_preflight: true lets it through to the next handler instead of answering 401. That is decided by headers anyone can forge: whatever answers OPTIONS behind it is public and must have no side effects. Give RequireScope the same flag.

Constants

MAX_TOKEN_BYTES = 8192
RETRY_AFTER = "1"
TOKEN68 = /\A[A-Za-z0-9\-._~+\/]+=*\z/

Constructors

new(verifier : TokenVerifier = StrategyTokenVerifier.new, realm : String = "authly", allow_query_token : Bool = false, prefixes : Array(String) | Nil = nil, except : Array(String) = [] of String, except_exact : Array(String) = [] of String, pass_preflight : Bool = false)
Source

Instance methods

call(context : HTTP::Server::Context)
Source
realm
Source