1package serviceauth
2
3import (
4 "context"
5 "encoding/json"
6 "log/slog"
7 "net/http"
8 "strings"
9
10 "github.com/bluesky-social/indigo/atproto/auth"
11 "tangled.sh/tangled.sh/core/idresolver"
12 xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors"
13)
14
15const ActorDid string = "ActorDid"
16
17type ServiceAuth struct {
18 logger *slog.Logger
19 resolver *idresolver.Resolver
20 audienceDid string
21}
22
23func NewServiceAuth(logger *slog.Logger, resolver *idresolver.Resolver, audienceDid string) *ServiceAuth {
24 return &ServiceAuth{
25 logger: logger,
26 resolver: resolver,
27 audienceDid: audienceDid,
28 }
29}
30
31func (sa *ServiceAuth) VerifyServiceAuth(next http.Handler) http.Handler {
32 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
33 l := sa.logger.With("url", r.URL)
34
35 token := r.Header.Get("Authorization")
36 token = strings.TrimPrefix(token, "Bearer ")
37
38 s := auth.ServiceAuthValidator{
39 Audience: sa.audienceDid,
40 Dir: sa.resolver.Directory(),
41 }
42
43 did, err := s.Validate(r.Context(), token, nil)
44 if err != nil {
45 l.Error("signature verification failed", "err", err)
46 writeError(w, xrpcerr.AuthError(err), http.StatusForbidden)
47 return
48 }
49
50 r = r.WithContext(
51 context.WithValue(r.Context(), ActorDid, did),
52 )
53
54 next.ServeHTTP(w, r)
55 })
56}
57
58// this is slightly different from http_util::write_error to follow the spec:
59//
60// the json object returned must include an "error" and a "message"
61func writeError(w http.ResponseWriter, e xrpcerr.XrpcError, status int) {
62 w.Header().Set("Content-Type", "application/json")
63 w.WriteHeader(status)
64 json.NewEncoder(w).Encode(e)
65}