authn
= authn :toc: left :toclevels: 2 :source-highlighter: rouge
Authentication building blocks for Crystal, tied to no web framework.
Formerly kemal-auth. The name was wrong: nothing in it ever depended on Kemal — shard.yml never declared it, and Session exchanges HTTP::Cookie from the standard library rather than a framework's request context. The name simply kept people from reaching for it elsewhere.
link:README.fr.adoc[Version française]
== Features
[cols="1,3"] |=== | Module | Description
| Authn::Password | BCrypt password hashing and validation with CNIL-compliant rules (12+ chars, uppercase, lowercase, digit, special char)
| Authn::Token | JWT token generation and verification
| Authn::Session | Session management via HTTP cookies, framework-agnostic
| Authn::SmtpConfig | Configurable SMTP server settings
| Authn::PasswordReset | Password recovery via email
| Authn::UserManager | User management and invitations |===
== Installation
Add to your shard.yml:
[source,yaml]
dependencies: authn: github: aloli-crystal/authn version: "~> 0.1"
Then run:
[source,bash]
shards install
== SMTP Configuration
SMTP settings can be provided in three ways (in order of priority):
=== 1. At instantiation (highest priority)
[source,crystal]
smtp = Authn::SmtpConfig.new( host: "smtp.example.com", port: 587, username: "user@example.com", password: "secret", from_address: "noreply@example.com", from_name: "My App", use_starttls: true )
=== 2. From a Hash (e.g. from database)
[source,crystal]
smtp = Authn::SmtpConfig.from_hash({ "smtp_host" => "smtp.example.com", "smtp_port" => "587", "smtp_username" => "user@example.com", "smtp_password" => "secret", "smtp_from_address" => "noreply@example.com", "smtp_from_name" => "My App" })
=== 3. Via environment variables (defaults)
[cols="1,2,1"] |=== | Variable | Description | Default
| SMTP_HOST | SMTP server host | localhost
| SMTP_PORT | SMTP port | 587
| SMTP_USERNAME | Username | (empty)
| SMTP_PASSWORD | Password | (empty)
| SMTP_FROM_ADDRESS | Sender address | noreply@example.com
| SMTP_FROM_NAME | Sender name | My App
| SMTP_TLS | Enable TLS/SMTPS | false
| SMTP_STARTTLS | Enable STARTTLS | true |===
== Usage
=== Password management
[source,crystal]
require "authn"
hash = Authn::Password.hash("MyStr0ng!Pass") Authn::Password.verify("MyStr0ng!Pass", hash) # => true
errors = Authn::Password.validate("weak")
=> ["Le mot de passe doit contenir au moins 12 caractères", ...]
=== JWT Tokens
[source,crystal]
token = Authn::Token.generate( secret: ENV["SESSION_SECRET"], sub: "1", email: "user@example.com", role: "admin" ) payload = Authn::Token.decode(token, ENV["SESSION_SECRET"])
=== Password reset
[source,crystal]
smtp = Authn::SmtpConfig.new(host: "smtp.example.com", ...) result = Authn::PasswordReset.send_reset_email( email: "user@example.com", reset_url: "https://myapp.com/reset-password", secret: ENV["SESSION_SECRET"], smtp: smtp )
=== User management
[source,crystal]
Validating a user's own fields — email, name, role — is your application's
business: it owns the wording, the languages and the list of roles.
Authn::UserManager.send_invitation_email( email: "new@example.com", prenom: "Jane", invitation_url: "https://myapp.com/set-password", secret: ENV["SESSION_SECRET"], smtp: smtp ) temp_pwd = Authn::UserManager.generate_temp_password
== Tests
[source,bash]
crystal spec
52 examples covering all modules.
== License
MIT
authn
- 3
- 0
- 0
- 1
- 5
- about 3 hours ago
- February 23, 2026
MIT License
Sat, 26 Sep 2026 20:02:30 GMT