| name | kemal-auth |
| description | User authentication and session management in Kemal, following established project patterns. |
| license | MIT |
Kemal Authentication & Sessions
This skill provides expert guidance on implementing user authentication and session management in Kemal, strictly following patterns from kemal-by-example/ecommerce and kemal-by-example/oauth-login.
Core Mandates
-
Dependencies: Always require "kemal-session".
-
Session Configuration: Use Kemal::Session.config to set secret, cookie_name, and gc_interval:
Kemal::Session.config do |config|
config.secret = ENV["KEMAL_SESSION_SECRET"]? || raise "KEMAL_SESSION_SECRET not set"
config.cookie_name = "your_app_session"
config.gc_interval = 2.minutes
end
-
Auth Helpers: Implement auth logic in a module (e.g., Ecommerce::Auth):
current_user(env): Use env.session.bigint?("user_id") to retrieve the ID and find the user
require_user(env): Call current_user(env) and redirect to /login if nil
sign_in(env, user): Set env.session.bigint("user_id", user.id || raise "User ID required")
sign_out(env): Call env.session.destroy
-
Password Hashing: Use Crypto::Bcrypt::Password for securely storing and authenticating passwords.
-
Error Handling: Use specific exception types (like DB::Error) in auth helpers.
def current_user(env) : User?
user_id = env.session.bigint?("user_id")
return unless user_id
User.find(user_id)
rescue DB::Error
nil
end
Patterns from Source Code
Auth Helper Module (ecommerce/src/helpers/auth.cr)
module Ecommerce
module Auth
extend self
def current_user(env) : User?
user_id = env.session.bigint?("user_id")
return unless user_id
User.find(user_id)
rescue DB::Error
nil
end
def require_user(env) : User?
user = current_user(env)
return user if user
env.redirect "/login"
nil
end
def sign_in(env, user : User)
user_id = user.id || raise ArgumentError.new("Cannot sign in user without ID")
env.session.bigint("user_id", user_id)
end
def sign_out(env)
env.session.destroy
end
end
end
Session Secret (Explicit Configuration)
From ecommerce and oauth-login:
Kemal::Session.config do |config|
config.secret = ENV["KEMAL_SESSION_SECRET"]? || raise "KEMAL_SESSION_SECRET not set"
config.cookie_name = "ecommerce_session_id"
config.gc_interval = 2.minutes
end
User Model with Bcrypt
From ecommerce/src/models/user.cr:
require "crypto/bcrypt"
class User
include DB::Serializable
getter id : Int64?
getter name : String
getter email : String
getter password_hash : String
getter created_at : String
getter updated_at : String
def self.create(name : String, email : String, password : String) : User
now = Time.utc.to_s
normalized_email = normalize_email(email)
password_hash = Crypto::Bcrypt::Password.create(password, cost: 12).to_s
# ... insert and return user
end
def self.authenticate(email : String, password : String) : User?
user = find_by_email(normalize_email(email))
return unless user
return user if Crypto::Bcrypt::Password.new(user.password_hash).verify(password)
nil
end
def self.normalize_email(value : String) : String
value.strip.downcase
end
end
Login Route Pattern
post "/login" do |env|
email = env.params.body["email"]?.try(&.strip) || ""
password = env.params.body["password"]?.try(&.strip) || ""
user = User.authenticate(email, password)
if user
Ecommerce::Auth.sign_in(env, user)
env.redirect "/products"
else
current_user = nil
cart_count = 0_i64
error_message = "Invalid email or password."
env.response.status = :unprocessable_entity
render "src/views/auth/login.ecr", "src/views/layouts/application.ecr"
end
end
post "/logout" do |env|
Ecommerce::Auth.sign_out(env)
env.redirect "/products"
end
Best Practices
- Local Variables for Errors: Pass error messages as local variables directly to the
render macro (e.g., error_message = "Invalid email or password.").
- Security: Use
POST (never GET) for login and logout so state changes are not triggerable via simple links — but note that using POST alone does not prevent CSRF. Add real CSRF protection: validate a per-session CSRF token on state-changing requests (e.g. the kemal-csrf handler) and set session cookies with SameSite. Regenerate the session on login to prevent session fixation.
- Session Safe Access: Use
env.session.bigint?("user_id") or similar to safely retrieve session data.
- Model Methods: Implement
User.authenticate(email, password) and User.find_by_email(email) in the model.
When to Use
- When implementing signup, login, or logout features.
- When protecting specific routes from unauthorized access.
- When managing user-specific state across requests.