diff --git a/pipeline/authn/authenticator.go b/pipeline/authn/authenticator.go index 5a13ac3e49..a7d3a87643 100644 --- a/pipeline/authn/authenticator.go +++ b/pipeline/authn/authenticator.go @@ -3,6 +3,7 @@ package authn import ( "encoding/json" "net/http" + "net/url" "github.com/pkg/errors" @@ -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 } @@ -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) { diff --git a/pipeline/authn/authenticator_anonymous.go b/pipeline/authn/authenticator_anonymous.go index 3d0add2edb..7ae6e2ffa4 100644 --- a/pipeline/authn/authenticator_anonymous.go +++ b/pipeline/authn/authenticator_anonymous.go @@ -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 } diff --git a/pipeline/authn/authenticator_anonymous_test.go b/pipeline/authn/authenticator_anonymous_test.go index 497ee950c6..ca55e275e5 100644 --- a/pipeline/authn/authenticator_anonymous_test.go +++ b/pipeline/authn/authenticator_anonymous_test.go @@ -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" @@ -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) }) diff --git a/pipeline/authn/authenticator_cookie_session.go b/pipeline/authn/authenticator_cookie_session.go index 0f8f563695..974d745827 100644 --- a/pipeline/authn/authenticator_cookie_session.go +++ b/pipeline/authn/authenticator_cookie_session.go @@ -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 ( @@ -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 { diff --git a/pipeline/authn/authenticator_cookie_session_test.go b/pipeline/authn/authenticator_cookie_session_test.go index 2754b161a5..6b955dc00d 100644 --- a/pipeline/authn/authenticator_cookie_session_test.go +++ b/pipeline/authn/authenticator_cookie_session_test.go @@ -23,6 +23,7 @@ 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) @@ -30,8 +31,9 @@ func TestAuthenticatorCookieSession(t *testing.T) { 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, ) @@ -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, ) @@ -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, ) @@ -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, ) @@ -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, ) @@ -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, ) @@ -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, ) @@ -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, ) @@ -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, ) diff --git a/pipeline/authn/authenticator_jwt.go b/pipeline/authn/authenticator_jwt.go index cb10bc8dd7..141f1fd462 100644 --- a/pipeline/authn/authenticator_jwt.go +++ b/pipeline/authn/authenticator_jwt.go @@ -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 { @@ -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{ @@ -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 } diff --git a/pipeline/authn/authenticator_jwt_test.go b/pipeline/authn/authenticator_jwt_test.go index 7ae1953e45..fe3c1f00ee 100644 --- a/pipeline/authn/authenticator_jwt_test.go +++ b/pipeline/authn/authenticator_jwt_test.go @@ -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 { diff --git a/pipeline/authn/authenticator_noop.go b/pipeline/authn/authenticator_noop.go index 5ee0960911..02eb818762 100644 --- a/pipeline/authn/authenticator_noop.go +++ b/pipeline/authn/authenticator_noop.go @@ -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 } diff --git a/pipeline/authn/authenticator_noop_test.go b/pipeline/authn/authenticator_noop_test.go index da1725e02f..38c0a8d05f 100644 --- a/pipeline/authn/authenticator_noop_test.go +++ b/pipeline/authn/authenticator_noop_test.go @@ -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) }) diff --git a/pipeline/authn/authenticator_oauth2_client_credentials.go b/pipeline/authn/authenticator_oauth2_client_credentials.go index 72e653e4e1..a3b583714e 100644 --- a/pipeline/authn/authenticator_oauth2_client_credentials.go +++ b/pipeline/authn/authenticator_oauth2_client_credentials.go @@ -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{ @@ -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 } diff --git a/pipeline/authn/authenticator_oauth2_client_credentials_test.go b/pipeline/authn/authenticator_oauth2_client_credentials_test.go index 1e81e6b88d..e0e88b6f27 100644 --- a/pipeline/authn/authenticator_oauth2_client_credentials_test.go +++ b/pipeline/authn/authenticator_oauth2_client_credentials_test.go @@ -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()) diff --git a/pipeline/authn/authenticator_oauth2_introspection.go b/pipeline/authn/authenticator_oauth2_introspection.go index 808346871b..6b6d8133a2 100644 --- a/pipeline/authn/authenticator_oauth2_introspection.go +++ b/pipeline/authn/authenticator_oauth2_introspection.go @@ -73,22 +73,22 @@ type AuthenticatorOAuth2IntrospectionResult struct { Scope string `json:"scope,omitempty"` } -func (a *AuthenticatorOAuth2Introspection) Authenticate(r *http.Request, config json.RawMessage, _ pipeline.Rule) (*AuthenticationSession, error) { +func (a *AuthenticatorOAuth2Introspection) Authenticate(r *http.Request, session *AuthenticationSession, config json.RawMessage, _ pipeline.Rule) error { var i AuthenticatorOAuth2IntrospectionResult 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) } body := url.Values{"token": {token}, "scope": {strings.Join(cf.Scopes, " ")}} introspectReq, err := http.NewRequest(http.MethodPost, cf.IntrospectionURL, strings.NewReader(body.Encode())) if err != nil { - return nil, errors.WithStack(err) + return errors.WithStack(err) } for key, value := range cf.IntrospectionRequestHeaders { introspectReq.Header.Set(key, value) @@ -97,42 +97,42 @@ func (a *AuthenticatorOAuth2Introspection) Authenticate(r *http.Request, config introspectReq.Header.Set("Content-Type", "application/x-www-form-urlencoded") resp, err := a.client.Do(introspectReq) if err != nil { - return nil, errors.WithStack(err) + return errors.WithStack(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return nil, errors.Errorf("Introspection returned status code %d but expected %d", resp.StatusCode, http.StatusOK) + return errors.Errorf("Introspection returned status code %d but expected %d", resp.StatusCode, http.StatusOK) } if err := json.NewDecoder(resp.Body).Decode(&i); err != nil { - return nil, errors.WithStack(err) + return errors.WithStack(err) } if len(i.TokenType) > 0 && i.TokenType != "access_token" { - return nil, errors.WithStack(helper.ErrForbidden.WithReason(fmt.Sprintf("Introspected token is not an access token but \"%s\"", i.TokenType))) + return errors.WithStack(helper.ErrForbidden.WithReason(fmt.Sprintf("Introspected token is not an access token but \"%s\"", i.TokenType))) } if !i.Active { - return nil, errors.WithStack(helper.ErrUnauthorized.WithReason("Access token i says token is not active")) + return errors.WithStack(helper.ErrUnauthorized.WithReason("Access token i says token is not active")) } for _, audience := range cf.Audience { if !stringslice.Has(i.Audience, audience) { - return nil, errors.WithStack(helper.ErrForbidden.WithReason(fmt.Sprintf("Token audience is not intended for target audience %s", audience))) + return errors.WithStack(helper.ErrForbidden.WithReason(fmt.Sprintf("Token audience is not intended for target audience %s", audience))) } } if len(cf.Issuers) > 0 { if !stringslice.Has(cf.Issuers, i.Issuer) { - return nil, errors.WithStack(helper.ErrForbidden.WithReason(fmt.Sprintf("Token issuer does not match any trusted issuer"))) + return errors.WithStack(helper.ErrForbidden.WithReason(fmt.Sprintf("Token issuer does not match any trusted issuer"))) } } if ss := a.c.ToScopeStrategy(cf.ScopeStrategy, "authenticators.oauth2_introspection.scope_strategy"); ss != nil { for _, scope := range cf.Scopes { if !ss(strings.Split(i.Scope, " "), scope) { - return nil, errors.WithStack(helper.ErrForbidden.WithReason(fmt.Sprintf("Scope %s was not granted", scope))) + return errors.WithStack(helper.ErrForbidden.WithReason(fmt.Sprintf("Scope %s was not granted", scope))) } } } @@ -145,10 +145,10 @@ func (a *AuthenticatorOAuth2Introspection) Authenticate(r *http.Request, config i.Extra["client_id"] = i.ClientID i.Extra["scope"] = i.Scope - return &AuthenticationSession{ - Subject: i.Subject, - Extra: i.Extra, - }, nil + session.Subject = i.Subject + session.Extra = i.Extra + + return nil } func (a *AuthenticatorOAuth2Introspection) Validate(config json.RawMessage) error { diff --git a/pipeline/authn/authenticator_oauth2_introspection_test.go b/pipeline/authn/authenticator_oauth2_introspection_test.go index 613d12d2f2..74e3951ac2 100644 --- a/pipeline/authn/authenticator_oauth2_introspection_test.go +++ b/pipeline/authn/authenticator_oauth2_introspection_test.go @@ -332,7 +332,8 @@ func TestAuthenticatorOAuth2Introspection(t *testing.T) { tc.config, _ = sjson.SetBytes(tc.config, "introspection_url", ts.URL+"/oauth2/introspect") tc.config, _ = sjson.SetBytes(tc.config, "scope_strategy", "exact") - sess, err := a.Authenticate(tc.r, tc.config, nil) + sess := new(AuthenticationSession) + err := a.Authenticate(tc.r, sess, tc.config, nil) if tc.expectErr { require.Error(t, err) if tc.expectExactErr != nil { diff --git a/pipeline/authn/authenticator_unauthorized.go b/pipeline/authn/authenticator_unauthorized.go index 6318c0332f..5fc87188fe 100644 --- a/pipeline/authn/authenticator_unauthorized.go +++ b/pipeline/authn/authenticator_unauthorized.go @@ -55,6 +55,6 @@ func (a *AuthenticatorUnauthorized) GetID() string { return "unauthorized" } -func (a *AuthenticatorUnauthorized) Authenticate(r *http.Request, config json.RawMessage, _ pipeline.Rule) (*AuthenticationSession, error) { - return nil, errors.WithStack(helper.ErrUnauthorized) +func (a *AuthenticatorUnauthorized) Authenticate(r *http.Request, session *AuthenticationSession, config json.RawMessage, _ pipeline.Rule) error { + return errors.WithStack(helper.ErrUnauthorized) } diff --git a/pipeline/authn/authenticator_unauthorized_test.go b/pipeline/authn/authenticator_unauthorized_test.go index b7cfc85a35..12c219aded 100644 --- a/pipeline/authn/authenticator_unauthorized_test.go +++ b/pipeline/authn/authenticator_unauthorized_test.go @@ -42,7 +42,7 @@ func TestAuthenticatorBroken(t *testing.T) { assert.Equal(t, "unauthorized", a.GetID()) t.Run("method=authenticate", func(t *testing.T) { - _, err := a.Authenticate(&http.Request{Header: http.Header{}}, nil, nil) + err := a.Authenticate(&http.Request{Header: http.Header{}}, nil, nil, nil) require.Error(t, err) }) diff --git a/pipeline/authz/keto_engine_acp_ory.go b/pipeline/authz/keto_engine_acp_ory.go index b81945511a..185e113c16 100644 --- a/pipeline/authz/keto_engine_acp_ory.go +++ b/pipeline/authz/keto_engine_acp_ory.go @@ -22,6 +22,7 @@ package authz import ( "bytes" + "crypto/sha256" "encoding/json" "fmt" "net/http" @@ -34,6 +35,7 @@ import ( "github.com/ory/oathkeeper/driver/configuration" "github.com/ory/oathkeeper/pipeline" "github.com/ory/oathkeeper/pipeline/authn" + "github.com/ory/oathkeeper/x" "github.com/ory/x/urlx" @@ -51,11 +53,24 @@ type AuthorizerKetoEngineACPORYConfiguration struct { BaseURL string `json:"base_url"` } +func (c *AuthorizerKetoEngineACPORYConfiguration) SubjectTemplateID() string { + return fmt.Sprintf("%x", sha256.Sum256([]byte(c.Subject))) +} + +func (c *AuthorizerKetoEngineACPORYConfiguration) ActionTemplateID() string { + return fmt.Sprintf("%x", sha256.Sum256([]byte(c.RequiredAction))) +} + +func (c *AuthorizerKetoEngineACPORYConfiguration) ResourceTemplateID() string { + return fmt.Sprintf("%x", sha256.Sum256([]byte(c.RequiredResource))) +} + type AuthorizerKetoEngineACPORY struct { c configuration.Provider client *http.Client contextCreator authorizerKetoWardenContext + t *template.Template } func NewAuthorizerKetoEngineACPORY(c configuration.Provider) *AuthorizerKetoEngineACPORY { @@ -68,6 +83,7 @@ func NewAuthorizerKetoEngineACPORY(c configuration.Provider) *AuthorizerKetoEngi "requestedAt": time.Now().UTC(), } }, + t: x.NewTemplate("keto_engine_acp_ory"), } } @@ -101,30 +117,29 @@ func (a *AuthorizerKetoEngineACPORY) Authorize(r *http.Request, session *authn.A subject := session.Subject if cf.Subject != "" { - templateId := fmt.Sprintf("%s:%s", rule.GetID(), "subject") - subject, err = a.ParseSubject(session, templateId, cf.Subject) + subject, err = a.parseParameter(session, cf.SubjectTemplateID(), cf.Subject) if err != nil { return errors.WithStack(err) } } - flavor := "regex" - if len(cf.Flavor) > 0 { - flavor = cf.Flavor - } - - var b bytes.Buffer - u := fmt.Sprintf("%s://%s%s", r.URL.Scheme, r.URL.Host, r.URL.Path) - - action, err := rule.ReplaceAllString(a.c.AccessRuleMatchingStrategy(), u, cf.RequiredAction) + action, err := a.parseParameter(session, cf.ActionTemplateID(), cf.RequiredAction) if err != nil { return errors.WithStack(err) } - resource, err := rule.ReplaceAllString(a.c.AccessRuleMatchingStrategy(), u, cf.RequiredResource) + + resource, err := a.parseParameter(session, cf.ResourceTemplateID(), cf.RequiredResource) if err != nil { return errors.WithStack(err) } + flavor := "regex" + if len(cf.Flavor) > 0 { + flavor = cf.Flavor + } + + var b bytes.Buffer + if err := json.NewEncoder(&b).Encode(&AuthorizerKetoEngineACPORYRequestBody{ Action: action, Resource: resource, @@ -171,29 +186,23 @@ func (a *AuthorizerKetoEngineACPORY) Authorize(r *http.Request, session *authn.A return nil } -func (a *AuthorizerKetoEngineACPORY) ParseSubject(session *authn.AuthenticationSession, templateId, templateString string) (string, error) { - tmplFn := template.New("rules"). - Option("missingkey=zero"). - Funcs(template.FuncMap{ - "print": func(i interface{}) string { - if i == nil { - return "" - } - return fmt.Sprintf("%v", i) - }, - }) - - tmpl, err := tmplFn.New(templateId).Parse(templateString) - if err != nil { - return "", err +func (a *AuthorizerKetoEngineACPORY) parseParameter(session *authn.AuthenticationSession, templateID, templateString string) (string, error) { + + t := a.t.Lookup(templateID) + if t == nil { + var err error + t, err = a.t.New(templateID).Parse(templateString) + if err != nil { + return "", err + } } - subject := bytes.Buffer{} - err = tmpl.Execute(&subject, session) - if err != nil { + var b bytes.Buffer + if err := t.Execute(&b, session); err != nil { return "", err } - return subject.String(), nil + + return b.String(), nil } func (a *AuthorizerKetoEngineACPORY) Validate(config json.RawMessage) error { diff --git a/pipeline/authz/keto_engine_acp_ory_test.go b/pipeline/authz/keto_engine_acp_ory_test.go index f6f1957919..760747e69e 100644 --- a/pipeline/authz/keto_engine_acp_ory_test.go +++ b/pipeline/authz/keto_engine_acp_ory_test.go @@ -35,7 +35,6 @@ import ( "github.com/ory/oathkeeper/driver/configuration" "github.com/ory/oathkeeper/internal" - "github.com/ory/oathkeeper/pipeline" "github.com/ory/oathkeeper/pipeline/authn" . "github.com/ory/oathkeeper/pipeline/authz" @@ -52,6 +51,8 @@ func TestAuthorizerKetoWarden(t *testing.T) { conf := internal.NewConfigurationWithDefaults() reg := internal.NewRegistry(conf) + rule := &rule.Rule{ID: "TestAuthorizer"} + a, err := reg.PipelineAuthorizer("keto_engine_acp_ory") require.NoError(t, err) assert.Equal(t, "keto_engine_acp_ory", a.GetID()) @@ -61,33 +62,20 @@ func TestAuthorizerKetoWarden(t *testing.T) { r *http.Request session *authn.AuthenticationSession config json.RawMessage - rule pipeline.Rule expectErr bool }{ { expectErr: true, }, { - config: []byte(`{ "required_action": "action", "required_resource": "resource" }`), - rule: &rule.Rule{ - Match: &rule.Match{ - Methods: []string{"POST"}, - URL: "https://localhost/", - }, - }, + config: []byte(`{ "required_action": "action", "required_resource": "resource" }`), r: &http.Request{URL: &url.URL{}}, session: new(authn.AuthenticationSession), expectErr: true, }, { config: []byte(`{ "required_action": "action", "required_resource": "resource", "flavor": "regex" }`), - rule: &rule.Rule{ - Match: &rule.Match{ - Methods: []string{"POST"}, - URL: "https://localhost/", - }, - }, - r: &http.Request{URL: &url.URL{}}, + r: &http.Request{URL: &url.URL{}}, setup: func(t *testing.T) *httptest.Server { return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusForbidden) @@ -98,13 +86,7 @@ func TestAuthorizerKetoWarden(t *testing.T) { }, { config: []byte(`{ "required_action": "action", "required_resource": "resource", "flavor": "exact" }`), - rule: &rule.Rule{ - Match: &rule.Match{ - Methods: []string{"POST"}, - URL: "https://localhost/", - }, - }, - r: &http.Request{URL: &url.URL{}}, + r: &http.Request{URL: &url.URL{}}, setup: func(t *testing.T) *httptest.Server { return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Contains(t, r.Header, "Content-Type") @@ -117,14 +99,8 @@ func TestAuthorizerKetoWarden(t *testing.T) { expectErr: true, }, { - config: []byte(`{ "required_action": "action:$1:$2", "required_resource": "resource:$1:$2" }`), - rule: &rule.Rule{ - Match: &rule.Match{ - Methods: []string{"POST"}, - URL: "https://localhost/api/users/<[0-9]+>/<[a-z]+>", - }, - }, - r: &http.Request{URL: urlx.ParseOrPanic("https://localhost/api/users/1234/abcde")}, + config: []byte(`{ "required_action": "action:{{ printIndex .MatchContext.RegexpCaptureGroups (sub 1 1 | int)}}:{{ index .MatchContext.RegexpCaptureGroups (sub 2 1 | int)}}", "required_resource": "resource:{{ index .MatchContext.RegexpCaptureGroups 0}}:{{ index .MatchContext.RegexpCaptureGroups 1}}" }`), + r: &http.Request{URL: urlx.ParseOrPanic("https://localhost/api/users/1234/abcde")}, setup: func(t *testing.T) *httptest.Server { return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var ki AuthorizerKetoEngineACPORYRequestBody @@ -139,18 +115,17 @@ func TestAuthorizerKetoWarden(t *testing.T) { w.Write([]byte(`{"allowed":true}`)) })) }, - session: &authn.AuthenticationSession{Subject: "peter"}, + session: &authn.AuthenticationSession{ + Subject: "peter", + MatchContext: authn.MatchContext{ + RegexpCaptureGroups: []string{"1234", "abcde"}, + }, + }, expectErr: false, }, { - config: []byte(`{ "required_action": "action:$1:$2", "required_resource": "resource:$1:$2", "subject": "{{ .Extra.name }}" }`), - rule: &rule.Rule{ - Match: &rule.Match{ - Methods: []string{"POST"}, - URL: "https://localhost/api/users/<[0-9]+>/<[a-z]+>", - }, - }, - r: &http.Request{URL: urlx.ParseOrPanic("https://localhost/api/users/1234/abcde")}, + config: []byte(`{ "required_action": "action:{{ index .MatchContext.RegexpCaptureGroups 0}}:{{ index .MatchContext.RegexpCaptureGroups 1}}", "required_resource": "resource:{{ index .MatchContext.RegexpCaptureGroups 0}}:{{ index .MatchContext.RegexpCaptureGroups 1}}", "subject": "{{ .Extra.name }}" }`), + r: &http.Request{URL: urlx.ParseOrPanic("https://localhost/api/users/1234/abcde")}, setup: func(t *testing.T) *httptest.Server { return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var ki AuthorizerKetoEngineACPORYRequestBody @@ -165,25 +140,23 @@ func TestAuthorizerKetoWarden(t *testing.T) { w.Write([]byte(`{"allowed":true}`)) })) }, - session: &authn.AuthenticationSession{Extra: map[string]interface{}{"name": "peter"}}, + session: &authn.AuthenticationSession{ + Extra: map[string]interface{}{"name": "peter"}, + MatchContext: authn.MatchContext{ + RegexpCaptureGroups: []string{"1234", "abcde"}, + }}, expectErr: false, }, { - config: []byte(`{ "required_action": "action:$1:$2", "required_resource": "resource:$1:$2", "subject": "{{ .Extra.name }}" }`), - rule: &rule.Rule{ - Match: &rule.Match{ - Methods: []string{"POST"}, - URL: "https://localhost/api/users/<[0-9]+>/<[a-z]+>", - }, - }, - r: &http.Request{URL: urlx.ParseOrPanic("https://localhost/api/users/1234/abcde?limit=10")}, + config: []byte(`{ "required_action": "action:{{ index .MatchContext.RegexpCaptureGroups 0 }}:{{ .Extra.name }}", "required_resource": "resource:{{ index .MatchContext.RegexpCaptureGroups 0}}:{{ .Extra.apiVersion }}", "subject": "{{ .Extra.name }}" }`), + r: &http.Request{URL: urlx.ParseOrPanic("https://localhost/api/users/1234/abcde?limit=10")}, setup: func(t *testing.T) *httptest.Server { return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var ki AuthorizerKetoEngineACPORYRequestBody require.NoError(t, json.NewDecoder(r.Body).Decode(&ki)) assert.EqualValues(t, AuthorizerKetoEngineACPORYRequestBody{ - Action: "action:1234:abcde", - Resource: "resource:1234:abcde", + Action: "action:1234:peter", + Resource: "resource:1234:1.0", Context: map[string]interface{}{}, Subject: "peter", }, ki) @@ -191,7 +164,12 @@ func TestAuthorizerKetoWarden(t *testing.T) { w.Write([]byte(`{"allowed":true}`)) })) }, - session: &authn.AuthenticationSession{Extra: map[string]interface{}{"name": "peter"}}, + session: &authn.AuthenticationSession{ + Extra: map[string]interface{}{ + "name": "peter", + "apiVersion": "1.0"}, + MatchContext: authn.MatchContext{RegexpCaptureGroups: []string{"1234"}}, + }, expectErr: false, }, } { @@ -211,7 +189,7 @@ func TestAuthorizerKetoWarden(t *testing.T) { }) tc.config, _ = sjson.SetBytes(tc.config, "base_url", baseURL) - err := a.Authorize(tc.r, tc.session, tc.config, tc.rule) + err := a.Authorize(tc.r, tc.session, tc.config, rule) if tc.expectErr { require.Error(t, err) } else { @@ -224,15 +202,13 @@ func TestAuthorizerKetoWarden(t *testing.T) { viper.Set(configuration.ViperKeyAuthorizerKetoEngineACPORYIsEnabled, false) require.Error(t, a.Validate(json.RawMessage(`{"base_url":"","required_action":"foo","required_resource":"bar"}`))) - viper.Reset() - viper.Set(configuration.ViperKeyAuthorizerKetoEngineACPORYIsEnabled, true) - require.Error(t, a.Validate(json.RawMessage(`{"base_url":"","required_action":"foo","required_resource":"bar"}`))) - - viper.Reset() viper.Set(configuration.ViperKeyAuthorizerKetoEngineACPORYIsEnabled, false) require.Error(t, a.Validate(json.RawMessage(`{"base_url":"http://foo/bar","required_action":"foo","required_resource":"bar"}`))) viper.Reset() + viper.Set(configuration.ViperKeyAuthorizerKetoEngineACPORYIsEnabled, true) + require.Error(t, a.Validate(json.RawMessage(`{"base_url":"","required_action":"foo","required_resource":"bar"}`))) + viper.Set(configuration.ViperKeyAuthorizerKetoEngineACPORYIsEnabled, true) require.NoError(t, a.Validate(json.RawMessage(`{"base_url":"http://foo/bar","required_action":"foo","required_resource":"bar"}`))) }) diff --git a/pipeline/mutate/mutator_cookie.go b/pipeline/mutate/mutator_cookie.go index 1c5cbf9d4b..541d3e2a24 100644 --- a/pipeline/mutate/mutator_cookie.go +++ b/pipeline/mutate/mutator_cookie.go @@ -10,6 +10,7 @@ import ( "github.com/ory/oathkeeper/driver/configuration" "github.com/ory/oathkeeper/pipeline" "github.com/ory/oathkeeper/pipeline/authn" + "github.com/ory/oathkeeper/x" "github.com/pkg/errors" ) @@ -26,7 +27,7 @@ type MutatorCookie struct { } func NewMutatorCookie(c configuration.Provider) *MutatorCookie { - return &MutatorCookie{c: c, t: newTemplate("cookie")} + return &MutatorCookie{c: c, t: x.NewTemplate("cookie")} } func (a *MutatorCookie) GetID() string { diff --git a/pipeline/mutate/mutator_header.go b/pipeline/mutate/mutator_header.go index 69df0b8ec7..c1a8c6b7b3 100644 --- a/pipeline/mutate/mutator_header.go +++ b/pipeline/mutate/mutator_header.go @@ -10,6 +10,7 @@ import ( "github.com/ory/oathkeeper/driver/configuration" "github.com/ory/oathkeeper/pipeline" "github.com/ory/oathkeeper/pipeline/authn" + "github.com/ory/oathkeeper/x" "github.com/pkg/errors" ) @@ -24,7 +25,7 @@ type MutatorHeader struct { } func NewMutatorHeader(c configuration.Provider) *MutatorHeader { - return &MutatorHeader{c: c, t: newTemplate("header")} + return &MutatorHeader{c: c, t: x.NewTemplate("header")} } func (a *MutatorHeader) GetID() string { diff --git a/pipeline/mutate/mutator_header_test.go b/pipeline/mutate/mutator_header_test.go index 9f76b6f00e..c958724aa3 100644 --- a/pipeline/mutate/mutator_header_test.go +++ b/pipeline/mutate/mutator_header_test.go @@ -140,6 +140,23 @@ func TestCredentialsIssuerHeaders(t *testing.T) { }, Err: nil, }, + "Use request captures to header": { + Session: &authn.AuthenticationSession{ + Subject: "foo", + MatchContext: authn.MatchContext{ + RegexpCaptureGroups: []string{"Foo", "Bar"}, + }, + }, + Rule: &rule.Rule{ID: "test-rule10"}, + Config: json.RawMessage([]byte(`{"headers":{ + "Example-Claims": "{{ index .MatchContext.RegexpCaptureGroups 0}}" + }}`)), + Request: &http.Request{Header: http.Header{}}, + Match: http.Header{ + "Example-Claims": []string{"Foo"}, + }, + Err: nil, + }, } t.Run("cache=disabled", func(t *testing.T) { diff --git a/pipeline/mutate/mutator_hydrator_test.go b/pipeline/mutate/mutator_hydrator_test.go index 8c2d88803f..d27097b654 100644 --- a/pipeline/mutate/mutator_hydrator_test.go +++ b/pipeline/mutate/mutator_hydrator_test.go @@ -39,6 +39,14 @@ func setSubject(subject string) func(a *authn.AuthenticationSession) { } } +func setMatchContext(groups []string) func(a *authn.AuthenticationSession) { + return func(a *authn.AuthenticationSession) { + a.MatchContext = authn.MatchContext{ + RegexpCaptureGroups: groups, + } + } +} + func newAuthenticationSession(modifications ...func(a *authn.AuthenticationSession)) *authn.AuthenticationSession { a := authn.AuthenticationSession{} for _, f := range modifications { @@ -135,6 +143,7 @@ func TestMutatorHydrator(t *testing.T) { "foo": "hello", "bar": 3.14, } + sampleCaptureGroups := []string{"resource", "context"} sampleUserId := "user" sampleValidPassword := "passwd1" sampleNotValidPassword := "passwd7" @@ -176,11 +185,11 @@ func TestMutatorHydrator(t *testing.T) { }, "No Changes": { Setup: defaultRouterSetup(), - Session: newAuthenticationSession(setExtra(sampleKey, sampleValue)), + Session: newAuthenticationSession(setExtra(sampleKey, sampleValue), setMatchContext(sampleCaptureGroups)), Rule: &rule.Rule{ID: "test-rule"}, Config: defaultConfigForMutator(), Request: &http.Request{}, - Match: newAuthenticationSession(setExtra(sampleKey, sampleValue)), + Match: newAuthenticationSession(setExtra(sampleKey, sampleValue), setMatchContext(sampleCaptureGroups)), Err: nil, }, "No Extra Before And After": { diff --git a/pipeline/mutate/mutator_id_token.go b/pipeline/mutate/mutator_id_token.go index 3de0aa4655..b1744eb55a 100644 --- a/pipeline/mutate/mutator_id_token.go +++ b/pipeline/mutate/mutator_id_token.go @@ -41,6 +41,7 @@ import ( "github.com/ory/oathkeeper/driver/configuration" "github.com/ory/oathkeeper/pipeline" "github.com/ory/oathkeeper/pipeline/authn" + "github.com/ory/oathkeeper/x" ) type MutatorIDTokenRegistry interface { @@ -73,7 +74,7 @@ func NewMutatorIDToken(c configuration.Provider, r MutatorIDTokenRegistry) *Muta MaxCost: 1 << 25, BufferItems: 64, }) - return &MutatorIDToken{r: r, c: c, templates: newTemplate("id_token"), tokenCache: cache, tokenCacheEnabled: true} + return &MutatorIDToken{r: r, c: c, templates: x.NewTemplate("id_token"), tokenCache: cache, tokenCacheEnabled: true} } func (a *MutatorIDToken) GetID() string { diff --git a/pipeline/mutate/mutator_id_token_test.go b/pipeline/mutate/mutator_id_token_test.go index 4906d7ee14..f878549a5a 100644 --- a/pipeline/mutate/mutator_id_token_test.go +++ b/pipeline/mutate/mutator_id_token_test.go @@ -141,6 +141,51 @@ var idTokenTestCases = []idTokenTestCase{ Match: jwt.MapClaims{}, K: "file://../../test/stub/jwks-ecdsa.json", }, + { + Rule: &rule.Rule{ID: "test-rule10"}, + Session: &authn.AuthenticationSession{ + Subject: "foo", + Extra: map[string]interface{}{"abc": "value1", "def": "value2"}, + MatchContext: authn.MatchContext{ + RegexpCaptureGroups: []string{"user", "pass"}, + }}, + Config: json.RawMessage([]byte(`{"claims": "{\"custom-claim\": \"{{ print .Extra.abc }}\", \"custom-claim2\": \"{{ printIndex .MatchContext.RegexpCaptureGroups 1}}\", \"aud\": [\"foo\", \"bar\"]}"}`)), + Match: jwt.MapClaims{ + "custom-claim": "value1", + "custom-claim2": "pass", + }, + K: "file://../../test/stub/jwks-ecdsa.json", + }, + { + Rule: &rule.Rule{ID: "test-rule11"}, + Session: &authn.AuthenticationSession{ + Subject: "foo", + Extra: map[string]interface{}{"abc": "value1", "def": "value2"}, + MatchContext: authn.MatchContext{ + RegexpCaptureGroups: []string{"user"}, + }}, + Config: json.RawMessage([]byte(`{"claims": "{\"custom-claim\": \"{{ print .Extra.abc }}\", \"custom-claim2\": \"{{ printIndex .MatchContext.RegexpCaptureGroups 1}}\", \"aud\": [\"foo\", \"bar\"]}"}`)), + Match: jwt.MapClaims{ + "custom-claim": "value1", + "custom-claim2": "", + }, + K: "file://../../test/stub/jwks-ecdsa.json", + }, + { + Rule: &rule.Rule{ID: "test-rule12"}, + Session: &authn.AuthenticationSession{ + Subject: "foo", + Extra: map[string]interface{}{"abc": "value1", "def": "value2"}, + MatchContext: authn.MatchContext{ + RegexpCaptureGroups: []string{}, + }}, + Config: json.RawMessage([]byte(`{"claims": "{\"custom-claim\": \"{{ print .Extra.abc }}\", \"custom-claim2\": \"{{ printIndex .MatchContext.RegexpCaptureGroups 0}}\", \"aud\": [\"foo\", \"bar\"]}"}`)), + Match: jwt.MapClaims{ + "custom-claim": "value1", + "custom-claim2": "", + }, + K: "file://../../test/stub/jwks-ecdsa.json", + }, } func parseToken(h http.Header) string { diff --git a/pipeline/mutate/template.go b/pipeline/mutate/template.go deleted file mode 100644 index b44be63821..0000000000 --- a/pipeline/mutate/template.go +++ /dev/null @@ -1,23 +0,0 @@ -package mutate - -import ( - "fmt" - "text/template" - - "github.com/Masterminds/sprig" -) - -func newTemplate(id string) *template.Template { - return template.New(id). - // Implies that zero value will be used if a key is missing. - Option("missingkey=zero"). - Funcs(template.FuncMap{ - "print": func(i interface{}) string { - if i == nil { - return "" - } - return fmt.Sprintf("%v", i) - }, - }). - Funcs(sprig.TxtFuncMap()) -} diff --git a/proxy/request_handler.go b/proxy/request_handler.go index 684f94315d..5bedcf3f01 100644 --- a/proxy/request_handler.go +++ b/proxy/request_handler.go @@ -178,6 +178,9 @@ func (d *RequestHandler) HandleRequest(r *http.Request, rl *rule.Rule) (session "rule_id": rl.ID, } + // initialize the session used during all the flow + session = d.InitializeAuthnSession(r, rl) + if len(rl.Authenticators) == 0 { err = errors.New("No authentication handler was set in the rule") d.r.Logger().WithError(err). @@ -210,7 +213,7 @@ func (d *RequestHandler) HandleRequest(r *http.Request, rl *rule.Rule) (session return nil, err } - session, err = anh.Authenticate(r, a.Config, rl) + err = anh.Authenticate(r, session, a.Config, rl) if err != nil { switch errors.Cause(err).Error() { case authn.ErrAuthenticatorNotResponsible.Error(): @@ -325,3 +328,27 @@ func (d *RequestHandler) HandleRequest(r *http.Request, rl *rule.Rule) (session return session, nil } + +// InitializeAuthnSession reates an authentication session and initializes it with a Match context if possible +func (d *RequestHandler) InitializeAuthnSession(r *http.Request, rl *rule.Rule) *authn.AuthenticationSession { + + session := &authn.AuthenticationSession{ + Subject: "", + } + + values, err := rl.ExtractRegexGroups(d.c.AccessRuleMatchingStrategy(), r.URL) + if err != nil { + d.r.Logger().WithError(err). + WithField("rule_id", rl.ID). + WithField("access_url", r.URL.String()). + WithField("reason_id", "capture_groups_error"). + Warn("Unable to capture the groups for the MatchContext") + } else { + session.MatchContext = authn.MatchContext{ + RegexpCaptureGroups: values, + URL: r.URL, + } + } + + return session +} diff --git a/proxy/request_handler_test.go b/proxy/request_handler_test.go index 0598083351..24acb9cca7 100644 --- a/proxy/request_handler_test.go +++ b/proxy/request_handler_test.go @@ -37,6 +37,7 @@ import ( "github.com/ory/oathkeeper/driver/configuration" "github.com/ory/oathkeeper/internal" + "github.com/ory/oathkeeper/pipeline/authn" "github.com/stretchr/testify/require" @@ -458,3 +459,93 @@ func TestRequestHandler(t *testing.T) { }) } } + +func TestInitializeSession(t *testing.T) { + for k, tc := range []struct { + d string + ruleMatch rule.Match + matchingStrategy configuration.MatchingStrategy + r *http.Request + expectContext authn.MatchContext + }{ + { + d: "Rule without capture", + r: newTestRequest("http://localhost"), + matchingStrategy: configuration.Regexp, + ruleMatch: rule.Match{ + URL: "http://localhost", + }, + expectContext: authn.MatchContext{ + RegexpCaptureGroups: []string{}, + URL: urlx.ParseOrPanic("http://localhost"), + }, + }, + { + d: "Rule with one capture", + r: newTestRequest("http://localhost/user"), + matchingStrategy: configuration.Regexp, + ruleMatch: rule.Match{ + URL: "http://localhost/<.*>", + }, + expectContext: authn.MatchContext{ + RegexpCaptureGroups: []string{"user"}, + URL: urlx.ParseOrPanic("http://localhost/user"), + }, + }, + { + d: "Request with query params", + r: newTestRequest("http://localhost/user?param=test"), + matchingStrategy: configuration.Regexp, + ruleMatch: rule.Match{ + URL: "http://localhost/<.*>", + }, + expectContext: authn.MatchContext{ + RegexpCaptureGroups: []string{"user"}, + URL: urlx.ParseOrPanic("http://localhost/user?param=test"), + }, + }, + { + d: "Rule with 2 captures", + r: newTestRequest("http://localhost/user?param=test"), + matchingStrategy: configuration.Regexp, + ruleMatch: rule.Match{ + URL: "://localhost/<.*>", + }, + expectContext: authn.MatchContext{ + RegexpCaptureGroups: []string{"http", "user"}, + URL: urlx.ParseOrPanic("http://localhost/user?param=test"), + }, + }, + { + d: "Rule with Glob matching strategy", + r: newTestRequest("http://localhost/user?param=test"), + matchingStrategy: configuration.Glob, + ruleMatch: rule.Match{ + URL: "://localhost/<*>", + }, + expectContext: authn.MatchContext{ + RegexpCaptureGroups: []string{}, + URL: urlx.ParseOrPanic("http://localhost/user?param=test"), + }, + }, + } { + t.Run(fmt.Sprintf("case=%d/description=%s", k, tc.d), func(t *testing.T) { + + conf := internal.NewConfigurationWithDefaults() + reg := internal.NewRegistry(conf) + viper.Set(configuration.ViperKeyAccessRuleMatchingStrategy, string(tc.matchingStrategy)) + + rule := rule.Rule{ + Match: &tc.ruleMatch, + Authenticators: []rule.Handler{}, + Authorizer: rule.Handler{}, + Mutators: []rule.Handler{}, + } + + session := reg.ProxyRequestHandler().InitializeAuthnSession(tc.r, &rule) + + assert.NotNil(t, session) + assert.EqualValues(t, tc.expectContext, session.MatchContext) + }) + } +} diff --git a/rule/engine_glob.go b/rule/engine_glob.go index a0c4693a00..36f91be9df 100644 --- a/rule/engine_glob.go +++ b/rule/engine_glob.go @@ -31,6 +31,11 @@ func (ge *globMatchingEngine) ReplaceAllString(_, _, _ string) (string, error) { return "", ErrMethodNotImplemented } +// FindStringSubmatch is noop for now and always returns an empty array +func (ge *globMatchingEngine) FindStringSubmatch(pattern, matchAgainst string) ([]string, error) { + return []string{}, nil +} + func (ge *globMatchingEngine) compile(pattern string) error { if ge.table == nil { ge.table = crc64.MakeTable(polynomial) diff --git a/rule/engine_regexp.go b/rule/engine_regexp.go index 7575cd7451..2cacdf3529 100644 --- a/rule/engine_regexp.go +++ b/rule/engine_regexp.go @@ -1,6 +1,7 @@ package rule import ( + "errors" "hash/crc64" "github.com/dlclark/regexp2" @@ -49,3 +50,22 @@ func (re *regexpMatchingEngine) ReplaceAllString(pattern, input, replacement str } return re.compiled.Replace(input, replacement, -1, -1) } + +// FindStringSubmatch returns all captures in matchAgainst following the pattern +func (re *regexpMatchingEngine) FindStringSubmatch(pattern, matchAgainst string) ([]string, error) { + if err := re.compile(pattern); err != nil { + return nil, err + } + + m, _ := re.compiled.FindStringMatch(matchAgainst) + if m == nil { + return nil, errors.New("not match") + } + + result := []string{} + for _, group := range m.Groups()[1:] { + result = append(result, group.String()) + } + + return result, nil +} diff --git a/rule/engine_regexp_test.go b/rule/engine_regexp_test.go new file mode 100644 index 0000000000..f8d312d824 --- /dev/null +++ b/rule/engine_regexp_test.go @@ -0,0 +1,69 @@ +package rule + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFindStringSubmatch(t *testing.T) { + type args struct { + pattern string + matchAgainst string + } + tests := []struct { + name string + args args + want []string + wantErr bool + }{ + { + name: "bad pattern", + args: args{ + pattern: `urn:foo:<.?>`, + matchAgainst: "urn:foo:user", + }, + want: nil, + wantErr: true, + }, + { + name: "one group", + args: args{ + pattern: `urn:foo:<.*>`, + matchAgainst: "urn:foo:user", + }, + want: []string{"user"}, + wantErr: false, + }, + { + name: "several groups", + args: args{ + pattern: `urn:foo:<.*>:<.*>`, + matchAgainst: "urn:foo:user:one", + }, + want: []string{"user", "one"}, + wantErr: false, + }, + { + name: "classic foo bar", + args: args{ + pattern: `urn:foo:`, + matchAgainst: "urn:foo:bar", + }, + want: []string{"bar"}, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + regexpEngine := new(regexpMatchingEngine) + got, err := regexpEngine.FindStringSubmatch(tt.args.pattern, tt.args.matchAgainst) + if (err != nil) != tt.wantErr { + t.Errorf("FindStringSubmatch() error = %v, wantErr %v", err, tt.wantErr) + return + } + + assert.ElementsMatch(t, got, tt.want, "FindStringSubmatch() got = %v, want %v", got, tt.want) + }) + } +} diff --git a/rule/matching_engine.go b/rule/matching_engine.go index 1acc3344fa..257143c30d 100644 --- a/rule/matching_engine.go +++ b/rule/matching_engine.go @@ -20,5 +20,6 @@ var ( type MatchingEngine interface { IsMatching(pattern, matchAgainst string) (bool, error) ReplaceAllString(pattern, input, replacement string) (string, error) + FindStringSubmatch(pattern, matchAgainst string) ([]string, error) Checksum() uint64 } diff --git a/rule/rule.go b/rule/rule.go index 5398ea475d..40a6f3dbf1 100644 --- a/rule/rule.go +++ b/rule/rule.go @@ -217,3 +217,22 @@ func ensureMatchingEngine(rule *Rule, strategy configuration.MatchingStrategy) e return errors.Wrap(ErrUnknownMatchingStrategy, string(strategy)) } + +// ExtractRegexGroups returns the values matching the rule pattern +func (r *Rule) ExtractRegexGroups(strategy configuration.MatchingStrategy, u *url.URL) ([]string, error) { + if err := ensureMatchingEngine(r, strategy); err != nil { + return nil, err + } + + if r.Match == nil { + return []string{}, nil + } + + matchAgainst := fmt.Sprintf("%s://%s%s", u.Scheme, u.Host, u.Path) + groups, err := r.matchingEngine.FindStringSubmatch(r.Match.URL, matchAgainst) + if err != nil { + return nil, err + } + + return groups, nil +} diff --git a/rule/rule_migrator.go b/rule/rule_migrator.go index 6448ccb42c..48ab0e4f58 100644 --- a/rule/rule_migrator.go +++ b/rule/rule_migrator.go @@ -2,6 +2,7 @@ package rule import ( "fmt" + "regexp" "strings" "github.com/blang/semver" @@ -80,10 +81,42 @@ func migrateRuleJSON(raw []byte) ([]byte, error) { } } - return raw, nil + version, _ = semver.Make("0.32.0-beta.1") + } + + if semver.MustParseRange("<=0.37.0")(version) { + // Applies the following patch: + // + // - in the keto_engine_acp_ory authorizer we have to use go/template instaead of replacement syntax ('$x') + // - "required_action": "my:action:$1" => "required_action": "my:action:{{ printIndex .MatchContext.RegexpCaptureGroups 0}}" + if authorizer := gjson.GetBytes(raw, `authorizer`); authorizer.Exists() { + if authorizer.Get("handler").String() == "keto_engine_acp_ory" { + + aj := gjson.GetBytes(raw, `authorizer.config.required_action`) + rj := gjson.GetBytes(raw, `authorizer.config.required_resource`) + + re := regexp.MustCompile(`\$([0-9]+)`) + var err error + if aj.Exists() { + result := re.ReplaceAllString(aj.Str, "{{ printIndex .MatchContext.RegexpCaptureGroups (sub $1 1 | int)}}") + if raw, err = sjson.SetBytes(raw, `authorizer.config.required_action`, result); err != nil { + return nil, errors.WithStack(err) + } + } + + if rj.Exists() { + result := re.ReplaceAllString(rj.Str, "{{ printIndex .MatchContext.RegexpCaptureGroups (sub $1 1 | int)}}") + if raw, err = sjson.SetBytes(raw, `authorizer.config.required_resource`, result); err != nil { + return nil, errors.WithStack(err) + } + } + } + } + + version, _ = semver.Make("0.37.0") } - if semver.MustParseRange(">0.32.0-beta.1")(version) { + if semver.MustParseRange(">=0.37.0")(version) { return raw, nil } diff --git a/rule/rule_migrator_test.go b/rule/rule_migrator_test.go index b254f3c5d7..4ef2e5d0e0 100644 --- a/rule/rule_migrator_test.go +++ b/rule/rule_migrator_test.go @@ -81,6 +81,38 @@ func TestRuleMigration(t *testing.T) { }`, version: "v0.33.0-beta.1+oryOS.12", }, + { + d: "should migrate to 0.37.0", + in: `{ + "version": "v0.33.0", + "authorizer": + { + "handler": "keto_engine_acp_ory", + "config": { + "required_action": "my:action:$1", + "required_resource": "my:resource:$2:foo:$1", + "flavor": "exact" + } + } + }`, + out: `{ + "id": "", + "version": "v0.37.0", + "description":"","match":null,"authenticators":null,"errors":null, + "authorizer": + { + "handler": "keto_engine_acp_ory", + "config": { + "required_action": "my:action:{{ printIndex .MatchContext.RegexpCaptureGroups (sub 1 1 | int)}}", + "required_resource": "my:resource:{{ printIndex .MatchContext.RegexpCaptureGroups (sub 2 1 | int)}}:foo:{{ printIndex .MatchContext.RegexpCaptureGroups (sub 1 1 | int)}}", + "flavor": "exact" + } + }, + "mutators": null, + "upstream":{"preserve_host":false,"strip_path":"","url":""} + }`, + version: "v0.37.0+oryOS.18", + }, } { t.Run(fmt.Sprintf("case=%d/description=%s", k, tc.d), func(t *testing.T) { var r Rule diff --git a/x/template.go b/x/template.go new file mode 100644 index 0000000000..258d041010 --- /dev/null +++ b/x/template.go @@ -0,0 +1,38 @@ +package x + +import ( + "fmt" + "reflect" + "text/template" + + "github.com/Masterminds/sprig" +) + +// NewTemplate creates a template with additional functions +func NewTemplate(id string) *template.Template { + return template.New(id). + // Implies that zero value will be used if a key is missing. + Option("missingkey=zero"). + Funcs(template.FuncMap{ + "print": func(i interface{}) string { + if i == nil { + return "" + } + return fmt.Sprintf("%v", i) + }, + "printIndex": func(element interface{}, i int) string { + if element == nil { + return "" + } + + list := reflect.ValueOf(element) + + if list.Kind() == reflect.Slice && i < list.Len() { + return fmt.Sprintf("%v", list.Index(i)) + } + + return "" + }, + }). + Funcs(sprig.TxtFuncMap()) +}