diff --git a/authorize_request_handler.go b/authorize_request_handler.go index 486481cd4..579224da6 100644 --- a/authorize_request_handler.go +++ b/authorize_request_handler.go @@ -390,7 +390,7 @@ func (f *Fosite) newAuthorizeRequest(ctx context.Context, r *http.Request, isPAR // A fallback handler to set the default response mode in cases where we can not reach the Authorize Handlers // but still need the e.g. correct error response mode. if request.GetResponseMode() == ResponseModeDefault { - if request.ResponseTypes.ExactOne("code") { + if request.ResponseTypes.ExactOne("code") || request.ResponseTypes.ExactOne("none") { request.SetDefaultResponseMode(ResponseModeQuery) } else { // If the response type is not `code` it is an implicit/hybrid (fragment) response mode. diff --git a/compose/compose.go b/compose/compose.go index 4ded32e5f..09f482661 100644 --- a/compose/compose.go +++ b/compose/compose.go @@ -75,6 +75,7 @@ func ComposeAllEnabled(config *fosite.Config, storage interface{}, key interface OAuth2AuthorizeImplicitFactory, OAuth2ClientCredentialsGrantFactory, OAuth2RefreshTokenGrantFactory, + OAuth2NoneResponseTypeFactory, OAuth2ResourceOwnerPasswordCredentialsFactory, RFC7523AssertionGrantFactory, diff --git a/compose/compose_oauth2.go b/compose/compose_oauth2.go index c71447a5c..8ff93d993 100644 --- a/compose/compose_oauth2.go +++ b/compose/compose_oauth2.go @@ -107,3 +107,10 @@ func OAuth2StatelessJWTIntrospectionFactory(config fosite.Configurator, storage Config: config, } } + +// OAuth2NoneResponseTypeFactory creates an OAuth2 handler which handles the "none" response type. +func OAuth2NoneResponseTypeFactory(config fosite.Configurator, storage interface{}, strategy interface{}) interface{} { + return &oauth2.NoneResponseTypeHandler{ + Config: config, + } +} diff --git a/handler/oauth2/flow_none_auth.go b/handler/oauth2/flow_none_auth.go new file mode 100644 index 000000000..89ea4f0a2 --- /dev/null +++ b/handler/oauth2/flow_none_auth.go @@ -0,0 +1,64 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oauth2 + +import ( + "context" + "net/url" + "strings" + + "github.com/ory/x/errorsx" + + "github.com/ory/fosite" +) + +var _ fosite.AuthorizeEndpointHandler = (*NoneResponseTypeHandler)(nil) + +// NoneResponseTypeHandler is a response handler for when the None response type is requested +// as defined in https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#none +type NoneResponseTypeHandler struct { + Config interface { + fosite.ScopeStrategyProvider + fosite.AudienceStrategyProvider + fosite.RedirectSecureCheckerProvider + fosite.OmitRedirectScopeParamProvider + } +} + +func (c *NoneResponseTypeHandler) secureChecker(ctx context.Context) func(context.Context, *url.URL) bool { + if c.Config.GetRedirectSecureChecker(ctx) == nil { + return fosite.IsRedirectURISecure + } + return c.Config.GetRedirectSecureChecker(ctx) +} + +func (c *NoneResponseTypeHandler) HandleAuthorizeEndpointRequest(ctx context.Context, ar fosite.AuthorizeRequester, resp fosite.AuthorizeResponder) error { + if !ar.GetResponseTypes().ExactOne("none") { + return nil + } + + ar.SetDefaultResponseMode(fosite.ResponseModeQuery) + + if !c.secureChecker(ctx)(ctx, ar.GetRedirectURI()) { + return errorsx.WithStack(fosite.ErrInvalidRequest.WithHint("Redirect URL is using an insecure protocol, http is only allowed for hosts with suffix 'localhost', for example: http://myapp.localhost/.")) + } + + client := ar.GetClient() + for _, scope := range ar.GetRequestedScopes() { + if !c.Config.GetScopeStrategy(ctx)(client.GetScopes(), scope) { + return errorsx.WithStack(fosite.ErrInvalidScope.WithHintf("The OAuth 2.0 Client is not allowed to request scope '%s'.", scope)) + } + } + + if err := c.Config.GetAudienceStrategy(ctx)(client.GetAudience(), ar.GetRequestedAudience()); err != nil { + return err + } + + resp.AddParameter("state", ar.GetState()) + if !c.Config.GetOmitRedirectScopeParam(ctx) { + resp.AddParameter("scope", strings.Join(ar.GetGrantedScopes(), " ")) + } + ar.SetResponseTypeHandled("none") + return nil +} diff --git a/handler/oauth2/flow_none_auth_test.go b/handler/oauth2/flow_none_auth_test.go new file mode 100644 index 000000000..5b01aa36d --- /dev/null +++ b/handler/oauth2/flow_none_auth_test.go @@ -0,0 +1,182 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oauth2 + +import ( + "context" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/fosite" +) + +func TestNone_HandleAuthorizeEndpointRequest(t *testing.T) { + handler := NoneResponseTypeHandler{ + Config: &fosite.Config{ + ScopeStrategy: fosite.HierarchicScopeStrategy, + AudienceMatchingStrategy: fosite.DefaultAudienceMatchingStrategy, + }, + } + for _, c := range []struct { + handler NoneResponseTypeHandler + areq *fosite.AuthorizeRequest + description string + expectErr error + expect func(t *testing.T, areq *fosite.AuthorizeRequest, aresp *fosite.AuthorizeResponse) + }{ + { + handler: handler, + areq: &fosite.AuthorizeRequest{ + ResponseTypes: fosite.Arguments{""}, + Request: *fosite.NewRequest(), + }, + description: "should pass because not responsible for handling an empty response type", + }, + { + handler: handler, + areq: &fosite.AuthorizeRequest{ + ResponseTypes: fosite.Arguments{"foo"}, + Request: *fosite.NewRequest(), + }, + description: "should pass because not responsible for handling an invalid response type", + }, + { + handler: handler, + areq: &fosite.AuthorizeRequest{ + ResponseTypes: fosite.Arguments{"none"}, + Request: fosite.Request{ + Client: &fosite.DefaultClient{ + ResponseTypes: fosite.Arguments{"code", "none"}, + RedirectURIs: []string{"http://asdf.com/cb"}, + }, + }, + RedirectURI: parseUrl("http://asdf.com/cb"), + }, + description: "should fail because redirect uri is not https", + expectErr: fosite.ErrInvalidRequest, + }, + { + handler: handler, + areq: &fosite.AuthorizeRequest{ + ResponseTypes: fosite.Arguments{"none"}, + Request: fosite.Request{ + Client: &fosite.DefaultClient{ + ResponseTypes: fosite.Arguments{"code", "none"}, + RedirectURIs: []string{"https://asdf.com/cb"}, + Audience: []string{"https://www.ory.sh/api"}, + }, + RequestedAudience: []string{"https://www.ory.sh/not-api"}, + }, + RedirectURI: parseUrl("https://asdf.com/cb"), + }, + description: "should fail because audience doesn't match", + expectErr: fosite.ErrInvalidRequest, + }, + { + handler: handler, + areq: &fosite.AuthorizeRequest{ + ResponseTypes: fosite.Arguments{"none"}, + Request: fosite.Request{ + Client: &fosite.DefaultClient{ + ResponseTypes: fosite.Arguments{"code", "none"}, + RedirectURIs: []string{"https://asdf.de/cb"}, + Audience: []string{"https://www.ory.sh/api"}, + }, + RequestedAudience: []string{"https://www.ory.sh/api"}, + GrantedScope: fosite.Arguments{"a", "b"}, + Session: &fosite.DefaultSession{ + ExpiresAt: map[fosite.TokenType]time.Time{fosite.AccessToken: time.Now().UTC().Add(time.Hour)}, + }, + RequestedAt: time.Now().UTC(), + }, + State: "superstate", + RedirectURI: parseUrl("https://asdf.de/cb"), + }, + description: "should pass", + expect: func(t *testing.T, areq *fosite.AuthorizeRequest, aresp *fosite.AuthorizeResponse) { + assert.Equal(t, strings.Join(areq.GrantedScope, " "), aresp.GetParameters().Get("scope")) + assert.Equal(t, areq.State, aresp.GetParameters().Get("state")) + assert.Equal(t, fosite.ResponseModeQuery, areq.GetResponseMode()) + }, + }, + { + handler: handler, + areq: &fosite.AuthorizeRequest{ + ResponseTypes: fosite.Arguments{"none"}, + Request: fosite.Request{ + Client: &fosite.DefaultClient{ + ResponseTypes: fosite.Arguments{"none"}, + RedirectURIs: []string{"https://asdf.de/cb"}, + Audience: []string{"https://www.ory.sh/api"}, + }, + RequestedAudience: []string{"https://www.ory.sh/api"}, + GrantedScope: fosite.Arguments{"a", "b"}, + Session: &fosite.DefaultSession{ + ExpiresAt: map[fosite.TokenType]time.Time{fosite.AccessToken: time.Now().UTC().Add(time.Hour)}, + }, + RequestedAt: time.Now().UTC(), + }, + State: "superstate", + RedirectURI: parseUrl("https://asdf.de/cb"), + }, + description: "should pass with no response types other than none", + expect: func(t *testing.T, areq *fosite.AuthorizeRequest, aresp *fosite.AuthorizeResponse) { + assert.Equal(t, strings.Join(areq.GrantedScope, " "), aresp.GetParameters().Get("scope")) + assert.Equal(t, areq.State, aresp.GetParameters().Get("state")) + assert.Equal(t, fosite.ResponseModeQuery, areq.GetResponseMode()) + }, + }, + { + handler: NoneResponseTypeHandler{ + Config: &fosite.Config{ + ScopeStrategy: fosite.HierarchicScopeStrategy, + AudienceMatchingStrategy: fosite.DefaultAudienceMatchingStrategy, + OmitRedirectScopeParam: true, + }, + }, + areq: &fosite.AuthorizeRequest{ + ResponseTypes: fosite.Arguments{"none"}, + Request: fosite.Request{ + Client: &fosite.DefaultClient{ + ResponseTypes: fosite.Arguments{"code", "none"}, + RedirectURIs: []string{"https://asdf.de/cb"}, + Audience: []string{"https://www.ory.sh/api"}, + }, + RequestedAudience: []string{"https://www.ory.sh/api"}, + GrantedScope: fosite.Arguments{"a", "b"}, + Session: &fosite.DefaultSession{ + ExpiresAt: map[fosite.TokenType]time.Time{fosite.AccessToken: time.Now().UTC().Add(time.Hour)}, + }, + RequestedAt: time.Now().UTC(), + }, + State: "superstate", + RedirectURI: parseUrl("https://asdf.de/cb"), + }, + description: "should pass but no scope in redirect uri", + expect: func(t *testing.T, areq *fosite.AuthorizeRequest, aresp *fosite.AuthorizeResponse) { + assert.Empty(t, aresp.GetParameters().Get("scope")) + assert.Equal(t, areq.State, aresp.GetParameters().Get("state")) + assert.Equal(t, fosite.ResponseModeQuery, areq.GetResponseMode()) + }, + }, + } { + t.Run("case="+c.description, func(t *testing.T) { + aresp := fosite.NewAuthorizeResponse() + err := c.handler.HandleAuthorizeEndpointRequest(context.Background(), c.areq, aresp) + if c.expectErr != nil { + require.EqualError(t, err, c.expectErr.Error()) + } else { + require.NoError(t, err) + } + + if c.expect != nil { + c.expect(t, c.areq, aresp) + } + }) + } +}