Skip to content

Commit

Permalink
pipeline/authn: Add token_from config to introspection and jwt (#271)
Browse files Browse the repository at this point in the history
Add additional optional configuration to jwt and oauth2_introspection authenticators allowing to set from where (which header or query parameter) the token should be received. The configuration is a token_from field in per-rule-configuration, as described in a linked issue.

Closes #257
  • Loading branch information
kubadz authored and aeneasr committed Oct 9, 2019
1 parent bc74e72 commit fc85ac8
Show file tree
Hide file tree
Showing 9 changed files with 322 additions and 44 deletions.
32 changes: 32 additions & 0 deletions .schemas/authenticators.jwt.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,38 @@
},
"scope_strategy": {
"$ref": "https://raw.githubusercontent.com/ory/oathkeeper/master/.schemas/scope_strategy.schema.json#"
},
"token_from": {
"title": "Token From",
"description": "The location of the token.\n If not configured, the token will be received from a default location - 'Authorization' header.\n One and only one location (header or query) must be specified.",
"oneOf": [
{
"type": "object",
"required": [
"header"
],
"properties": {
"header": {
"title": "Header",
"type": "string",
"description": "The header (case insensitive) that must contain a token for request authentication. It can't be set along with query_parameter."
}
}
},
{
"type": "object",
"required": [
"query_parameter"
],
"properties": {
"query_parameter": {
"title": "Query Parameter",
"type": "string",
"description": "The query parameter (case sensitive) that must contain a token for request authentication. It can't be set along with header."
}
}
}
]
}
},
"additionalProperties": false
Expand Down
32 changes: 32 additions & 0 deletions .schemas/authenticators.oauth2_introspection.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,38 @@
"items": {
"type": "string"
}
},
"token_from": {
"title": "Token From",
"description": "The location of the token.\n If not configured, the token will be received from a default location - 'Authorization' header.\n One and only one location (header or query) must be specified.",
"oneOf": [
{
"type": "object",
"required": [
"header"
],
"properties": {
"header": {
"title": "Header",
"type": "string",
"description": "The header (case insensitive) that must contain a token for request authentication.\n It can't be set along with query_parameter."
}
}
},
{
"type": "object",
"required": [
"query_parameter"
],
"properties": {
"query_parameter": {
"title": "Query Parameter",
"type": "string",
"description": "The query parameter (case sensitive) that must contain a token for request authentication.\n It can't be set along with header."
}
}
}
]
}
},
"required": [
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ require (
github.com/blang/semver v3.5.1+incompatible
github.com/bxcodec/faker v2.0.1+incompatible
github.com/cenkalti/backoff v2.1.1+incompatible
github.com/codegangsta/negroni v1.0.0 // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/fsnotify/fsnotify v1.4.7
github.com/ghodss/yaml v1.0.0
Expand Down
22 changes: 19 additions & 3 deletions helper/bearer.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,25 @@ import (
"strings"
)

func BearerTokenFromRequest(r *http.Request) string {
auth := r.Header.Get("Authorization")
split := strings.SplitN(auth, " ", 2)
const (
defaultAuthorizationHeader = "Authorization"
)

type BearerTokenLocation struct {
Header *string `json:"header"`
QueryParameter *string `json:"query_parameter"`
}

func BearerTokenFromRequest(r *http.Request, tokenLocation *BearerTokenLocation) string {
if tokenLocation != nil {
if tokenLocation.Header != nil {
return r.Header.Get(*tokenLocation.Header)
} else if tokenLocation.QueryParameter != nil {
return r.FormValue(*tokenLocation.QueryParameter)
}
}
token := r.Header.Get(defaultAuthorizationHeader)
split := strings.SplitN(token, " ", 2)
if len(split) != 2 || !strings.EqualFold(split[0], "bearer") {
return ""
}
Expand Down
68 changes: 68 additions & 0 deletions helper/bearer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package helper_test

import (
"net/http"
"testing"

"github.com/ory/oathkeeper/helper"
"github.com/stretchr/testify/assert"
)

const (
defaultHeaderName = "Authorization"
)

func TestBearerTokenFromRequest(t *testing.T) {
t.Run("case=token should be received from default header if custom location is not set and token is present", func(t *testing.T) {
expectedToken := "token"
request := &http.Request{Header: http.Header{defaultHeaderName: {"bearer " + expectedToken}}}
token := helper.BearerTokenFromRequest(request, nil)
assert.Equal(t, expectedToken, token)
})
t.Run("case=should return empty string if custom location is not set and token is not present in default header", func(t *testing.T) {
request := &http.Request{}
token := helper.BearerTokenFromRequest(request, nil)
assert.Empty(t, token)
})
t.Run("case=should return empty string if custom location is set to header and token is not present in that header", func(t *testing.T) {
customHeaderName := "Custom-Auth-Header"
request := &http.Request{Header: http.Header{defaultHeaderName: {"bearer token"}}}
tokenLocation := helper.BearerTokenLocation{Header: &customHeaderName}
token := helper.BearerTokenFromRequest(request, &tokenLocation)
assert.Empty(t, token)
})
t.Run("case=should return empty string if custom location is set to query parameter and token is not present in that query parameter", func(t *testing.T) {
customQueryParameterName := "Custom-Auth"
request := &http.Request{Header: http.Header{defaultHeaderName: {"bearer token"}}}
tokenLocation := helper.BearerTokenLocation{QueryParameter: &customQueryParameterName}
token := helper.BearerTokenFromRequest(request, &tokenLocation)
assert.Empty(t, token)
})
t.Run("case=token should be received from custom header if custom location is set to header and token is present", func(t *testing.T) {
expectedToken := "token"
customHeaderName := "Custom-Auth-Header"
request := &http.Request{Header: http.Header{customHeaderName: {expectedToken}}}
tokenLocation := helper.BearerTokenLocation{Header: &customHeaderName}
token := helper.BearerTokenFromRequest(request, &tokenLocation)
assert.Equal(t, expectedToken, token)
})
t.Run("case=token should be received from custom header if custom location is set to query parameter and token is present", func(t *testing.T) {
expectedToken := "token"
customQueryParameterName := "Custom-Auth"
request := &http.Request{
Form: map[string][]string{
customQueryParameterName: []string{expectedToken},
},
}
tokenLocation := helper.BearerTokenLocation{QueryParameter: &customQueryParameterName}
token := helper.BearerTokenFromRequest(request, &tokenLocation)
assert.Equal(t, expectedToken, token)
})
t.Run("case=token should be received from default header if custom token location is set, but neither Header nor Query Param is configured", func(t *testing.T) {
expectedToken := "token"
request := &http.Request{Header: http.Header{defaultHeaderName: {"bearer " + expectedToken}}}
tokenLocation := helper.BearerTokenLocation{}
token := helper.BearerTokenFromRequest(request, &tokenLocation)
assert.Equal(t, expectedToken, token)
})
}
23 changes: 12 additions & 11 deletions pipeline/authn/authenticator_jwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,13 @@ type AuthenticatorJWTRegistry interface {
}

type AuthenticatorOAuth2JWTConfiguration struct {
Scope []string `json:"required_scope"`
Audience []string `json:"target_audience"`
Issuers []string `json:"trusted_issuers"`
AllowedAlgorithms []string `json:"allowed_algorithms"`
JWKSURLs []string `json:"jwks_urls"`
ScopeStrategy string `json:"scope_strategy"`
Scope []string `json:"required_scope"`
Audience []string `json:"target_audience"`
Issuers []string `json:"trusted_issuers"`
AllowedAlgorithms []string `json:"allowed_algorithms"`
JWKSURLs []string `json:"jwks_urls"`
ScopeStrategy string `json:"scope_strategy"`
BearerTokenLocation *helper.BearerTokenLocation `json:"token_from"`
}

type AuthenticatorJWT struct {
Expand Down Expand Up @@ -67,16 +68,16 @@ func (a *AuthenticatorJWT) Config(config json.RawMessage) (*AuthenticatorOAuth2J
}

func (a *AuthenticatorJWT) Authenticate(r *http.Request, config json.RawMessage, _ pipeline.Rule) (*AuthenticationSession, error) {
token := helper.BearerTokenFromRequest(r)
if token == "" {
return nil, errors.WithStack(ErrAuthenticatorNotResponsible)
}

cf, err := a.Config(config)
if err != nil {
return nil, err
}

token := helper.BearerTokenFromRequest(r, cf.BearerTokenLocation)
if token == "" {
return nil, errors.WithStack(ErrAuthenticatorNotResponsible)
}

if len(cf.AllowedAlgorithms) == 0 {
cf.AllowedAlgorithms = []string{"RS256"}
}
Expand Down
89 changes: 78 additions & 11 deletions pipeline/authn/authenticator_jwt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,14 @@ func TestAuthenticatorJWT(t *testing.T) {

t.Run("method=authenticate", func(t *testing.T) {
for k, tc := range []struct {
setup func()
d string
r *http.Request
config string
expectErr bool
expectCode int
expectSess *AuthenticationSession
setup func()
d string
r *http.Request
config string
expectErr bool
expectExactErr error
expectCode int
expectSess *AuthenticationSession
}{
{
d: "should fail because no payloads",
Expand All @@ -85,6 +86,69 @@ func TestAuthenticatorJWT(t *testing.T) {
r: &http.Request{Header: http.Header{"Authorization": []string{"bearer invalid.token.sign"}}},
expectErr: true,
},
{
d: "should return error saying that authenticator is not responsible for validating the request, as the token was not provided in a proper location (default)",
r: &http.Request{Header: http.Header{"Foobar": []string{"bearer " + gen(keys[1], jwt.MapClaims{
"sub": "sub",
"exp": now.Add(time.Hour).Unix(),
})}}},
expectErr: true,
expectExactErr: ErrAuthenticatorNotResponsible,
},
{
d: "should return error saying that authenticator is not responsible for validating the request, as the token was not provided in a proper location (custom header)",
r: &http.Request{Header: http.Header{"Authorization": []string{"bearer " + gen(keys[1], jwt.MapClaims{
"sub": "sub",
"exp": now.Add(time.Hour).Unix(),
})}}},
config: `{"token_from": {"header": "X-Custom-Header"}}`,
expectErr: true,
expectExactErr: ErrAuthenticatorNotResponsible,
},
{
d: "should return error saying that authenticator is not responsible for validating the request, as the token was not provided in a proper location (custom query parameter)",
r: &http.Request{
Form: map[string][]string{
"someOtherQueryParam": []string{
gen(keys[1], jwt.MapClaims{
"sub": "sub",
"exp": now.Add(time.Hour).Unix(),
}),
},
},
Header: http.Header{"Authorization": []string{"bearer " + gen(keys[1], jwt.MapClaims{
"sub": "sub",
"exp": now.Add(time.Hour).Unix(),
})}},
},
config: `{"token_from": {"query_parameter": "token"}}`,
expectErr: true,
expectExactErr: ErrAuthenticatorNotResponsible,
},
{
d: "should pass because the valid JWT token was provided in a proper location (custom header)",
r: &http.Request{Header: http.Header{"X-Custom-Header": []string{gen(keys[1], jwt.MapClaims{
"sub": "sub",
"exp": now.Add(time.Hour).Unix(),
})}}},
config: `{"token_from": {"header": "X-Custom-Header"}}`,
expectErr: false,
},
{
d: "should pass because the valid JWT token was provided in a proper location (custom query parameter)",
r: &http.Request{
Form: map[string][]string{
"token": []string{
gen(keys[1], jwt.MapClaims{
"sub": "sub",
"exp": now.Add(time.Hour).Unix(),
}),
},
},
},
config: `{"token_from": {"query_parameter": "token"}}`,
expectErr: false,
},
{
d: "should pass because JWT is valid",
r: &http.Request{Header: http.Header{"Authorization": []string{"bearer " + gen(keys[1], jwt.MapClaims{
Expand Down Expand Up @@ -187,7 +251,7 @@ func TestAuthenticatorJWT(t *testing.T) {
expectCode: 401,
},
{
d: "should pass because JWT is missing scope",
d: "should fail because JWT is missing scope",
r: &http.Request{Header: http.Header{"Authorization": []string{"bearer " + gen(keys[2], jwt.MapClaims{
"sub": "sub",
"exp": now.Add(time.Hour).Unix(),
Expand All @@ -197,7 +261,7 @@ func TestAuthenticatorJWT(t *testing.T) {
expectErr: true,
},
{
d: "should pass because JWT issuer is untrusted",
d: "should fail because JWT issuer is untrusted",
r: &http.Request{Header: http.Header{"Authorization": []string{"bearer " + gen(keys[1], jwt.MapClaims{
"sub": "sub",
"exp": now.Add(time.Hour).Unix(),
Expand All @@ -207,7 +271,7 @@ func TestAuthenticatorJWT(t *testing.T) {
expectErr: true,
},
{
d: "should pass because JWT is missing audience",
d: "should fail because JWT is missing audience",
r: &http.Request{Header: http.Header{"Authorization": []string{"bearer " + gen(keys[1], jwt.MapClaims{
"sub": "sub",
"exp": now.Add(time.Hour).Unix(),
Expand Down Expand Up @@ -235,10 +299,13 @@ func TestAuthenticatorJWT(t *testing.T) {
tc.config, _ = sjson.Set(tc.config, "jwks_urls", keys)
session, err := a.Authenticate(tc.r, json.RawMessage([]byte(tc.config)), nil)
if tc.expectErr {
require.Error(t, err)
if tc.expectCode != 0 {
assert.Equal(t, tc.expectCode, herodot.ToDefaultError(err, "").StatusCode(), "Status code mismatch")
}
require.Error(t, err)
if tc.expectExactErr != nil {
assert.EqualError(t, err, tc.expectExactErr.Error())
}
} else {
require.NoError(t, err, "%#v", errors.Cause(err))
}
Expand Down
15 changes: 8 additions & 7 deletions pipeline/authn/authenticator_oauth2_introspection.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,13 @@ import (
)

type AuthenticatorOAuth2IntrospectionConfiguration struct {
Scopes []string `json:"required_scope"`
Audience []string `json:"target_audience"`
Issuers []string `json:"trusted_issuers"`
PreAuth *AuthenticatorOAuth2IntrospectionPreAuthConfiguration `json:"pre_authorization"`
ScopeStrategy string `json:"scope_strategy"`
IntrospectionURL string `json:"introspection_url"`
Scopes []string `json:"required_scope"`
Audience []string `json:"target_audience"`
Issuers []string `json:"trusted_issuers"`
PreAuth *AuthenticatorOAuth2IntrospectionPreAuthConfiguration `json:"pre_authorization"`
ScopeStrategy string `json:"scope_strategy"`
IntrospectionURL string `json:"introspection_url"`
BearerTokenLocation *helper.BearerTokenLocation `json:"token_from"`
}

type AuthenticatorOAuth2IntrospectionPreAuthConfiguration struct {
Expand Down Expand Up @@ -70,7 +71,7 @@ func (a *AuthenticatorOAuth2Introspection) Authenticate(r *http.Request, config
return nil, err
}

token := helper.BearerTokenFromRequest(r)
token := helper.BearerTokenFromRequest(r, cf.BearerTokenLocation)
if token == "" {
return nil, errors.WithStack(ErrAuthenticatorNotResponsible)
}
Expand Down
Loading

0 comments on commit fc85ac8

Please sign in to comment.