porting all github actions from bluesky-social/indigo to tangled CI
at main 3.1 kB view raw
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}