Skip to content
This repository was archived by the owner on Sep 24, 2025. It is now read-only.

Commit d3af071

Browse files
Supports for the aud claim as string or array
This was created based on the following PR: dgrijalva#286 As per the JWT spec, the aud claim field can be either a single string value or an array of strings. jwt-go would completely drop array values as the StandardClaims struct's Audience field is a string value and the value is dropped upon deserialization.
1 parent 3af4c74 commit d3af071

File tree

4 files changed

+195
-15
lines changed

4 files changed

+195
-15
lines changed

claims.go

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@ type Claims interface {
1616
// https://tools.ietf.org/html/rfc7519#section-4.1
1717
// See examples for how to use this with your own claim types
1818
type StandardClaims struct {
19-
Audience string `json:"aud,omitempty"`
20-
ExpiresAt int64 `json:"exp,omitempty"`
21-
Id string `json:"jti,omitempty"`
22-
IssuedAt int64 `json:"iat,omitempty"`
23-
Issuer string `json:"iss,omitempty"`
24-
NotBefore int64 `json:"nbf,omitempty"`
25-
Subject string `json:"sub,omitempty"`
19+
Audience interface{} `json:"aud,omitempty"`
20+
ExpiresAt int64 `json:"exp,omitempty"`
21+
Id string `json:"jti,omitempty"`
22+
IssuedAt int64 `json:"iat,omitempty"`
23+
Issuer string `json:"iss,omitempty"`
24+
NotBefore int64 `json:"nbf,omitempty"`
25+
Subject string `json:"sub,omitempty"`
2626
}
2727

2828
// Validates time based claims "exp, iat, nbf".
@@ -58,10 +58,27 @@ func (c StandardClaims) Valid() error {
5858
return vErr
5959
}
6060

61+
// Extracts an array of audience values from the aud field.
62+
func ExtractAudience(c *StandardClaims) []string {
63+
switch c.Audience.(type) {
64+
case []interface{}:
65+
auds := make([]string, len(c.Audience.([]interface{})))
66+
for i, value := range c.Audience.([]interface{}) {
67+
auds[i] = value.(string)
68+
}
69+
return auds
70+
case []string:
71+
return c.Audience.([]string)
72+
default:
73+
return []string{c.Audience.(string)}
74+
}
75+
}
76+
6177
// Compares the aud claim against cmp.
6278
// If required is false, this method will return true if the value matches or is unset
6379
func (c *StandardClaims) VerifyAudience(cmp string, req bool) bool {
64-
return verifyAud(c.Audience, cmp, req)
80+
audiences := ExtractAudience(c)
81+
return verifyAud(audiences, cmp, req)
6582
}
6683

6784
// Compares the exp claim against cmp.
@@ -90,13 +107,15 @@ func (c *StandardClaims) VerifyNotBefore(cmp int64, req bool) bool {
90107

91108
// ----- helpers
92109

93-
func verifyAud(aud string, cmp string, required bool) bool {
94-
if aud == "" {
110+
func verifyAud(auds []string, cmp string, required bool) bool {
111+
if len(auds) == 0 {
95112
return !required
96-
}
97-
if subtle.ConstantTimeCompare([]byte(aud), []byte(cmp)) != 0 {
98-
return true
99113
} else {
114+
for _, aud := range auds {
115+
if len(aud) == len(cmp) && subtle.ConstantTimeCompare([]byte(aud), []byte(cmp)) != 0 {
116+
return true
117+
}
118+
}
100119
return false
101120
}
102121
}

claims_test.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package jwt
2+
3+
import (
4+
"testing"
5+
)
6+
7+
// Test StandardClaims instances with an audience value populated in a string, []string and []interface{}
8+
var audienceValue = "Aud"
9+
var unmatchedAudienceValue = audienceValue + "Test"
10+
var claimWithAudience = []StandardClaims{
11+
{
12+
audienceValue,
13+
123123,
14+
"Id",
15+
12312,
16+
"Issuer",
17+
12312,
18+
"Subject",
19+
},
20+
{
21+
[]string{audienceValue, unmatchedAudienceValue},
22+
123123,
23+
"Id",
24+
12312,
25+
"Issuer",
26+
12312,
27+
"Subject",
28+
},
29+
{
30+
[]interface{}{audienceValue, unmatchedAudienceValue},
31+
123123,
32+
"Id",
33+
12312,
34+
"Issuer",
35+
12312,
36+
"Subject",
37+
},
38+
}
39+
40+
// Test StandardClaims instances with no aduences within empty []string and []interface{} collections.
41+
var claimWithoutAudience = []StandardClaims{
42+
{
43+
[]string{},
44+
123123,
45+
"Id",
46+
12312,
47+
"Issuer",
48+
12312,
49+
"Subject",
50+
},
51+
{
52+
[]interface{}{},
53+
123123,
54+
"Id",
55+
12312,
56+
"Issuer",
57+
12312,
58+
"Subject",
59+
},
60+
}
61+
62+
func TestExtractAudienceWithAudienceValues(t *testing.T) {
63+
for _, data := range claimWithAudience {
64+
var aud = ExtractAudience(&data)
65+
if len(aud) == 0 || aud[0] != audienceValue {
66+
t.Errorf("The audience value was not extracted properly")
67+
}
68+
}
69+
}
70+
71+
func TestExtractAudience_WithoutAudienceValues(t *testing.T) {
72+
for _, data := range claimWithoutAudience {
73+
var aud = ExtractAudience(&data)
74+
if len(aud) != 0 {
75+
t.Errorf("An audience value should not have been extracted")
76+
}
77+
}
78+
}
79+
80+
var audWithValues = [][]string{
81+
[]string{audienceValue},
82+
[]string{"Aud1", "Aud2", audienceValue},
83+
}
84+
85+
var audWithLackingOriginalValue = [][]string{
86+
[]string{},
87+
[]string{audienceValue + "1"},
88+
[]string{"Aud1", "Aud2", audienceValue + "1"},
89+
}
90+
91+
func TestVerifyAud_ShouldVerifyExists(t *testing.T) {
92+
for _, data := range audWithValues {
93+
if !verifyAud(data, audienceValue, true) {
94+
t.Errorf("The audience value was not verified properly")
95+
}
96+
}
97+
}
98+
99+
func TestVerifyAud_ShouldVerifyDoesNotExist(t *testing.T) {
100+
for _, data := range audWithValues {
101+
if !verifyAud(data, audienceValue, true) {
102+
t.Errorf("The audience value was not verified properly")
103+
}
104+
}
105+
}

map_claims.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,18 @@ type MapClaims map[string]interface{}
1313
// Compares the aud claim against cmp.
1414
// If required is false, this method will return true if the value matches or is unset
1515
func (m MapClaims) VerifyAudience(cmp string, req bool) bool {
16-
aud, _ := m["aud"].(string)
17-
return verifyAud(aud, cmp, req)
16+
switch aud := m["aud"].(type) {
17+
case []string:
18+
return verifyAud(aud, cmp, req)
19+
case []interface{}:
20+
auds := make([]string, len(aud))
21+
for i, value := range aud {
22+
auds[i] = value.(string)
23+
}
24+
return verifyAud(auds, cmp, req)
25+
default:
26+
return verifyAud([]string{aud.(string)}, cmp, req)
27+
}
1828
}
1929

2030
// Compares the exp claim against cmp.

map_claims_tests.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package jwt
2+
3+
import (
4+
"testing"
5+
)
6+
7+
var audFixedValue = "Aud"
8+
var audClaimsMapsWithValues = []MapClaims{
9+
{
10+
"aud": audFixedValue,
11+
},
12+
{
13+
"aud": []string{audFixedValue},
14+
},
15+
{
16+
"aud": []interface{}{audFixedValue},
17+
},
18+
}
19+
20+
var audClaimsMapsWithoutValues = []MapClaims{
21+
{},
22+
{
23+
"aud": []string{},
24+
},
25+
{
26+
"aud": []interface{}{},
27+
},
28+
}
29+
30+
// Verifies that for every form of the "aud" field, the audFixedValue is always verifiable
31+
func TestVerifyAudienceWithVerifiableValues(t *testing.T) {
32+
for _, data := range audClaimsMapsWithValues {
33+
if !data.VerifyAudience(audFixedValue, true) {
34+
t.Errorf("The audience value was not extracted properly")
35+
}
36+
}
37+
}
38+
39+
// Verifies that for every empty form of the "aud" field, the audFixedValue cannot be verified
40+
func TestVerifyAudienceWithoutVerifiableValues(t *testing.T) {
41+
for _, data := range audClaimsMapsWithoutValues {
42+
if data.VerifyAudience(audFixedValue, true) {
43+
t.Errorf("The audience should not verify")
44+
}
45+
}
46+
}

0 commit comments

Comments
 (0)