Skip to content

Commit 2aa6fe4

Browse files
authored
Merge pull request #21 from mutablelogic/djt/0420/apikey
Add API key database objects
2 parents e5f721f + 5b1aef3 commit 2aa6fe4

8 files changed

Lines changed: 175 additions & 29 deletions

File tree

.github/workflows/docker.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,11 @@ jobs:
3232
sudo apt -y install build-essential git
3333
git config --global advice.detachedHead false
3434
- name: Checkout
35-
uses: actions/checkout@v4
35+
uses: actions/checkout@v6
3636
with:
3737
fetch-depth: 0
3838
- name: Login
39-
uses: docker/login-action@v3
39+
uses: docker/login-action@v4
4040
with:
4141
registry: ghcr.io
4242
username: ${{ github.actor }}
@@ -58,7 +58,7 @@ jobs:
5858
packages: write
5959
steps:
6060
- name: Login
61-
uses: docker/login-action@v3
61+
uses: docker/login-action@v4
6262
with:
6363
registry: ghcr.io
6464
username: ${{ github.actor }}

README.md

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
# go-auth
2+
3+
`go-auth` is a hosted authentication, authorization, TLS certificate, and LDAP management application.
4+
It combines three subsystems in one server process:
5+
6+
- Auth management for local users, sessions, identity providers, and bearer tokens.
7+
- Certificate management for storing and working with TLS certificate material.
8+
- LDAP management for projecting users and groups into an LDAP directory.
9+
10+
It requires a PostgreSQL database for storage and can be configured
11+
with environment variables and CLI flags.
12+
13+
## Quick Start
14+
15+
Run the container:
16+
17+
```bash
18+
export PG_URL='postgres://user@host.docker.internal/database?sslmode=prefer'
19+
docker run --rm --name go-auth \
20+
-e PG_URL -e PG_PASSWORD \
21+
-e ADDR=0.0.0.0:8084 \
22+
-p 8084:8084 \
23+
ghcr.io/mutablelogic/go-auth:latest
24+
```
25+
26+
Notes:
27+
28+
- `PG_URL` is required. The service will not start without a PostgreSQL connection.
29+
- The image entrypoint is `authmanager` and the default command is `run`, so you only need to pass environment variables unless you want to add extra CLI flags.
30+
31+
## Auth Parameters
32+
33+
These parameters configure the authentication manager and identity provider integration.
34+
35+
| Parameter | Type | Description |
36+
| --- | --- | --- |
37+
| `AUTH_SCHEMA` | env | Database schema used for auth tables. |
38+
| `AUTH_SESSION_TTL` | env | Session lifetime. Defaults to 15 minutes when unset. |
39+
| `AUTH_REFRESH_TTL` | env | Refresh token lifetime. Defaults to 7 days when unset. |
40+
| `GOOGLE_CLIENT_ID` | env | Enables the Google provider when set. |
41+
| `GOOGLE_CLIENT_SECRET` | env | Google OAuth client secret. |
42+
| `--auth.enabled` / `--no-auth` | cli | Enable or disable authentication on management endpoints. Default is enabled. |
43+
| `--auth.signer=file:///path/to/key.pem?kid=main` | cli | Add a private key PEM file used to sign issued tokens. Repeat to add multiple signers. |
44+
| `--local.enabled` / `--no-local.enabled` | cli | Enable or disable the built-in local browser-flow identity provider. Default is disabled. |
45+
46+
Examples:
47+
48+
```bash
49+
docker run --rm \
50+
-p 8084:80 \
51+
-e AUTHMANAGER_ADDR=:80 \
52+
-e PG_URL='postgres://user:password@host.docker.internal/authdb?sslmode=disable' \
53+
-e AUTH_SCHEMA='auth' \
54+
-e AUTH_SESSION_TTL='30m' \
55+
-e AUTH_REFRESH_TTL='168h' \
56+
-e GOOGLE_CLIENT_ID='your-google-client-id' \
57+
-e GOOGLE_CLIENT_SECRET='your-google-client-secret' \
58+
"$IMAGE"
59+
```
60+
61+
## Cert Parameters
62+
63+
These parameters configure the certificate manager.
64+
65+
| Parameter | Type | Description |
66+
| --- | --- | --- |
67+
| `CERT_ENABLED` | env | Enable or disable the certificate manager. Default is `true`. |
68+
| `CERT_PASSPHRASE` | env | Passphrase used for encrypting private keys. Can be supplied multiple times in CLI usage; in Docker this is typically provided once per environment variable name supported by your runtime. |
69+
| `CERT_SCHEMA` | env | Database schema used for certificate tables. |
70+
71+
Example:
72+
73+
```bash
74+
docker run --rm \
75+
-p 8084:80 \
76+
-e AUTHMANAGER_ADDR=:80 \
77+
-e PG_URL='postgres://user:password@host.docker.internal/authdb?sslmode=disable' \
78+
-e CERT_ENABLED='true' \
79+
-e CERT_SCHEMA='cert' \
80+
-e CERT_PASSPHRASE='change-me' \
81+
"$IMAGE"
82+
```
83+
84+
## LDAP Parameters
85+
86+
These parameters configure the LDAP manager. The LDAP manager is only started when `LDAP_URL` is set.
87+
88+
| Parameter | Type | Description |
89+
| --- | --- | --- |
90+
| `LDAP_URL` | env | LDAP server URL to connect to or manage. When unset, the LDAP manager is disabled. |
91+
| `LDAP_BASEDN` | env | Base DN for LDAP entries. Default is `dc=example,dc=org`. |
92+
| `LDAP_USER` | env | Bind user DN used by the LDAP manager. Default is `cn=admin,dc=example,dc=org`. |
93+
| `LDAP_PASS` | env | Bind password for the LDAP manager. |
94+
| `LDAP_USER_DN` | env | Relative DN for the user subtree, for example `ou=users`. |
95+
| `LDAP_GROUP_DN` | env | Relative DN for the group subtree, for example `ou=groups`. |
96+
97+
Example:
98+
99+
```bash
100+
docker run --rm \
101+
-p 8084:80 \
102+
-e AUTHMANAGER_ADDR=:80 \
103+
-e PG_URL='postgres://user:password@host.docker.internal/authdb?sslmode=disable' \
104+
-e LDAP_URL='ldap://ldap.example.org:389' \
105+
-e LDAP_BASEDN='dc=example,dc=org' \
106+
-e LDAP_USER='cn=admin,dc=example,dc=org' \
107+
-e LDAP_PASS='change-me' \
108+
-e LDAP_USER_DN='ou=users' \
109+
-e LDAP_GROUP_DN='ou=groups' \
110+
"$IMAGE"
111+
```
112+
113+
## Common Container Parameters
114+
115+
These are not subsystem-specific, but they are usually required in Docker deployments:
116+
117+
| Parameter | Type | Description |
118+
| --- | --- | --- |
119+
| `PG_URL` | env | PostgreSQL connection URL. Required for startup. |
120+
| `PG_PASSWORD` | env | PostgreSQL password override. |
121+
| `AUTHMANAGER_ADDR` | env | HTTP listen address. Set this to `:80` or `:443` in containers. |
122+
| `ADDR` | env | Alternate generic listen-address environment variable. |
123+
| `--http.prefix` | cli | HTTP API path prefix. Default is `/api`. |
124+
| `--http.timeout` | cli | HTTP server read/write timeout. Default is `15m`. |
125+
| `--http.origin` | cli | Cross-origin protection origin configuration. |
126+
| `--tls.name` | cli | TLS server name. |
127+
| `--tls.cert` | cli | TLS certificate file inside the container. |
128+
| `--tls.key` | cli | TLS private key file inside the container. |
129+
| `--ui` / `--no-ui` | cli | Enable or disable the embedded UI. Default is enabled. |
130+
| `--openapi` / `--no-openapi` | cli | Enable or disable the OpenAPI endpoints. Default is enabled. |
131+
132+
If you mount certificate files into the container, a TLS-enabled run looks like this:
133+
134+
```bash
135+
docker run --rm \
136+
-p 8443:443 \
137+
-e AUTHMANAGER_ADDR=:443 \
138+
-e PG_URL='postgres://user:password@host.docker.internal/authdb?sslmode=disable' \
139+
-v "$PWD/certs:/certs:ro" \
140+
"$IMAGE" \
141+
run --tls.cert=/certs/tls.crt --tls.key=/certs/tls.key
142+
```

