cloudflare-native port of the tangled knot server
2.5 kB
91 lines
1package serviceauth
2
3import (
4 "context"
5 "encoding/json"
6 "log/slog"
7 "net/http"
8 "path"
9 "strings"
10
11 "github.com/bluesky-social/indigo/atproto/auth"
12 "github.com/bluesky-social/indigo/atproto/identity"
13 "github.com/bluesky-social/indigo/atproto/syntax"
14 "tangled.org/core/log"
15 xrpcerr "tangled.org/core/xrpc/errors"
16)
17
18type contextKey string
19
20const ActorDid contextKey = "ActorDid"
21
22func DidWeb(hostname string) syntax.DID {
23 return syntax.DID("did:web:" + strings.ReplaceAll(hostname, ":", "%3A"))
24}
25
26type ServiceAuth struct {
27 logger *slog.Logger
28 dir identity.Directory
29 audienceDid string
30}
31
32func NewServiceAuth(logger *slog.Logger, dir identity.Directory, audienceDid string) *ServiceAuth {
33 return &ServiceAuth{
34 logger: log.SubLogger(logger, "serviceauth"),
35 dir: dir,
36 audienceDid: audienceDid,
37 }
38}
39
40func (sa *ServiceAuth) VerifyServiceAuth(next http.Handler) http.Handler {
41 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
42 lxm, err := syntax.ParseNSID(path.Base(r.URL.Path))
43 if err != nil {
44 sa.logger.Error("could not derive lexicon method from request path", "path", r.URL.Path, "err", err)
45 writeError(w, xrpcerr.AuthError(err), http.StatusForbidden)
46 return
47 }
48
49 sa.verifyServiceAuthFor(next, lxm).ServeHTTP(w, r)
50 })
51}
52
53func (sa *ServiceAuth) VerifyServiceAuthFor(next http.Handler, lxm syntax.NSID) http.Handler {
54 return sa.verifyServiceAuthFor(next, lxm)
55}
56
57func (sa *ServiceAuth) verifyServiceAuthFor(next http.Handler, lxm syntax.NSID) http.Handler {
58 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
59 token := r.Header.Get("Authorization")
60 token = strings.TrimPrefix(token, "Bearer ")
61
62 s := auth.ServiceAuthValidator{
63 Audience: sa.audienceDid,
64 Dir: sa.dir,
65 }
66
67 did, err := s.Validate(r.Context(), token, &lxm)
68 if err != nil {
69 sa.logger.Error("signature verification failed", "err", err)
70 writeError(w, xrpcerr.AuthError(err), http.StatusForbidden)
71 return
72 }
73
74 sa.logger.Debug("valid signature", "did", did)
75
76 r = r.WithContext(
77 context.WithValue(r.Context(), ActorDid, did),
78 )
79
80 next.ServeHTTP(w, r)
81 })
82}
83
84// this is slightly different from http_util::write_error to follow the spec:
85//
86// the json object returned must include an "error" and a "message"
87func writeError(w http.ResponseWriter, e xrpcerr.XrpcError, status int) {
88 w.Header().Set("Content-Type", "application/json")
89 w.WriteHeader(status)
90 json.NewEncoder(w).Encode(e)
91}