Skip to content

Commit

Permalink
feat: Add MatchContext in the AuthenticationSession (#358)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: This feature allows to use the regex capture groups from the URL matcher to be used in several places, including the ID Token generator and elsewhere. To get this working, existing `keto_engine_acp_ory` authorizers are no longer able to use regex substition in the form of `my:action:$1` but instead must use the new format which is `{{ printIndex .MatchContext.RegexpCaptureGroups 0}}` (notice that the index changed by *-1*). A rule migrator exists which makes old rules compatible with the new format, if a version string is given. More details on the rule migration can be found here: fd16ceb#diff-6177fb19f1b7d7bc392f5062b838df15
  • Loading branch information
Sbou authored Mar 13, 2020
1 parent 2117171 commit a421293
Show file tree
Hide file tree
Showing 34 changed files with 595 additions and 196 deletions.
15 changes: 11 additions & 4 deletions pipeline/authn/authenticator.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package authn
import (
"encoding/json"
"net/http"
"net/url"

"github.com/pkg/errors"

Expand All @@ -19,7 +20,7 @@ var ErrAuthenticatorNotEnabled = herodot.DefaultError{
}

type Authenticator interface {
Authenticate(r *http.Request, config json.RawMessage, rule pipeline.Rule) (*AuthenticationSession, error)
Authenticate(r *http.Request, session *AuthenticationSession, config json.RawMessage, rule pipeline.Rule) error
GetID() string
Validate(config json.RawMessage) error
}
Expand All @@ -37,9 +38,15 @@ func NewErrAuthenticatorMisconfigured(a Authenticator, err error) *herodot.Defau
}

type AuthenticationSession struct {
Subject string `json:"subject"`
Extra map[string]interface{} `json:"extra"`
Header http.Header `json:"header"`
Subject string `json:"subject"`
Extra map[string]interface{} `json:"extra"`
Header http.Header `json:"header"`
MatchContext MatchContext `json:"match_context"`
}

type MatchContext struct {
RegexpCaptureGroups []string `json:"regexp_capture_groups"`
URL *url.URL `json:"url"`
}

func (a *AuthenticationSession) SetHeader(key, val string) {
Expand Down
12 changes: 6 additions & 6 deletions pipeline/authn/authenticator_anonymous.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,17 +48,17 @@ func (a *AuthenticatorAnonymous) Config(config json.RawMessage) (*AuthenticatorA
return &c, nil
}

func (a *AuthenticatorAnonymous) Authenticate(r *http.Request, config json.RawMessage, _ pipeline.Rule) (*AuthenticationSession, error) {
func (a *AuthenticatorAnonymous) Authenticate(r *http.Request, session *AuthenticationSession, config json.RawMessage, _ pipeline.Rule) error {
if len(r.Header.Get("Authorization")) != 0 {
return nil, errors.WithStack(ErrAuthenticatorNotResponsible)
return errors.WithStack(ErrAuthenticatorNotResponsible)
}

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

return &AuthenticationSession{
Subject: stringsx.Coalesce(cf.Subject, "anonymous"),
}, nil
session.Subject = stringsx.Coalesce(cf.Subject, "anonymous")

return nil
}
15 changes: 13 additions & 2 deletions pipeline/authn/authenticator_anonymous_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (

"github.com/ory/oathkeeper/driver/configuration"
"github.com/ory/oathkeeper/internal"
"github.com/ory/oathkeeper/pipeline/authn"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand All @@ -39,18 +40,28 @@ func TestAuthenticatorAnonymous(t *testing.T) {
// viper.Set(configuration.ViperKeyAuthenticatorAnonymousIdentifier, "anon")
reg := internal.NewRegistry(conf)

session := new(authn.AuthenticationSession)

a, err := reg.PipelineAuthenticator("anonymous")
require.NoError(t, err)
assert.Equal(t, "anonymous", a.GetID())

t.Run("method=authenticate/case=is anonymous user", func(t *testing.T) {
session, err := a.Authenticate(&http.Request{Header: http.Header{}}, json.RawMessage(`{"subject":"anon"}`), nil)
err := a.Authenticate(
&http.Request{Header: http.Header{}},
session,
json.RawMessage(`{"subject":"anon"}`),
nil)
require.NoError(t, err)
assert.Equal(t, "anon", session.Subject)
})

t.Run("method=authenticate/case=has credentials", func(t *testing.T) {
_, err := a.Authenticate(&http.Request{Header: http.Header{"Authorization": {"foo"}}}, json.RawMessage(`{"subject":"anon"}`), nil)
err := a.Authenticate(
&http.Request{Header: http.Header{"Authorization": {"foo"}}},
session,
json.RawMessage(`{"subject":"anon"}`),
nil)
require.Error(t, err)
})

Expand Down
19 changes: 9 additions & 10 deletions pipeline/authn/authenticator_cookie_session.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,19 +75,19 @@ func (a *AuthenticatorCookieSession) Config(config json.RawMessage) (*Authentica
return &c, nil
}

func (a *AuthenticatorCookieSession) Authenticate(r *http.Request, config json.RawMessage, _ pipeline.Rule) (*AuthenticationSession, error) {
func (a *AuthenticatorCookieSession) Authenticate(r *http.Request, session *AuthenticationSession, config json.RawMessage, _ pipeline.Rule) error {
cf, err := a.Config(config)
if err != nil {
return nil, err
return err
}

if !cookieSessionResponsible(r, cf.Only) {
return nil, errors.WithStack(ErrAuthenticatorNotResponsible)
return errors.WithStack(ErrAuthenticatorNotResponsible)
}

body, err := forwardRequestToSessionStore(r, cf.CheckSessionURL, cf.PreservePath)
if err != nil {
return nil, err
return err
}

var (
Expand All @@ -99,17 +99,16 @@ func (a *AuthenticatorCookieSession) Authenticate(r *http.Request, config json.R
)

if err = json.Unmarshal(subjectRaw, &subject); err != nil {
return nil, helper.ErrForbidden.WithReasonf("The configured subject_from GJSON path returned an error on JSON output: %s", err.Error()).WithDebugf("GJSON path: %s\nBody: %s\nResult: %s", cf.SubjectFrom, body, subjectRaw).WithTrace(err)
return helper.ErrForbidden.WithReasonf("The configured subject_from GJSON path returned an error on JSON output: %s", err.Error()).WithDebugf("GJSON path: %s\nBody: %s\nResult: %s", cf.SubjectFrom, body, subjectRaw).WithTrace(err)
}

if err = json.Unmarshal(extraRaw, &extra); err != nil {
return nil, helper.ErrForbidden.WithReasonf("The configured extra_from GJSON path returned an error on JSON output: %s", err.Error()).WithDebugf("GJSON path: %s\nBody: %s\nResult: %s", cf.ExtraFrom, body, extraRaw).WithTrace(err)
return helper.ErrForbidden.WithReasonf("The configured extra_from GJSON path returned an error on JSON output: %s", err.Error()).WithDebugf("GJSON path: %s\nBody: %s\nResult: %s", cf.ExtraFrom, body, extraRaw).WithTrace(err)
}

return &AuthenticationSession{
Subject: subject,
Extra: extra,
}, nil
session.Subject = subject
session.Extra = extra
return nil
}

func cookieSessionResponsible(r *http.Request, only []string) bool {
Expand Down
26 changes: 18 additions & 8 deletions pipeline/authn/authenticator_cookie_session_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,17 @@ import (
func TestAuthenticatorCookieSession(t *testing.T) {
conf := internal.NewConfigurationWithDefaults()
reg := internal.NewRegistry(conf)
session := new(AuthenticationSession)

pipelineAuthenticator, err := reg.PipelineAuthenticator("cookie_session")
require.NoError(t, err)

t.Run("method=authenticate", func(t *testing.T) {
t.Run("description=should fail because session store returned 400", func(t *testing.T) {
testServer, _ := makeServer(400, `{}`)
_, err := pipelineAuthenticator.Authenticate(
err := pipelineAuthenticator.Authenticate(
makeRequest("GET", "/", map[string]string{"sessionid": "zyx"}, ""),
session,
json.RawMessage(fmt.Sprintf(`{"check_session_url": "%s"}`, testServer.URL)),
nil,
)
Expand All @@ -40,8 +42,9 @@ func TestAuthenticatorCookieSession(t *testing.T) {

t.Run("description=should pass because session store returned 200", func(t *testing.T) {
testServer, _ := makeServer(200, `{"subject": "123", "extra": {"foo": "bar"}}`)
session, err := pipelineAuthenticator.Authenticate(
err := pipelineAuthenticator.Authenticate(
makeRequest("GET", "/", map[string]string{"sessionid": "zyx"}, ""),
session,
json.RawMessage(fmt.Sprintf(`{"check_session_url": "%s"}`, testServer.URL)),
nil,
)
Expand All @@ -54,8 +57,9 @@ func TestAuthenticatorCookieSession(t *testing.T) {

t.Run("description=should pass through method, path, and headers to auth server", func(t *testing.T) {
testServer, requestRecorder := makeServer(200, `{"subject": "123"}`)
session, err := pipelineAuthenticator.Authenticate(
err := pipelineAuthenticator.Authenticate(
makeRequest("PUT", "/users/123?query=string", map[string]string{"sessionid": "zyx"}, ""),
session,
json.RawMessage(fmt.Sprintf(`{"check_session_url": "%s"}`, testServer.URL)),
nil,
)
Expand All @@ -70,8 +74,9 @@ func TestAuthenticatorCookieSession(t *testing.T) {

t.Run("description=should pass through method and headers ONLY to auth server when PreservePath is true", func(t *testing.T) {
testServer, requestRecorder := makeServer(200, `{"subject": "123"}`)
session, err := pipelineAuthenticator.Authenticate(
err := pipelineAuthenticator.Authenticate(
makeRequest("PUT", "/users/123?query=string", map[string]string{"sessionid": "zyx"}, ""),
session,
json.RawMessage(fmt.Sprintf(`{"check_session_url": "%s", "preserve_path": true}`, testServer.URL)),
nil,
)
Expand All @@ -88,6 +93,7 @@ func TestAuthenticatorCookieSession(t *testing.T) {
testServer, requestRecorder := makeServer(200, `{}`)
pipelineAuthenticator.Authenticate(
makeRequest("POST", "/", map[string]string{"sessionid": "zyx"}, "Some body..."),
session,
json.RawMessage(fmt.Sprintf(`{"check_session_url": "%s"}`, testServer.URL)),
nil,
)
Expand All @@ -100,8 +106,9 @@ func TestAuthenticatorCookieSession(t *testing.T) {

t.Run("description=should fallthrough if only is specified and no cookie specified is set", func(t *testing.T) {
testServer, requestRecorder := makeServer(200, `{}`)
_, err := pipelineAuthenticator.Authenticate(
err := pipelineAuthenticator.Authenticate(
makeRequest("GET", "/", map[string]string{"sessionid": "zyx"}, ""),
session,
json.RawMessage(fmt.Sprintf(`{"only": ["session", "sid"], "check_session_url": "%s"}`, testServer.URL)),
nil,
)
Expand All @@ -111,8 +118,9 @@ func TestAuthenticatorCookieSession(t *testing.T) {

t.Run("description=should not fallthrough if only is specified and cookie specified is set", func(t *testing.T) {
testServer, _ := makeServer(200, `{}`)
_, err := pipelineAuthenticator.Authenticate(
err := pipelineAuthenticator.Authenticate(
makeRequest("GET", "/", map[string]string{"sid": "zyx"}, ""),
session,
json.RawMessage(fmt.Sprintf(`{"only": ["session", "sid"], "check_session_url": "%s"}`, testServer.URL)),
nil,
)
Expand All @@ -121,8 +129,9 @@ func TestAuthenticatorCookieSession(t *testing.T) {

t.Run("description=should work with nested extra keys", func(t *testing.T) {
testServer, _ := makeServer(200, `{"subject": "123", "session": {"foo": "bar"}}`)
session, err := pipelineAuthenticator.Authenticate(
err := pipelineAuthenticator.Authenticate(
makeRequest("GET", "/", map[string]string{"sessionid": "zyx"}, ""),
session,
json.RawMessage(fmt.Sprintf(`{"check_session_url": "%s", "extra_from": "session"}`, testServer.URL)),
nil,
)
Expand All @@ -135,8 +144,9 @@ func TestAuthenticatorCookieSession(t *testing.T) {

t.Run("description=should work with the root key for extra and a custom subject key", func(t *testing.T) {
testServer, _ := makeServer(200, `{"identity": {"id": "123"}, "session": {"foo": "bar"}}`)
session, err := pipelineAuthenticator.Authenticate(
err := pipelineAuthenticator.Authenticate(
makeRequest("GET", "/", map[string]string{"sessionid": "zyx"}, ""),
session,
json.RawMessage(fmt.Sprintf(`{"check_session_url": "%s", "subject_from": "identity.id", "extra_from": "@this"}`, testServer.URL)),
nil,
)
Expand Down
21 changes: 10 additions & 11 deletions pipeline/authn/authenticator_jwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,15 +67,15 @@ func (a *AuthenticatorJWT) Config(config json.RawMessage) (*AuthenticatorOAuth2J
return &c, nil
}

func (a *AuthenticatorJWT) Authenticate(r *http.Request, config json.RawMessage, _ pipeline.Rule) (*AuthenticationSession, error) {
func (a *AuthenticatorJWT) Authenticate(r *http.Request, session *AuthenticationSession, config json.RawMessage, _ pipeline.Rule) error {
cf, err := a.Config(config)
if err != nil {
return nil, err
return err
}

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

if len(cf.AllowedAlgorithms) == 0 {
Expand All @@ -84,7 +84,7 @@ func (a *AuthenticatorJWT) Authenticate(r *http.Request, config json.RawMessage,

jwksu, err := a.c.ParseURLs(cf.JWKSURLs)
if err != nil {
return nil, err
return err
}

pt, err := a.r.CredentialsVerifier().Verify(r.Context(), token, &credentials.ValidationContext{
Expand All @@ -96,17 +96,16 @@ func (a *AuthenticatorJWT) Authenticate(r *http.Request, config json.RawMessage,
ScopeStrategy: a.c.ToScopeStrategy(cf.ScopeStrategy, "authenticators.jwt.Config.scope_strategy"),
})
if err != nil {
return nil, helper.ErrUnauthorized.WithReason(err.Error()).WithTrace(err)
return helper.ErrUnauthorized.WithReason(err.Error()).WithTrace(err)
}

claims, ok := pt.Claims.(jwt.MapClaims)
if !ok {
return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Expected JSON Web Token claims to be of type jwt.MapClaims but got: %T", pt.Claims))
return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Expected JSON Web Token claims to be of type jwt.MapClaims but got: %T", pt.Claims))
}

parsedClaims := jwtx.ParseMapStringInterfaceClaims(claims)
return &AuthenticationSession{
Subject: parsedClaims.Subject,
Extra: claims,
}, nil
session.Subject = jwtx.ParseMapStringInterfaceClaims(claims).Subject
session.Extra = claims

return nil
}
3 changes: 2 additions & 1 deletion pipeline/authn/authenticator_jwt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,8 @@ 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)
session := new(AuthenticationSession)
err := a.Authenticate(tc.r, session, json.RawMessage([]byte(tc.config)), nil)
if tc.expectErr {
require.Error(t, err)
if tc.expectCode != 0 {
Expand Down
4 changes: 2 additions & 2 deletions pipeline/authn/authenticator_noop.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,6 @@ func (a *AuthenticatorNoOp) Validate(config json.RawMessage) error {
return nil
}

func (a *AuthenticatorNoOp) Authenticate(r *http.Request, config json.RawMessage, _ pipeline.Rule) (*AuthenticationSession, error) {
return &AuthenticationSession{Subject: ""}, nil
func (a *AuthenticatorNoOp) Authenticate(r *http.Request, session *AuthenticationSession, config json.RawMessage, _ pipeline.Rule) error {
return nil
}
2 changes: 1 addition & 1 deletion pipeline/authn/authenticator_noop_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func TestAuthenticatorNoop(t *testing.T) {
assert.Equal(t, "noop", a.GetID())

t.Run("method=authenticate", func(t *testing.T) {
_, err := a.Authenticate(nil, nil, nil)
err := a.Authenticate(nil, nil, nil, nil)
require.NoError(t, err)
})

Expand Down
19 changes: 9 additions & 10 deletions pipeline/authn/authenticator_oauth2_client_credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,25 +55,25 @@ func (a *AuthenticatorOAuth2ClientCredentials) Config(config json.RawMessage) (*
return &c, nil
}

func (a *AuthenticatorOAuth2ClientCredentials) Authenticate(r *http.Request, config json.RawMessage, _ pipeline.Rule) (*AuthenticationSession, error) {
func (a *AuthenticatorOAuth2ClientCredentials) Authenticate(r *http.Request, session *AuthenticationSession, config json.RawMessage, _ pipeline.Rule) error {
cf, err := a.Config(config)
if err != nil {
return nil, err
return err
}

user, password, ok := r.BasicAuth()
if !ok {
return nil, errors.WithStack(ErrAuthenticatorNotResponsible)
return errors.WithStack(ErrAuthenticatorNotResponsible)
}

user, err = url.QueryUnescape(user)
if err != nil {
return nil, errors.Wrapf(helper.ErrUnauthorized, err.Error())
return errors.Wrapf(helper.ErrUnauthorized, err.Error())
}

password, err = url.QueryUnescape(password)
if err != nil {
return nil, errors.Wrapf(helper.ErrUnauthorized, err.Error())
return errors.Wrapf(helper.ErrUnauthorized, err.Error())
}

c := &clientcredentials.Config{
Expand All @@ -90,14 +90,13 @@ func (a *AuthenticatorOAuth2ClientCredentials) Authenticate(r *http.Request, con
httpx.NewResilientClientLatencyToleranceMedium(nil),
))
if err != nil {
return nil, errors.Wrapf(helper.ErrUnauthorized, err.Error())
return errors.Wrapf(helper.ErrUnauthorized, err.Error())
}

if token.AccessToken == "" {
return nil, errors.WithStack(helper.ErrUnauthorized)
return errors.WithStack(helper.ErrUnauthorized)
}

return &AuthenticationSession{
Subject: user,
}, nil
session.Subject = user
return nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@ func TestAuthenticatorOAuth2ClientCredentials(t *testing.T) {
},
} {
t.Run(fmt.Sprintf("method=authenticate/case=%d", k), func(t *testing.T) {
session, err := a.Authenticate(tc.r, tc.config, nil)
session := new(authn.AuthenticationSession)
err := a.Authenticate(tc.r, session, tc.config, nil)

if tc.expectErr != nil {
require.EqualError(t, errors.Cause(err), tc.expectErr.Error())
Expand Down
Loading

0 comments on commit a421293

Please sign in to comment.