1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
package ingress
import (
"context"
"errors"
"fmt"
"net/http"
"strings"
"time"
"github.com/bluesky-social/indigo/atproto/atcrypto"
"github.com/bluesky-social/indigo/atproto/auth"
"github.com/bluesky-social/indigo/atproto/identity"
"github.com/bluesky-social/indigo/atproto/syntax"
"github.com/golang-jwt/jwt/v5"
"github.com/johnandersen777/socialweb-computer-kcp/internal/didkey"
"github.com/johnandersen777/socialweb-computer-kcp/internal/kit/opt"
)
var serviceAuthMethods = []string{"ES256K", "ES256"}
func NewDirectory(plcURL string) identity.Directory {
if plcURL == "" {
return identity.DefaultDirectory()
}
base := &identity.BaseDirectory{
PLCURL: strings.TrimSuffix(plcURL, "/"),
HTTPClient: http.Client{Timeout: 10 * time.Second},
PLCClient: &http.Client{Timeout: 10 * time.Second},
SkipHandleVerification: true,
}
return identity.NewCacheDirectory(base, 100_000, time.Hour, 2*time.Minute, 5*time.Minute)
}
func BearerToken(authHeader, queryToken string) (string, error) {
if authHeader != "" {
parts := strings.Split(authHeader, " ")
if len(parts) != 2 || !strings.EqualFold(parts[0], "bearer") {
return "", errors.New("Authorization header must be Bearer <token>")
}
return parts[1], nil
}
if queryToken != "" {
return queryToken, nil
}
return "", errors.New("missing Authorization header")
}
type ServiceAuth struct {
Issuer string
ExpiresAt time.Time
}
func VerifyServiceAuth(ctx context.Context, token, aud, lxm string, dir identity.Directory) (ServiceAuth, error) {
if token == "" {
return ServiceAuth{}, errors.New("missing Authorization header")
}
claims := jwt.MapClaims{}
opts := []jwt.ParserOption{
jwt.WithValidMethods(serviceAuthMethods),
jwt.WithAudience(aud),
jwt.WithExpirationRequired(),
jwt.WithLeeway(5 * time.Second),
}
keyFunc := func(t *jwt.Token) (any, error) {
iss, err := claims.GetIssuer()
if err != nil {
return nil, fmt.Errorf("missing iss claim: %w", err)
}
return resolveSigningKey(ctx, iss, dir)
}
parsed, err := jwt.ParseWithClaims(token, claims, keyFunc, opts...)
if err != nil && errors.Is(err, jwt.ErrTokenSignatureInvalid) {
insecure := jwt.MapClaims{}
if _, _, uerr := jwt.NewParser().ParseUnverified(token, insecure); uerr == nil {
if iss, ierr := insecure.GetIssuer(); ierr == nil && dir != nil {
iss, _, _ = strings.Cut(iss, "#")
if did, derr := syntax.ParseDID(iss); derr == nil {
_ = dir.Purge(ctx, did.AtIdentifier())
}
}
claims = jwt.MapClaims{}
parsed, err = jwt.ParseWithClaims(token, claims, keyFunc, opts...)
}
}
if err != nil {
return ServiceAuth{}, err
}
if !parsed.Valid {
return ServiceAuth{}, errors.New("service auth token is not valid")
}
gotLXM, _ := claims["lxm"].(string)
if gotLXM != lxm {
return ServiceAuth{}, fmt.Errorf("lxm mismatch: expected %s, got %s", lxm, gotLXM)
}
iss, err := claims.GetIssuer()
if err != nil {
return ServiceAuth{}, err
}
exp, err := claims.GetExpirationTime()
if err != nil {
return ServiceAuth{}, fmt.Errorf("missing exp claim: %w", err)
}
if exp == nil {
return ServiceAuth{}, errors.New("missing exp claim")
}
return ServiceAuth{Issuer: iss, ExpiresAt: exp.Time}, nil
}
func resolveSigningKey(ctx context.Context, did string, dir identity.Directory) (atcrypto.PublicKey, error) {
did, _, _ = strings.Cut(did, "#")
if strings.HasPrefix(did, didkey.Prefix) {
return didkey.ParsePublic(did)
}
if dir == nil {
return nil, fmt.Errorf("cannot resolve signing key for %s", did)
}
parsed, err := syntax.ParseDID(did)
if err != nil {
return nil, err
}
ident, err := dir.LookupDID(ctx, parsed)
if err != nil {
return nil, fmt.Errorf("resolving DID (%s): %w", did, err)
}
return ident.PublicKey()
}
func SignServiceAuth(priv atcrypto.PrivateKey, iss, aud, lxm string, ttl time.Duration) (string, error) {
nsid, err := syntax.ParseNSID(lxm)
if err != nil {
return "", err
}
ttl = opt.Positive(ttl, 5*time.Minute)
return auth.SignServiceAuth(syntax.DID(iss), aud, ttl, &nsid, priv)
}