Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Jwt token value #345

Merged
merged 8 commits into from
Oct 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Unreleased changes are available as `avenga/couper:edge` container.
* Documentation of [logs](docs/LOGS.md) ([#310](https://github.com/avenga/couper/pull/310))
* `signing_ttl` and `signing_key`/`signing_key_file` to [`jwt` block](./docs/REFERENCE.md#jwt-block) for use with [`jwt_sign()` function](#functions) ([#309](https://github.com/avenga/couper/pull/309))
* `jwks_url` and `jwks_ttl` to [`jwt` block](./docs/REFERENCE.md#jwt-block) ([#312](https://github.com/avenga/couper/pull/312))
* `token_value` attribute in [`jwt` block](./docs/REFERENCE.md#jwt-block) ([#345](https://github.com/avenga/couper/issues/345))
* `headers` attribute in [`jwt_signing_profile` block](./docs/REFERENCE.md#jwt-signing-profile-block) ([#329](https://github.com/avenga/couper/issues/329))

* **Changed**
Expand Down
36 changes: 28 additions & 8 deletions accesscontrol/jwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@ const (
Invalid JWTSourceType = iota
Cookie
Header
Value
)

var _ AccessControl = &JWT{}

type (
JWTSourceType uint8
JWTSource struct {
Expr hcl.Expression
Name string
Type JWTSourceType
}
Expand Down Expand Up @@ -63,18 +65,30 @@ type JWTOptions struct {
JWKS *JWKS
}

func NewJWTSource(cookie, header string) JWTSource {
func NewJWTSource(cookie, header string, value hcl.Expression) JWTSource {
c, h := strings.TrimSpace(cookie), strings.TrimSpace(header)
if c != "" && h != "" { // both are invalid
return JWTSource{}
}

if c != "" {
if value != nil {
v, _ := value.Value(nil)
if !v.IsNull() {
if h != "" || c != "" {
return JWTSource{}
}

return JWTSource{
Name: "",
Type: Value,
Expr: value,
}
}
}
if c != "" && h == "" {
return JWTSource{
Name: c,
Type: Cookie,
}
} else if h != "" {
}
if h != "" && c == "" {
return JWTSource{
Name: h,
Type: Header,
Expand Down Expand Up @@ -169,9 +183,16 @@ func (j *JWT) Validate(req *http.Request) error {
} else {
tokenValue = req.Header.Get(j.source.Name)
}
case Value:
requestContext := eval.ContextFromRequest(req).HCLContext()
value, diags := eval.Value(requestContext, j.source.Expr)
if diags != nil {
return diags
}

tokenValue = seetie.ValueToString(value)
}

// TODO j.PostParam, j.QueryParam
if tokenValue == "" {
return errors.JwtTokenMissing.Message("token required")
}
Expand Down Expand Up @@ -285,7 +306,6 @@ func (j *JWT) validateClaims(token *jwt.Token, claims map[string]interface{}) (m
}

for k, v := range claims {

if k == "iss" || k == "aud" { // gets validated during parsing
continue
}
Expand Down
143 changes: 107 additions & 36 deletions accesscontrol/jwt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (

"github.com/dgrijalva/jwt-go/v4"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hcltest"
"github.com/zclconf/go-cty/cty"

ac "github.com/avenga/couper/accesscontrol"
Expand Down Expand Up @@ -124,7 +125,7 @@ QolLGgj3tz4NbDEitq+zKMr0uTHvP1Vyu1mXAflcpYcJA4ZmuB3Oj39e0U0gnmr/
ClaimsRequired: tt.fields.claimsRequired,
Name: "test_ac",
Key: key,
Source: ac.NewJWTSource("", "Authorization"),
Source: ac.NewJWTSource("", "Authorization", nil),
})
if jerr != nil {
if tt.wantErr != jerr.Error() {
Expand Down Expand Up @@ -185,27 +186,27 @@ func Test_JWT_Validate(t *testing.T) {
}{
{"src: header /w empty bearer", fields{
algorithm: algo,
source: ac.NewJWTSource("", "Authorization"),
source: ac.NewJWTSource("", "Authorization", nil),
pubKey: pubKeyBytes,
}, httptest.NewRequest(http.MethodGet, "/", nil), true},
{"src: header /w valid bearer", fields{
algorithm: algo,
source: ac.NewJWTSource("", "Authorization"),
source: ac.NewJWTSource("", "Authorization", nil),
pubKey: pubKeyBytes,
}, setCookieAndHeader(httptest.NewRequest(http.MethodGet, "/", nil), "Authorization", "BeAreR "+token), false},
{"src: header /w no cookie", fields{
algorithm: algo,
source: ac.NewJWTSource("token", ""),
source: ac.NewJWTSource("token", "", nil),
pubKey: pubKeyBytes,
}, httptest.NewRequest(http.MethodGet, "/", nil), true},
{"src: header /w empty cookie", fields{
algorithm: algo,
source: ac.NewJWTSource("token", ""),
source: ac.NewJWTSource("token", "", nil),
pubKey: pubKeyBytes,
}, setCookieAndHeader(httptest.NewRequest(http.MethodGet, "/", nil), "token", ""), true},
{"src: header /w valid cookie", fields{
algorithm: algo,
source: ac.NewJWTSource("token", ""),
source: ac.NewJWTSource("token", "", nil),
pubKey: pubKeyBytes,
}, setCookieAndHeader(httptest.NewRequest(http.MethodGet, "/", nil), "token", token), false},
{"src: header /w valid bearer & claims", fields{
Expand All @@ -215,7 +216,7 @@ func Test_JWT_Validate(t *testing.T) {
"test123": "value123",
},
claimsRequired: []string{"aud"},
source: ac.NewJWTSource("", "Authorization"),
source: ac.NewJWTSource("", "Authorization", nil),
pubKey: pubKeyBytes,
}, setContext(setCookieAndHeader(httptest.NewRequest(http.MethodGet, "/", nil), "Authorization", "BeAreR "+token)), false},
{"src: header /w valid bearer & w/o claims", fields{
Expand All @@ -224,7 +225,7 @@ func Test_JWT_Validate(t *testing.T) {
"aud": "peter",
"cptn": "hook",
},
source: ac.NewJWTSource("", "Authorization"),
source: ac.NewJWTSource("", "Authorization", nil),
pubKey: pubKeyBytes,
}, setContext(setCookieAndHeader(httptest.NewRequest(http.MethodGet, "/", nil), "Authorization", "BeAreR "+token)), true},
{"src: header /w valid bearer & w/o required claims", fields{
Expand All @@ -233,9 +234,31 @@ func Test_JWT_Validate(t *testing.T) {
"aud": "peter",
},
claimsRequired: []string{"exp"},
source: ac.NewJWTSource("", "Authorization"),
source: ac.NewJWTSource("", "Authorization", nil),
pubKey: pubKeyBytes,
}, setContext(setCookieAndHeader(httptest.NewRequest(http.MethodGet, "/", nil), "Authorization", "BeAreR "+token)), true},
{
"token_value number",
fields{
algorithm: algo,
source: ac.NewJWTSource("", "", hcltest.MockExprLiteral(cty.NumberIntVal(42))),
pubKey: pubKeyBytes,
},
setContext(httptest.NewRequest(http.MethodGet, "/", nil)),
true,
},
{
"token_value string",
fields{
algorithm: algo,
claims: map[string]string{"aud": "peter", "test123": "value123"},
claimsRequired: []string{"aud", "test123"},
source: ac.NewJWTSource("", "", hcltest.MockExprLiteral(cty.StringVal(token))),
pubKey: pubKeyBytes,
},
setContext(httptest.NewRequest(http.MethodGet, "/", nil)),
false,
},
}
for _, tt := range tests {
t.Run(fmt.Sprintf("%v_%s", signingMethod, tt.name), func(subT *testing.T) {
Expand Down Expand Up @@ -541,7 +564,7 @@ func Test_JWT_yields_scopes(t *testing.T) {
subT.Error(tokenErr)
}

source := ac.NewJWTSource("", "Authorization")
source := ac.NewJWTSource("", "Authorization", nil)
j, err := ac.NewJWT(&ac.JWTOptions{
Algorithm: algo.String(),
Name: "test_ac",
Expand Down Expand Up @@ -582,15 +605,6 @@ func TestJwtConfig(t *testing.T) {
hcl string
error string
}{
{
"missing definition for access_control",
`
server "test" {
access_control = ["myac"]
}
`,
"", // FIXME Missing myac
},
{
"missing both signature_algorithm/jwks_url",
`
Expand All @@ -616,7 +630,7 @@ func TestJwtConfig(t *testing.T) {
"jwt key: read error: required: configured attribute or file",
},
{
"ok: signature_algorithm + key",
"ok: signature_algorithm + key + header",
`
server "test" {}
definitions {
Expand All @@ -629,6 +643,79 @@ func TestJwtConfig(t *testing.T) {
`,
"",
},
{
"ok: signature_algorithm + key + cookie",
`
server "test" {}
definitions {
jwt "myac" {
signature_algorithm = "HS256"
cookie = "..."
key = "..."
}
}
`,
"",
},
{
"ok: signature_algorithm + key + token_value",
`
server "test" {}
definitions {
jwt "myac" {
signature_algorithm = "HS256"
token_value = env.TOKEN
key = "..."
}
}
`,
"",
},
{
"token_value + header",
`
server "test" {}
definitions {
jwt "myac" {
signature_algorithm = "HS256"
token_value = env.TOKEN
header = "..."
key = "..."
}
}
`,
"token source is invalid",
},
{
"token_value + cookie",
`
server "test" {}
definitions {
jwt "myac" {
signature_algorithm = "HS256"
token_value = env.TOKEN
cookie = "..."
key = "..."
}
}
`,
"token source is invalid",
},
{
"cookie + header",
`
server "test" {}
definitions {
jwt "myac" {
signature_algorithm = "HS256"
cookie = "..."
header = "..."
key = "..."
}
}
`,
"token source is invalid",
},
{
"ok: signature_algorithm + key_file",
`
Expand Down Expand Up @@ -727,22 +814,6 @@ func TestJwtConfig(t *testing.T) {
`,
"",
},
/*
{
"inline backend block, missing jwks_url",
`
server "test" {}
definitions {
jwt "myac" {
backend {
}
header = "..."
}
}
`,
"backend not needed without jwks_url",
},
*/
}

log, _ := logrustest.NewNullLogger()
Expand Down
3 changes: 1 addition & 2 deletions config/ac_jwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ type JWT struct {
Key string `hcl:"key,optional"`
KeyFile string `hcl:"key_file,optional"`
Name string `hcl:"name,label"`
PostParam string `hcl:"post_param,optional"`
QueryParam string `hcl:"query_param,optional"`
Remain hcl.Body `hcl:",remain"`
RoleClaim string `hcl:"beta_role_claim,optional"`
RoleMap map[string][]string `hcl:"beta_role_map,optional"`
Expand All @@ -36,6 +34,7 @@ type JWT struct {
SigningKey string `hcl:"signing_key,optional"`
SigningKeyFile string `hcl:"signing_key_file,optional"`
SigningTTL string `hcl:"signing_ttl,optional"`
TokenValue hcl.Expression `hcl:"token_value,optional"`
afflerbach marked this conversation as resolved.
Show resolved Hide resolved

// Internally used
BodyContent *hcl.BodyContent
Expand Down
4 changes: 2 additions & 2 deletions config/runtime/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -483,7 +483,7 @@ func configureAccessControls(conf *config.Couper, confCtx *hcl.EvalContext, log
RoleClaim: jwtConf.RoleClaim,
RoleMap: jwtConf.RoleMap,
ScopeClaim: jwtConf.ScopeClaim,
Source: ac.NewJWTSource(jwtConf.Cookie, jwtConf.Header),
Source: ac.NewJWTSource(jwtConf.Cookie, jwtConf.Header, jwtConf.TokenValue),
JWKS: jwks,
})
if err != nil {
Expand All @@ -504,7 +504,7 @@ func configureAccessControls(conf *config.Couper, confCtx *hcl.EvalContext, log
RoleClaim: jwtConf.RoleClaim,
RoleMap: jwtConf.RoleMap,
ScopeClaim: jwtConf.ScopeClaim,
Source: ac.NewJWTSource(jwtConf.Cookie, jwtConf.Header),
Source: ac.NewJWTSource(jwtConf.Cookie, jwtConf.Header, jwtConf.TokenValue),
})

if err != nil {
Expand Down
5 changes: 3 additions & 2 deletions docs/REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -344,8 +344,9 @@ required _label_.

| Attribute(s) | Type |Default|Description|Characteristic(s)| Example|
| :-------- | :--------------- | :--------------- | :--------------- | :--------------- | :--------------- |
| `cookie` |string|-|Read `AccessToken` key to gain the token value from a cookie.|⚠ available value: `AccessToken`|`cookie = "AccessToken"`|
| `header` |string|-|-|⚠ Implies `Bearer` if `Authorization` (case-insensitive) is used, otherwise any other header name can be used.|`header = "Authorization"` |
| `cookie` |string|-|Read token value from a cookie.|cannot be used together with `header` or `token_value` |`cookie = "AccessToken"`|
| `header` |string|-|Read token value from a request header field.|⚠ Implies `Bearer` if `Authorization` (case-insensitive) is used, otherwise any other header name can be used. Cannot be used together with `cookie` or `token_value`.|`header = "Authorization"` |
| `token_value` | string | - | expression to obtain the token | cannot be used together with `cookie` or `header` | `token_value = request.form_body.token[0]`|
| `key` |string|-|Public key (in PEM format) for `RS*` variants or the secret for `HS*` algorithm.|-|-|
| `key_file` |string|-|Optional file reference instead of `key` usage.|-|-|
| `signature_algorithm` |string|-|-|Valid values are: `RS256` `RS384` `RS512` `HS256` `HS384` `HS512`.|-|
Expand Down
Loading