|
|
1 月之前 | |
|---|---|---|
| .. | ||
| README.md | 1 月之前 | |
| main.go | 1 月之前 | |
| sources.go | 1 月之前 | |
| telegrambots.go | 1 月之前 | |
| tenants.go | 1 月之前 | |
In-house JWT issuer + refresh-token store for the M13 admin UI. No
third-party auth, no SSO (deferred to v2). The only thing the rest
of the system needs to know is: "send a Bearer <access_token> and
I'll know who you are".
| Method | Path | Auth | Purpose |
|---|---|---|---|
POST |
/v1/auth/login |
— | email+password → {access_token, refresh_token, ...} |
POST |
/v1/auth/refresh |
— | refresh_token → new pair (rotates, kills family on re-use) |
POST |
/v1/auth/logout |
— | refresh_token → revoked (idempotent) |
POST |
/v1/auth/magic |
— | magic_token + new_password → password set, user active |
POST |
/v1/users/invite |
bearer (admin) | tenant_slug + email + role → new pending user + magic token |
GET |
/v1/users/me |
bearer | current user info |
GET |
/health |
— | liveness |
GET |
/metrics |
— | prom |
| Var | Default | Notes |
|---|---|---|
BA_AUTHD_HTTP_ADDR |
:8804 |
|
BA_AUTHD_ISSUER |
broad-announce |
the iss claim on JWTs |
BA_AUTHD_JWT_SECRET |
(required) | HS256 secret, ≥32 bytes. Use sealed-secrets in prod. |
BA_AUTHD_ALLOW_GENERATED_SECRET |
1 in dev |
if 1 and no secret in env, generate + persist to BA_AUTHD_SECRET_FILE |
BA_AUTHD_SECRET_FILE |
/var/run/broad-announce/authd.jwt |
where the generated secret is stored |
BA_AUTHD_ACCESS_TTL |
15m |
access JWT TTL |
BA_AUTHD_REFRESH_TTL |
168h (7d) |
refresh token TTL |
BA_AUTHD_BCRYPT_COST |
12 |
bcrypt work factor (12 ≈ 250ms on modern CPU) |
BA_AUTHD_SHUTDOWN_GRACE |
10s |
graceful shutdown timeout |
BA_AUTHD_TRUST_FORWARDED |
0 |
if 1, trust X-Forwarded-For for client IP. Only set behind a known reverse proxy. |
BA_POSTGRES_DSN |
(required) | e.g. postgres://authd:...@postgres:5432/broad-announce?sslmode=disable |
BA_ENV |
dev |
dev → allows generated secret, JSON logs. prod → requires BA_AUTHD_JWT_SECRET. |
# 1. Apply migration
go run ./cmd/seed
# 2. Generate a JWT secret (≥32 bytes of randomness)
openssl rand -base64 48
# → store in K8s sealed-secret, mount as BA_AUTHD_JWT_SECRET
# 3. Create the first super_admin (psql fallback per M13 decision 2.4)
psql -c "INSERT INTO auth.tenants (...) VALUES (...);"
psql -c "INSERT INTO auth.users (tenant_id, email, role, status, password_hash) \
VALUES (NULL, 'you@broad-announce.net', 'super_admin', 'active', \
'\$2a\$12\$...bcrypt-hash...');"
# 4. Run
go run ./cmd/authd
# Login
RESP=$(curl -s -X POST http://localhost:8804/v1/auth/login \
-H 'Content-Type: application/json' \
-d '{"email":"you@broad-announce.net","password":"your-password"}')
ACCESS=$(echo "$RESP" | jq -r .access_token)
REFRESH=$(echo "$RESP" | jq -r .refresh_token)
# /me
curl -H "Authorization: Bearer $ACCESS" http://localhost:8804/v1/users/me
# Refresh
curl -X POST http://localhost:8804/v1/auth/refresh \
-H 'Content-Type: application/json' \
-d "{\"refresh_token\":\"$REFRESH\"}"
# Re-use the OLD refresh — should be session_killed
curl -X POST http://localhost:8804/v1/auth/refresh \
-H 'Content-Type: application/json' \
-d "{\"refresh_token\":\"$REFRESH\"}"
# → {"error":"session_killed","message":"refresh token re-use detected, please log in again"}
authd.VerifyAccessToken) rejects any non-HMAC alg
(the classic alg=none and RS256-confused-as-HS256 attacks).crypto/rand → 64 hex chars.
Stored in Postgres as digest(token, 'sha256'). The plaintext
is in the httpOnly cookie / mobile secure storage, never in
Postgres.BA_AUTHD_BCRYPT_COST for tests./v1/auth/login (brute-force protection): v1.1.
For now, the network-level limits in front of authd are the
only protection./v1/users/invite: v1.1.# Unit tests (no DB)
go test ./internal/authd/
# Integration tests (Postgres required)
TEST_AUTH_DSN=postgres://postgres:testing@127.0.0.1:5432/test_auth?sslmode=disable \
go test -tags=postgres ./internal/authd/
The integration tests cover login → refresh → re-use-detection
→ logout end-to-end. The auth schema is created from
migrations/009_auth.up.sql if not present, and truncated between
tests.
Other services (ingestd, admind, etc.) use authd.NewFromEnv() to
construct a verifier-only *Authd and then apply the middleware
(ad.RequireAuth, ad.RequireRole) to their routes:
import "git3.techno-world.net/lrosales/broad-announce/internal/authd"
func main() {
if !authd.EnvEnabled() {
// gate disabled (LAN deploys)
mux.HandleFunc("GET /v1/...", openHandler)
return
}
ad := authd.MustNewFromEnv()
mux.Handle("GET /v1/...", ad.RequireAuth(authedHandler))
mux.Handle("POST /v1/...", ad.RequireRole("super_admin", "tenant_admin")(destructiveHandler))
}
In production, all services that need to verify tokens share the
same BA_AUTHD_JWT_SECRET (rotated together). The services
that don't issue tokens (admind, ingestd, routerd) can run
without a Postgres connection.
The HTTP middleware in internal/authd/middleware.go:
RequireAuth(next) — reads Authorization: Bearer <token>, calls
VerifyAccessToken, stuffs claims into request context. Rejects
alg=none and any non-HMAC signing method.RequireRole(allowed...) — composes on RequireAuth, returns
403 if the role doesn't match.ClaimsFromContext(ctx) — accessor for handlers downstream.See cmd/admind/main.go::wireDLQRoutes and
cmd/ingestd/http.go::RegisterAdminRoutes for example wiring.