Skip to content

Commit 0f726ea

Browse files
Waterdripsoxisto
andauthored
Fix issue with MapClaims VerifyAudience []string (#12)
* Fix issue with MapClaims VerifyAudience []string There was an issue in MapClaims's VerifyAudiance where a []string (which is valid in the spec) would return true (claim is found, or nil) when required was not set. It now checks interface types correctly and has tests written Signed-off-by: Alistair Hey <[email protected]> * Keep aud validation constant time compare Keep aud validation using constant time compare by not instantly returning on a true comparison, keep comparing all options and store result in a variable Signed-off-by: Alistair Hey <[email protected]> Co-authored-by: Banse, Christian <[email protected]>
1 parent 6a07921 commit 0f726ea

File tree

4 files changed

+104
-10
lines changed

4 files changed

+104
-10
lines changed

.gitignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
.DS_Store
22
bin
3-
3+
.idea/
44

claims.go

+19-7
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ func (c StandardClaims) Valid() error {
6161
// Compares the aud claim against cmp.
6262
// If required is false, this method will return true if the value matches or is unset
6363
func (c *StandardClaims) VerifyAudience(cmp string, req bool) bool {
64-
return verifyAud(c.Audience, cmp, req)
64+
return verifyAud([]string{c.Audience}, cmp, req)
6565
}
6666

6767
// Compares the exp claim against cmp.
@@ -90,15 +90,27 @@ func (c *StandardClaims) VerifyNotBefore(cmp int64, req bool) bool {
9090

9191
// ----- helpers
9292

93-
func verifyAud(aud string, cmp string, required bool) bool {
94-
if aud == "" {
93+
func verifyAud(aud []string, cmp string, required bool) bool {
94+
if len(aud) == 0 {
9595
return !required
9696
}
97-
if subtle.ConstantTimeCompare([]byte(aud), []byte(cmp)) != 0 {
98-
return true
99-
} else {
100-
return false
97+
// use a var here to keep constant time compare when looping over a number of claims
98+
result := false
99+
100+
var stringClaims string
101+
for _, a := range aud {
102+
if subtle.ConstantTimeCompare([]byte(a), []byte(cmp)) != 0 {
103+
result = true
104+
}
105+
stringClaims = stringClaims + a
101106
}
107+
108+
// case where "" is sent in one or many aud claims
109+
if len(stringClaims) == 0 {
110+
return !required
111+
}
112+
113+
return result
102114
}
103115

104116
func verifyExp(exp int64, now int64, required bool) bool {

map_claims.go

+16-2
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,24 @@ import (
1010
// This is the default claims type if you don't supply one
1111
type MapClaims map[string]interface{}
1212

13-
// Compares the aud claim against cmp.
13+
// VerifyAudience 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)
16+
var aud []string
17+
switch v := m["aud"].(type) {
18+
case string:
19+
aud = append(aud, v)
20+
case []string:
21+
aud = v
22+
case []interface{}:
23+
for _, a := range v {
24+
vs, ok := a.(string)
25+
if !ok {
26+
return false
27+
}
28+
aud = append(aud, vs)
29+
}
30+
}
1731
return verifyAud(aud, cmp, req)
1832
}
1933

map_claims_test.go

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package jwt
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestVerifyAud(t *testing.T) {
8+
var nilInterface interface{}
9+
var nilListInterface []interface{}
10+
var intListInterface interface{} = []int{1,2,3}
11+
type test struct{
12+
Name string
13+
MapClaims MapClaims
14+
Expected bool
15+
Comparison string
16+
Required bool
17+
}
18+
tests := []test{
19+
// Matching Claim in aud
20+
// Required = true
21+
{ Name: "String Aud matching required", MapClaims: MapClaims{"aud": "example.com"}, Expected: true , Required: true, Comparison: "example.com"},
22+
{ Name: "[]String Aud with match required", MapClaims: MapClaims{"aud": []string{"example.com", "example.example.com"}}, Expected: true, Required: true, Comparison: "example.com"},
23+
24+
// Required = false
25+
{ Name: "String Aud with match not required", MapClaims: MapClaims{"aud": "example.com"}, Expected: true , Required: false, Comparison: "example.com"},
26+
{ Name: "Empty String Aud with match not required", MapClaims: MapClaims{}, Expected: true , Required: false, Comparison: "example.com"},
27+
{ Name: "Empty String Aud with match not required", MapClaims: MapClaims{"aud": ""}, Expected: true , Required: false, Comparison: "example.com"},
28+
{ Name: "Nil String Aud with match not required", MapClaims: MapClaims{"aud": nil}, Expected: true , Required: false, Comparison: "example.com"},
29+
30+
{ Name: "[]String Aud with match not required", MapClaims: MapClaims{"aud": []string{"example.com", "example.example.com"}}, Expected: true, Required: false, Comparison: "example.com"},
31+
{ Name: "Empty []String Aud with match not required", MapClaims: MapClaims{"aud": []string{}}, Expected: true, Required: false, Comparison: "example.com"},
32+
33+
// Non-Matching Claim in aud
34+
// Required = true
35+
{ Name: "String Aud without match required", MapClaims: MapClaims{"aud": "not.example.com"}, Expected: false, Required: true, Comparison: "example.com"},
36+
{ Name: "Empty String Aud without match required", MapClaims: MapClaims{"aud": ""}, Expected: false, Required: true, Comparison: "example.com"},
37+
{ Name: "[]String Aud without match required", MapClaims: MapClaims{"aud": []string{"not.example.com", "example.example.com"}}, Expected: false, Required: true, Comparison: "example.com"},
38+
{ Name: "Empty []String Aud without match required", MapClaims: MapClaims{"aud": []string{""}}, Expected: false, Required: true, Comparison: "example.com"},
39+
{ Name: "String Aud without match not required", MapClaims: MapClaims{"aud": "not.example.com"}, Expected: false, Required: true, Comparison: "example.com"},
40+
{ Name: "Empty String Aud without match not required", MapClaims: MapClaims{"aud": ""}, Expected: false, Required: true, Comparison: "example.com"},
41+
{ Name: "[]String Aud without match not required", MapClaims: MapClaims{"aud": []string{"not.example.com", "example.example.com"}}, Expected: false, Required: true, Comparison: "example.com"},
42+
43+
// Required = false
44+
{ Name: "Empty []String Aud without match required", MapClaims: MapClaims{"aud": []string{""}}, Expected: false, Required: true, Comparison: "example.com"},
45+
46+
// []interface{}
47+
{ Name: "Empty []interface{} Aud without match required", MapClaims: MapClaims{"aud": nilListInterface}, Expected: true, Required: false, Comparison: "example.com"},
48+
{ Name: "[]interface{} Aud wit match required", MapClaims: MapClaims{"aud": []interface{}{"a", "foo", "example.com"}}, Expected: true, Required: true, Comparison: "example.com"},
49+
{ Name: "[]interface{} Aud wit match but invalid types", MapClaims: MapClaims{"aud": []interface{}{"a", 5, "example.com"}}, Expected: false, Required: true, Comparison: "example.com"},
50+
{ Name: "[]interface{} Aud int wit match required", MapClaims: MapClaims{"aud": intListInterface}, Expected: false, Required: true, Comparison: "example.com"},
51+
52+
53+
// interface{}
54+
{ Name: "Empty interface{} Aud without match not required", MapClaims: MapClaims{"aud": nilInterface}, Expected: true, Required: false, Comparison: "example.com"},
55+
56+
}
57+
58+
59+
for _, test := range tests {
60+
t.Run(test.Name, func(t *testing.T) {
61+
got := test.MapClaims.VerifyAudience(test.Comparison, test.Required)
62+
63+
if got != test.Expected {
64+
t.Errorf("Expected %v, got %v", test.Expected, got)
65+
}
66+
})
67+
}
68+
}

0 commit comments

Comments
 (0)