placeos-auth

Version, currently master branch1 version
  • master branchlatestAug 13, 2026

github.com/PlaceOS/auth.cr

No description declared in shard.yml.

0 stars
0 dependents
License: WTFPL

Installation

# Add this to your shard.yml
dependencies:
  placeos-auth:
    github: PlaceOS/auth.cr
    branch: master

master is a branch, not a release, so this tracks it rather than pinning a version.

Then run:

shards install

shard.yml

Crystal
>= 1.1.1
Target
  • placeos-auth from src/app.cr

Dependencies

Runtime Dependencies

  • jwt~> 1github: crystal-community/jwt
  • redis*github: stefanwille/crystal-redis
  • authly*github: place-labs/authly
  • pg-orm*github: spider-gazelle/pg-orm
  • multi_auth*github: msa7/multi_auth, branch: master
  • secrets-env~> 1github: spider-gazelle/secrets-env
  • CrystalEmail~> 0.2github: place-labs/CrystalEmail
  • placeos-models*github: placeos/models
  • multi_auth_saml*github: spider-gazelle/multi_auth_saml
  • action-controller>= 8.1github: spider-gazelle/action-controller
  • placeos-log-backend>= 0.11.2github: place-labs/log-backend

Development Dependencies

  • ameba*github: crystal-ameba/amebadev
  • faker*github: askn/fakerdev
  • timecop*github: crystal-community/timecop.crdev
  • webmock*github: manastech/webmock.cr, branch: masterdev
  • hot_topic*github: jgaskins/hot_topicdev

README

auth.cr

CI

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-inPOST /auth/signin (bcrypt verify).
  • OAuth2 client flows/auth/oauth2, /auth/oauth2/callback — per-tenant provider configs loaded from the oauth_strat table via spider-gazelle/multi_auth (and its custom GenericOAuth2 provider).
  • SAML SP/auth/saml, /auth/saml/callback — backed by spider-gazelle/multi_auth_saml reading the adfs_strat table.
  • OAuth2 / OIDC server/auth/authorize, /auth/token, /auth/revoke, /auth/userinfo, /.well-known/openid-configuration. Backed by place-labs/authly. JWTs are RS256 with the legacy u: {n, e, p, r} claim block + aud=authority.domain so downstream PlaceOS services keep validating tokens issued before cutover.
  • API key authX-API-Key header (HMAC-SHA512 secret format {id}.{secret}).
  • Login event publisher — fires {user_id, provider} JSON on the placeos/auth/login Redis 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.md and tasks/lessons.md (please update both as you go — lessons.md has caught at least five upstream library quirks already).
  • PLAN.md is 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

  1. Ensure JWT_SECRET matches the Ruby deployment — downstream services validate by signature, so a key rotation breaks them.
  2. Roll out placeos-models with the oauth_tokens migration first (or use the auth-replacement branch).
  3. Set COOKIE_SESSION_SECRET to a real ≥32-byte value. Existing _coauth_session cookies will be invalid — users re-sign-in.
  4. Switch the auth service container image to this build. The route table is wire-compatible.
  5. Watch the placeos/auth/login Redis channel + /auth/healthz for regressions.