1package auth
2
3import (
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)
18func testTime() time.Time {
19 return time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
20}
21
22func 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
39func 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
83func 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.VerificationMethod{
104 "atproto": {
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
142func TestP256SigningValidation(t *testing.T) {
143 priv, err := crypto.GeneratePrivateKeyP256()
144 if err != nil {
145 t.Fatal(err)
146 }
147 testSigningValidation(t, priv)
148}
149
150func TestK256SigningValidation(t *testing.T) {
151 priv, err := crypto.GeneratePrivateKeyK256()
152 if err != nil {
153 t.Fatal(err)
154 }
155 testSigningValidation(t, priv)
156}