1package auth
2
3import (
4 "context"
5 "crypto/subtle"
6 "encoding/json"
7 "fmt"
8 "net/http"
9 "strings"
10
11 "github.com/bluesky-social/indigo/atproto/syntax"
12)
13
14// HTTP Middleware for atproto admin auth, which is HTTP Basic auth with the username "admin".
15//
16// This supports multiple admin passwords, which makes it easier to rotate service secrets.
17//
18// This can be used with `echo.WrapMiddleware` (part of the echo web framework)
19func AdminAuthMiddleware(handler http.HandlerFunc, adminPasswords []string) http.HandlerFunc {
20 return func(w http.ResponseWriter, r *http.Request) {
21 username, password, ok := r.BasicAuth()
22 if ok && username == "admin" {
23 for _, pw := range adminPasswords {
24 if subtle.ConstantTimeCompare([]byte(pw), []byte(password)) == 1 {
25 handler(w, r)
26 return
27 }
28 }
29 }
30 w.Header().Set("WWW-Authenticate", `Basic realm="admin", charset="UTF-8"`)
31 w.Header().Set("Content-Type", "application/json")
32 w.WriteHeader(http.StatusUnauthorized)
33 json.NewEncoder(w).Encode(map[string]string{
34 "error": "Unauthorized",
35 "message": "atproto admin auth required, but missing or incorrect password",
36 })
37 }
38}
39
40// HTTP Middleware for inter-service auth, which is HTTP Bearer with JWT.
41//
42// 'mandatory' indicates whether valid inter-service auth must be present, or just optional.
43func (v *ServiceAuthValidator) Middleware(handler http.HandlerFunc, mandatory bool) http.HandlerFunc {
44 return func(w http.ResponseWriter, r *http.Request) {
45
46 if hdr := r.Header.Get("Authorization"); hdr != "" {
47 parts := strings.Split(hdr, " ")
48 if parts[0] != "Bearer" || len(parts) != 2 {
49 w.Header().Set("WWW-Authenticate", "Bearer")
50 w.Header().Set("Content-Type", "application/json")
51 w.WriteHeader(http.StatusUnauthorized)
52 json.NewEncoder(w).Encode(map[string]string{
53 "error": "Unauthorized",
54 "message": "atproto service auth required, but missing or incorrect formatting",
55 })
56 return
57 }
58
59 var lxm *syntax.NSID
60 uparts := strings.Split(r.URL.Path, "/")
61 // TODO: should this "fail closed"? eg, reject if not a valid XRPC endpoint
62 if len(uparts) >= 3 && uparts[1] == "xrpc" {
63 nsid, err := syntax.ParseNSID(uparts[2])
64 if nil == err {
65 lxm = &nsid
66 }
67 }
68
69 did, err := v.Validate(r.Context(), parts[1], lxm)
70 if err != nil {
71 w.Header().Set("WWW-Authenticate", "Bearer")
72 w.Header().Set("Content-Type", "application/json")
73 w.WriteHeader(http.StatusUnauthorized)
74 json.NewEncoder(w).Encode(map[string]string{
75 "error": "Unauthorized",
76 "message": fmt.Sprintf("invalid service auth: %s", err),
77 })
78 return
79 }
80 ctx := context.WithValue(r.Context(), "did", did)
81 handler(w, r.WithContext(ctx))
82 return
83 }
84
85 if mandatory {
86 w.Header().Set("WWW-Authenticate", "Bearer")
87 w.Header().Set("Content-Type", "application/json")
88 w.WriteHeader(http.StatusUnauthorized)
89 json.NewEncoder(w).Encode(map[string]string{
90 "error": "Unauthorized",
91 "message": "atproto service auth required",
92 })
93 return
94 }
95 handler(w, r)
96 }
97}