Access Denied
++ You are logged in as {{ .Handle }} ({{ .DID + }}), but you are not authorized to access this resource. +
+ Log Out +diff --git a/README.md b/README.md index 676a717..5f31141 100644 --- a/README.md +++ b/README.md @@ -19,39 +19,85 @@ Build a custom Caddy binary with `xcaddy`: ```bash xcaddy build \ - --with github.com/vvill/caddy-atproto-auth + --with tangled.org/vvill.dev/caddy-atproto-auth ``` -### Example: Centralized Auth Hub +## Configuration + +### Global Options + +The `atproto` global block configures the shared storage and security settings. ```caddyfile { atproto { + # Path to the SQLite database. + # Default: "atproto.db" storage_path /var/lib/caddy/atproto.db - cookie_secret "your-very-long-random-secret-key" + + # A random 32+ character string used to sign session cookies. + # REQUIRED. + cookie_secret "change-me-to-a-secure-random-string-at-least-32-chars" } } +``` + +### Authentication Portal (`atproto_portal`) -# The Portal (Login page and OAuth endpoints) +The `atproto_portal` directive configures the central authentication server. This handles the OAuth flow, serves the login page, and issues session cookies. + +```caddyfile auth.example.com { atproto_portal { - name "My HomeLab" + # The public domain of the portal. + # REQUIRED. domain auth.example.com + + # The display name shown on the login page. + # Default: "Authentication Portal" + name "My Services" + + # Custom UI templates (optional) + ui { + # Path to a custom HTML template for the login page. + login_template /path/to/login.html + } } } +``` + +### Authentication Gate (`atproto_gate`) -# A protected application +The `atproto_gate` directive protects your services. It verifies the session cookie and enforces access control. + +```caddyfile app.example.com { atproto_gate { + # List of allowed identities (DIDs or Handles). + # REQUIRED. allow @alice.bsky.social - allow did:plc:1234... + allow did:plc:1234abcd... + + # URL of the central Auth Portal. + # Requests without a valid session will be redirected here. + # REQUIRED (unless in Standalone Mode). portal_url https://auth.example.com + + # Standalone Mode Configuration (Alternative to portal_url) + # If set, this gate acts as its own portal. + # domain app.example.com + + # Custom UI templates (optional) + ui { + # Path to a custom HTML template for the "Access Denied" page. + forbidden_template /path/to/forbidden.html + } } - + reverse_proxy localhost:8080 } ``` ## Documentation -See the `docs/` folder for detailed architectural constraints and configuration options. +See the `docs/` folder for detailed architectural constraints and implementation details. diff --git a/cmd/caddy/main.go b/cmd/caddy/main.go index c078fd6..14de824 100644 --- a/cmd/caddy/main.go +++ b/cmd/caddy/main.go @@ -3,8 +3,8 @@ package main import ( caddycmd "github.com/caddyserver/caddy/v2/cmd" _ "github.com/caddyserver/caddy/v2/modules/standard" - - _ "github.com/vvill/caddy-atproto-auth" + + _ "tangled.org/vvill.dev/caddy-atproto-auth" ) func main() { diff --git a/gate.go b/gate.go index 0663fce..4895a01 100644 --- a/gate.go +++ b/gate.go @@ -11,11 +11,11 @@ import ( "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" "github.com/caddyserver/caddy/v2/modules/caddyhttp" - "github.com/vvill/caddy-atproto-auth/internal/oauth" - "github.com/vvill/caddy-atproto-auth/internal/resolver" - "github.com/vvill/caddy-atproto-auth/internal/session" - "github.com/vvill/caddy-atproto-auth/internal/ui" "go.uber.org/zap" + "tangled.org/vvill.dev/caddy-atproto-auth/internal/oauth" + "tangled.org/vvill.dev/caddy-atproto-auth/internal/resolver" + "tangled.org/vvill.dev/caddy-atproto-auth/internal/session" + "tangled.org/vvill.dev/caddy-atproto-auth/internal/ui" ) func init() { @@ -161,7 +161,7 @@ func (g *Gate) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp. } if r.URL.Path == "/callback" { // Process callback - sessionData, err := g.oauth.ProcessCallback(r.Context(), r.URL.Query()) + sessionData, handle, err := g.oauth.ProcessCallback(r.Context(), r.URL.Query()) if err != nil { return caddyhttp.Error(http.StatusBadRequest, err) } @@ -169,7 +169,7 @@ func (g *Gate) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp. // Create Session Cookie cookie, err := g.sessions.CreateCookie( sessionData.AccountDID, - "user", // Placeholder handle + handle, 24*7*time.Hour, g.Domain, ) diff --git a/global.go b/global.go index 051e17c..367f6d5 100644 --- a/global.go +++ b/global.go @@ -8,8 +8,8 @@ import ( "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" - "github.com/vvill/caddy-atproto-auth/internal/db" - "github.com/vvill/caddy-atproto-auth/internal/oauth" + "tangled.org/vvill.dev/caddy-atproto-auth/internal/db" + "tangled.org/vvill.dev/caddy-atproto-auth/internal/oauth" ) func init() { @@ -39,10 +39,8 @@ func (App) CaddyModule() caddy.ModuleInfo { func (a *App) Provision(ctx caddy.Context) error { // Defaults if a.StoragePath == "" { - a.StoragePath = "atproto.db" // Relative to workdir or specific path + a.StoragePath = "atproto.db" } - // Resolve relative path against Caddy's storage or workdir if needed. - // For simplicity, we assume absolute or relative to CWD. // Initialize DB store, err := db.NewStore(a.StoragePath) @@ -51,23 +49,6 @@ func (a *App) Provision(ctx caddy.Context) error { } a.Store = store - // Initialize OAuth Manager (requires client ID and callback URL to be fully configured, - // but those might be per-portal or global. The spec says "acts as an OAuth Client". - // If the plugin acts as a *single* client for many subdomains, we need global config for client ID. - // But spec says: "Path A: The Self-Contained Route" and "Path B: The Auth Hub". - // This implies potentially different client IDs for different sites OR one central hub. - // For now, let's defer OAuthManager creation to the Portal or Gate if it's per-route, - // OR we need to add ClientID/CallbackURL to the global config if it's shared. - // - // Looking at the spec: - // "The module acts as an OAuth Client" - // "Global Configuration: storage_path, cookie_secret" - // - // It seems the App module holds the *Storage* and *Keys*. - // The *Portal* (or Gate) defines the "Client" identity (metadata, callback). - // However, `oauth.NewManager` takes a `db.Store`. So the App owns the Store. - // The Portal will instantiate the Manager using the App's Store. - return nil } diff --git a/go.mod b/go.mod index b365e40..2fe94e2 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/vvill/caddy-atproto-auth +module tangled.org/vvill.dev/caddy-atproto-auth go 1.25.5 diff --git a/internal/oauth/manager.go b/internal/oauth/manager.go index 476663b..9407ce8 100644 --- a/internal/oauth/manager.go +++ b/internal/oauth/manager.go @@ -9,7 +9,7 @@ import ( "github.com/bluesky-social/indigo/atproto/atcrypto" indigoOauth "github.com/bluesky-social/indigo/atproto/auth/oauth" - "github.com/vvill/caddy-atproto-auth/internal/db" + "tangled.org/vvill.dev/caddy-atproto-auth/internal/db" ) // Manager wraps the bluesky oauth client app to handle the lifecycle. @@ -88,11 +88,18 @@ func (m *Manager) StartAuthFlow(ctx context.Context, identifier string) (string, } // ProcessCallback exchanges the authorization code for an access token -func (m *Manager) ProcessCallback(ctx context.Context, query url.Values) (*indigoOauth.ClientSessionData, error) { +func (m *Manager) ProcessCallback(ctx context.Context, query url.Values) (*indigoOauth.ClientSessionData, string, error) { sess, err := m.App.ProcessCallback(ctx, query) if err != nil { - return nil, fmt.Errorf("failed to process callback: %w", err) + return nil, "", fmt.Errorf("failed to process callback: %w", err) + } + + // Resolve the handle from the DID + ident, err := m.App.Dir.LookupDID(ctx, sess.AccountDID) + handle := "" + if err == nil && ident != nil { + handle = ident.Handle.String() } - return sess, nil -} + return sess, handle, nil +} diff --git a/internal/test/integration_test.go b/internal/test/integration_test.go index e2543e1..efe6d9f 100644 --- a/internal/test/integration_test.go +++ b/internal/test/integration_test.go @@ -11,7 +11,7 @@ import ( "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" _ "github.com/caddyserver/caddy/v2/modules/standard" - _ "github.com/vvill/caddy-atproto-auth" // Register modules + _ "tangled.org/vvill.dev/caddy-atproto-auth" // Register modules ) func TestCaddyIntegration(t *testing.T) { diff --git a/internal/ui/templates/forbidden.html b/internal/ui/templates/forbidden.html index 06a5f1c..f38d22c 100644 --- a/internal/ui/templates/forbidden.html +++ b/internal/ui/templates/forbidden.html @@ -1,107 +1,136 @@ - + -
- - -You are logged in as {{ .Handle }} ({{ .DID }}), but you are not authorized to access this resource.
- Log Out -+ You are logged in as {{ .Handle }} ({{ .DID + }}), but you are not authorized to access this resource. +
+ Log Out +