Pluggable authenticators
– sessions, JWT, API tokens, whatever fits your app
Identity abstraction with a built-in IdentityUser(ID)
Optional per-rule authenticators
Customize responses for
401 Unauthorized
403 Forbidden
Small Example
require "gatekeeper"
class AppHandler
include HTTP::Handler
def call(context)
case context.request.path
when "/"
context.response.print "Hello World!"
when "/admin"
context.response.print "Hello admin!"
else
context.response.status = HTTP::Status::NOT_FOUND
context.response.print "Not found"
end
end
end
# Configure Gatekeeper
Gatekeeper.config do |config|
config.on_unauthenticated = Gatekeeper::ContextHandler.new do |ctx|
ctx.response.print "You must log in first."
end
config.on_unauthorized = Gatekeeper::ContextHandler.new do |ctx|
ctx.response.print "You do not have permission."
end
# Simple authenticator example
config.authenticators << Gatekeeper.authenticator do |ctx|
Gatekeeper::IdentityUser(Int32).new(1, Set{"admin"})
end
# Optional: role hierarchy
# "admin" inherits permissions from "user" and "reader"
config.role_hierarchy = {
"admin" => ["user", "reader"],
"manager" => ["user"],
}
end
Gatekeeper.rules do |r|
r.allow_get "/" # allow GET / (exact match)
r.allow_post "/login" # allow POST /login
r.allow "/admin", roles: ["admin"] # exact match
r.allow /^\/api/ # prefix /api (regex as-is)
end
handlers = [
Gatekeeper::AuthHandler.new,
AppHandler.new,
]
server = HTTP::Server.new(handlers)
address = server.bind_tcp "0.0.0.0", 3000
puts "Listening on http://#{address}"
server.listen
Status
This is v0.5.0, but the API is stable, fully tested, and intentionally small.
I plan to iterate slowly with community feedback (rule helpers, role hierarchies, route attributes, etc.).
Any thoughts, suggestions, or ideas are very welcome!
Technically Kemal::Handler is just a light wrapper around the stdlib’s HTTP::Handler. So with some small refactoring, you could likely make this usable beyond just kemal itself. Just a thought .
Noted - already removed the dependency and instead `include HTTP::Handler`.
Now that it is not a kemal-only shard (when I publish v0.2.0 after work) anymore what about the name? Should it be updated to something like `Guardian` instead?
Absolutely — Basic Auth isn’t built in, but it works perfectly through a custom authenticator.
Gatekeeper intentionally doesn’t implement any authentication schemes (Basic, Bearer, cookies, sessions, JWT, etc.). Instead it just consumes whatever you return from an authenticator.
So adding Basic Auth is only a few lines of code:
require "base64"
Gatekeeper.config do |config|
config.authenticators << Gatekeeper.authenticator "basic auth" do |ctx|
header = ctx.request.headers["Authorization"]?
next nil unless header && header.starts_with?("Basic ")
encoded = header.lchop("Basic ").strip
decoded = String.new(Base64.decode(encoded).to_slice)
username, password = decoded.split(":", 2)
# Replace this with your own credential check
if username == "admin" && password == "secret"
Gatekeeper::IdentityUser(String).new(username, Set{"admin"})
else
nil
end
end
end
This keeps Gatekeeper focused on authorization only, while letting users plug in whatever authentication strategy fits their needs — including Basic Auth, JWT tokens, API keys, sessions, cookies, OAuth, etc.
Just a quick update for anyone following the shard:
Gatekeeper v0.4.0 introduces a much nicer way to define authorization rules.
Instead of always constructing Rule objects manually, you can now use a small helper API:
Just a small update on Gatekeeper — I’ve released v0.5.0, which adds support for role hierarchy.
This is a feature I’ve wanted for a while because even simple applications tend to develop layered roles over time. Without hierarchy, identities often end up bloated with every role they implicitly need. With hierarchy, you can keep identities clean and let Gatekeeper infer the rest.
The idea is straightforward: define how your roles relate to each other, and Gatekeeper expands a user’s roles automatically during authorization. This keeps your rule definitions the same, but greatly simplifies how you represent users internally.