Skip to content

Commit

Permalink
feat: add support for none response_type
Browse files Browse the repository at this point in the history
  • Loading branch information
dtam-cybozu committed Nov 10, 2023
1 parent 44fd2cc commit f59d68c
Show file tree
Hide file tree
Showing 5 changed files with 255 additions and 1 deletion.
2 changes: 1 addition & 1 deletion authorize_request_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions compose/compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ func ComposeAllEnabled(config *fosite.Config, storage interface{}, key interface
OAuth2AuthorizeImplicitFactory,
OAuth2ClientCredentialsGrantFactory,
OAuth2RefreshTokenGrantFactory,
OAuth2NoneResponseTypeFactory,
OAuth2ResourceOwnerPasswordCredentialsFactory,
RFC7523AssertionGrantFactory,

Expand Down
7 changes: 7 additions & 0 deletions compose/compose_oauth2.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}
64 changes: 64 additions & 0 deletions handler/oauth2/flow_none_auth.go
Original file line number Diff line number Diff line change
@@ -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
}
182 changes: 182 additions & 0 deletions handler/oauth2/flow_none_auth_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}

0 comments on commit f59d68c

Please sign in to comment.