Monorepo for Tangled
tangled.org
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 token := r.Header.Get("Authorization")
43 token = strings.TrimPrefix(token, "Bearer ")
44
45 lxm, err := syntax.ParseNSID(path.Base(r.URL.Path))
46 if err != nil {
47 sa.logger.Error("could not derive lexicon method from request path", "path", r.URL.Path, "err", err)
48 writeError(w, xrpcerr.AuthError(err), http.StatusForbidden)
49 return
50 }
51
52 s := auth.ServiceAuthValidator{
53 Audience: sa.audienceDid,
54 Dir: sa.dir,
55 }
56
57 did, err := s.Validate(r.Context(), token, &lxm)
58 if err != nil {
59 sa.logger.Error("signature verification failed", "err", err)
60 writeError(w, xrpcerr.AuthError(err), http.StatusForbidden)
61 return
62 }
63
64 sa.logger.Debug("valid signature", "did", did)
65
66 r = r.WithContext(
67 context.WithValue(r.Context(), ActorDid, did),
68 )
69
70 next.ServeHTTP(w, r)
71 })
72}
73
74// this is slightly different from http_util::write_error to follow the spec:
75//
76// the json object returned must include an "error" and a "message"
77func writeError(w http.ResponseWriter, e xrpcerr.XrpcError, status int) {
78 w.Header().Set("Content-Type", "application/json")
79 w.WriteHeader(status)
80 json.NewEncoder(w).Encode(e)
81}