Skip to content

Commit fdf7cf4

Browse files
authored
feat(model): add support for family and given name claims (#69)
1 parent bbdeb77 commit fdf7cf4

File tree

4 files changed

+390
-56
lines changed

4 files changed

+390
-56
lines changed

jwt/model.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ type WebToken struct {
3737
func New(idToken string, signatureAlgorithms []jose.SignatureAlgorithm) (webToken WebToken, err error) {
3838
token, parseErr := jwt.ParseSigned(idToken, signatureAlgorithms)
3939
if parseErr != nil {
40-
err = fmt.Errorf("unable to parse id_token: [%s], %w", idToken, parseErr)
40+
err = fmt.Errorf("unable to parse id_token: %w", parseErr)
4141
return
4242
}
4343

@@ -52,6 +52,8 @@ func New(idToken string, signatureAlgorithms []jose.SignatureAlgorithm) (webToke
5252
webToken.IssuerAttributes = rawToken.IssuerAttributes
5353
webToken.Audiences = rawToken.getAudiences()
5454
webToken.Mail = rawToken.getMail()
55+
webToken.FirstName = rawToken.getFirstName()
56+
webToken.LastName = rawToken.getLastName()
5557

5658
return
5759
}

jwt/model_test.go

Lines changed: 181 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -11,41 +11,191 @@ import (
1111
var signatureAlgorithms = []jose.SignatureAlgorithm{jose.HS256}
1212
var joseTestKey = []byte("0123456789abcdef0123456789abcdef") // 32 bytes
1313

14-
func TestNew(t *testing.T) {
15-
issuer := "my-issuer"
16-
token := jwt.NewWithClaims(jwt.SigningMethodHS256, &jwt.RegisteredClaims{
17-
Issuer: issuer,
18-
})
19-
tokenString, err := token.SignedString(joseTestKey)
20-
assert.NoError(t, err)
21-
22-
webToken, err := New(tokenString, signatureAlgorithms)
23-
assert.NoError(t, err)
24-
assert.NotNil(t, webToken)
25-
assert.Equal(t, issuer, webToken.Issuer)
26-
}
14+
func TestNew_Success(t *testing.T) {
15+
tests := []struct {
16+
name string
17+
claims map[string]interface{}
18+
expectedWT WebToken
19+
}{
20+
{
21+
name: "basic issuer claim",
22+
claims: map[string]interface{}{
23+
"iss": "my-issuer",
24+
},
25+
expectedWT: WebToken{
26+
IssuerAttributes: IssuerAttributes{
27+
Issuer: "my-issuer",
28+
},
29+
},
30+
},
31+
{
32+
name: "with first_name and last_name",
33+
claims: map[string]interface{}{
34+
"iss": "test-issuer",
35+
"sub": "test-subject",
36+
"first_name": "John",
37+
"last_name": "Doe",
38+
},
39+
expectedWT: WebToken{
40+
IssuerAttributes: IssuerAttributes{
41+
Issuer: "test-issuer",
42+
Subject: "test-subject",
43+
},
44+
UserAttributes: UserAttributes{
45+
FirstName: "John",
46+
LastName: "Doe",
47+
},
48+
},
49+
},
50+
{
51+
name: "with given_name and family_name",
52+
claims: map[string]interface{}{
53+
"iss": "test-issuer",
54+
"sub": "test-subject",
55+
"given_name": "Jonathan",
56+
"family_name": "Smith",
57+
},
58+
expectedWT: WebToken{
59+
IssuerAttributes: IssuerAttributes{
60+
Issuer: "test-issuer",
61+
Subject: "test-subject",
62+
},
63+
UserAttributes: UserAttributes{
64+
FirstName: "Jonathan",
65+
LastName: "Smith",
66+
},
67+
},
68+
},
69+
{
70+
name: "prefer first_name/last_name over given_name/family_name",
71+
claims: map[string]interface{}{
72+
"iss": "test-issuer",
73+
"sub": "test-subject",
74+
"first_name": "John",
75+
"last_name": "Doe",
76+
"given_name": "Jonathan",
77+
"family_name": "Smith",
78+
},
79+
expectedWT: WebToken{
80+
IssuerAttributes: IssuerAttributes{
81+
Issuer: "test-issuer",
82+
Subject: "test-subject",
83+
},
84+
UserAttributes: UserAttributes{
85+
FirstName: "John",
86+
LastName: "Doe",
87+
},
88+
},
89+
},
90+
{
91+
name: "fallback to given_name/family_name when first_name/last_name are empty",
92+
claims: map[string]interface{}{
93+
"iss": "test-issuer",
94+
"sub": "test-subject",
95+
"first_name": "",
96+
"last_name": "",
97+
"given_name": "Jonathan",
98+
"family_name": "Smith",
99+
},
100+
expectedWT: WebToken{
101+
IssuerAttributes: IssuerAttributes{
102+
Issuer: "test-issuer",
103+
Subject: "test-subject",
104+
},
105+
UserAttributes: UserAttributes{
106+
FirstName: "Jonathan",
107+
LastName: "Smith",
108+
},
109+
},
110+
},
111+
{
112+
name: "partial fallback - first_name present, last_name empty",
113+
claims: map[string]interface{}{
114+
"iss": "test-issuer",
115+
"sub": "test-subject",
116+
"first_name": "John",
117+
"last_name": "",
118+
"given_name": "Jonathan",
119+
"family_name": "Smith",
120+
},
121+
expectedWT: WebToken{
122+
IssuerAttributes: IssuerAttributes{
123+
Issuer: "test-issuer",
124+
Subject: "test-subject",
125+
},
126+
UserAttributes: UserAttributes{
127+
FirstName: "John",
128+
LastName: "Smith",
129+
},
130+
},
131+
},
132+
}
133+
134+
for _, tt := range tests {
135+
t.Run(tt.name, func(t *testing.T) {
136+
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims(tt.claims))
137+
tokenString, err := token.SignedString(joseTestKey)
138+
assert.NoError(t, err)
27139

28-
func TestNewAndFail(t *testing.T) {
29-
tokenString := "just a string"
30-
_, err := New(tokenString, signatureAlgorithms)
31-
assert.Error(t, err)
140+
webToken, err := New(tokenString, signatureAlgorithms)
141+
assert.NoError(t, err)
142+
assert.NotNil(t, webToken)
143+
assert.Equal(t, tt.expectedWT.Issuer, webToken.Issuer)
144+
assert.Equal(t, tt.expectedWT.Subject, webToken.Subject)
145+
assert.Equal(t, tt.expectedWT.FirstName, webToken.FirstName)
146+
assert.Equal(t, tt.expectedWT.LastName, webToken.LastName)
147+
})
148+
}
32149
}
33150

34-
func TestNew_DeserializationError(t *testing.T) {
35-
// Create a valid JWT header and signature, but with a payload that is not valid JSON
36-
// or cannot be unmarshaled into the expected struct.
37-
// We'll use jose to construct a token with a payload that is not a JSON object.
38-
invalidPayload := "not-a-json-object"
39-
signer, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.HS256, Key: joseTestKey}, nil)
40-
assert.NoError(t, err)
151+
func TestNew_Errors(t *testing.T) {
152+
tests := []struct {
153+
name string
154+
tokenString string
155+
setupToken func() (string, error)
156+
expectedError string
157+
}{
158+
{
159+
name: "invalid token string",
160+
tokenString: "just a string",
161+
expectedError: "unable to parse id_token",
162+
},
163+
{
164+
name: "deserialization error with invalid payload",
165+
setupToken: func() (string, error) {
166+
// Create a valid JWT header and signature, but with a payload that is not valid JSON
167+
invalidPayload := "not-a-json-object"
168+
signer, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.HS256, Key: joseTestKey}, nil)
169+
if err != nil {
170+
return "", err
171+
}
172+
173+
object, err := signer.Sign([]byte(invalidPayload))
174+
if err != nil {
175+
return "", err
176+
}
177+
178+
return object.CompactSerialize()
179+
},
180+
expectedError: "unable to deserialize claims",
181+
},
182+
}
41183

42-
object, err := signer.Sign([]byte(invalidPayload))
43-
assert.NoError(t, err)
184+
for _, tt := range tests {
185+
t.Run(tt.name, func(t *testing.T) {
186+
var tokenString string
187+
var err error
44188

45-
tokenString, err := object.CompactSerialize()
46-
assert.NoError(t, err)
189+
if tt.setupToken != nil {
190+
tokenString, err = tt.setupToken()
191+
assert.NoError(t, err)
192+
} else {
193+
tokenString = tt.tokenString
194+
}
47195

48-
_, err = New(tokenString, signatureAlgorithms)
49-
assert.Error(t, err)
50-
assert.Contains(t, err.Error(), "unable to deserialize claims")
196+
_, err = New(tokenString, signatureAlgorithms)
197+
assert.Error(t, err)
198+
assert.Contains(t, err.Error(), tt.expectedError)
199+
})
200+
}
51201
}

jwt/raw.go

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
package jwt
22

3+
import "strings"
4+
35
type rawClaims struct {
4-
RawAudiences interface{} `json:"aud"` // RawAudiences could be a []string or string depending on the serialization in IdP site
5-
RawEmail string `json:"email,omitempty"`
6-
RawMail string `json:"mail,omitempty"`
6+
RawAudiences interface{} `json:"aud"` // RawAudiences could be a []string or string depending on the serialization in IdP site
7+
RawEmail string `json:"email,omitempty"`
8+
RawMail string `json:"mail,omitempty"`
9+
RawGivenName string `json:"given_name,omitempty"`
10+
RawFamilyName string `json:"family_name,omitempty"`
711
}
812

913
type rawWebToken struct {
@@ -13,9 +17,25 @@ type rawWebToken struct {
1317
}
1418

1519
func (r rawWebToken) getMail() (mail string) {
16-
mail = r.RawMail
20+
mail = strings.TrimSpace(r.RawMail)
1721
if mail == "" {
18-
mail = r.RawEmail
22+
mail = strings.TrimSpace(r.RawEmail)
23+
}
24+
return
25+
}
26+
27+
func (r rawWebToken) getLastName() (lastName string) {
28+
lastName = strings.TrimSpace(r.LastName)
29+
if lastName == "" {
30+
lastName = strings.TrimSpace(r.RawFamilyName)
31+
}
32+
return
33+
}
34+
35+
func (r rawWebToken) getFirstName() (firstName string) {
36+
firstName = strings.TrimSpace(r.FirstName)
37+
if firstName == "" {
38+
firstName = strings.TrimSpace(r.RawGivenName)
1939
}
2040
return
2141
}

0 commit comments

Comments
 (0)