From 296e01254b50b645fae67e51aa668d39652b0778 Mon Sep 17 00:00:00 2001 From: hackerman Date: Wed, 22 Aug 2018 09:08:53 +0200 Subject: [PATCH] proxy: Improve compatibility with ORY Hydra 1.0.0-beta.8 (#108) This patch improves compatibility with ORY Hydra 1.0.0-beta.8 and updates vendored dependencies. Closes #101 Signed-off-by: aeneasr --- Gopkg.lock | 5 +- UPGRADE.md | 23 ++ cmd/helper_messages.go | 34 ++- cmd/helper_server.go | 13 +- cmd/serve_proxy.go | 48 ++-- ...authenticator_oauth2_client_credentials.go | 2 - proxy/authenticator_oauth2_introspection.go | 84 ++++--- ...authenticator_oauth2_introspection_mock.go | 42 ---- ...authenticator_oauth2_introspection_test.go | 215 ++++++++++++++---- rsakey/manager_test.go | 8 +- 10 files changed, 315 insertions(+), 159 deletions(-) delete mode 100644 proxy/authenticator_oauth2_introspection_mock.go diff --git a/Gopkg.lock b/Gopkg.lock index 1ade27568a..920e9b0175 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -270,14 +270,13 @@ "sdk/go/hydra", "sdk/go/hydra/swagger" ] - revision = "98044aa796e64e3b9bcc27f08188c46a26f07532" + revision = "251bd5c5b1cf84b012c33cda0fc27db2cfdf48fa" version = "master" [[projects]] branch = "master" name = "github.com/ory/keto" packages = [ - "authentication", "sdk/go/keto", "sdk/go/keto/swagger" ] @@ -541,6 +540,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "833b7210006d4fa0e660d0cfccd5335656fb8c27d1700cf4b8d1490fa8b40e02" + inputs-digest = "1213e51765e6ba29f566f40c6f5ab3f0e03ab2bbaa548e98d398994b37607618" solver-name = "gps-cdcl" solver-version = 1 diff --git a/UPGRADE.md b/UPGRADE.md index 9b875097a6..c79d279fbb 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -18,6 +18,29 @@ before finalizing the upgrade process. ## 1.0.0-rc.1 +### Configuration changes + +To improve compatibility with ORY Hydra v1.0.0-beta.8, which introduces the public and admin endpoint, the following +environment variables have now been made optional: + +- `CREDENTIALS_ISSUER_ID_TOKEN_HYDRA_CLIENT_ID` +- `CREDENTIALS_ISSUER_ID_TOKEN_HYDRA_CLIENT_SECRET` +- `CREDENTIALS_ISSUER_ID_TOKEN_HYDRA_CLIENT_SCOPES` +- `AUTHENTICATOR_OAUTH2_INTROSPECTION_CLIENT_ID` +- `AUTHENTICATOR_OAUTH2_INTROSPECTION_CLIENT_SECRET` +- `AUTHENTICATOR_OAUTH2_INTROSPECTION_TOKEN_URL` +- `AUTHENTICATOR_OAUTH2_INTROSPECTION_SCOPE` + +They are optional because ORY Hydra's administrative endpoints no longer require authorization as they now +run on a privileged port. If you are running ORY Hydra behind a firewall that requires OAuth 2.0 Access tokens, +or you are using another OAuth 2.0 Server that requires an access token, you can still use these settings. + +And the following environment variables have changed: + +- `CREDENTIALS_ISSUER_ID_TOKEN_HYDRA_URL` is now `CREDENTIALS_ISSUER_ID_TOKEN_HYDRA_ADMIN_URL` and +`CREDENTIALS_ISSUER_ID_TOKEN_HYDRA_PUBLIC_URL` if ORY Hydra is protected with OAuth 2.0. +- `AUTHENTICATOR_OAUTH2_INTROSPECTION_INTROSPECT_URL` is now `AUTHENTICATOR_OAUTH2_INTROSPECTION_URL`. + ### CORS is disabled by default A new environment variable `CORS_ENABLED` was introduced. It sets whether CORS is enabled ("true") or not ("false")". diff --git a/cmd/helper_messages.go b/cmd/helper_messages.go index e6d191be04..8a4b43cfe5 100644 --- a/cmd/helper_messages.go +++ b/cmd/helper_messages.go @@ -129,10 +129,9 @@ var credentialsIssuer = `CREDENTIALS ISSUERS ORY-HYDRA ALGORITHM =============== - - CREDENTIALS_ISSUER_ID_TOKEN_HYDRA_URL: The URL where ORY Hydra is located. - -------------------------------------------------------------- - Example: CREDENTIALS_ISSUER_ID_TOKEN_HYDRA_URL=http://hydra-url/ + - CREDENTIALS_ISSUER_ID_TOKEN_HYDRA_ADMIN_URL: The URL where ORY Hydra's Admin API is located. -------------------------------------------------------------- + Example: CREDENTIALS_ISSUER_ID_TOKEN_HYDRA_ADMIN_URL=http://hydra-url/ - CREDENTIALS_ISSUER_ID_TOKEN_HYDRA_REFRESH_INTERVAL: This value sets how often ORY Oathkeeper checks if a new key for signing is available at ORY Hydra. Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". @@ -144,7 +143,34 @@ var credentialsIssuer = `CREDENTIALS ISSUERS store, and retrieve the JSON Web Key from ORY Hydra. -------------------------------------------------------------- Default: CREDENTIALS_ISSUER_ID_TOKEN_HYDRA_JWK_SET_ID=oathkeeper:id-token - --------------------------------------------------------------` + -------------------------------------------------------------- + + If ORY Hydra's Admin API itself is protected with OAuth 2.0, you can provide the access credentials to perform + an OAuth 2.0 Client Credentials flow before accessing ORY Hydra's APIs. + + These settings are usually not required and an optional! If you don't need this feature, leave them undefined. + + + - CREDENTIALS_ISSUER_ID_TOKEN_HYDRA_CLIENT_ID: The ID of the OAuth 2.0 Client. + -------------------------------------------------------------- + Example: CREDENTIALS_ISSUER_ID_TOKEN_HYDRA_CLIENT_ID=my-client + -------------------------------------------------------------- + + - CREDENTIALS_ISSUER_ID_TOKEN_HYDRA_CLIENT_SECRET: The secret of the OAuth 2.0 Client. + -------------------------------------------------------------- + Example: CREDENTIALS_ISSUER_ID_TOKEN_HYDRA_CLIENT_SECRET=my-secret + -------------------------------------------------------------- + + - CREDENTIALS_ISSUER_ID_TOKEN_HYDRA_CLIENT_SCOPES: The OAuth 2.0 Scope the client should request. + -------------------------------------------------------------- + Example: CREDENTIALS_ISSUER_ID_TOKEN_HYDRA_CLIENT_SCOPES=foo,bar + -------------------------------------------------------------- + + - CREDENTIALS_ISSUER_ID_TOKEN_HYDRA_PUBLIC_URL: The public URL where endpoint /oauth2/token is located. + -------------------------------------------------------------- + Example: CREDENTIALS_ISSUER_ID_TOKEN_HYDRA_PUBLIC_URL=http://hydra-url/ + -------------------------------------------------------------- +` func fatalf(msg string, args ...interface{}) { fmt.Printf(msg+"\n", args...) diff --git a/cmd/helper_server.go b/cmd/helper_server.go index 22a13ab196..2bc68b2865 100644 --- a/cmd/helper_server.go +++ b/cmd/helper_server.go @@ -39,7 +39,8 @@ func getHydraSDK() hydra.SDK { sdk, err := hydra.NewSDK(&hydra.Configuration{ ClientID: viper.GetString("CREDENTIALS_ISSUER_ID_TOKEN_HYDRA_CLIENT_ID"), ClientSecret: viper.GetString("CREDENTIALS_ISSUER_ID_TOKEN_HYDRA_CLIENT_SECRET"), - EndpointURL: viper.GetString("CREDENTIALS_ISSUER_ID_TOKEN_HYDRA_URL"), + AdminURL: viper.GetString("CREDENTIALS_ISSUER_ID_TOKEN_HYDRA_ADMIN_URL"), + PublicURL: viper.GetString("CREDENTIALS_ISSUER_ID_TOKEN_HYDRA_PUBLIC_URL"), Scopes: strings.Split(viper.GetString("CREDENTIALS_ISSUER_ID_TOKEN_HYDRA_CLIENT_SCOPES"), ","), }) @@ -181,11 +182,11 @@ func handlerFactories(keyManager rsakey.Manager) ([]proxy.Authenticator, []proxy proxy.NewAuthenticatorNoOp(), proxy.NewAuthenticatorAnonymous(viper.GetString("AUTHENTICATOR_ANONYMOUS_USERNAME")), proxy.NewAuthenticatorOAuth2Introspection( - viper.GetString("AUTHENTICATOR_OAUTH2_INTROSPECTION_CLIENT_ID"), - viper.GetString("AUTHENTICATOR_OAUTH2_INTROSPECTION_CLIENT_SECRET"), - viper.GetString("AUTHENTICATOR_OAUTH2_INTROSPECTION_TOKEN_URL"), - viper.GetString("AUTHENTICATOR_OAUTH2_INTROSPECTION_INTROSPECT_URL"), - strings.Split(viper.GetString("AUTHENTICATOR_OAUTH2_INTROSPECTION_SCOPE"), ","), + viper.GetString("AUTHENTICATOR_OAUTH2_INTROSPECTION_AUTHORIZATION_CLIENT_ID"), + viper.GetString("AUTHENTICATOR_OAUTH2_INTROSPECTION_AUTHORIZATION_CLIENT_SECRET"), + viper.GetString("AUTHENTICATOR_OAUTH2_INTROSPECTION_AUTHORIZATION_TOKEN_URL"), + viper.GetString("AUTHENTICATOR_OAUTH2_INTROSPECTION_URL"), + strings.Split(viper.GetString("AUTHENTICATOR_OAUTH2_INTROSPECTION_AUTHORIZATION_SCOPE"), ","), fosite.WildcardScopeStrategy, ), proxy.NewAuthenticatorOAuth2ClientCredentials( diff --git a/cmd/serve_proxy.go b/cmd/serve_proxy.go index b02e57d574..40f7208b17 100644 --- a/cmd/serve_proxy.go +++ b/cmd/serve_proxy.go @@ -93,33 +93,39 @@ AUTHENTICATORS -------------------------------------------------------------- - OAuth 2.0 Token Introspection Authenticator: - - AUTHENTICATOR_OAUTH2_INTROSPECTION_CLIENT_ID: The OAuth 2.0 Client ID the client that performs the OAuth 2.0 - Token Introspection. The OAuth 2.0 Token Introspection endpoint is typically protected and requires a valid - OAuth 2.0 Client in order to check if a token is valid or not. + - AUTHENTICATOR_OAUTH2_INTROSPECTION_INTROSPECT_URL: The OAuth 2.0 Token Introspection URL. -------------------------------------------------------------- - Example: AUTHENTICATOR_OAUTH2_INTROSPECTION_CLIENT_ID=my-client-id + Example: AUTHENTICATOR_OAUTH2_INTROSPECTION_INTROSPECT_URL=http://my-oauth2-server/oauth2/introspect -------------------------------------------------------------- - - AUTHENTICATOR_OAUTH2_INTROSPECTION_CLIENT_SECRET:T he OAuth 2.0 Client Secret of the client that performs the OAuth 2.0 Token Introspection. - -------------------------------------------------------------- - Example: AUTHENTICATOR_OAUTH2_INTROSPECTION_CLIENT_ID=my-client-secret - -------------------------------------------------------------- + If the OAuth 2.0 Token Introspection Endpoint itself is protected with OAuth 2.0, you can provide the access credentials to perform + an OAuth 2.0 Client Credentials flow before accessing ORY Hydra's APIs. - - AUTHENTICATOR_OAUTH2_INTROSPECTION_TOKEN_URL: The OAuth 2.0 Token URL. - -------------------------------------------------------------- - Example: AUTHENTICATOR_OAUTH2_INTROSPECTION_TOKEN_URL=http://my-oauth2-server/oauth2/token - -------------------------------------------------------------- + These settings are usually not required and an optional! If you don't need this feature, leave them undefined. - - AUTHENTICATOR_OAUTH2_INTROSPECTION_INTROSPECT_URL: The OAuth 2.0 Token Introspection URL. - -------------------------------------------------------------- - Example: AUTHENTICATOR_OAUTH2_INTROSPECTION_INTROSPECT_URL=http://my-oauth2-server/oauth2/introspect - -------------------------------------------------------------- - - AUTHENTICATOR_OAUTH2_INTROSPECTION_SCOPE: If the OAuth 2.0 Token Introspection endpoint requires a certain OAuth 2.0 Scope - in order to be accessed, you can set it using this environment variable. Use commas to define more than one OAuth 2.0 Scope. - -------------------------------------------------------------- - Example: AUTHENTICATOR_OAUTH2_INTROSPECTION_SCOPE=scope-a,scope-b - -------------------------------------------------------------- + - AUTHENTICATOR_OAUTH2_INTROSPECTION_CLIENT_ID: The OAuth 2.0 Client ID the client that performs the OAuth 2.0 + Token Introspection. The OAuth 2.0 Token Introspection endpoint is typically protected and requires a valid + OAuth 2.0 Client in order to check if a token is valid or not. + -------------------------------------------------------------- + Example: AUTHENTICATOR_OAUTH2_INTROSPECTION_CLIENT_ID=my-client-id + -------------------------------------------------------------- + + - AUTHENTICATOR_OAUTH2_INTROSPECTION_CLIENT_SECRET: The OAuth 2.0 Client Secret of the client that performs the OAuth 2.0 Token Introspection. + -------------------------------------------------------------- + Example: AUTHENTICATOR_OAUTH2_INTROSPECTION_CLIENT_ID=my-client-secret + -------------------------------------------------------------- + + - AUTHENTICATOR_OAUTH2_INTROSPECTION_TOKEN_URL: The OAuth 2.0 Token URL. + -------------------------------------------------------------- + Example: AUTHENTICATOR_OAUTH2_INTROSPECTION_TOKEN_URL=http://my-oauth2-server/oauth2/token + -------------------------------------------------------------- + + - AUTHENTICATOR_OAUTH2_INTROSPECTION_SCOPE: If the OAuth 2.0 Token Introspection endpoint requires a certain OAuth 2.0 Scope + in order to be accessed, you can set it using this environment variable. Use commas to define more than one OAuth 2.0 Scope. + -------------------------------------------------------------- + Example: AUTHENTICATOR_OAUTH2_INTROSPECTION_SCOPE=scope-a,scope-b + -------------------------------------------------------------- AUTHORIZERS diff --git a/proxy/authenticator_oauth2_client_credentials.go b/proxy/authenticator_oauth2_client_credentials.go index cdcf492e2b..1e6b37c087 100644 --- a/proxy/authenticator_oauth2_client_credentials.go +++ b/proxy/authenticator_oauth2_client_credentials.go @@ -11,7 +11,6 @@ import ( "github.com/ory/oathkeeper/helper" "github.com/ory/oathkeeper/rule" "github.com/pkg/errors" - "github.com/sirupsen/logrus" "golang.org/x/oauth2/clientcredentials" ) @@ -63,7 +62,6 @@ func (a *AuthenticatorOAuth2ClientCredentials) Authenticate(r *http.Request, con return nil, errors.Wrapf(helper.ErrUnauthorized, err.Error()) } - logrus.New().Printf("Got wow user pw, %s, %s", user, password) c := &clientcredentials.Config{ ClientID: user, ClientSecret: password, diff --git a/proxy/authenticator_oauth2_introspection.go b/proxy/authenticator_oauth2_introspection.go index db9a854224..d00ab3cb73 100644 --- a/proxy/authenticator_oauth2_introspection.go +++ b/proxy/authenticator_oauth2_introspection.go @@ -6,11 +6,17 @@ import ( "fmt" "net/http" + "context" + "net/url" + "strings" + "github.com/ory/fosite" - "github.com/ory/keto/authentication" + "github.com/ory/go-convenience/stringslice" + "github.com/ory/hydra/sdk/go/hydra/swagger" "github.com/ory/oathkeeper/helper" "github.com/ory/oathkeeper/rule" "github.com/pkg/errors" + "golang.org/x/oauth2/clientcredentials" ) type AuthenticatorOAuth2IntrospectionConfiguration struct { @@ -27,18 +33,24 @@ type AuthenticatorOAuth2IntrospectionConfiguration struct { } type AuthenticatorOAuth2Introspection struct { - helper authenticatorOAuth2IntrospectionHelper + client *http.Client introspectionURL string scopeStrategy fosite.ScopeStrategy } -type authenticatorOAuth2IntrospectionHelper interface { - Introspect(token string, scopes []string, strategy fosite.ScopeStrategy) (*authentication.IntrospectionResponse, error) -} - func NewAuthenticatorOAuth2Introspection(clientID, clientSecret, tokenURL, introspectionURL string, scopes []string, strategy fosite.ScopeStrategy) *AuthenticatorOAuth2Introspection { + c := http.DefaultClient + if len(clientID)+len(clientSecret)+len(tokenURL)+len(scopes) > 0 { + c = (&clientcredentials.Config{ + ClientID: clientID, + ClientSecret: clientSecret, + TokenURL: tokenURL, + Scopes: scopes, + }).Client(context.Background()) + } + return &AuthenticatorOAuth2Introspection{ - helper: authentication.NewOAuth2IntrospectionAuthentication(clientID, clientSecret, tokenURL, introspectionURL, scopes, strategy), + client: c, introspectionURL: introspectionURL, } } @@ -65,42 +77,60 @@ func (a *AuthenticatorOAuth2Introspection) Authenticate(r *http.Request, config return nil, errors.WithStack(ErrAuthenticatorNotResponsible) } - ir, err := a.helper.Introspect(token, cf.Scopes, a.scopeStrategy) + body := url.Values{"token": {token}, "scope": {strings.Join(cf.Scopes, " ")}} + resp, err := a.client.Post(a.introspectionURL, "application/x-www-form-urlencoded", strings.NewReader(body.Encode())) if err != nil { - return nil, err + return nil, 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) + } + + var ir swagger.OAuth2TokenIntrospection + if err := json.NewDecoder(resp.Body).Decode(&ir); err != nil { + return nil, errors.WithStack(err) + } + + if len(ir.TokenType) > 0 && ir.TokenType != "access_token" { + return nil, errors.WithStack(helper.ErrForbidden.WithReason(fmt.Sprintf("Introspected token is not an access token but \"%s\"", ir.TokenType))) + } + + if !ir.Active { + return nil, errors.WithStack(helper.ErrForbidden.WithReason("Access token introspection says token is not active")) } for _, audience := range cf.Audience { - if !stringInSlice(audience, ir.Audience) { + if !stringslice.Has(ir.Aud, audience) { return nil, errors.WithStack(helper.ErrForbidden.WithReason(fmt.Sprintf("Token audience is not intended for target audience %s", audience))) } } if len(cf.Issuers) > 0 { - if !stringInSlice(ir.Issuer, cf.Issuers) { + if !stringslice.Has(cf.Issuers, ir.Iss) { return nil, errors.WithStack(helper.ErrForbidden.WithReason(fmt.Sprintf("Token issuer does not match any trusted issuer"))) } } - if len(ir.Extra) == 0 { - ir.Extra = map[string]interface{}{} + if a.scopeStrategy != nil { + for _, scope := range cf.Scopes { + if !a.scopeStrategy(strings.Split(ir.Scope, " "), scope) { + return nil, errors.WithStack(helper.ErrForbidden.WithReason(fmt.Sprintf("Scope %s was not granted", scope))) + } + } } - ir.Extra["username"] = ir.Username - ir.Extra["client_id"] = ir.ClientID - ir.Extra["scope"] = ir.Scope + if len(ir.Ext) == 0 { + ir.Ext = map[string]interface{}{} + } + + ir.Ext["username"] = ir.Username + ir.Ext["client_id"] = ir.ClientId + ir.Ext["scope"] = ir.Scope return &AuthenticationSession{ - Subject: ir.Subject, - Extra: ir.Extra, + Subject: ir.Sub, + Extra: ir.Ext, }, nil } - -func stringInSlice(needle string, haystack []string) bool { - for _, b := range haystack { - if b == needle { - return true - } - } - return false -} diff --git a/proxy/authenticator_oauth2_introspection_mock.go b/proxy/authenticator_oauth2_introspection_mock.go deleted file mode 100644 index 8def61f9e4..0000000000 --- a/proxy/authenticator_oauth2_introspection_mock.go +++ /dev/null @@ -1,42 +0,0 @@ -// Automatically generated by MockGen. DO NOT EDIT! -// Source: ./proxy/authenticator_oauth2_introspection.go - -package proxy - -import ( - gomock "github.com/golang/mock/gomock" - fosite "github.com/ory/fosite" - authentication "github.com/ory/keto/authentication" -) - -// Mock of authenticatorOAuth2IntrospectionHelper interface -type MockauthenticatorOAuth2IntrospectionHelper struct { - ctrl *gomock.Controller - recorder *_MockauthenticatorOAuth2IntrospectionHelperRecorder -} - -// Recorder for MockauthenticatorOAuth2IntrospectionHelper (not exported) -type _MockauthenticatorOAuth2IntrospectionHelperRecorder struct { - mock *MockauthenticatorOAuth2IntrospectionHelper -} - -func NewMockauthenticatorOAuth2IntrospectionHelper(ctrl *gomock.Controller) *MockauthenticatorOAuth2IntrospectionHelper { - mock := &MockauthenticatorOAuth2IntrospectionHelper{ctrl: ctrl} - mock.recorder = &_MockauthenticatorOAuth2IntrospectionHelperRecorder{mock} - return mock -} - -func (_m *MockauthenticatorOAuth2IntrospectionHelper) EXPECT() *_MockauthenticatorOAuth2IntrospectionHelperRecorder { - return _m.recorder -} - -func (_m *MockauthenticatorOAuth2IntrospectionHelper) Introspect(token string, scopes []string, strategy fosite.ScopeStrategy) (*authentication.IntrospectionResponse, error) { - ret := _m.ctrl.Call(_m, "Introspect", token, scopes, strategy) - ret0, _ := ret[0].(*authentication.IntrospectionResponse) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -func (_mr *_MockauthenticatorOAuth2IntrospectionHelperRecorder) Introspect(arg0, arg1, arg2 interface{}) *gomock.Call { - return _mr.mock.ctrl.RecordCall(_mr.mock, "Introspect", arg0, arg1, arg2) -} diff --git a/proxy/authenticator_oauth2_introspection_test.go b/proxy/authenticator_oauth2_introspection_test.go index 913e24be32..ca7de90a2f 100644 --- a/proxy/authenticator_oauth2_introspection_test.go +++ b/proxy/authenticator_oauth2_introspection_test.go @@ -22,14 +22,15 @@ package proxy import ( "encoding/json" - "errors" "fmt" "net/http" "testing" - "github.com/golang/mock/gomock" + "net/http/httptest" + + "github.com/julienschmidt/httprouter" "github.com/ory/fosite" - "github.com/ory/keto/authentication" + "github.com/ory/hydra/sdk/go/hydra/swagger" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -39,91 +40,205 @@ func TestAuthenticatorOAuth2Introspection(t *testing.T) { assert.NotEmpty(t, a.GetID()) for k, tc := range []struct { - setup func(*testing.T, *MockauthenticatorOAuth2IntrospectionHelper) + d string + setup func(*testing.T, *httprouter.Router) r *http.Request config json.RawMessage expectErr bool expectSess *AuthenticationSession }{ { + d: "should fail because no payloads", r: &http.Request{Header: http.Header{}}, expectErr: true, }, { + d: "should fail because wrong response", r: &http.Request{Header: http.Header{"Authorization": {"bearer token"}}}, - config: []byte(`{ "required_scope": ["scope-a"] }`), - setup: func(t *testing.T, m *MockauthenticatorOAuth2IntrospectionHelper) { - m.EXPECT().Introspect(gomock.Eq("token"), gomock.Eq([]string{"scope-a"}), gomock.Eq(a.scopeStrategy)).Return(nil, errors.New("some error")) + config: []byte(`{ "required_scope": ["scope-a", "scope-b"] }`), + setup: func(t *testing.T, m *httprouter.Router) { + m.POST("/oauth2/introspect", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + require.NoError(t, r.ParseForm()) + require.Equal(t, "token", r.Form.Get("token")) + require.Equal(t, "scope-a scope-b", r.Form.Get("scope")) + w.WriteHeader(http.StatusNotFound) + }) }, expectErr: true, }, { + d: "should fail because not active", r: &http.Request{Header: http.Header{"Authorization": {"bearer token"}}}, - config: []byte(`{ "required_scope": ["scope-a"], "trusted_issuers": ["foo", "bar"]}`), - setup: func(t *testing.T, m *MockauthenticatorOAuth2IntrospectionHelper) { - m.EXPECT().Introspect(gomock.Eq("token"), gomock.Eq([]string{"scope-a"}), gomock.Eq(a.scopeStrategy)).Return(&authentication.IntrospectionResponse{ - Subject: "subject", - Audience: []string{"audience"}, - Issuer: "issuer", - Username: "username", - Extra: map[string]interface{}{"extra": "foo"}, - }, nil) + config: []byte(`{}`), + setup: func(t *testing.T, m *httprouter.Router) { + m.POST("/oauth2/introspect", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + require.NoError(t, r.ParseForm()) + require.Equal(t, "token", r.Form.Get("token")) + require.NoError(t, json.NewEncoder(w).Encode(&swagger.OAuth2TokenIntrospection{ + Active: false, + Sub: "subject", + Aud: []string{"audience"}, + Iss: "issuer", + Username: "username", + Ext: map[string]interface{}{"extra": "foo"}, + })) + }) }, expectErr: true, }, { + d: "should pass because active and no issuer / audience expected", + r: &http.Request{Header: http.Header{"Authorization": {"bearer token"}}}, + config: []byte(`{}`), + setup: func(t *testing.T, m *httprouter.Router) { + m.POST("/oauth2/introspect", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + require.NoError(t, r.ParseForm()) + require.Equal(t, "token", r.Form.Get("token")) + require.NoError(t, json.NewEncoder(w).Encode(&swagger.OAuth2TokenIntrospection{ + Active: true, + Sub: "subject", + Aud: []string{"audience"}, + Iss: "issuer", + Username: "username", + Ext: map[string]interface{}{"extra": "foo"}, + })) + }) + }, + expectErr: false, + }, + { + d: "should pass because active and scope matching", + r: &http.Request{Header: http.Header{"Authorization": {"bearer token"}}}, + config: []byte(`{ "required_scope": ["scope-a", "scope-b"] }`), + setup: func(t *testing.T, m *httprouter.Router) { + m.POST("/oauth2/introspect", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + require.NoError(t, r.ParseForm()) + require.Equal(t, "token", r.Form.Get("token")) + require.Equal(t, "scope-a scope-b", r.Form.Get("scope")) + require.NoError(t, json.NewEncoder(w).Encode(&swagger.OAuth2TokenIntrospection{ + Active: true, + Sub: "subject", + Aud: []string{"audience"}, + Iss: "issuer", + Username: "username", + Ext: map[string]interface{}{"extra": "foo"}, + Scope: "scope-a scope-b", + })) + }) + }, + expectErr: false, + }, + { + d: "should fail because active but scope not matching", + r: &http.Request{Header: http.Header{"Authorization": {"bearer token"}}}, + config: []byte(`{ "required_scope": ["scope-a", "scope-b", "scope-c"] }`), + setup: func(t *testing.T, m *httprouter.Router) { + m.POST("/oauth2/introspect", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + require.NoError(t, r.ParseForm()) + require.Equal(t, "token", r.Form.Get("token")) + require.Equal(t, "scope-a scope-b scope-c", r.Form.Get("scope")) + require.NoError(t, json.NewEncoder(w).Encode(&swagger.OAuth2TokenIntrospection{ + Active: true, + Sub: "subject", + Aud: []string{"audience"}, + Iss: "issuer", + Username: "username", + Ext: map[string]interface{}{"extra": "foo"}, + Scope: "scope-a scope-b", + })) + }) + }, + expectErr: false, + }, + { + d: "should fail because active but issuer not matching", r: &http.Request{Header: http.Header{"Authorization": {"bearer token"}}}, - config: []byte(`{ "required_scope": ["scope-a"], "target_audience": ["foo", "bar"]}`), - setup: func(t *testing.T, m *MockauthenticatorOAuth2IntrospectionHelper) { - m.EXPECT().Introspect(gomock.Eq("token"), gomock.Eq([]string{"scope-a"}), gomock.Eq(a.scopeStrategy)).Return(&authentication.IntrospectionResponse{ - Subject: "subject", - Audience: []string{"audience"}, - Issuer: "issuer", - Username: "username", - Extra: map[string]interface{}{"extra": "foo"}, - }, nil) + config: []byte(`{ "required_scope": ["scope-a"], "trusted_issuers": ["foo", "bar"]}`), + setup: func(t *testing.T, m *httprouter.Router) { + m.POST("/oauth2/introspect", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + require.NoError(t, r.ParseForm()) + require.NoError(t, json.NewEncoder(w).Encode(&swagger.OAuth2TokenIntrospection{ + Active: true, + Scope: "scope-a", + Sub: "subject", + Aud: []string{"audience"}, + Iss: "not-foo", + Username: "username", + Ext: map[string]interface{}{"extra": "foo"}, + })) + }) }, expectErr: true, }, { + d: "should pass because active and issuer matching", r: &http.Request{Header: http.Header{"Authorization": {"bearer token"}}}, - config: []byte(`{ "required_scope": ["scope-a"] }`), - setup: func(t *testing.T, m *MockauthenticatorOAuth2IntrospectionHelper) { - m.EXPECT().Introspect(gomock.Eq("token"), gomock.Eq([]string{"scope-a"}), gomock.Eq(a.scopeStrategy)).Return(&authentication.IntrospectionResponse{ - Subject: "subject", - Audience: []string{"audience"}, - Issuer: "issuer", - Username: "username", - Extra: map[string]interface{}{"extra": "foo"}, - }, nil) + config: []byte(`{ "required_scope": ["scope-a"], "trusted_issuers": ["foo", "bar"]}`), + setup: func(t *testing.T, m *httprouter.Router) { + m.POST("/oauth2/introspect", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + require.NoError(t, r.ParseForm()) + require.NoError(t, json.NewEncoder(w).Encode(&swagger.OAuth2TokenIntrospection{ + Active: true, + Scope: "scope-a", + Sub: "subject", + Aud: []string{"audience"}, + Iss: "foo", + Username: "username", + Ext: map[string]interface{}{"extra": "foo"}, + })) + }) }, expectErr: false, }, { + d: "should fail because active but audience not matching", r: &http.Request{Header: http.Header{"Authorization": {"bearer token"}}}, - config: []byte(`{ "required_scope": ["scope-a"], "trusted_issuers": ["issuer", "issuer-bar"], "target_audience": ["audience"] }`), - setup: func(t *testing.T, m *MockauthenticatorOAuth2IntrospectionHelper) { - m.EXPECT().Introspect(gomock.Eq("token"), gomock.Eq([]string{"scope-a"}), gomock.Eq(a.scopeStrategy)).Return(&authentication.IntrospectionResponse{ - Subject: "subject", - Audience: []string{"audience"}, - Issuer: "issuer", - Username: "username", - Extra: map[string]interface{}{"extra": "foo"}, - }, nil) + config: []byte(`{ "required_scope": ["scope-a"], "trusted_issuers": ["foo", "bar"], "target_audience": ["audience"] }`), + setup: func(t *testing.T, m *httprouter.Router) { + m.POST("/oauth2/introspect", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + require.NoError(t, r.ParseForm()) + require.NoError(t, json.NewEncoder(w).Encode(&swagger.OAuth2TokenIntrospection{ + Active: true, + Scope: "scope-a", + Sub: "subject", + Aud: []string{"not-audience"}, + Iss: "foo", + Username: "username", + Ext: map[string]interface{}{"extra": "foo"}, + })) + }) + }, + expectErr: true, + }, + { + d: "should pass", + r: &http.Request{Header: http.Header{"Authorization": {"bearer token"}}}, + config: []byte(`{ "required_scope": ["scope-a"], "trusted_issuers": ["foo", "bar"], "target_audience": ["audience"] }`), + setup: func(t *testing.T, m *httprouter.Router) { + m.POST("/oauth2/introspect", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + require.NoError(t, r.ParseForm()) + require.NoError(t, json.NewEncoder(w).Encode(&swagger.OAuth2TokenIntrospection{ + Active: true, + Scope: "scope-a", + Sub: "subject", + Aud: []string{"audience"}, + Iss: "foo", + Username: "username", + Ext: map[string]interface{}{"extra": "foo"}, + })) + }) }, expectErr: false, }, } { t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - m := NewMockauthenticatorOAuth2IntrospectionHelper(ctrl) + router := httprouter.New() if tc.setup != nil { - tc.setup(t, m) + tc.setup(t, router) } - - a.helper = m + ts := httptest.NewServer(router) + defer ts.Close() + a.introspectionURL = ts.URL + "/oauth2/introspect" sess, err := a.Authenticate(tc.r, tc.config, nil) if tc.expectErr { diff --git a/rsakey/manager_test.go b/rsakey/manager_test.go index 644ebe809d..7aaba1ba32 100644 --- a/rsakey/manager_test.go +++ b/rsakey/manager_test.go @@ -95,7 +95,7 @@ func TestManager(t *testing.T) { func connectToHydra(t *testing.T) *hydra.CodeGenSDK { if url := os.Getenv("TEST_HYDRA_URL"); url != "" { sdk, err := hydra.NewSDK(&hydra.Configuration{ - EndpointURL: url, + AdminURL: url, }) require.NoError(t, err) return sdk @@ -109,8 +109,8 @@ func connectToHydra(t *testing.T) *hydra.CodeGenSDK { resource, err := pool.RunWithOptions(&dockertest.RunOptions{ Repository: "oryd/hydra", - Tag: "v1.0.0-beta.2", - Cmd: []string{"serve", "--dangerous-force-http"}, + Tag: "v1.0.0-beta.8", + Cmd: []string{"serve", "all", "--dangerous-force-http"}, Env: []string{"DATABASE_URL=memory"}, ExposedPorts: []string{"4444/tcp"}, }) @@ -140,7 +140,7 @@ func connectToHydra(t *testing.T) *hydra.CodeGenSDK { resources = append(resources, resource) sdk, err := hydra.NewSDK(&hydra.Configuration{ - EndpointURL: "http://localhost:" + resource.GetPort("4444/tcp") + "/", + AdminURL: "http://localhost:" + resource.GetPort("4444/tcp") + "/", }) require.NoError(t, err) return sdk