auth.cr
auth.cr
Crystal/spider-gazelle replacement for the legacy Ruby/Rails auth service. Route-for-route compatible with the existing wire protocol so clients can flip over without changes.
What it does
- Local password sign-in —
POST /auth/signin(bcrypt verify). - OAuth2 client flows —
/auth/oauth2,/auth/oauth2/callback— per-tenant provider configs loaded from theoauth_strattable viaspider-gazelle/multi_auth(and its customGenericOAuth2provider). - SAML SP —
/auth/saml,/auth/saml/callback— backed byspider-gazelle/multi_auth_samlreading theadfs_strattable. - OAuth2 / OIDC server —
/auth/authorize,/auth/token,/auth/revoke,/auth/userinfo,/.well-known/openid-configuration. Backed byplace-labs/authly. JWTs are RS256 with the legacyu: {n, e, p, r}claim block +aud=authority.domainso downstream PlaceOS services keep validating tokens issued before cutover. - API key auth —
X-API-Keyheader (HMAC-SHA512 secret format{id}.{secret}). - Login event publisher — fires
{user_id, provider}JSON on theplaceos/auth/loginRedis channel after every successful login.
What's intentionally out of scope
| Dropped | Why |
|---|---|
OAuth2 password grant |
Deprecated by OAuth 2.1; security risk. Token endpoint returns unsupported_grant_type. Local logins still work via the cookie session on POST /auth/signin. |
| LDAP | All current tenants have migrated off. |
POST /auth/signup |
Unused in production. OAuth users are auto-created inline in the callback when no UserAuthLookup exists. |
OmniAuth :developer strategy |
Rails-dev convenience only. |
Routes
| Method | Path | Purpose |
|---|---|---|
GET |
/auth/healthz |
Liveness |
GET |
/auth/authority |
Authority info + session/token flags (?health= makes it a probe) |
POST |
/auth/signin |
Local password login |
GET |
/auth/logout |
Session teardown + Bearer revoke + logged_out_at stamp |
GET |
/auth/login |
Inline login dispatcher |
GET |
/auth/failure |
OAuth / SAML failure landing page |
GET |
/auth/oauth2 |
Kickoff OAuth2 client flow |
GET/POST |
/auth/oauth2/callback[/:strategy] |
Consume OAuth2 callback |
GET |
/auth/saml |
Kickoff SAML SP flow |
GET/POST |
/auth/saml/callback[/:strategy] |
Consume SAMLResponse |
GET |
/auth/authorize |
OAuth2 authorization endpoint (code flow) |
POST |
/auth/token |
OAuth2 token endpoint (authorization_code, client_credentials, refresh_token) |
POST |
/auth/revoke |
RFC 7009 token revocation |
GET |
/auth/userinfo |
OIDC userinfo (Bearer-gated) |
GET |
/.well-known/openid-configuration |
OIDC discovery |
Environment
Required in production
| Var | Purpose |
|---|---|
JWT_SECRET |
Base64-encoded RSA private PEM. Signs every issued JWT (RS256). Public key derived. Falls back to a hardcoded dev key if unset — DO NOT ship that to production. |
COOKIE_SESSION_SECRET |
≥32 bytes. Encrypts/signs the session cookie. Dev fallback generates an ephemeral key per boot. |
PG_DATABASE_URL or PG_HOST / PG_PORT / PG_DB / PG_USER / PG_PASSWORD |
PostgreSQL connection. |
REDIS_URL |
Used to publish login events. If unset, LoginEvents.publish is a no-op (logged at warn). |
Optional / tunable
| Var | Default | Purpose |
|---|---|---|
SG_ENV |
development |
production flips secure-cookie + production logging. |
JWT_ISSUER |
POS |
iss claim on issued JWTs. Match the legacy Ruby value or services that pin issuer will reject. |
SESSION_TIMEOUT_MINUTES |
1440 |
Session-cookie max age. Per-authority override available via authority.internals["session_timeout"]. |
LOGIN_EVENTS_CHANNEL |
placeos/auth/login |
Redis pub/sub channel for login events. |
PLACE_URI |
(unset) | Base URL used when a legacy X-API-Key validation needs to round-trip to the core engine. |
Run
Locally (against a running Postgres + Redis)
shards install
crystal run src/app.cr -- -b 0.0.0.0 -p 3000
Useful flags
./placeos-auth --routes # dump the route table
./placeos-auth --docs # OpenAPI YAML on stdout
./placeos-auth --env # list every ENV var the app touched
./placeos-auth --version
./placeos-auth -c http://127.0.0.1:3000/auth/healthz # health-check (exit 0 on 2xx-4xx)
Container
docker build -t placeos/auth .
docker run --rm -p 3000:3000 -e JWT_SECRET=... -e COOKIE_SESSION_SECRET=... placeos/auth
The HEALTHCHECK probes /auth/healthz.
Test
./test spins up Postgres + Redis + a migrator container (clones placeos/models@<branch> and runs micrate up) and runs the spec suite end-to-end.
./test # full suite
./test spec/controllers/oauth_spec.cr # one file
Iterating on placeos-models migrations
The migrator's git clone is Docker-cached. After pushing new migrations to the placeos/models branch this project points at:
docker compose build --no-cache migrator
./test
Working on the codebase
- Code style:
crystal tool format && ./bin/ameba— both must be clean before committing. CI rejects either. - Plans + lessons live in
tasks/todo.mdandtasks/lessons.md(please update both as you go —lessons.mdhas caught at least five upstream library quirks already). PLAN.mdis the high-level phase map.
Migration from the Ruby service
| Aspect | Ruby | auth.cr |
|---|---|---|
| Session cookie | _coauth_session at path /auth, Rails AES encryption |
Same name + path, but action-controller MessageEncryptor wire format. Users are forced to re-sign-in at cutover (acceptable for an auth-service rotation). |
JWT issuer (iss) |
POS |
POS |
| JWT shape | {iss, iat, exp, jti, aud, scope, sub, u:{n,e,p,r}} |
Same |
| Bcrypt password digest | Stored in user.password_digest |
Same (placeos-models reused). |
OmniAuth :generic_oauth |
DB-driven oauth_strat |
multi_auth factory under the oauth2 provider name; same oauth_strat rows. |
OmniAuth :generic_adfs |
DB-driven adfs_strat |
multi_auth_saml factory under the saml provider name; same adfs_strat rows. |
OAuth password grant |
Allowed | Rejected with unsupported_grant_type. |
POST /auth/signup |
Manual signup endpoint | Removed; OAuth users are auto-created inline in the callback. |
| Redis login channel | placeos/auth/login |
Same. |
| LDAP | Supported | Dropped. |
Cutover steps
- Ensure
JWT_SECRETmatches the Ruby deployment — downstream services validate by signature, so a key rotation breaks them. - Roll out
placeos-modelswith theoauth_tokensmigration first (or use theauth-replacementbranch). - Set
COOKIE_SESSION_SECRETto a real ≥32-byte value. Existing_coauth_sessioncookies will be invalid — users re-sign-in. - Switch the auth service container image to this build. The route table is wire-compatible.
- Watch the
placeos/auth/loginRedis channel +/auth/healthzfor regressions.
Repository
auth.cr
Owner
Statistic
- 0
- 0
- 0
- 0
- 16
- about 4 hours ago
- May 18, 2026
License
Do What The F*ck You Want To Public License
Links
Synced at
Tue, 19 May 2026 02:50:58 GMT
Languages