A community based topic aggregation platform built on atproto
1package oauth
2
3import (
4 "encoding/base64"
5 "encoding/json"
6 "strings"
7 "testing"
8)
9
10// TestCreateDPoPProof tests DPoP proof generation and structure
11func TestCreateDPoPProof(t *testing.T) {
12 // Generate a test DPoP key
13 dpopKey, err := GenerateDPoPKey()
14 if err != nil {
15 t.Fatalf("Failed to generate DPoP key: %v", err)
16 }
17
18 // Create a DPoP proof
19 proof, err := CreateDPoPProof(dpopKey, "POST", "https://example.com/token", "", "")
20 if err != nil {
21 t.Fatalf("Failed to create DPoP proof: %v", err)
22 }
23
24 // DPoP proof should be a JWT in form: header.payload.signature
25 parts := strings.Split(proof, ".")
26 if len(parts) != 3 {
27 t.Fatalf("Expected 3 parts in JWT, got %d", len(parts))
28 }
29
30 // Decode and inspect the header
31 headerJSON, decodeErr := base64.RawURLEncoding.DecodeString(parts[0])
32 if decodeErr != nil {
33 t.Fatalf("Failed to decode header: %v", decodeErr)
34 }
35
36 var header map[string]interface{}
37 if unmarshalErr := json.Unmarshal(headerJSON, &header); unmarshalErr != nil {
38 t.Fatalf("Failed to unmarshal header: %v", unmarshalErr)
39 }
40
41 t.Logf("DPoP Header: %s", string(headerJSON))
42
43 // Verify required header fields
44 if header["alg"] != "ES256" {
45 t.Errorf("Expected alg=ES256, got %v", header["alg"])
46 }
47 if header["typ"] != "dpop+jwt" {
48 t.Errorf("Expected typ=dpop+jwt, got %v", header["typ"])
49 }
50
51 // Verify JWK is present and is a JSON object
52 jwkValue, hasJWK := header["jwk"]
53 if !hasJWK {
54 t.Fatal("Header missing 'jwk' field")
55 }
56
57 // JWK should be a map/object, not a string
58 jwkMap, ok := jwkValue.(map[string]interface{})
59 if !ok {
60 t.Fatalf("JWK is not a JSON object, got type: %T, value: %v", jwkValue, jwkValue)
61 }
62
63 // Verify JWK has required fields for EC key
64 if jwkMap["kty"] != "EC" {
65 t.Errorf("Expected kty=EC, got %v", jwkMap["kty"])
66 }
67 if jwkMap["crv"] != "P-256" {
68 t.Errorf("Expected crv=P-256, got %v", jwkMap["crv"])
69 }
70 if _, hasX := jwkMap["x"]; !hasX {
71 t.Error("JWK missing 'x' coordinate")
72 }
73 if _, hasY := jwkMap["y"]; !hasY {
74 t.Error("JWK missing 'y' coordinate")
75 }
76
77 // Verify private key is NOT in the public JWK
78 if _, hasD := jwkMap["d"]; hasD {
79 t.Error("SECURITY: JWK contains private key component 'd'!")
80 }
81
82 // Decode and inspect the payload
83 payloadJSON, err := base64.RawURLEncoding.DecodeString(parts[1])
84 if err != nil {
85 t.Fatalf("Failed to decode payload: %v", err)
86 }
87
88 var payload map[string]interface{}
89 if err := json.Unmarshal(payloadJSON, &payload); err != nil {
90 t.Fatalf("Failed to unmarshal payload: %v", err)
91 }
92
93 t.Logf("DPoP Payload: %s", string(payloadJSON))
94
95 // Verify required payload claims
96 if payload["htm"] != "POST" {
97 t.Errorf("Expected htm=POST, got %v", payload["htm"])
98 }
99 if payload["htu"] != "https://example.com/token" {
100 t.Errorf("Expected htu=https://example.com/token, got %v", payload["htu"])
101 }
102 if _, hasIAT := payload["iat"]; !hasIAT {
103 t.Error("Payload missing 'iat' (issued at)")
104 }
105 if _, hasJTI := payload["jti"]; !hasJTI {
106 t.Error("Payload missing 'jti' (JWT ID)")
107 }
108}
109
110// TestDPoPProofWithNonce tests DPoP proof with nonce
111func TestDPoPProofWithNonce(t *testing.T) {
112 dpopKey, err := GenerateDPoPKey()
113 if err != nil {
114 t.Fatalf("Failed to generate DPoP key: %v", err)
115 }
116
117 testNonce := "test-nonce-12345"
118 proof, err := CreateDPoPProof(dpopKey, "POST", "https://example.com/token", testNonce, "")
119 if err != nil {
120 t.Fatalf("Failed to create DPoP proof: %v", err)
121 }
122
123 // Decode payload
124 parts := strings.Split(proof, ".")
125 payloadJSON, err := base64.RawURLEncoding.DecodeString(parts[1])
126 if err != nil {
127 t.Fatalf("Failed to decode payload: %v", err)
128 }
129 var payload map[string]interface{}
130 if err := json.Unmarshal(payloadJSON, &payload); err != nil {
131 t.Fatalf("Failed to unmarshal payload: %v", err)
132 }
133
134 if payload["nonce"] != testNonce {
135 t.Errorf("Expected nonce=%s, got %v", testNonce, payload["nonce"])
136 }
137}
138
139// TestDPoPProofWithAccessToken tests DPoP proof with access token hash
140func TestDPoPProofWithAccessToken(t *testing.T) {
141 dpopKey, err := GenerateDPoPKey()
142 if err != nil {
143 t.Fatalf("Failed to generate DPoP key: %v", err)
144 }
145
146 testToken := "test-access-token"
147 proof, err := CreateDPoPProof(dpopKey, "GET", "https://example.com/resource", "", testToken)
148 if err != nil {
149 t.Fatalf("Failed to create DPoP proof: %v", err)
150 }
151
152 // Decode payload
153 parts := strings.Split(proof, ".")
154 payloadJSON, err := base64.RawURLEncoding.DecodeString(parts[1])
155 if err != nil {
156 t.Fatalf("Failed to decode payload: %v", err)
157 }
158 var payload map[string]interface{}
159 if err := json.Unmarshal(payloadJSON, &payload); err != nil {
160 t.Fatalf("Failed to unmarshal payload: %v", err)
161 }
162
163 ath, hasATH := payload["ath"]
164 if !hasATH {
165 t.Fatal("Payload missing 'ath' (access token hash)")
166 }
167 if ath == "" {
168 t.Error("Access token hash is empty")
169 }
170
171 t.Logf("Access token hash: %v", ath)
172}