Skip to content

Commit c997fd2

Browse files
Introduces JWT token parser (zalando#1651)
Signed-off-by: Alexander Yastrebov <[email protected]>
1 parent 4dcbf4b commit c997fd2

File tree

5 files changed

+128
-60
lines changed

5 files changed

+128
-60
lines changed

filters/apiusagemonitoring/filter.go

+5-19
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
package apiusagemonitoring
22

33
import (
4-
"encoding/base64"
5-
"encoding/json"
64
"fmt"
75
"net/http"
86
"net/url"
97
"strings"
108
"time"
119

1210
"github.com/zalando/skipper/filters"
11+
"github.com/zalando/skipper/jwt"
1312
)
1413

1514
const (
@@ -211,30 +210,17 @@ func createAndCacheMetricsNames(path *pathInfo, method string, methodIndex int)
211210
// It returns `nil` if it was not possible to parse the JWT body.
212211
func parseJwtBody(req *http.Request) jwtTokenPayload {
213212
ahead := req.Header.Get(authorizationHeaderName)
214-
if !strings.HasPrefix(ahead, authorizationHeaderPrefix) {
213+
tv := strings.TrimPrefix(ahead, authorizationHeaderPrefix)
214+
if tv == ahead {
215215
return nil
216216
}
217217

218-
// split the header into the 3 JWT parts
219-
fields := strings.Split(ahead, ".")
220-
if len(fields) != 3 {
221-
return nil
222-
}
223-
224-
// base64-decode the JWT body part
225-
bodyJSON, err := base64.RawURLEncoding.DecodeString(fields[1])
226-
if err != nil {
227-
return nil
228-
}
229-
230-
// un-marshall the JWT body from JSON
231-
var bodyObject map[string]interface{}
232-
err = json.Unmarshal(bodyJSON, &bodyObject)
218+
token, err := jwt.Parse(tv)
233219
if err != nil {
234220
return nil
235221
}
236222

237-
return bodyObject
223+
return token.Claims
238224
}
239225

240226
type jwtTokenPayload map[string]interface{}

filters/log/log.go

+12-22
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ package log
77

88
import (
99
"bytes"
10-
"encoding/base64"
1110
"encoding/json"
1211
"io"
1312
"os"
@@ -17,6 +16,7 @@ import (
1716
log "github.com/sirupsen/logrus"
1817

1918
"github.com/zalando/skipper/filters"
19+
"github.com/zalando/skipper/jwt"
2020
)
2121

2222
const (
@@ -209,31 +209,21 @@ func (ual *unverifiedAuditLogSpec) CreateFilter(args []interface{}) (filters.Fil
209209
func (ual *unverifiedAuditLogFilter) Request(ctx filters.FilterContext) {
210210
req := ctx.Request()
211211
ahead := req.Header.Get(authHeaderName)
212-
if !strings.HasPrefix(ahead, authHeaderPrefix) {
212+
tv := strings.TrimPrefix(ahead, authHeaderPrefix)
213+
if tv == ahead {
213214
return
214215
}
215216

216-
fields := strings.FieldsFunc(ahead, func(r rune) bool {
217-
return r == []rune(".")[0]
218-
})
219-
if len(fields) == 3 {
220-
sDec, err := base64.RawURLEncoding.DecodeString(fields[1])
221-
if err != nil {
222-
return
223-
}
224-
225-
var j map[string]interface{}
226-
err = json.Unmarshal(sDec, &j)
227-
if err != nil {
228-
return
229-
}
217+
token, err := jwt.Parse(tv)
218+
if err != nil {
219+
return
220+
}
230221

231-
for i := 0; i < len(ual.TokenKeys); i++ {
232-
if k, ok := j[ual.TokenKeys[i]]; ok {
233-
if v, ok2 := k.(string); ok2 {
234-
req.Header.Add(UnverifiedAuditHeader, cleanSub(v))
235-
return
236-
}
222+
for i := 0; i < len(ual.TokenKeys); i++ {
223+
if k, ok := token.Claims[ual.TokenKeys[i]]; ok {
224+
if v, ok2 := k.(string); ok2 {
225+
req.Header.Add(UnverifiedAuditHeader, cleanSub(v))
226+
return
237227
}
238228
}
239229
}

jwt/token.go

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package jwt
2+
3+
import (
4+
"encoding/base64"
5+
"encoding/json"
6+
"errors"
7+
"strings"
8+
)
9+
10+
var (
11+
errInvalidToken = errors.New("invalid jwt token")
12+
)
13+
14+
type Token struct {
15+
Claims map[string]interface{}
16+
}
17+
18+
func Parse(value string) (*Token, error) {
19+
parts := strings.Split(value, ".")
20+
if len(parts) != 3 {
21+
return nil, errInvalidToken
22+
}
23+
24+
var token Token
25+
err := unmarshalBase64JSON(parts[1], &token.Claims)
26+
if err != nil {
27+
return nil, errInvalidToken
28+
}
29+
30+
return &token, nil
31+
}
32+
33+
func unmarshalBase64JSON(s string, v interface{}) error {
34+
d, err := base64.RawURLEncoding.DecodeString(s)
35+
if err != nil {
36+
return err
37+
}
38+
return json.Unmarshal(d, v)
39+
}

jwt/token_test.go

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package jwt
2+
3+
import (
4+
"encoding/base64"
5+
"encoding/json"
6+
"reflect"
7+
"testing"
8+
)
9+
10+
func TestParse(t *testing.T) {
11+
for _, tt := range []struct {
12+
value string
13+
ok bool
14+
claims map[string]interface{}
15+
}{
16+
{
17+
value: "",
18+
ok: false,
19+
}, {
20+
value: "x",
21+
ok: false,
22+
}, {
23+
value: "x.y",
24+
ok: false,
25+
}, {
26+
value: "x.y.z",
27+
ok: false,
28+
}, {
29+
value: "..",
30+
ok: false,
31+
}, {
32+
value: "x..z",
33+
ok: false,
34+
}, {
35+
value: "x." + marshalBase64JSON(t, map[string]interface{}{"hello": "world"}) + ".z",
36+
ok: true,
37+
claims: map[string]interface{}{"hello": "world"},
38+
}, {
39+
value: "." + marshalBase64JSON(t, map[string]interface{}{"no header": "no signature"}) + ".",
40+
ok: true,
41+
claims: map[string]interface{}{"no header": "no signature"},
42+
},
43+
} {
44+
token, err := Parse(tt.value)
45+
if tt.ok {
46+
if err != nil {
47+
t.Errorf("unexpected error for %s: %v", tt.value, err)
48+
continue
49+
}
50+
} else {
51+
continue
52+
}
53+
54+
if !reflect.DeepEqual(tt.claims, token.Claims) {
55+
t.Errorf("claims mismatch, expected: %v, got %v", tt.claims, token.Claims)
56+
}
57+
}
58+
}
59+
60+
func marshalBase64JSON(t *testing.T, v interface{}) string {
61+
d, err := json.Marshal(v)
62+
if err != nil {
63+
t.Fatalf("failed to marshal json: %v, %v", v, err)
64+
}
65+
return base64.RawURLEncoding.EncodeToString(d)
66+
}

predicates/auth/jwt.go

+6-19
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,11 @@ Examples:
1717
package auth
1818

1919
import (
20-
"encoding/base64"
21-
"encoding/json"
2220
"net/http"
2321
"regexp"
2422
"strings"
2523

24+
"github.com/zalando/skipper/jwt"
2625
"github.com/zalando/skipper/predicates"
2726
"github.com/zalando/skipper/routing"
2827
)
@@ -153,33 +152,21 @@ func (m regexMatcher) Match(jwtValue string) bool {
153152

154153
func (p *predicate) Match(r *http.Request) bool {
155154
ahead := r.Header.Get(authHeaderName)
156-
if !strings.HasPrefix(ahead, authHeaderPrefix) {
155+
tv := strings.TrimPrefix(ahead, authHeaderPrefix)
156+
if tv == ahead {
157157
return false
158158
}
159159

160-
fields := strings.FieldsFunc(ahead, func(r rune) bool {
161-
return r == []rune(".")[0]
162-
})
163-
if len(fields) != 3 {
164-
return false
165-
}
166-
167-
sDec, err := base64.RawURLEncoding.DecodeString(fields[1])
168-
if err != nil {
169-
return false
170-
}
171-
172-
var payload map[string]interface{}
173-
err = json.Unmarshal(sDec, &payload)
160+
token, err := jwt.Parse(tv)
174161
if err != nil {
175162
return false
176163
}
177164

178165
switch p.matchBehavior {
179166
case matchBehaviorAll:
180-
return allMatch(p.kv, payload)
167+
return allMatch(p.kv, token.Claims)
181168
case matchBehaviorAny:
182-
return anyMatch(p.kv, payload)
169+
return anyMatch(p.kv, token.Claims)
183170
default:
184171
return false
185172
}

0 commit comments

Comments
 (0)