Authly is a zero-framework OAuth 2.0 and OpenID Connect library built directly on Crystal's standard HTTP::Handler. Mount it as an Authorization Server (Authly::Handler), protect your API routes as a Resource Server (Authly::Handler::Bearer and Authly::Handler::RequireScope), or run both together in the same process or across microservices.
| Area | Supported Standards & Features |
|---|---|
| OAuth 2.0 Grants | Authorization Code (authorization_code), Client Credentials (client_credentials), Refresh Token (refresh_token with rotation & family reuse detection), Device Authorization (urn:ietf:params:oauth:grant-type:device_code), Cryptographic Challenge-Response (challenge_response / urn:ietf:params:oauth:grant-type:challenge), Social Sign-In (social / urn:ietf:params:oauth:grant-type:social), Password (password), Implicit (implicit) |
| Social Sign-In | Built-in adapters for Google, GitHub, Apple, Microsoft / Entra ID, Discord, and generic OAuth2/OIDC providers (GET /oauth/social/:provider, GET|POST /oauth/social/:provider/callback, and POST /oauth/token) |
| OpenID Connect | id_token issuance (nonce, acr, amr, auth_time), GET /oauth/userinfo, OIDC Discovery (/.well-known/openid-configuration), OAuth 2.0 Authorization Server Metadata (/.well-known/oauth-authorization-server — RFC 8414), JWKS (/.well-known/jwks.json — RFC 7517) |
| FAPI & Advanced Security | PKCE with S256 enforcement (RFC 7636), Pushed Authorization Requests (POST /oauth/par — RFC 9126), JWT Secured Authorization Response Mode (JARM — RFC 9101), Certificate-Bound Access Tokens (cnf.x5t#S256 mTLS — RFC 8705), Dynamic Client Registration (POST /oauth/register — RFC 7591) |
| Token Lifecycle | :jwt (HS*, RS*, ES*) and :opaque token strategies, asymmetric key rotation (signing_key_id + verification_keys), Token Introspection (POST /oauth/introspect — RFC 7662), Token Revocation (POST /oauth/revoke — RFC 7009), host-controlled IssuanceOptions |
| Resource Server Protection | Authly::Handler::Bearer (RFC 6750 verification, audience checking, context.authly_claims!) and Authly::Handler::RequireScope (route-level all / any scope enforcement) |
Add authly to your shard.yml:
dependencies:
authly:
github: azutoolkit/authly
Then run:
shards install
Requirements: Crystal >= 1.22.0
Create a working OAuth 2.0 Authorization Server and protected Resource API in a single file:
require "http/server"
require "json"
require "authly"
Authly.configure do |config|
config.issuer = "http://localhost:8080"
config.base_url = "http://localhost:8080"
config.secret_key = ENV.fetch("AUTHLY_SECRET_KEY", "dev-secret-change-in-production-32b")
config.algorithm = JWT::Algorithm::HS256
config.token_strategy = :jwt
end
# Register a client for machine-to-machine authentication
Authly.clients << Authly::Client.new(
name: "Billing Service",
secret: "billing-secret-123",
redirect_uri: "http://localhost:8080/callback",
id: "billing-service",
scopes: "invoices:read invoices:write",
allowed_grant_types: ["client_credentials"]
)
app = HTTP::Handler::HandlerProc.new do |context|
case context.request.path
when "/health"
context.response.content_type = "application/json"
context.response.print({status: "ok"}.to_json)
when "/api/invoices"
claims = context.authly_claims!
context.response.content_type = "application/json"
context.response.print({
client_id: claims.client_id,
scopes: claims.scopes,
invoices: [{id: "inv_01", amount_cents: 4500}],
}.to_json)
else
context.response.status = HTTP::Status::NOT_FOUND
end
end
server = HTTP::Server.new([
Authly::Handler.new,
Authly::Handler::Bearer.new(
realm: "api",
except_exact: ["/health"],
except: ["/oauth", "/.well-known"]
),
Authly::Handler::RequireScope.all("invoices:read", prefixes: ["/api/invoices"]),
app,
] of HTTP::Handler)
address = server.bind_tcp("127.0.0.1", 8080)
puts "Listening on http://#{address}"
server.listen
Issue a token and call the protected API:
# 1. Request an access token
TOKEN=$(curl -s -X POST http://127.0.0.1:8080/oauth/token \
-u "billing-service:billing-secret-123" \
-d "grant_type=client_credentials&scope=invoices:read" | jq -r .access_token)
# 2. Call the protected endpoint
curl -i http://127.0.0.1:8080/api/invoices \
-H "Authorization: Bearer $TOKEN"
Full documentation is available in docs/:
Step-by-step guides that walk you through building working applications from scratch:
- Getting Started: Your First OAuth2 Server & Protected API — Launch an OAuth2 server, issue a
client_credentialstoken, and protect an endpoint. - Interactive User Sign-In with Authorization Code, PKCE & OIDC — Connect host user sessions, render consent, verify
S256PKCE, and fetch UserInfo. - Passwordless Authentication with Challenge-Response — Authenticate clients and users with Ed25519 or HMAC-SHA256 challenge nonces.
Practical recipes for configuring production features:
- Implement Custom Clients, Owners & Persistent Redis Stores — Wire
AuthorizableClient,AuthorizableOwner,TokenStore,ChallengeStore,StateStore, andAttemptLimiterto SQL and Redis. - Protect Resource APIs & Mount in Azu — Configure
Authly::Handler::Bearer,Authly::Handler::RequireScope, audience validation, CORS preflights, and Azu middleware chains. - Enable FAPI & Advanced Security (PKCE, PAR, JARM, mTLS) — Enforce
S256PKCE, Pushed Authorization Requests (POST /oauth/par), JARM signed/encrypted responses, and certificate-bound tokens. - Configure Asymmetric Keys, Key Rotation & JWKS — Sign with
RS256/ES256, rotate keys without downtime viaverification_keys, and customize claims withIssuanceOptions. - Configure Dynamic Client Registration & Device Authorization — Enable RFC 7591 registration (
POST /oauth/register) and RFC 8628 smart-TV/CLI device flows (POST /oauth/device/code&/device). - Configure Social Sign-In — Integrate Google, GitHub, Apple, Microsoft, Discord, and custom OAuth2/OIDC providers via redirect callbacks or
POST /oauth/token.
Complete technical specifications:
Authly::ConfigurationReference — Every setting, TTL, store, security flag, and callback onAuthly.config.- HTTP Endpoints & Wire Protocol — Request parameters, headers, and JSON responses for all endpoints.
- Crystal API, Grants & Store Interfaces — Signatures for
Authly,AuthorizableClient,AuthorizableOwner,ChallengeVerifier,Claims, and all stores. - Errors & RFC 6750 Challenge Responses —
Authly::Errorcodes, HTTP statuses, andWWW-Authenticateerror responses.
Architectural overviews and security design:
- Architecture & Design Principles — How Authly separates the Authorization Server, Resource Server, host login session, and stores.
- Token Strategies & Refresh Token Lifecycle —
:jwtvs.:opaquetrade-offs, token-type separation, and refresh token rotation with family theft detection. - Security Model & Multi-Node Deployment — Atomic
consumeguarantees, constant-time challenge verification, fail-closed rate limiting, and multi-node checklists.
Authly::Handler mounts the following endpoints out of the box:
| Method(s) | Path | Purpose |
|---|---|---|
GET, POST |
/oauth/authorize |
Interactive authorization & user consent (RFC 6749, OIDC, PKCE, PAR, JARM) |
POST |
/oauth/token |
Token issuance & refresh (authorization_code, client_credentials, refresh_token, password, device_code, challenge_response) |
POST |
/oauth/par |
Pushed Authorization Requests (RFC 9126) |
POST |
/oauth/challenge |
Cryptographic Challenge-Response nonce issuance |
GET |
/oauth/social/:provider |
Social Sign-In redirect initiation (PKCE + signed CSRF state) |
GET, POST |
/oauth/social/:provider/callback |
Social Sign-In provider callback |
POST |
/oauth/device/code |
Device Authorization code issuance (RFC 8628) |
GET, POST |
/device |
End-user device code verification page (RFC 8628) |
POST |
/oauth/introspect, /introspect |
Token Introspection (RFC 7662) |
POST |
/oauth/revoke, /revoke |
Token Revocation (RFC 7009) |
GET |
/oauth/userinfo |
OpenID Connect UserInfo claims |
POST |
/oauth/register |
Dynamic Client Registration (RFC 7591) |
GET, HEAD |
/.well-known/openid-configuration |
OpenID Connect Discovery 1.0 metadata |
GET, HEAD |
/.well-known/oauth-authorization-server |
OAuth 2.0 Authorization Server Metadata (RFC 8414) |
GET, HEAD |
/.well-known/jwks.json |
JSON Web Key Set (RFC 7517) |
Contributions are welcome! To work on Authly locally:
# Install dependencies
shards install
# Run the test suite
crystal spec
# Check formatting
crystal tool format --check src spec
# Run static code analysis
crystal run lib/ameba/src/cli.cr
- Fork the repository (
https://github.com/azutoolkit/authly/fork) - Create your feature branch (
git checkout -b feat/my-feature) - Write specs covering your changes in
spec/ - Ensure
crystal spec,crystal tool format --check src spec, andamebapass - Open a Pull Request
Distributed under the MIT License. See LICENSE for details.
Elias J. Perez — @eliasjpr · https://github.com/azutoolkit/authly
