Monorepo for Tangled tangled.org
6

Configure Feed

Select the types of activity you want to include in your feed.

1package serviceauth 2 3import ( 4 "io" 5 "log/slog" 6 "net/http" 7 "net/http/httptest" 8 "testing" 9 "time" 10 11 "github.com/bluesky-social/indigo/atproto/atcrypto" 12 "github.com/bluesky-social/indigo/atproto/auth" 13 "github.com/bluesky-social/indigo/atproto/identity" 14 "github.com/bluesky-social/indigo/atproto/syntax" 15) 16 17const ( 18 testIssuer = "did:plc:boltless" 19 testAudience = "did:web:knot.example" 20 testLxm = "sh.tangled.repo.create" 21) 22 23func newTestServiceAuth(t *testing.T) (*ServiceAuth, atcrypto.PrivateKey) { 24 t.Helper() 25 priv, err := atcrypto.GeneratePrivateKeyP256() 26 if err != nil { 27 t.Fatalf("generate key: %v", err) 28 } 29 pub, err := priv.PublicKey() 30 if err != nil { 31 t.Fatalf("derive pubkey: %v", err) 32 } 33 dir := identity.NewMockDirectory() 34 dir.Insert(identity.Identity{ 35 DID: syntax.DID(testIssuer), 36 Keys: map[string]identity.VerificationMethod{ 37 "atproto": {Type: "Multikey", PublicKeyMultibase: pub.Multibase()}, 38 }, 39 }) 40 logger := slog.New(slog.NewTextHandler(io.Discard, nil)) 41 return NewServiceAuth(logger, dir, testAudience), priv 42} 43 44func signed(t *testing.T, priv atcrypto.PrivateKey, lxm *syntax.NSID) string { 45 t.Helper() 46 token, err := auth.SignServiceAuth(syntax.DID(testIssuer), testAudience, time.Minute, lxm, priv) 47 if err != nil { 48 t.Fatalf("sign service auth: %v", err) 49 } 50 return token 51} 52 53func serve(sa *ServiceAuth, path, token string) (*httptest.ResponseRecorder, *syntax.DID) { 54 var seen *syntax.DID 55 next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 56 if did, ok := r.Context().Value(ActorDid).(syntax.DID); ok { 57 seen = &did 58 } 59 w.WriteHeader(http.StatusOK) 60 }) 61 req := httptest.NewRequest(http.MethodPost, path, nil) 62 if token != "" { 63 req.Header.Set("Authorization", "Bearer "+token) 64 } 65 rec := httptest.NewRecorder() 66 sa.VerifyServiceAuth(next).ServeHTTP(rec, req) 67 return rec, seen 68} 69 70func TestVerifyServiceAuth_MatchingLxmPasses(t *testing.T) { 71 sa, priv := newTestServiceAuth(t) 72 lxm := syntax.NSID(testLxm) 73 rec, seen := serve(sa, "/xrpc/"+testLxm, signed(t, priv, &lxm)) 74 if rec.Code != http.StatusOK { 75 t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String()) 76 } 77 if seen == nil || seen.String() != testIssuer { 78 t.Fatalf("ActorDid = %v, want %s", seen, testIssuer) 79 } 80} 81 82func TestVerifyServiceAuth_MismatchedLxmRejected(t *testing.T) { 83 sa, priv := newTestServiceAuth(t) 84 other := syntax.NSID("sh.tangled.knot.addMember") 85 rec, _ := serve(sa, "/xrpc/"+testLxm, signed(t, priv, &other)) 86 if rec.Code != http.StatusForbidden { 87 t.Fatalf("status = %d, want 403 for lxm bound to a different method", rec.Code) 88 } 89} 90 91func TestVerifyServiceAuth_NoLxmClaimRejected(t *testing.T) { 92 sa, priv := newTestServiceAuth(t) 93 rec, _ := serve(sa, "/xrpc/"+testLxm, signed(t, priv, nil)) 94 if rec.Code != http.StatusForbidden { 95 t.Fatalf("status = %d, want 403 for a token carrying no lxm claim", rec.Code) 96 } 97} 98 99func TestVerifyServiceAuth_UnparseablePathRejected(t *testing.T) { 100 sa, priv := newTestServiceAuth(t) 101 lxm := syntax.NSID(testLxm) 102 rec, _ := serve(sa, "/xrpc/notansid", signed(t, priv, &lxm)) 103 if rec.Code != http.StatusForbidden { 104 t.Fatalf("status = %d, want 403 when the path tail is not a valid NSID", rec.Code) 105 } 106} 107 108func TestVerifyServiceAuth_GarbageTokenRejected(t *testing.T) { 109 sa, _ := newTestServiceAuth(t) 110 rec, _ := serve(sa, "/xrpc/"+testLxm, "not.a.jwt") 111 if rec.Code != http.StatusForbidden { 112 t.Fatalf("status = %d, want 403 for an unverifiable token", rec.Code) 113 } 114}