auth/httphandler/AUTHN.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Registers the public auth, OIDC, and protected-resource endpoints exposed by the
66

77
Auth is the server's local authentication layer. It starts browser sign-in, exchanges provider credentials for locally signed tokens, publishes discovery metadata, and returns the authenticated user's client-facing claims.
88

9-
### `GET /auth/config`
9+
### `GET /config`
1010

1111
Returns a `PublicClientConfigurations` object keyed by provider name. Each value is a `PublicClientConfiguration` containing only client-safe fields such as `issuer` and `client_id`.
1212

auth/httphandler/authorize.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ func authorize(ctx context.Context, manager *manager.Manager, w http.ResponseWri
7676
}
7777

7878
// Get the identity provider for the request
79-
identity_provider, err := authorizationProvider(manager, req.Provider)
79+
identity_provider, err := authorizationProvider(ctx, manager, req.Provider)
8080
if err != nil {
8181
return authorizeError(w, r, req, err)
8282
}
@@ -99,8 +99,8 @@ func authorize(ctx context.Context, manager *manager.Manager, w http.ResponseWri
9999
return nil
100100
}
101101

102-
func authorizationProvider(manager *manager.Manager, key string) (provider.Provider, error) {
103-
config, err := manager.AuthConfig()
102+
func authorizationProvider(ctx context.Context, manager *manager.Manager, key string) (provider.Provider, error) {
103+
config, err := manager.AuthConfig(ctx)
104104
if err != nil {
105105
return nil, err
106106
} else if len(config) == 0 {

auth/httphandler/config.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,15 @@ func ConfigHandler(manager *manager.Manager, doc *opts.MarkdownDoc) (string, *js
3737
"Auth",
3838
).Get(
3939
func(w http.ResponseWriter, r *http.Request) {
40-
config, err := manager.AuthConfig()
40+
config, err := manager.AuthConfig(r.Context())
4141
if err != nil {
4242
_ = httpresponse.Error(w, autherr.HTTPError(err))
4343
return
4444
}
4545
_ = httpresponse.JSON(w, http.StatusOK, httprequest.Indent(r), config)
4646
},
4747
"Get public auth configuration",
48-
opts.WithDescription(doc.Section(3, "GET /auth/config").Body),
48+
opts.WithDescription(doc.Section(3, "GET /config").Body),
4949
opts.WithJSONResponse(http.StatusOK, jsonschema.MustFor[schema.PublicClientConfigurations]()),
5050
opts.WithErrorResponse(http.StatusNotFound, "No upstream providers are configured."),
5151
)

auth/manager/oidc.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ import (
3636

3737
// AuthConfig returns the shareable upstream provider configuration exposed by
3838
// /auth/config. The client secret remains server-side.
39-
func (m *Manager) AuthConfig() (_ schema.PublicClientConfigurations, err error) {
40-
ctx, endSpan := otel.StartSpan(m.tracer, context.Background(), "manager.AuthConfig")
39+
func (m *Manager) AuthConfig(ctx context.Context) (_ schema.PublicClientConfigurations, err error) {
40+
ctx, endSpan := otel.StartSpan(m.tracer, ctx, "manager.AuthConfig")
4141
defer func() { endSpan(err) }()
4242

4343
config := make(schema.PublicClientConfigurations)

auth/schema/objects.sql

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -41,24 +41,22 @@ CREATE TABLE IF NOT EXISTS ${"schema"}.session (
4141
"revoked_at" TIMESTAMPTZ NULL
4242
);
4343

44-
-- auth.session.refresh_columns
45-
DO $$ BEGIN
46-
ALTER TABLE ${"schema"}.session ADD COLUMN IF NOT EXISTS refresh_expires_at TIMESTAMPTZ;
47-
ALTER TABLE ${"schema"}.session ADD COLUMN IF NOT EXISTS refresh_counter BIGINT;
48-
UPDATE ${"schema"}.session
49-
SET refresh_expires_at = COALESCE(refresh_expires_at, expires_at)
50-
WHERE refresh_expires_at IS NULL;
51-
UPDATE ${"schema"}.session
52-
SET refresh_counter = COALESCE(refresh_counter, 0)
53-
WHERE refresh_counter IS NULL;
54-
ALTER TABLE ${"schema"}.session ALTER COLUMN refresh_expires_at SET NOT NULL;
55-
ALTER TABLE ${"schema"}.session ALTER COLUMN refresh_counter SET NOT NULL;
56-
ALTER TABLE ${"schema"}.session ALTER COLUMN refresh_counter SET DEFAULT 0;
57-
END $$;
58-
5944
-- auth.session.user_index
6045
CREATE INDEX IF NOT EXISTS session_user_idx ON ${"schema"}.session ("user");
6146

47+
-- auth.apikey
48+
CREATE TABLE IF NOT EXISTS ${"schema"}.apikey (
49+
"hash" BYTEA NOT NULL PRIMARY KEY,
50+
"user" UUID NOT NULL REFERENCES ${"schema"}."user" (id) ON DELETE CASCADE,
51+
"name" TEXT NOT NULL DEFAULT '',
52+
"created_at" TIMESTAMPTZ NOT NULL DEFAULT now(),
53+
"modified_at" TIMESTAMPTZ NOT NULL DEFAULT now(),
54+
"expires_at" TIMESTAMPTZ NULL
55+
);
56+
57+
-- auth.apikey.user_index
58+
CREATE INDEX IF NOT EXISTS apikey_user_idx ON ${"schema"}.apikey ("user");
59+
6260
-- auth.group
6361
CREATE TABLE IF NOT EXISTS ${"schema"}.group (
6462
"id" TEXT NOT NULL PRIMARY KEY CONSTRAINT groups_name_identifier CHECK (id ~ '^([a-zA-Z][a-zA-Z0-9_-]{0,63}|\$[a-zA-Z][a-zA-Z0-9_]*\$)$'),
@@ -139,6 +137,15 @@ DO $$ BEGIN
139137
EXECUTE FUNCTION ${"schema"}.notify_table();
140138
END $$;
141139

140+
-- auth.notify.apikey.trigger
141+
DO $$ BEGIN
142+
DROP TRIGGER IF EXISTS apikey_table_changes_notify ON ${"schema"}.apikey;
143+
CREATE TRIGGER apikey_table_changes_notify
144+
AFTER INSERT OR UPDATE OR DELETE ON ${"schema"}.apikey
145+
FOR EACH STATEMENT
146+
EXECUTE FUNCTION ${"schema"}.notify_table();
147+
END $$;
148+
142149
-- auth.notify.group.trigger
143150
DO $$ BEGIN
144151
DROP TRIGGER IF EXISTS group_table_changes_notify ON ${"schema"}."group";

cert/manager/opt.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,6 @@ func WithSchema(name string) Opt {
8484
// WithTracer sets the OpenTelemetry tracer used for manager spans.
8585
func WithTracer(tracer trace.Tracer) Opt {
8686
return func(o *opt) error {
87-
if tracer == nil {
88-
return fmt.Errorf("tracer is required")
89-
}
9087
o.tracer = tracer
9188
return nil
9289
}

0 commit comments

Comments
 (0)