// Package middleware provides HTTP middleware for AT Protocol authentication. package middleware import ( "context" "encoding/json" "net/http" "github.com/bluesky-social/indigo/atproto/syntax" atp "tangled.org/pdewey.com/atp" ) type contextKey string const ( ctxKeyDID contextKey = "atp_did" ctxKeySessionID contextKey = "atp_session_id" ) // CookieAuthConfig configures the [CookieAuth] middleware. type CookieAuthConfig struct { // OAuthApp is the OAuth application to validate sessions against. OAuthApp *atp.OAuthApp // DIDCookieName is the cookie holding the user's DID. // Defaults to "account_did". DIDCookieName string // SessCookieName is the cookie holding the session ID. // Defaults to "session_id". SessCookieName string // OnAuth is called when a valid session is found. Implementations should // be idempotent (this is called on every authenticated request). OnAuth func(did string) } // CookieAuth returns middleware that reads DID + session cookies, validates the // session against the store, and adds auth info to the request context. // Unauthenticated requests pass through without error. func CookieAuth(cfg CookieAuthConfig) func(http.Handler) http.Handler { didCookie := cfg.DIDCookieName if didCookie == "" { didCookie = "account_did" } sessCookie := cfg.SessCookieName if sessCookie == "" { sessCookie = "session_id" } return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { dc, err1 := r.Cookie(didCookie) sc, err2 := r.Cookie(sessCookie) if err1 != nil || err2 != nil { next.ServeHTTP(w, r) return } did, err := syntax.ParseDID(dc.Value) if err != nil { next.ServeHTTP(w, r) return } // Validate session exists in store _, err = cfg.OAuthApp.Store().GetSession(r.Context(), did, sc.Value) if err != nil { next.ServeHTTP(w, r) return } if cfg.OnAuth != nil { cfg.OnAuth(did.String()) } ctx := context.WithValue(r.Context(), ctxKeyDID, did.String()) ctx = context.WithValue(ctx, ctxKeySessionID, sc.Value) next.ServeHTTP(w, r.WithContext(ctx)) }) } } // GetDID retrieves the authenticated user's DID from the request context. func GetDID(ctx context.Context) (string, bool) { did, ok := ctx.Value(ctxKeyDID).(string) return did, ok && did != "" } // GetSessionID retrieves the session ID from the request context. func GetSessionID(ctx context.Context) (string, bool) { sid, ok := ctx.Value(ctxKeySessionID).(string) return sid, ok && sid != "" } // ContextWithAuth returns ctx with the given DID and session ID set under the // keys read by GetDID and GetSessionID. Useful for tests and any code path // that authenticates outside CookieAuth (e.g. an alternative auth middleware). func ContextWithAuth(ctx context.Context, did, sessionID string) context.Context { ctx = context.WithValue(ctx, ctxKeyDID, did) return context.WithValue(ctx, ctxKeySessionID, sessionID) } // ClientMetadataHandler returns an http.Handler that serves the OAuth client // metadata JSON document. Register it at both your client_id URL and // /.well-known/oauth-client-metadata. // // mux.Handle("GET /client-metadata.json", middleware.ClientMetadataHandler(app)) // mux.Handle("GET /.well-known/oauth-client-metadata", middleware.ClientMetadataHandler(app)) func ClientMetadataHandler(app *atp.OAuthApp) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(app.ClientMetadata()) }) }