- 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`](https://github.com/sdogruyol/kemal-by-example/tree/master/ecommerce) and [`kemal-by-example/oauth-login`](https://github.com/sdogruyol/kemal-by-example/tree/master/oauth-login).
## Core Mandates
- **Dependencies:** Always `require "kemal-session"`.
- **Session Configuration:** Use `Kemal::Session.config` to set `secret`, `cookie_name`, and `gc_interval`:
```crystal
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.
```crystal
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)
```crystal
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:
```crystal
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:
```crystal
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
```crystal
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`](https://github.com/kemalcr/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.
Ver no GitHub