Skip to content

Commit 2a2c08d

Browse files
committed
Add token redaction to Identity serialization
Implement String() and MarshalJSON() methods on the Identity struct to prevent accidental token leakage when logging or serializing identities.
1 parent 0c71cc2 commit 2a2c08d

File tree

2 files changed

+195
-0
lines changed

2 files changed

+195
-0
lines changed

pkg/vmcp/auth/auth.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ package auth
1414

1515
import (
1616
"context"
17+
"encoding/json"
18+
"fmt"
1719
"net/http"
1820

1921
"github.com/golang-jwt/jwt/v5"
@@ -98,6 +100,58 @@ type Identity struct {
98100
Metadata map[string]string
99101
}
100102

103+
// String returns a string representation of the Identity with sensitive fields redacted.
104+
// This prevents accidental token leakage when the Identity is logged or printed.
105+
func (i *Identity) String() string {
106+
if i == nil {
107+
return "<nil>"
108+
}
109+
110+
token := "REDACTED"
111+
if i.Token == "" {
112+
token = "<empty>"
113+
}
114+
115+
return fmt.Sprintf("Identity{Subject:%q, Name:%q, Email:%q, Groups:%v, Token:%s, TokenType:%q}",
116+
i.Subject, i.Name, i.Email, i.Groups, token, i.TokenType)
117+
}
118+
119+
// MarshalJSON implements json.Marshaler to redact sensitive fields during JSON serialization.
120+
// This prevents accidental token leakage in structured logs, API responses, or audit logs.
121+
func (i *Identity) MarshalJSON() ([]byte, error) {
122+
if i == nil {
123+
return []byte("null"), nil
124+
}
125+
126+
// Create a safe representation with lowercase field names and redacted token
127+
type SafeIdentity struct {
128+
Subject string `json:"subject"`
129+
Name string `json:"name"`
130+
Email string `json:"email"`
131+
Groups []string `json:"groups"`
132+
Claims map[string]any `json:"claims"`
133+
Token string `json:"token"`
134+
TokenType string `json:"tokenType"`
135+
Metadata map[string]string `json:"metadata"`
136+
}
137+
138+
token := i.Token
139+
if token != "" {
140+
token = "REDACTED"
141+
}
142+
143+
return json.Marshal(&SafeIdentity{
144+
Subject: i.Subject,
145+
Name: i.Name,
146+
Email: i.Email,
147+
Groups: i.Groups,
148+
Claims: i.Claims,
149+
Token: token,
150+
TokenType: i.TokenType,
151+
Metadata: i.Metadata,
152+
})
153+
}
154+
101155
// TokenAuthenticator validates JWT tokens and provides HTTP middleware for authentication.
102156
// This interface abstracts the token validation functionality from pkg/auth to enable
103157
// testing with mocks while the concrete *auth.TokenValidator implementation satisfies

pkg/vmcp/auth/auth_test.go

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package auth
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func TestIdentity_String(t *testing.T) {
12+
t.Parallel()
13+
14+
testCases := []struct {
15+
name string
16+
identity *Identity
17+
contains []string
18+
notContains []string
19+
}{
20+
{
21+
name: "redacts_token",
22+
identity: &Identity{
23+
Subject: "[email protected]",
24+
Name: "Test User",
25+
26+
Groups: []string{"admin", "users"},
27+
Token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.sensitive-token-data",
28+
TokenType: "Bearer",
29+
},
30+
contains: []string{
31+
"Subject:\"[email protected]\"",
32+
"Name:\"Test User\"",
33+
"Token:REDACTED",
34+
"TokenType:\"Bearer\"",
35+
},
36+
notContains: []string{
37+
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9",
38+
"sensitive-token-data",
39+
},
40+
},
41+
{
42+
name: "shows_empty_token",
43+
identity: &Identity{
44+
Subject: "[email protected]",
45+
Token: "",
46+
},
47+
contains: []string{"Token:<empty>"},
48+
notContains: []string{"REDACTED"},
49+
},
50+
{
51+
name: "handles_nil_identity",
52+
identity: nil,
53+
contains: []string{"<nil>"},
54+
notContains: []string{},
55+
},
56+
}
57+
58+
for _, tc := range testCases {
59+
t.Run(tc.name, func(t *testing.T) {
60+
t.Parallel()
61+
62+
result := tc.identity.String()
63+
64+
for _, expected := range tc.contains {
65+
assert.Contains(t, result, expected)
66+
}
67+
68+
for _, forbidden := range tc.notContains {
69+
assert.NotContains(t, result, forbidden)
70+
}
71+
})
72+
}
73+
}
74+
75+
func TestIdentity_MarshalJSON(t *testing.T) {
76+
t.Parallel()
77+
78+
testCases := []struct {
79+
name string
80+
identity *Identity
81+
contains []string
82+
notContains []string
83+
}{
84+
{
85+
name: "redacts_token",
86+
identity: &Identity{
87+
Subject: "[email protected]",
88+
Name: "Test User",
89+
90+
Groups: []string{"admin", "users"},
91+
Token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.sensitive-token-data",
92+
TokenType: "Bearer",
93+
},
94+
contains: []string{
95+
`"subject":"[email protected]"`,
96+
`"name":"Test User"`,
97+
`"email":"[email protected]"`,
98+
`"token":"REDACTED"`,
99+
`"tokenType":"Bearer"`,
100+
},
101+
notContains: []string{
102+
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9",
103+
"sensitive-token-data",
104+
},
105+
},
106+
{
107+
name: "preserves_empty_token",
108+
identity: &Identity{
109+
Subject: "[email protected]",
110+
Token: "",
111+
},
112+
contains: []string{`"subject":"[email protected]"`},
113+
notContains: []string{`"token":"REDACTED"`},
114+
},
115+
{
116+
name: "handles_nil_identity",
117+
identity: nil,
118+
contains: []string{"null"},
119+
notContains: []string{},
120+
},
121+
}
122+
123+
for _, tc := range testCases {
124+
t.Run(tc.name, func(t *testing.T) {
125+
t.Parallel()
126+
127+
data, err := json.Marshal(tc.identity)
128+
require.NoError(t, err)
129+
130+
result := string(data)
131+
132+
for _, expected := range tc.contains {
133+
assert.Contains(t, result, expected)
134+
}
135+
136+
for _, forbidden := range tc.notContains {
137+
assert.NotContains(t, result, forbidden)
138+
}
139+
})
140+
}
141+
}

0 commit comments

Comments
 (0)