forked from open-policy-agent/opa
-
Notifications
You must be signed in to change notification settings - Fork 0
/
tokens.go
125 lines (106 loc) · 3.83 KB
/
tokens.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
package topdown
import (
"encoding/hex"
"errors"
"fmt"
"strings"
"github.com/open-policy-agent/opa/ast"
"github.com/open-policy-agent/opa/topdown/builtins"
)
var (
jwtEncKey = ast.StringTerm("enc")
jwtCtyKey = ast.StringTerm("cty")
)
// Implements JWT decoding/validation based on RFC 7519 Section 7.2:
// https://tools.ietf.org/html/rfc7519#section-7.2
// It does no data validation, it merely checks that the given string
// represents a structurally valid JWT. It supports JWTs using JWS compact
// serialization.
func builtinJWTDecode(a ast.Value) (ast.Value, ast.Value, ast.Value, error) {
astEncode, err := builtins.StringOperand(a, 1)
encoding := string(astEncode)
if !strings.Contains(encoding, ".") {
return nil, nil, nil, errors.New("encoded JWT had no period separators")
}
parts := strings.Split(encoding, ".")
if len(parts) != 3 {
return nil, nil, nil, fmt.Errorf("encoded JWT must have 3 sections, found %d", len(parts))
}
h, err := builtinBase64UrlDecode(ast.String(parts[0]))
if err != nil {
return nil, nil, nil, fmt.Errorf("JWT header had invalid encoding: %v", err)
}
header, err := validateJWTHeader(string(h.(ast.String)))
if err != nil {
return nil, nil, nil, err
}
p, err := builtinBase64UrlDecode(ast.String(parts[1]))
if err != nil {
return nil, nil, nil, fmt.Errorf("JWT payload had invalid encoding: %v", err)
}
if cty := header.Get(jwtCtyKey); cty != nil {
ctyVal := string(cty.Value.(ast.String))
// It is possible for the contents of a token to be another
// token as a result of nested signing or encryption. To handle
// the case where we are given a token such as this, we check
// the content type and recurse on the payload if the content
// is "JWT".
// When the payload is itself another encoded JWT, then its
// contents are quoted (behavior of https://jwt.io/). To fix
// this, remove leading and trailing quotes.
if ctyVal == "JWT" {
p, err = builtinTrim(p, ast.String(`"'`))
if err != nil {
panic("not reached")
}
return builtinJWTDecode(p)
}
}
payload, err := extractJSONObject(string(p.(ast.String)))
if err != nil {
return nil, nil, nil, err
}
s, err := builtinBase64UrlDecode(ast.String(parts[2]))
if err != nil {
return nil, nil, nil, fmt.Errorf("JWT signature had invalid encoding: %v", err)
}
sign := hex.EncodeToString([]byte(s.(ast.String)))
return header, payload, ast.String(sign), nil
}
// Extract, validate and return the JWT header as an ast.Object.
func validateJWTHeader(h string) (ast.Object, error) {
header, err := extractJSONObject(h)
if err != nil {
return nil, fmt.Errorf("bad JWT header: %v", err)
}
// There are two kinds of JWT tokens, a JSON Web Signature (JWS) and
// a JSON Web Encryption (JWE). The latter is very involved, and we
// won't support it for now.
// This code checks which kind of JWT we are dealing with according to
// RFC 7516 Section 9: https://tools.ietf.org/html/rfc7516#section-9
if header.Get(jwtEncKey) != nil {
return nil, errors.New("JWT is a JWE object, which is not supported")
}
return header, nil
}
func extractJSONObject(s string) (ast.Object, error) {
// XXX: This code relies on undocumented behavior of Go's
// json.Unmarshal using the last occurrence of duplicate keys in a JSON
// Object. If duplicate keys are present in a JWT, the last must be
// used or the token rejected. Since detecting duplicates is tantamount
// to parsing it ourselves, we're relying on the Go implementation
// using the last occuring instance of the key, which is the behavior
// as of Go 1.8.1.
v, err := builtinJSONUnmarshal(ast.String(s))
if err != nil {
return nil, fmt.Errorf("invalid JSON: %v", err)
}
o, ok := v.(ast.Object)
if !ok {
return nil, errors.New("decoded JSON type was not an Object")
}
return o, nil
}
func init() {
RegisterFunctionalBuiltin1Out3(ast.JWTDecode.Name, builtinJWTDecode)
}