initial commit
This commit is contained in:
commit
54576d9d33
61 changed files with 5569 additions and 0 deletions
80
docs/architecture.md
Normal file
80
docs/architecture.md
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
# Architecture and decisions
|
||||
|
||||
## Scope
|
||||
|
||||
Go Loose is an authorization control plane, not an API gateway. Protected applications keep serving their own traffic and use the client middleware to ask Go Loose whether an incoming key is authorized. This minimizes coupling and lets a service adopt Go Loose one route group at a time.
|
||||
|
||||
## Components
|
||||
|
||||
```text
|
||||
cmd/go-loose
|
||||
configuration → migrations → HTTP server
|
||||
|
||||
internal/server
|
||||
OIDC/session auth, management API, authorization endpoint, embedded web UI
|
||||
|
||||
internal/store
|
||||
tenant-isolated PostgreSQL queries and authorization audit writes
|
||||
|
||||
internal/key
|
||||
cryptographically random key generation and one-way lookup hashing
|
||||
|
||||
internal/contract
|
||||
OpenAPI 2/3 JSON/YAML normalization and endpoint extraction
|
||||
|
||||
client
|
||||
public net/http middleware package used by other Go modules
|
||||
```
|
||||
|
||||
PostgreSQL is the source of truth. The frontend is dependency-free HTML/CSS/JavaScript embedded in the Go binary. This creates one immutable artifact and avoids a second package ecosystem while the product is young.
|
||||
|
||||
## Tenant model
|
||||
|
||||
- A **tenant** is an organization such as `nmbs`.
|
||||
- An **application** is a protected API such as `guess` and always belongs to one tenant.
|
||||
- An **API key** belongs to exactly one application and tenant.
|
||||
- A **membership** grants a user `owner`, `admin`, or `viewer` access to a tenant.
|
||||
- An **application access grant** allows a user to sign in to one client application.
|
||||
- `allowed_hosts` optionally binds keys for an application to hosts such as `nmbs.guess.local`.
|
||||
- Tenant and application slugs are sent by middleware, so identical application names can safely exist in different tenants.
|
||||
|
||||
Hostnames are authorization context, not tenant identity. Do not trust a host alone: the key, expected tenant, expected application, and optional allowed host must all agree.
|
||||
|
||||
## Security decisions
|
||||
|
||||
1. **Fail closed.** Missing keys, Go Loose outages, malformed responses, inactive keys, expired keys, scope mismatches, and host mismatches deny the request.
|
||||
2. **No plaintext key persistence.** Keys contain 256 random bits and are stored as SHA-256 digests. High-entropy secrets do not require a slow password hash for offline resistance.
|
||||
3. **Session isolation.** Management uses signed, HTTP-only, SameSite cookies. Mutations additionally require the configured same origin.
|
||||
4. **OIDC validation.** The server uses provider discovery and validates issuer, audience, signature, and expiry through `go-oidc`.
|
||||
5. **SSRF resistance.** Contract import allows only HTTP(S), resolves hostnames, and blocks loopback/private/link-local destinations unless explicitly enabled.
|
||||
6. **Auditing.** Every runtime decision records tenant, application, key, route context, outcome, and reason. Raw keys are never logged.
|
||||
7. **Least privilege.** Management writes require tenant `owner` or `admin`. Read access requires membership.
|
||||
8. **Separate login purpose.** Client login sessions cannot open the management console. Hosted login uses exact callback matching, one-time codes, confidential client secrets, and opaque revocable sessions.
|
||||
|
||||
For production, add rate limiting at the ingress, PostgreSQL backups, metrics/tracing, audit retention, membership invitations, key rotation overlap, and a tightly controlled egress proxy for contract scanning.
|
||||
|
||||
## OpenAPI contracts
|
||||
|
||||
Imported OpenAPI 2.x/3.x documents are normalized to JSON and stored unchanged in meaning. Methods, paths, and operation IDs are extracted into relational rows for discovery. Contracts are currently informational: they do not restrict runtime paths. Enforcing contract endpoints later should be opt-in per application to avoid breaking existing consumers.
|
||||
|
||||
## Decision log
|
||||
|
||||
| Decision | Choice | Reason |
|
||||
|---|---|---|
|
||||
| Deployment | One Go binary | Simple operations and atomic frontend/backend releases |
|
||||
| Database | PostgreSQL | Strong tenant relations, transactions, JSONB contracts, and mature operations |
|
||||
| Router | Go `http.ServeMux` | Modern method/path routing without another framework |
|
||||
| UI | Embedded vanilla web assets | Fast startup, small supply chain, no Node runtime/build |
|
||||
| Identity | OIDC Authorization Code flow | Works with Keycloak, Entra ID, Auth0, Okta, and other compliant IdPs |
|
||||
| API keys | Random opaque secret + SHA-256 digest | Efficient indexed validation without recoverable secrets |
|
||||
| Migrations | Embedded ordered SQL | Reviewable schema history and automatic transactional upgrades |
|
||||
| Runtime integration | Remote authorization middleware | Immediate revocation and centralized audit; availability must be engineered |
|
||||
|
||||
## Planned evolution
|
||||
|
||||
1. Membership invitations and IdP group-to-role mapping.
|
||||
2. Key rotation pairs and usage analytics.
|
||||
3. Optional short-lived signed authorization assertions to reduce control-plane latency.
|
||||
4. Per-application contract enforcement and route-level scopes.
|
||||
5. Webhooks and audit export.
|
||||
6. High-availability deployment, telemetry, and policy-based rate limits.
|
||||
134
docs/client-login.md
Normal file
134
docs/client-login.md
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
# Client application login
|
||||
|
||||
Go Loose can host login for browser-facing Go applications. Your application never handles the user's IdP password. It redirects the browser to Go Loose, receives a short-lived one-time authorization code, exchanges that code from its backend using a client secret, and stores the returned opaque session in an HTTP-only cookie.
|
||||
|
||||
## 1. Configure the application in Go Loose
|
||||
|
||||
1. Open **Applications** and choose **Configure client login**.
|
||||
2. Add the exact callback URL, for example `https://nmbs.guess.local/auth/callback`.
|
||||
3. Save and copy the client secret. It is shown once.
|
||||
4. Open **Users**, invite the user by the email returned by the IdP, and choose **Manage** to grant access to the application.
|
||||
|
||||
For database login, the invitation also requires an initial password. The same tenant login screen supports both database credentials and SSO.
|
||||
|
||||
Saving client configuration rotates the secret. Callback URLs require HTTPS, except `localhost` and `127.0.0.1` while Go Loose development login is enabled.
|
||||
|
||||
## 2. Add the Go client
|
||||
|
||||
```bash
|
||||
go get github.com/danyel/go-loose/client
|
||||
```
|
||||
|
||||
Configure the hosted login and register its handlers:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
goloose "github.com/danyel/go-loose/client"
|
||||
)
|
||||
|
||||
func main() {
|
||||
auth, err := goloose.NewBrowserAuth(goloose.BrowserAuthConfig{
|
||||
BaseURL: "https://nmbs.auth.example.com",
|
||||
ClientID: os.Getenv("GO_LOOSE_CLIENT_ID"),
|
||||
ClientSecret: os.Getenv("GO_LOOSE_CLIENT_SECRET"),
|
||||
RedirectURL: "https://nmbs.guess.local/auth/callback",
|
||||
AfterLoginURL: "/app",
|
||||
AfterLogoutURL: "/",
|
||||
LoginPath: "/login",
|
||||
CookieSecure: true,
|
||||
HTTPClient: &http.Client{Timeout: 3 * time.Second},
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("GET /login", auth.LoginHandler)
|
||||
mux.HandleFunc("GET /auth/callback", auth.CallbackHandler)
|
||||
mux.HandleFunc("POST /logout", auth.LogoutHandler)
|
||||
|
||||
protected := auth.Middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
user, ok := goloose.UserFromContext(r.Context())
|
||||
if !ok {
|
||||
http.Error(w, "user missing", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(w, "Hello %s from tenant %s", user.DisplayName, user.TenantSlug)
|
||||
}))
|
||||
mux.Handle("GET /app", protected)
|
||||
mux.Handle("GET /api/profile", protected)
|
||||
|
||||
log.Fatal(http.ListenAndServe(":8090", mux))
|
||||
}
|
||||
```
|
||||
|
||||
Keep `GO_LOOSE_CLIENT_SECRET` in a secret manager, never source control. In local development, configure an exact callback such as `http://localhost:8090/auth/callback` and set `CookieSecure: false`.
|
||||
|
||||
## Tenant login hostname
|
||||
|
||||
Each tenant has its own login origin:
|
||||
|
||||
```text
|
||||
https://<tenant-slug>.<GO_LOOSE_AUTH_DOMAIN>
|
||||
```
|
||||
|
||||
For tenant `nmbs` with `GO_LOOSE_AUTH_DOMAIN=auth.local`, configure the Go client with:
|
||||
|
||||
```go
|
||||
BaseURL: "http://nmbs.auth.local:18080"
|
||||
```
|
||||
|
||||
For local development, add the tenant hostname to `/etc/hosts`:
|
||||
|
||||
```text
|
||||
127.0.0.1 nmbs.auth.local
|
||||
```
|
||||
|
||||
Production should configure wildcard DNS and TLS for `*.auth.example.com`, route those hosts to Go Loose, and use:
|
||||
|
||||
```dotenv
|
||||
GO_LOOSE_BASE_URL=https://auth.example.com
|
||||
GO_LOOSE_AUTH_DOMAIN=auth.example.com
|
||||
GO_LOOSE_OIDC_REDIRECT_URL=https://auth.example.com/auth/callback
|
||||
```
|
||||
|
||||
The client package initially redirects to `/connect/authorize`. Go Loose verifies the registered client and exact callback, canonicalizes the browser onto the tenant hostname, and renders `<Tenant name> login` for the requested application.
|
||||
|
||||
## 3. Browser flow
|
||||
|
||||
```text
|
||||
Browser → GET /login on client application
|
||||
→ GET /connect/authorize on Go Loose
|
||||
→ separate Go Loose client login
|
||||
→ organization OIDC provider
|
||||
→ Go Loose validates user + application grant
|
||||
→ client /auth/callback?code=...&state=...
|
||||
Client → POST /connect/token with Authorization: Basic <client-id:secret>
|
||||
← opaque 12-hour session token
|
||||
Browser ← HTTP-only SameSite session cookie on client application
|
||||
Request → client middleware → GET /connect/userinfo → protected handler
|
||||
Authorization: Bearer <opaque-session>
|
||||
```
|
||||
|
||||
The authorization code expires after two minutes and can be used once. Redirect URIs must match exactly. Removing a user's application grant immediately makes `/connect/userinfo` reject existing sessions. Logout revokes the central session and clears the local cookie.
|
||||
|
||||
## Security and availability
|
||||
|
||||
- Use TLS for both Go Loose and the client application.
|
||||
- Keep client secrets server-side and rotate them after suspected disclosure.
|
||||
- Do not replace the middleware's fail-closed behavior with an allow-on-error fallback.
|
||||
- Apply CSRF protection to state-changing routes in the client application; the package handles login `state`, not your application forms.
|
||||
- The middleware checks Go Loose on every request for immediate revocation. Deploy Go Loose highly available and use a bounded HTTP timeout.
|
||||
- API-key authentication and browser-user authentication are independent. Use `client.Middleware` for service/API keys and `BrowserAuth.Middleware` for logged-in users.
|
||||
|
||||
## Troubleshooting `403`
|
||||
|
||||
`403 your account does not have access to this application` means authentication succeeded but the user lacks an application grant. In Go Loose, open **Users**, select the user, choose **Manage**, and enable the target application. A missing or incorrect token header returns `401`, not `403`.
|
||||
46
docs/coding-standards.md
Normal file
46
docs/coding-standards.md
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# Coding standards
|
||||
|
||||
## Go
|
||||
|
||||
- Use the standard library unless a dependency clearly reduces security or protocol risk.
|
||||
- Keep public APIs small, documented, and backward compatible.
|
||||
- Pass `context.Context` through I/O boundaries and always bound network calls with timeouts.
|
||||
- Wrap errors with the operation; never log secrets, cookies, authorization headers, or full API keys.
|
||||
- Return explicit errors. Runtime authorization must fail closed.
|
||||
- Keep SQL tenant-aware: management queries must join through membership, and writes must enforce role in the query or transaction.
|
||||
- Use cryptographically secure randomness for credentials and constant-time comparison for signatures.
|
||||
- Format with `gofmt`; run `go test -race ./...`, `go vet ./...`, and `go build ./...`.
|
||||
|
||||
## HTTP API
|
||||
|
||||
- Routes are versioned under `/api/v1`; health and authentication routes are infrastructure exceptions.
|
||||
- JSON handlers reject unknown fields, extra JSON values, and oversized bodies.
|
||||
- Errors use `{"error":"human-readable message"}` without internal implementation details.
|
||||
- Use `401` for absent/unusable authentication, `403` for authenticated credentials outside policy, `404` when existence must not disclose access, and `422` for semantically invalid contracts.
|
||||
- Any management mutation requires a valid user session and same-origin browser request.
|
||||
- Update `web/openapi.json` whenever endpoint behavior changes.
|
||||
|
||||
## Database
|
||||
|
||||
- Use UUID primary keys for tenant data and UTC `timestamptz` timestamps.
|
||||
- Foreign keys declare deletion behavior; frequently queried authorization fields get explicit indexes.
|
||||
- Applied migrations are immutable and forward-compatible.
|
||||
- Multi-step writes use transactions. Do not add application-side checks that can race when a database constraint can enforce the invariant.
|
||||
|
||||
## Frontend
|
||||
|
||||
- Preserve keyboard access, semantic elements, visible focus, responsive layout, and sufficient contrast in every theme.
|
||||
- Escape all server-provided strings before assigning HTML. Prefer `textContent` for new dynamic surfaces.
|
||||
- Store only visual preferences in local storage; authentication remains in HTTP-only cookies.
|
||||
- Keep the console dependency-free unless complexity justifies a reviewed build tool decision.
|
||||
|
||||
## Tests and reviews
|
||||
|
||||
- New domain behavior needs table-driven unit tests and negative cases.
|
||||
- Security boundaries need explicit tests for missing, malformed, expired, cross-tenant, and unauthorized inputs.
|
||||
- Integration tests that need PostgreSQL should use a disposable database and never shared developer data.
|
||||
- Reviews prioritize tenant isolation, fail-closed behavior, migration compatibility, secret handling, and error visibility.
|
||||
|
||||
## Commit style
|
||||
|
||||
Use focused commits with imperative subjects, for example `Add scoped API key authorization`. Do not mix unrelated refactors with behavior changes. Explain migration and security implications in the commit body.
|
||||
75
docs/installation.md
Normal file
75
docs/installation.md
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
# First-run installation and demo data
|
||||
|
||||
## Clean installation
|
||||
|
||||
Start Go Loose with PostgreSQL and a stable `GO_LOOSE_SESSION_SECRET` of at least 32 characters. No OIDC environment variables or bootstrap account are required:
|
||||
|
||||
```bash
|
||||
docker compose up --build
|
||||
```
|
||||
|
||||
Open `http://localhost:8080/install`. The installer asks for a Google OAuth client ID and client secret. In Google Cloud Console, configure this authorized redirect URI before submitting:
|
||||
|
||||
```text
|
||||
http://localhost:8080/auth/callback
|
||||
```
|
||||
|
||||
The supplied **CMS Demo** client ID is prefilled:
|
||||
|
||||
```text
|
||||
554628171917-forvpfs44pqajv77dfhl503prfqnhor3.apps.googleusercontent.com
|
||||
```
|
||||
|
||||
The Google client secret is not included in the repository and must be copied from Google Cloud Console. Go Loose validates Google OIDC discovery, encrypts the secret with AES-GCM using a key derived from `GO_LOOSE_SESSION_SECRET`, and stores only the ciphertext in PostgreSQL. Keep the session secret stable: losing it makes the installed OIDC secret undecryptable.
|
||||
|
||||
Installation is a one-time operation. Submitting the installer immediately starts Google login. Google redirects to `/auth/callback`, where Go Loose validates the ID token, creates a signed HTTP-only session cookie, and redirects to the management console. Browser redirects cannot carry a caller-defined `Authorization` header, so no bearer token is exposed in the URL or to frontend JavaScript.
|
||||
|
||||
Later requests to `/install` redirect to login, and duplicate submissions are rejected.
|
||||
|
||||
## First administrator and waiting room
|
||||
|
||||
The first user who completes Google SSO becomes the system administrator. A PostgreSQL advisory transaction lock makes this first-user claim atomic when multiple users sign in concurrently. The system administrator receives owner access to every tenant and application that exists at that moment.
|
||||
|
||||
Later first-time SSO users are created without tenant membership. Their management page shows only the waiting room. A system administrator approves a waiting user from **Users & access**, selecting:
|
||||
|
||||
- the tenant;
|
||||
- tenant role (`viewer`, `admin`, or `owner`);
|
||||
- the applications the user may log in to.
|
||||
|
||||
Tenant owners and administrators can manage users, applications, API keys, client login, and OpenAPI contracts only inside their assigned tenants. A user assigned to multiple tenants can switch the active tenant from the console header. Database membership checks—not the selector—enforce isolation.
|
||||
|
||||
## Optional Guess demonstration seed
|
||||
|
||||
The installer selects the demonstration seed by default. It creates the `nmbs` and `ypto` tenants, one `guess` application in each tenant, and these local database users:
|
||||
|
||||
| Tenant | Email | Password | Role |
|
||||
|---|---|---|---|
|
||||
| NMBS | `interview@nmbs.auth.local` | `admin123` | Tenant administrator |
|
||||
| YPTO | `interview@ypto.auth.local` | `admin123` | Tenant administrator |
|
||||
| NMBS | `reviewer@nmbs.auth.local` | `Demo-Nmbs-2026!` | Tenant administrator |
|
||||
| YPTO | `reviewer@ypto.auth.local` | `Demo-Ypto-2026!` | Tenant administrator |
|
||||
|
||||
All four users receive access only to their tenant's Guess application. Passwords are stored as Argon2id hashes; plaintext credentials are never written to PostgreSQL.
|
||||
|
||||
These accounts and especially `admin123` are intentionally non-production credentials. Disable the seed for production, or delete/rotate every demo account before exposing the service.
|
||||
|
||||
For local hosted-login testing, add:
|
||||
|
||||
```text
|
||||
127.0.0.1 nmbs.auth.local ypto.auth.local
|
||||
```
|
||||
|
||||
The tenant login origins are then:
|
||||
|
||||
```text
|
||||
http://nmbs.auth.local:8080
|
||||
http://ypto.auth.local:8080
|
||||
```
|
||||
|
||||
An application must first have exact redirect URIs and a generated client secret configured in the management console before `/connect/authorize` can start its login flow.
|
||||
|
||||
## Environment-configured compatibility mode
|
||||
|
||||
Existing deployments may continue to supply all three `GO_LOOSE_OIDC_ISSUER`, `GO_LOOSE_OIDC_CLIENT_ID`, and `GO_LOOSE_OIDC_CLIENT_SECRET` variables. Go Loose treats that as an already configured deployment and bypasses the installer. Supplying `GO_LOOSE_LOCAL_LOGIN=true` together with a non-empty `GO_LOOSE_LOCAL_ADMIN_PASSWORD` does the same for a database-only development deployment.
|
||||
|
||||
For new installations, prefer the web installer. `GO_LOOSE_LOCAL_LOGIN=true` without a bootstrap password enables seeded and invited database users but does not bypass installation.
|
||||
64
docs/integration.md
Normal file
64
docs/integration.md
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
# Integrating a Go API
|
||||
|
||||
Install the module from its eventual repository location:
|
||||
|
||||
```bash
|
||||
go get github.com/danyel/go-loose/client
|
||||
```
|
||||
|
||||
Wrap a router or selected route group:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
goloose "github.com/danyel/go-loose/client"
|
||||
)
|
||||
|
||||
func main() {
|
||||
access, err := goloose.New(goloose.Config{
|
||||
BaseURL: "https://nmbs.goloose.local",
|
||||
Tenant: "nmbs",
|
||||
Application: "guess",
|
||||
HTTPClient: &http.Client{Timeout: 2 * time.Second},
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
protected := access.Middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
principal, _ := goloose.PrincipalFromContext(r.Context())
|
||||
w.Write([]byte("authorized by key " + principal.APIKeyName))
|
||||
}))
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("GET /api/guesses", protected)
|
||||
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
log.Fatal(http.ListenAndServe(":8090", mux))
|
||||
}
|
||||
```
|
||||
|
||||
Call the protected API:
|
||||
|
||||
```bash
|
||||
curl -H 'X-API-Key: gl_REPLACE_WITH_ISSUED_SECRET' \
|
||||
https://nmbs.guess.local/api/guesses
|
||||
```
|
||||
|
||||
The middleware sends the original method, URL path, and request host to Go Loose. It denies missing/invalid keys and control-plane errors, then removes `X-API-Key` before invoking the application handler. Read the authorized tenant/application/key identity with `PrincipalFromContext`.
|
||||
|
||||
## Importing the service contract
|
||||
|
||||
Expose an OpenAPI JSON or YAML route from the service, then use **Contracts → Import contract → Scan URL**. Private/internal URLs are blocked by default; production deployments that need internal discovery should use a restricted egress network and explicitly set `GO_LOOSE_ALLOW_PRIVATE_CONTRACT_URLS=true`.
|
||||
|
||||
Contracts can also be pasted into the console, which avoids server-side egress entirely.
|
||||
|
||||
## Availability and latency
|
||||
|
||||
The current client validates remotely on every request so revocation is immediate. Run Go Loose highly available and set a strict client timeout. Do not implement an “allow on error” fallback. A future signed-assertion mode can reduce latency while retaining bounded revocation delay.
|
||||
78
docs/operations.md
Normal file
78
docs/operations.md
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
# Operations
|
||||
|
||||
## First-run installer
|
||||
|
||||
New deployments need only PostgreSQL, the public URL settings, and a stable session secret. Open `/install` to configure Google SSO and optional Guess demo data. See [First-run installation and demo data](installation.md).
|
||||
|
||||
The first Google SSO user is claimed as system administrator. All later SSO users remain in the waiting room until that administrator grants tenant membership and application access.
|
||||
|
||||
## OIDC / SSO
|
||||
|
||||
Register an OIDC confidential web client with this callback:
|
||||
|
||||
```text
|
||||
https://<go-loose-host>/auth/callback
|
||||
```
|
||||
|
||||
The web installer is preferred. Environment configuration remains available for existing or fully automated deployments:
|
||||
|
||||
```dotenv
|
||||
GO_LOOSE_BASE_URL=https://nmbs.goloose.local
|
||||
GO_LOOSE_AUTH_DOMAIN=auth.example.com
|
||||
GO_LOOSE_OIDC_ISSUER=https://id.example.com/realms/platform
|
||||
GO_LOOSE_OIDC_CLIENT_ID=go-loose
|
||||
GO_LOOSE_OIDC_CLIENT_SECRET=<from-secret-manager>
|
||||
GO_LOOSE_OIDC_REDIRECT_URL=https://nmbs.goloose.local/auth/callback
|
||||
GO_LOOSE_ALLOWED_EMAIL_DOMAINS=example.com
|
||||
GO_LOOSE_DEV_LOGIN=false
|
||||
```
|
||||
|
||||
Client applications are served on `https://<tenant>.auth.example.com`; configure wildcard DNS and TLS for `*.auth.example.com`. The OIDC callback host and `GO_LOOSE_BASE_URL` must be inside the configured auth domain so the callback can issue the shared tenant-login cookie.
|
||||
|
||||
Database authentication is controlled by `GO_LOOSE_LOCAL_LOGIN`. It may remain enabled for seeded or invited users without creating a bootstrap account. Set `GO_LOOSE_LOCAL_ADMIN_PASSWORD` only when compatibility bootstrap behavior is explicitly required, and provide it through a secret manager.
|
||||
|
||||
The provider must return `sub`, `email`, and preferably `name` claims.
|
||||
|
||||
## Migrations
|
||||
|
||||
Migration files live in `internal/database/migrations` and are embedded into the binary. Files run lexicographically once, in individual transactions.
|
||||
|
||||
Create upgrades with an immutable numbered file:
|
||||
|
||||
```text
|
||||
0002_membership_invitations.sql
|
||||
```
|
||||
|
||||
Use this structure:
|
||||
|
||||
```sql
|
||||
-- +goose Up
|
||||
CREATE TABLE membership_invitations (...);
|
||||
|
||||
-- +goose Down
|
||||
DROP TABLE membership_invitations;
|
||||
```
|
||||
|
||||
The built-in runner applies only the `Up` section. Down sections document rollback intent but are never run automatically. Never edit an applied migration; create a new one. Prefer backward-compatible expansion first, deploy code that understands both shapes, then remove old fields in a later release.
|
||||
|
||||
Back up the database before production upgrades. Restore and test backups regularly. Startup fails if a migration fails, preventing the application from running against a partially upgraded schema.
|
||||
|
||||
## Production checklist
|
||||
|
||||
- TLS terminates at a trusted ingress and forwards the original host.
|
||||
- `GO_LOOSE_BASE_URL` is the exact HTTPS browser origin.
|
||||
- Session and OIDC secrets come from a secret manager.
|
||||
- Demo users are not seeded, or every demo credential is removed/rotated before exposure.
|
||||
- Development login and unrestricted private URL scanning are disabled.
|
||||
- PostgreSQL uses TLS, backups, point-in-time recovery, and least-privilege credentials.
|
||||
- `/healthz` is used for readiness; logs are shipped without request headers.
|
||||
- Authorization endpoint rate limits and network policies are configured at ingress.
|
||||
- Multiple replicas share PostgreSQL; rolling deployment starts only after migration compatibility is verified.
|
||||
- The external Swagger UI assets are mirrored locally if internet-free operation is required.
|
||||
|
||||
## Key incident response
|
||||
|
||||
1. Revoke the exposed key in the console.
|
||||
2. Issue a replacement and update the consumer through its secret manager.
|
||||
3. Review `authorization_events` for the key ID and affected time window.
|
||||
4. Investigate the exposure source; never restore a revoked key.
|
||||
56
docs/password-authentication.md
Normal file
56
docs/password-authentication.md
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
# Database user authentication
|
||||
|
||||
Go Loose supports local email/password accounts stored in PostgreSQL. Passwords are never stored in plaintext: the server uses Argon2id with a random 16-byte salt, 64 MiB of memory, three iterations, and two lanes.
|
||||
|
||||
## Seeded Docker credentials
|
||||
|
||||
On a fresh Compose deployment, use `/install` and keep **Seed NMBS and YPTO Guess demo data** selected. The complete credential table is in the [installation guide](installation.md). The two primary interview accounts are:
|
||||
|
||||
```text
|
||||
interview@nmbs.auth.local / admin123
|
||||
interview@ypto.auth.local / admin123
|
||||
```
|
||||
|
||||
For tenant application login, add `127.0.0.1 nmbs.auth.local ypto.auth.local` to `/etc/hosts`.
|
||||
|
||||
```text
|
||||
http://nmbs.auth.local:8080
|
||||
```
|
||||
|
||||
If alternate ports are needed:
|
||||
|
||||
```bash
|
||||
GO_LOOSE_PORT=18080 \
|
||||
GO_LOOSE_POSTGRES_PORT=15432 \
|
||||
docker compose up --build
|
||||
```
|
||||
|
||||
Then use `http://localhost:18080/login` and `http://nmbs.auth.local:18080`.
|
||||
|
||||
## Configuration
|
||||
|
||||
```dotenv
|
||||
GO_LOOSE_LOCAL_LOGIN=true
|
||||
GO_LOOSE_LOCAL_ADMIN_EMAIL=admin@example.com # optional bootstrap
|
||||
GO_LOOSE_LOCAL_ADMIN_PASSWORD=<at-least-12-characters> # optional bootstrap
|
||||
```
|
||||
|
||||
`GO_LOOSE_LOCAL_LOGIN=true` enables all database-backed users. A bootstrap account is created only when `GO_LOOSE_LOCAL_ADMIN_PASSWORD` is non-empty; its password is applied only when the account has no password hash. Changing the environment variable later does not silently overwrite an existing database password. If a bootstrap account is required, inject its password from a secret manager and never document a production credential.
|
||||
|
||||
## Creating client users
|
||||
|
||||
1. Sign in to the management console.
|
||||
2. Open **Users** and choose **Invite user**.
|
||||
3. Enter the user's email, display name, initial password, and tenant role.
|
||||
4. Choose **Manage** for that user and grant the required applications.
|
||||
5. Give the initial credential to the user through a secure channel.
|
||||
|
||||
The user can then sign in on `<tenant>.auth.local` or the production tenant auth hostname. Re-inviting an existing email explicitly replaces its password and display name.
|
||||
|
||||
## Brute-force and audit behavior
|
||||
|
||||
- Five failed attempts for the same IP/email pair within ten minutes cause a fifteen-minute lock.
|
||||
- Invalid emails and invalid passwords return the same response.
|
||||
- Unknown-user attempts still run Argon2id verification to reduce timing differences.
|
||||
- Successful, failed, and rate-limited attempts are written to `user_login_events`.
|
||||
- Authentication sessions remain signed HTTP-only cookies. Tenant client-login sessions cannot access the management console.
|
||||
Loading…
Add table
Add a link
Reference in a new issue