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, err := base64.RawURLEncoding.DecodeString(parts[0])
32 if err != nil {
33 t.Fatalf("Failed to decode header: %v", err)
34 }
35
36 var header map[string]interface{}
37 if err := json.Unmarshal(headerJSON, &header); err != nil {
38 t.Fatalf("Failed to unmarshal header: %v", err)
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, _ := base64.RawURLEncoding.DecodeString(parts[1])
126 var payload map[string]interface{}
127 json.Unmarshal(payloadJSON, &payload)
128
129 if payload["nonce"] != testNonce {
130 t.Errorf("Expected nonce=%s, got %v", testNonce, payload["nonce"])
131 }
132}
133
134// TestDPoPProofWithAccessToken tests DPoP proof with access token hash
135func TestDPoPProofWithAccessToken(t *testing.T) {
136 dpopKey, err := GenerateDPoPKey()
137 if err != nil {
138 t.Fatalf("Failed to generate DPoP key: %v", err)
139 }
140
141 testToken := "test-access-token"
142 proof, err := CreateDPoPProof(dpopKey, "GET", "https://example.com/resource", "", testToken)
143 if err != nil {
144 t.Fatalf("Failed to create DPoP proof: %v", err)
145 }
146
147 // Decode payload
148 parts := strings.Split(proof, ".")
149 payloadJSON, _ := base64.RawURLEncoding.DecodeString(parts[1])
150 var payload map[string]interface{}
151 json.Unmarshal(payloadJSON, &payload)
152
153 ath, hasATH := payload["ath"]
154 if !hasATH {
155 t.Fatal("Payload missing 'ath' (access token hash)")
156 }
157 if ath == "" {
158 t.Error("Access token hash is empty")
159 }
160
161 t.Logf("Access token hash: %v", ath)
162}