fork of indigo with slightly nicer lexgen

admin auth and service helpers in SDK (#1059)

- `net/http` server middleware for "atproto admin auth" (which is just
Basic auth with user "admin), supporting multiple passwords for
operational flexibility (eg, new relay could use this)
- inter-service auth validation, including `net/http` middleware. fairly
complete and correct, though might have some performance issues
(purging/retrying identity resolution; and parsing keys on every
request)

The service auth code is loosely inspired by code in Discover (which is
not open source), which has been running in prod a long time now. For
example the test cases. But that code caches parsed keys much more
aggressively (with an in-process LRU).

There are some TODOs in here about XRPC error responses (aka, reply with
JSON). I'm not sure if we want to manage that here in middleware library
code, or leave it to service error handler to fill in.

The logic around LXM validation/enforcement could also use review, both
of the code and against the atproto XRPC specs.

Note that this package intentionally does not use "XRPC" in
method/variable names, as we are slowly deprecating that terminology.

authored by bnewbold.net and committed by

GitHub d4516ea1 be0a9b5e

+652
+97
atproto/auth/http.go
··· 1 + package auth 2 + 3 + import ( 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) 19 + func 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. 43 + func (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 + }
+155
atproto/auth/http_test.go
··· 1 + package auth 2 + 3 + import ( 4 + "net/http" 5 + "net/http/httptest" 6 + "testing" 7 + "time" 8 + 9 + "github.com/bluesky-social/indigo/atproto/crypto" 10 + "github.com/bluesky-social/indigo/atproto/identity" 11 + "github.com/bluesky-social/indigo/atproto/syntax" 12 + 13 + "github.com/stretchr/testify/assert" 14 + "github.com/stretchr/testify/require" 15 + ) 16 + 17 + func webHome(w http.ResponseWriter, r *http.Request) { 18 + ctx := r.Context() 19 + 20 + w.WriteHeader(http.StatusOK) 21 + did, ok := ctx.Value("did").(syntax.DID) 22 + if ok { 23 + w.Write([]byte(did.String())) 24 + } else { 25 + w.Write([]byte("hello world")) 26 + } 27 + } 28 + 29 + func TestAdminAuthMiddleware(t *testing.T) { 30 + assert := assert.New(t) 31 + 32 + pw1 := "secret123" 33 + pw2 := "secret789" 34 + 35 + req := httptest.NewRequest(http.MethodGet, "/", nil) 36 + middle := AdminAuthMiddleware(webHome, []string{pw1, pw2}) 37 + 38 + { 39 + resp := httptest.NewRecorder() 40 + middle(resp, req) 41 + assert.Equal(http.StatusUnauthorized, resp.Code) 42 + } 43 + 44 + { 45 + resp := httptest.NewRecorder() 46 + req.SetBasicAuth("admin", pw1) 47 + middle(resp, req) 48 + assert.Equal(http.StatusOK, resp.Code) 49 + } 50 + 51 + { 52 + resp := httptest.NewRecorder() 53 + req.SetBasicAuth("admin", pw2) 54 + middle(resp, req) 55 + assert.Equal(http.StatusOK, resp.Code) 56 + } 57 + 58 + { 59 + resp := httptest.NewRecorder() 60 + req.SetBasicAuth("wrong", pw2) 61 + middle(resp, req) 62 + assert.Equal(http.StatusUnauthorized, resp.Code) 63 + } 64 + 65 + { 66 + resp := httptest.NewRecorder() 67 + req.SetBasicAuth("admin", "wrong") 68 + middle(resp, req) 69 + assert.Equal(http.StatusUnauthorized, resp.Code) 70 + } 71 + } 72 + 73 + func TestServiceAuthMiddleware(t *testing.T) { 74 + assert := assert.New(t) 75 + require := require.New(t) 76 + 77 + iss := syntax.DID("did:example:iss") 78 + aud := "did:example:aud#svc" 79 + lxm := syntax.NSID("com.example.api") 80 + 81 + priv, err := crypto.GeneratePrivateKeyP256() 82 + require.NoError(err) 83 + pub, err := priv.PublicKey() 84 + require.NoError(err) 85 + 86 + dir := identity.NewMockDirectory() 87 + dir.Insert(identity.Identity{ 88 + DID: iss, 89 + Keys: map[string]identity.Key{ 90 + "atproto": identity.Key{ 91 + Type: "Multikey", 92 + PublicKeyMultibase: pub.Multibase(), 93 + }, 94 + }, 95 + }) 96 + 97 + v := ServiceAuthValidator{ 98 + Audience: aud, 99 + Dir: &dir, 100 + } 101 + 102 + { 103 + // optional middleware, no auth 104 + req := httptest.NewRequest(http.MethodGet, "/xrpc/com.example.api", nil) 105 + middle := v.Middleware(webHome, false) 106 + resp := httptest.NewRecorder() 107 + middle(resp, req) 108 + assert.Equal(http.StatusOK, resp.Code) 109 + assert.Equal("hello world", string(resp.Body.Bytes())) 110 + } 111 + 112 + { 113 + // mandatory middleware, no auth 114 + req := httptest.NewRequest(http.MethodGet, "/xrpc/com.example.api", nil) 115 + middle := v.Middleware(webHome, true) 116 + resp := httptest.NewRecorder() 117 + middle(resp, req) 118 + assert.Equal(http.StatusUnauthorized, resp.Code) 119 + } 120 + 121 + { 122 + // mandatory middleware, valid auth 123 + tok, err := SignServiceAuth(iss, aud, time.Minute, &lxm, priv) 124 + require.NoError(err) 125 + req := httptest.NewRequest(http.MethodGet, "/xrpc/com.example.api", nil) 126 + req.Header.Set("Authorization", "Bearer "+tok) 127 + middle := v.Middleware(webHome, true) 128 + resp := httptest.NewRecorder() 129 + middle(resp, req) 130 + assert.Equal(http.StatusOK, resp.Code) 131 + assert.Equal(iss.String(), string(resp.Body.Bytes())) 132 + } 133 + 134 + { 135 + // mangled header 136 + req := httptest.NewRequest(http.MethodGet, "/xrpc/com.example.api", nil) 137 + req.Header.Set("Authorization", "Bearer dummy") 138 + middle := v.Middleware(webHome, false) 139 + resp := httptest.NewRecorder() 140 + middle(resp, req) 141 + assert.Equal(http.StatusUnauthorized, resp.Code) 142 + } 143 + 144 + { 145 + // wrong path 146 + tok, err := SignServiceAuth(iss, aud, time.Minute, &lxm, priv) 147 + require.NoError(err) 148 + req := httptest.NewRequest(http.MethodGet, "/xrpc/com.example.other.api", nil) 149 + req.Header.Set("Authorization", "Bearer "+tok) 150 + middle := v.Middleware(webHome, true) 151 + resp := httptest.NewRecorder() 152 + middle(resp, req) 153 + assert.Equal(http.StatusUnauthorized, resp.Code) 154 + } 155 + }
+153
atproto/auth/jwt.go
··· 1 + package auth 2 + 3 + import ( 4 + "context" 5 + "crypto/rand" 6 + "encoding/base64" 7 + "errors" 8 + "fmt" 9 + "log/slog" 10 + "time" 11 + 12 + "github.com/bluesky-social/indigo/atproto/crypto" 13 + "github.com/bluesky-social/indigo/atproto/identity" 14 + "github.com/bluesky-social/indigo/atproto/syntax" 15 + 16 + "github.com/golang-jwt/jwt/v5" 17 + ) 18 + 19 + // TODO: check for uniqueness of JTI (random nonce) to prevent token replay 20 + 21 + type ServiceAuthValidator struct { 22 + // Service DID reference for this validator: a DID with optional #-separated fragment 23 + Audience string 24 + Dir identity.Directory 25 + TimestampLeeway time.Duration 26 + } 27 + 28 + type serviceAuthClaims struct { 29 + jwt.RegisteredClaims 30 + 31 + LexMethod string `json:"lxm,omitempty"` 32 + } 33 + 34 + func (s *ServiceAuthValidator) Validate(ctx context.Context, tokenString string, lexMethod *syntax.NSID) (syntax.DID, error) { 35 + 36 + leeway := s.TimestampLeeway 37 + if leeway == 0 { 38 + leeway = 5 * time.Second 39 + } 40 + 41 + opts := []jwt.ParserOption{ 42 + jwt.WithValidMethods(supportedAlgs), 43 + jwt.WithAudience(s.Audience), 44 + jwt.WithExpirationRequired(), 45 + jwt.WithIssuedAt(), 46 + jwt.WithLeeway(leeway), 47 + } 48 + 49 + token, err := jwt.ParseWithClaims(tokenString, &serviceAuthClaims{}, s.fetchIssuerKeyFunc(ctx), opts...) 50 + if err != nil && errors.Is(err, jwt.ErrTokenSignatureInvalid) { 51 + // if signature validation fails, purge the directory and try again 52 + // TODO: probably need to cache or rate-limit this? 53 + 54 + // do an unvalidated extraction of 'iss' from JWT 55 + insecure := jwt.NewParser(jwt.WithoutClaimsValidation()) 56 + t, _, err := insecure.ParseUnverified(tokenString, &jwt.MapClaims{}) 57 + claims, ok := t.Claims.(*jwt.MapClaims) 58 + if !ok { 59 + return "", jwt.ErrTokenInvalidClaims 60 + } 61 + iss, err := claims.GetIssuer() 62 + if err != nil { 63 + return "", err 64 + } 65 + did, err := syntax.ParseDID(iss) 66 + if err != nil { 67 + return "", fmt.Errorf("%w: invalid DID: %w", jwt.ErrTokenInvalidIssuer, err) 68 + } 69 + 70 + slog.Info("purging directory and retrying service auth signature validation", "did", did) 71 + err = s.Dir.Purge(ctx, did.AtIdentifier()) 72 + if err != nil { 73 + slog.Error("purging identity directory", "did", did, "err", err) 74 + } 75 + token, err = jwt.ParseWithClaims(tokenString, &serviceAuthClaims{}, s.fetchIssuerKeyFunc(ctx), opts...) 76 + } 77 + if err != nil { 78 + return "", err 79 + } 80 + claims, ok := token.Claims.(*serviceAuthClaims) 81 + if !ok { 82 + // TODO: is the error message returned descriptive enough? 83 + return "", jwt.ErrTokenInvalidClaims 84 + } 85 + 86 + if lexMethod != nil && claims.LexMethod != lexMethod.String() { 87 + return "", fmt.Errorf("%w: Lexicon endpoint (LXM)", jwt.ErrTokenInvalidClaims) 88 + } 89 + 90 + // NOTE: KeyFunc has already parsed issuer, so we know it is a valid DID 91 + did := syntax.DID(claims.Issuer) 92 + return did, nil 93 + } 94 + 95 + // resolves public key from identity directory 96 + func (s *ServiceAuthValidator) fetchIssuerKeyFunc(ctx context.Context) func(token *jwt.Token) (any, error) { 97 + return func(token *jwt.Token) (any, error) { 98 + claims, ok := token.Claims.(*serviceAuthClaims) 99 + if !ok { 100 + return nil, jwt.ErrTokenInvalidClaims 101 + } 102 + iss, err := claims.GetIssuer() 103 + if err != nil { 104 + return nil, fmt.Errorf("%w: missing 'iss' claim", jwt.ErrTokenInvalidIssuer) 105 + } 106 + did, err := syntax.ParseDID(iss) 107 + if err != nil { 108 + return nil, fmt.Errorf("%w: invalid DID: %w", jwt.ErrTokenInvalidIssuer, err) 109 + } 110 + // NOTE: this will do handle resolution by default 111 + ident, err := s.Dir.LookupDID(ctx, did) 112 + if err != nil { 113 + return nil, fmt.Errorf("%w: resolving DID (%s): %w", jwt.ErrTokenInvalidIssuer, did, err) 114 + } 115 + return ident.PublicKey() 116 + } 117 + } 118 + 119 + func randomNonce() string { 120 + buf := make([]byte, 16) 121 + rand.Read(buf) 122 + return base64.RawURLEncoding.EncodeToString(buf) 123 + } 124 + 125 + func SignServiceAuth(iss syntax.DID, aud string, ttl time.Duration, lexMethod *syntax.NSID, priv crypto.PrivateKey) (string, error) { 126 + claims := serviceAuthClaims{ 127 + RegisteredClaims: jwt.RegisteredClaims{ 128 + ExpiresAt: jwt.NewNumericDate(time.Now().Add(ttl)), 129 + IssuedAt: jwt.NewNumericDate(time.Now()), 130 + Issuer: iss.String(), 131 + Audience: []string{aud}, 132 + ID: randomNonce(), 133 + }, 134 + } 135 + if lexMethod != nil { 136 + claims.LexMethod = lexMethod.String() 137 + } 138 + 139 + var sm *signingMethodAtproto 140 + 141 + // NOTE: could also have a crypto.PrivateKey.Alg() method which returns a string 142 + switch priv.(type) { 143 + case *crypto.PrivateKeyP256: 144 + sm = signingMethodES256 145 + case *crypto.PrivateKeyK256: 146 + sm = signingMethodES256K 147 + default: 148 + return "", fmt.Errorf("unknown signing key type: %T", priv) 149 + } 150 + 151 + token := jwt.NewWithClaims(sm, claims) 152 + return token.SignedString(priv) 153 + }
+88
atproto/auth/jwt_signing.go
··· 1 + package auth 2 + 3 + import ( 4 + "crypto" 5 + 6 + atcrypto "github.com/bluesky-social/indigo/atproto/crypto" 7 + "github.com/golang-jwt/jwt/v5" 8 + ) 9 + 10 + var ( 11 + signingMethodES256K *signingMethodAtproto 12 + signingMethodES256 *signingMethodAtproto 13 + supportedAlgs []string 14 + ) 15 + 16 + // Implementation of jwt.SigningMethod for the `atproto/crypto` types. 17 + type signingMethodAtproto struct { 18 + alg string 19 + hash crypto.Hash 20 + toOutSig toOutSig 21 + sigLen int 22 + } 23 + 24 + type toOutSig func(sig []byte) []byte 25 + 26 + func init() { 27 + // tells JWT library to serialize 'aud' as regular string, not array of strings (when signing) 28 + jwt.MarshalSingleStringAsArray = false 29 + 30 + signingMethodES256K = &signingMethodAtproto{ 31 + alg: "ES256K", 32 + hash: crypto.SHA256, 33 + toOutSig: toES256K, 34 + sigLen: 64, 35 + } 36 + jwt.RegisterSigningMethod(signingMethodES256K.Alg(), func() jwt.SigningMethod { 37 + return signingMethodES256K 38 + }) 39 + signingMethodES256 = &signingMethodAtproto{ 40 + alg: "ES256", 41 + hash: crypto.SHA256, 42 + toOutSig: toES256, 43 + sigLen: 64, 44 + } 45 + jwt.RegisterSigningMethod(signingMethodES256.Alg(), func() jwt.SigningMethod { 46 + return signingMethodES256 47 + }) 48 + supportedAlgs = []string{signingMethodES256K.Alg(), signingMethodES256.Alg()} 49 + } 50 + 51 + func (sm *signingMethodAtproto) Verify(signingString string, sig []byte, key interface{}) error { 52 + pub, ok := key.(atcrypto.PublicKey) 53 + if !ok { 54 + return jwt.ErrInvalidKeyType 55 + } 56 + 57 + if !sm.hash.Available() { 58 + return jwt.ErrHashUnavailable 59 + } 60 + 61 + if len(sig) != sm.sigLen { 62 + return jwt.ErrTokenSignatureInvalid 63 + } 64 + 65 + // NOTE: important to use using "lenient" variant here 66 + return pub.HashAndVerifyLenient([]byte(signingString), sig) 67 + } 68 + 69 + func (sm *signingMethodAtproto) Sign(signingString string, key interface{}) ([]byte, error) { 70 + priv, ok := key.(atcrypto.PrivateKey) 71 + if !ok { 72 + return nil, jwt.ErrInvalidKeyType 73 + } 74 + 75 + return priv.HashAndSign([]byte(signingString)) 76 + } 77 + 78 + func (sm *signingMethodAtproto) Alg() string { 79 + return sm.alg 80 + } 81 + 82 + func toES256K(sig []byte) []byte { 83 + return sig[:64] 84 + } 85 + 86 + func toES256(sig []byte) []byte { 87 + return sig[:64] 88 + }
+156
atproto/auth/jwt_test.go
··· 1 + package auth 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "testing" 7 + "time" 8 + 9 + "github.com/bluesky-social/indigo/atproto/crypto" 10 + "github.com/bluesky-social/indigo/atproto/identity" 11 + "github.com/bluesky-social/indigo/atproto/syntax" 12 + 13 + "github.com/golang-jwt/jwt/v5" 14 + "github.com/stretchr/testify/assert" 15 + ) 16 + 17 + // Returns an early-2024 timestamp as a point in time for validating known JWTs (which contain expires-at) 18 + func testTime() time.Time { 19 + return time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC) 20 + } 21 + 22 + func validateMinimal(token string, iss, aud string, pub crypto.PublicKey) error { 23 + 24 + p := jwt.NewParser( 25 + jwt.WithValidMethods(supportedAlgs), 26 + jwt.WithTimeFunc(testTime), 27 + jwt.WithIssuer(iss), 28 + jwt.WithAudience(aud), 29 + ) 30 + _, err := p.Parse(token, func(tok *jwt.Token) (any, error) { 31 + return pub, nil 32 + }) 33 + if err != nil { 34 + return fmt.Errorf("failed to parse auth header JWT: %w", err) 35 + } 36 + return nil 37 + } 38 + 39 + func TestSignatureMethods(t *testing.T) { 40 + assert := assert.New(t) 41 + 42 + jwtTestFixtures := []struct { 43 + name string 44 + pubkey string 45 + iss string 46 + aud string 47 + jwt string 48 + }{ 49 + { 50 + name: "secp256k1 (K-256)", 51 + pubkey: "did:key:zQ3shscXNYZQZSPwegiv7uQZZV5kzATLBRtgJhs7uRY7pfSk4", 52 + iss: "did:example:iss", 53 + aud: "did:example:aud", 54 + jwt: "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJpc3MiOiJkaWQ6ZXhhbXBsZTppc3MiLCJhdWQiOiJkaWQ6ZXhhbXBsZTphdWQiLCJleHAiOjE3MTM1NzEwMTJ9.J_In_PQCMjygeeoIKyjybORD89ZnEy1bZTd--sdq_78qv3KCO9181ZAh-2Pl0qlXZjfUlxgIa6wiak2NtsT98g", 55 + }, 56 + { 57 + name: "secp256k1 (K-256)", 58 + pubkey: "did:key:zQ3shqKrpHzQ5HDfhgcYMWaFcpBK3SS39wZLdTjA5GeakX8G5", 59 + iss: "did:example:iss", 60 + aud: "did:example:aud", 61 + jwt: "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJhdWQiOiJkaWQ6ZXhhbXBsZTphdWQiLCJpc3MiOiJkaWQ6ZXhhbXBsZTppc3MiLCJleHAiOjE3MTM1NzExMzJ9.itNeYcF5oFMZIGxtnbJhE4McSniv_aR-Yk1Wj8uWk1K8YjlS2fzuJMo0-fILV3payETxn6r45f0FfpTaqY0EZQ", 62 + }, 63 + { 64 + name: "P-256", 65 + pubkey: "did:key:zDnaeXRDKRCEUoYxi8ZJS2pDsgfxUh3pZiu3SES9nbY4DoART", 66 + iss: "did:example:iss", 67 + aud: "did:example:aud", 68 + jwt: "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJkaWQ6ZXhhbXBsZTppc3MiLCJhdWQiOiJkaWQ6ZXhhbXBsZTphdWQiLCJleHAiOjE3MTM1NzE1NTR9.FFRLm7SGbDUp6cL0WoCs0L5oqNkjCXB963TqbgI-KxIjbiqMQATVCalcMJx17JGTjMmfVHJP6Op_V4Z0TTjqog", 69 + }, 70 + } 71 + 72 + for _, fix := range jwtTestFixtures { 73 + 74 + pubk, err := crypto.ParsePublicDIDKey(fix.pubkey) 75 + if err != nil { 76 + t.Fatal(err) 77 + } 78 + 79 + assert.NoError(validateMinimal(fix.jwt, fix.iss, fix.aud, pubk)) 80 + } 81 + } 82 + 83 + func testSigningValidation(t *testing.T, priv crypto.PrivateKey) { 84 + assert := assert.New(t) 85 + ctx := context.Background() 86 + 87 + iss := syntax.DID("did:example:iss") 88 + aud := "did:example:aud#svc" 89 + lxm := syntax.NSID("com.example.api") 90 + 91 + priv, err := crypto.GeneratePrivateKeyP256() 92 + if err != nil { 93 + t.Fatal(err) 94 + } 95 + pub, err := priv.PublicKey() 96 + if err != nil { 97 + t.Fatal(err) 98 + } 99 + 100 + dir := identity.NewMockDirectory() 101 + dir.Insert(identity.Identity{ 102 + DID: iss, 103 + Keys: map[string]identity.Key{ 104 + "atproto": identity.Key{ 105 + Type: "Multikey", 106 + PublicKeyMultibase: pub.Multibase(), 107 + }, 108 + }, 109 + }) 110 + 111 + v := ServiceAuthValidator{ 112 + Audience: aud, 113 + Dir: &dir, 114 + } 115 + 116 + t1, err := SignServiceAuth(iss, aud, time.Minute, nil, priv) 117 + if err != nil { 118 + t.Fatal(err) 119 + } 120 + d1, err := v.Validate(ctx, t1, nil) 121 + assert.NoError(err) 122 + assert.Equal(d1, iss) 123 + _, err = v.Validate(ctx, t1, &lxm) 124 + assert.Error(err) 125 + 126 + t2, err := SignServiceAuth(iss, aud, time.Minute, &lxm, priv) 127 + if err != nil { 128 + t.Fatal(err) 129 + } 130 + d2, err := v.Validate(ctx, t2, nil) 131 + assert.NoError(err) 132 + assert.Equal(d2, iss) 133 + _, err = v.Validate(ctx, t2, &lxm) 134 + assert.NoError(err) 135 + 136 + _, err = v.Validate(ctx, t2, nil) 137 + assert.NoError(err) 138 + _, err = v.Validate(ctx, t2, &lxm) 139 + assert.NoError(err) 140 + } 141 + 142 + func TestP256SigningValidation(t *testing.T) { 143 + priv, err := crypto.GeneratePrivateKeyP256() 144 + if err != nil { 145 + t.Fatal(err) 146 + } 147 + testSigningValidation(t, priv) 148 + } 149 + 150 + func TestK256SigningValidation(t *testing.T) { 151 + priv, err := crypto.GeneratePrivateKeyK256() 152 + if err != nil { 153 + t.Fatal(err) 154 + } 155 + testSigningValidation(t, priv) 156 + }
+1
go.mod
··· 18 18 github.com/go-redis/cache/v9 v9.0.0 19 19 github.com/gocql/gocql v1.7.0 20 20 github.com/golang-jwt/jwt v3.2.2+incompatible 21 + github.com/golang-jwt/jwt/v5 v5.2.2 21 22 github.com/gorilla/websocket v1.5.1 22 23 github.com/hashicorp/go-retryablehttp v0.7.5 23 24 github.com/hashicorp/golang-lru/arc/v2 v2.0.6
+2
go.sum
··· 117 117 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 118 118 github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= 119 119 github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= 120 + github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= 121 + github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 120 122 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 121 123 github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 122 124 github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=