diff --git a/authorize_request_handler_test.go b/authorize_request_handler_test.go index afdc341d6..60b248a35 100644 --- a/authorize_request_handler_test.go +++ b/authorize_request_handler_test.go @@ -152,9 +152,9 @@ func TestNewAuthorizeRequest(t *testing.T) { }, expectedError: ErrInvalidScope, }, - /* fails because scope not given */ + /* fails because audience not given */ { - desc: "should fail because client does not have scope baz", + desc: "should fail because client does not have audience https://www.ory.sh/api", conf: &Fosite{Store: store, Config: &Config{ScopeStrategy: ExactScopeStrategy, AudienceMatchingStrategy: DefaultAudienceMatchingStrategy}}, query: url.Values{ "redirect_uri": {"https://foo.bar/cb"}, diff --git a/compose/compose.go b/compose/compose.go index 4ded32e5f..9f5b95f54 100644 --- a/compose/compose.go +++ b/compose/compose.go @@ -53,6 +53,12 @@ func Compose(config *fosite.Config, storage interface{}, strategy interface{}, f if ph, ok := res.(fosite.PushedAuthorizeEndpointHandler); ok { config.PushedAuthorizeEndpointHandlers.Append(ph) } + if dh, ok := res.(fosite.DeviceEndpointHandler); ok { + config.DeviceEndpointHandlers.Append(dh) + } + if duh, ok := res.(fosite.DeviceUserEndpointHandler); ok { + config.DeviceUserEndpointHandlers.Append(duh) + } } return f @@ -68,20 +74,25 @@ func ComposeAllEnabled(config *fosite.Config, storage interface{}, key interface storage, &CommonStrategy{ CoreStrategy: NewOAuth2HMACStrategy(config), + RFC8628CodeStrategy: NewDeviceStrategy(config), OpenIDConnectTokenStrategy: NewOpenIDConnectStrategy(keyGetter, config), Signer: &jwt.DefaultSigner{GetPrivateKey: keyGetter}, }, - OAuth2AuthorizeExplicitFactory, + OAuth2AuthorizeExplicitAuthFactory, + OAuth2AuthorizeExplicitTokenFactory, OAuth2AuthorizeImplicitFactory, OAuth2ClientCredentialsGrantFactory, OAuth2RefreshTokenGrantFactory, OAuth2ResourceOwnerPasswordCredentialsFactory, RFC7523AssertionGrantFactory, + RFC8628DeviceFactory, + RFC8628DeviceAuthorizationTokenFactory, OpenIDConnectExplicitFactory, OpenIDConnectImplicitFactory, OpenIDConnectHybridFactory, OpenIDConnectRefreshFactory, + OpenIDConnectDeviceFactory, OAuth2TokenIntrospectionFactory, OAuth2TokenRevocationFactory, diff --git a/compose/compose_oauth2.go b/compose/compose_oauth2.go index c71447a5c..813eef1aa 100644 --- a/compose/compose_oauth2.go +++ b/compose/compose_oauth2.go @@ -11,14 +11,26 @@ import ( // OAuth2AuthorizeExplicitFactory creates an OAuth2 authorize code grant ("authorize explicit flow") handler and registers // an access token, refresh token and authorize code validator. -func OAuth2AuthorizeExplicitFactory(config fosite.Configurator, storage interface{}, strategy interface{}) interface{} { - return &oauth2.AuthorizeExplicitGrantHandler{ - AccessTokenStrategy: strategy.(oauth2.AccessTokenStrategy), - RefreshTokenStrategy: strategy.(oauth2.RefreshTokenStrategy), - AuthorizeCodeStrategy: strategy.(oauth2.AuthorizeCodeStrategy), - CoreStorage: storage.(oauth2.CoreStorage), - TokenRevocationStorage: storage.(oauth2.TokenRevocationStorage), - Config: config, +func OAuth2AuthorizeExplicitAuthFactory(config fosite.Configurator, storage interface{}, strategy interface{}) interface{} { + return &oauth2.AuthorizeExplicitGrantAuthHandler{ + AuthorizeCodeStrategy: strategy.(oauth2.AuthorizeCodeStrategy), + AuthorizeCodeStorage: storage.(oauth2.AuthorizeCodeStorage), + Config: config, + } +} +func OAuth2AuthorizeExplicitTokenFactory(config fosite.Configurator, storage interface{}, strategy interface{}) interface{} { + return &oauth2.AuthorizeExplicitTokenEndpointHandler{ + GenericCodeTokenEndpointHandler: oauth2.GenericCodeTokenEndpointHandler{ + CodeTokenEndpointHandler: &oauth2.AuthorizeExplicitGrantTokenHandler{ + AuthorizeCodeStrategy: strategy.(oauth2.AuthorizeCodeStrategy), + AuthorizeCodeStorage: storage.(oauth2.AuthorizeCodeStorage), + }, + AccessTokenStrategy: strategy.(oauth2.AccessTokenStrategy), + RefreshTokenStrategy: strategy.(oauth2.RefreshTokenStrategy), + CoreStorage: storage.(oauth2.CoreStorage), + TokenRevocationStorage: storage.(oauth2.TokenRevocationStorage), + Config: config, + }, } } diff --git a/compose/compose_openid.go b/compose/compose_openid.go index 122ddf154..b9851e371 100644 --- a/compose/compose_openid.go +++ b/compose/compose_openid.go @@ -59,11 +59,9 @@ func OpenIDConnectImplicitFactory(config fosite.Configurator, storage interface{ // **Important note:** You must add this handler *after* you have added an OAuth2 authorize code handler! func OpenIDConnectHybridFactory(config fosite.Configurator, storage interface{}, strategy interface{}) interface{} { return &openid.OpenIDConnectHybridHandler{ - AuthorizeExplicitGrantHandler: &oauth2.AuthorizeExplicitGrantHandler{ - AccessTokenStrategy: strategy.(oauth2.AccessTokenStrategy), - RefreshTokenStrategy: strategy.(oauth2.RefreshTokenStrategy), + AuthorizeExplicitGrantAuthHandler: &oauth2.AuthorizeExplicitGrantAuthHandler{ AuthorizeCodeStrategy: strategy.(oauth2.AuthorizeCodeStrategy), - CoreStorage: storage.(oauth2.CoreStorage), + AuthorizeCodeStorage: storage.(oauth2.AuthorizeCodeStorage), Config: config, }, Config: config, @@ -79,3 +77,17 @@ func OpenIDConnectHybridFactory(config fosite.Configurator, storage interface{}, OpenIDConnectRequestValidator: openid.NewOpenIDConnectRequestValidator(strategy.(jwt.Signer), config), } } + +// OpenIDConnectDeviceFactory creates an OpenID Connect device ("device code flow") grant handler. +// +// **Important note:** You must add this handler *after* you have added an OAuth2 authorize code handler! +func OpenIDConnectDeviceFactory(config fosite.Configurator, storage interface{}, strategy interface{}) interface{} { + return &openid.OpenIDConnectDeviceHandler{ + OpenIDConnectRequestStorage: storage.(openid.OpenIDConnectRequestStorage), + IDTokenHandleHelper: &openid.IDTokenHandleHelper{ + IDTokenStrategy: strategy.(openid.OpenIDConnectTokenStrategy), + }, + OpenIDConnectRequestValidator: openid.NewOpenIDConnectRequestValidator(strategy.(jwt.Signer), config), + Config: config, + } +} diff --git a/compose/compose_rfc8628.go b/compose/compose_rfc8628.go new file mode 100644 index 000000000..bbdfcf8df --- /dev/null +++ b/compose/compose_rfc8628.go @@ -0,0 +1,39 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package compose + +import ( + "github.com/ory/fosite" + "github.com/ory/fosite/handler/oauth2" + "github.com/ory/fosite/handler/rfc8628" +) + +// RFC8628DeviceFactory creates an OAuth2 device code grant ("Device Authorization Grant") handler and registers +// an user code, device code, access token and a refresh token validator. +func RFC8628DeviceFactory(config fosite.Configurator, storage interface{}, strategy interface{}) interface{} { + return &rfc8628.DeviceAuthHandler{ + Strategy: strategy.(rfc8628.RFC8628CodeStrategy), + Storage: storage.(rfc8628.RFC8628CodeStorage), + Config: config, + } +} + +// RFC8628DeviceAuthorizationTokenFactory creates an OAuth2 device authorization grant ("Device Authorization Grant") handler and registers +// an access token, refresh token and authorize code validator. +func RFC8628DeviceAuthorizationTokenFactory(config fosite.Configurator, storage interface{}, strategy interface{}) interface{} { + return &rfc8628.DeviceCodeTokenEndpointHandler{ + GenericCodeTokenEndpointHandler: oauth2.GenericCodeTokenEndpointHandler{ + CodeTokenEndpointHandler: &rfc8628.DeviceHandler{ + DeviceRateLimitStrategy: strategy.(rfc8628.DeviceRateLimitStrategy), + DeviceStrategy: strategy.(rfc8628.DeviceCodeStrategy), + DeviceStorage: storage.(rfc8628.DeviceCodeStorage), + }, + AccessTokenStrategy: strategy.(oauth2.AccessTokenStrategy), + RefreshTokenStrategy: strategy.(oauth2.RefreshTokenStrategy), + CoreStorage: storage.(oauth2.CoreStorage), + TokenRevocationStorage: storage.(oauth2.TokenRevocationStorage), + Config: config, + }, + } +} diff --git a/compose/compose_strategy.go b/compose/compose_strategy.go index cddcea5ef..748d8eab4 100644 --- a/compose/compose_strategy.go +++ b/compose/compose_strategy.go @@ -9,12 +9,15 @@ import ( "github.com/ory/fosite" "github.com/ory/fosite/handler/oauth2" "github.com/ory/fosite/handler/openid" + "github.com/ory/fosite/handler/rfc8628" "github.com/ory/fosite/token/hmac" "github.com/ory/fosite/token/jwt" + "github.com/patrickmn/go-cache" ) type CommonStrategy struct { oauth2.CoreStrategy + rfc8628.RFC8628CodeStrategy openid.OpenIDConnectTokenStrategy jwt.Signer } @@ -27,6 +30,7 @@ type HMACSHAStrategyConfigurator interface { fosite.GlobalSecretProvider fosite.RotatedGlobalSecretsProvider fosite.HMACHashingProvider + fosite.DeviceAndUserCodeLifespanProvider } func NewOAuth2HMACStrategy(config HMACSHAStrategyConfigurator) *oauth2.HMACSHAStrategy { @@ -50,3 +54,14 @@ func NewOpenIDConnectStrategy(keyGetter func(context.Context) (interface{}, erro Config: config, } } + +func NewDeviceStrategy(config fosite.Configurator) *rfc8628.DefaultDeviceStrategy { + return &rfc8628.DefaultDeviceStrategy{ + Enigma: &hmac.HMACStrategy{Config: config}, + RateLimiterCache: cache.New( + config.GetDeviceAndUserCodeLifespan(context.TODO()), + config.GetDeviceAndUserCodeLifespan(context.TODO())*2, + ), + Config: config, + } +} diff --git a/config.go b/config.go index 165e7b4b0..79f08f9e5 100644 --- a/config.go +++ b/config.go @@ -22,6 +22,19 @@ type AuthorizeCodeLifespanProvider interface { GetAuthorizeCodeLifespan(ctx context.Context) time.Duration } +type DeviceAndUserCodeLifespanProvider interface { + GetDeviceAndUserCodeLifespan(ctx context.Context) time.Duration +} + +type DeviceUserProvider interface { + GetDeviceDone(ctx context.Context) string +} + +type DeviceProvider interface { + GetDeviceVerificationURL(ctx context.Context) string + GetDeviceAuthTokenPollingInterval(ctx context.Context) time.Duration +} + // RefreshTokenLifespanProvider returns the provider for configuring the refresh token lifespan. type RefreshTokenLifespanProvider interface { // GetRefreshTokenLifespan returns the refresh token lifespan. @@ -281,6 +294,18 @@ type PushedAuthorizeRequestHandlersProvider interface { GetPushedAuthorizeEndpointHandlers(ctx context.Context) PushedAuthorizeEndpointHandlers } +// DeviceEndpointHandlersProvider returns the provider for setting up the Device handlers. +type DeviceEndpointHandlersProvider interface { + // GetDeviceEndpointHandlers returns the handlers. + GetDeviceEndpointHandlers(ctx context.Context) DeviceEndpointHandlers +} + +// DeviceUserEndpointHandlersProvider returns the provider for setting up the Device Authorize handlers. +type DeviceUserEndpointHandlersProvider interface { + // GetDeviceUserEndpointHandlers returns the handlers. + GetDeviceUserEndpointHandlers(ctx context.Context) DeviceUserEndpointHandlers +} + // UseLegacyErrorFormatProvider returns the provider for configuring whether to use the legacy error format. // // DEPRECATED: Do not use this flag anymore. diff --git a/config_default.go b/config_default.go index a2ae5ccec..987f5eb42 100644 --- a/config_default.go +++ b/config_default.go @@ -62,6 +62,7 @@ var ( _ RevocationHandlersProvider = (*Config)(nil) _ PushedAuthorizeRequestHandlersProvider = (*Config)(nil) _ PushedAuthorizeRequestConfigProvider = (*Config)(nil) + _ DeviceEndpointHandlersProvider = (*Config)(nil) ) type Config struct { @@ -78,6 +79,18 @@ type Config struct { // AuthorizeCodeLifespan sets how long an authorize code is going to be valid. Defaults to fifteen minutes. AuthorizeCodeLifespan time.Duration + // Sets how long a device user/device code pair is valid for + DeviceAndUserCodeLifespan time.Duration + + // DeviceAuthTokenPollingInterval sets the interval that clients should check for device code grants + DeviceAuthTokenPollingInterval time.Duration + + // DeviceVerificationURL is the URL of the device verification endpoint, this is is included with the device code request responses + DeviceVerificationURL string + + // DeviceDoneURL is the URL of the user is redirected to once the verification is completed + DeviceDoneURL string + // IDTokenLifespan sets the default id token lifetime. Defaults to one hour. IDTokenLifespan time.Duration @@ -197,6 +210,12 @@ type Config struct { // PushedAuthorizeEndpointHandlers is a list of handlers that are called before the PAR endpoint is served. PushedAuthorizeEndpointHandlers PushedAuthorizeEndpointHandlers + // DeviceEndpointHandlers is a list of handlers that are called before the device endpoint is served. + DeviceEndpointHandlers DeviceEndpointHandlers + + // DeviceUserEndpointHandlers is a list of handlers that are called before the device authorize endpoint is served. + DeviceUserEndpointHandlers DeviceUserEndpointHandlers + // GlobalSecret is the global secret used to sign and verify signatures. GlobalSecret []byte @@ -245,6 +264,14 @@ func (c *Config) GetTokenIntrospectionHandlers(ctx context.Context) TokenIntrosp return c.TokenIntrospectionHandlers } +func (c *Config) GetDeviceEndpointHandlers(ctx context.Context) DeviceEndpointHandlers { + return c.DeviceEndpointHandlers +} + +func (c *Config) GetDeviceUserEndpointHandlers(ctx context.Context) DeviceUserEndpointHandlers { + return c.DeviceUserEndpointHandlers +} + func (c *Config) GetRevocationHandlers(ctx context.Context) RevocationHandlers { return c.RevocationHandlers } @@ -363,6 +390,13 @@ func (c *Config) GetAuthorizeCodeLifespan(_ context.Context) time.Duration { return c.AuthorizeCodeLifespan } +func (c *Config) GetDeviceAndUserCodeLifespan(_ context.Context) time.Duration { + if c.DeviceAndUserCodeLifespan == 0 { + return time.Minute * 10 + } + return c.DeviceAndUserCodeLifespan +} + // GetIDTokenLifespan returns how long an id token should be valid. Defaults to one hour. func (c *Config) GetIDTokenLifespan(_ context.Context) time.Duration { if c.IDTokenLifespan == 0 { @@ -499,3 +533,18 @@ func (c *Config) GetPushedAuthorizeContextLifespan(ctx context.Context) time.Dur func (c *Config) EnforcePushedAuthorize(ctx context.Context) bool { return c.IsPushedAuthorizeEnforced } + +func (c *Config) GetDeviceDone(ctx context.Context) string { + return c.DeviceDoneURL +} + +func (c *Config) GetDeviceVerificationURL(ctx context.Context) string { + return c.DeviceVerificationURL +} + +func (c *Config) GetDeviceAuthTokenPollingInterval(ctx context.Context) time.Duration { + if c.DeviceAuthTokenPollingInterval == 0 { + return time.Second * 5 + } + return c.DeviceAuthTokenPollingInterval +} diff --git a/device_request.go b/device_request.go new file mode 100644 index 000000000..3c2df0636 --- /dev/null +++ b/device_request.go @@ -0,0 +1,15 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package fosite + +// DeviceRequest is an implementation of DeviceRequester +type DeviceRequest struct { + Request +} + +func NewDeviceRequest() *DeviceRequest { + return &DeviceRequest{ + Request: *NewRequest(), + } +} diff --git a/device_request_handler.go b/device_request_handler.go new file mode 100644 index 000000000..2167ad6f0 --- /dev/null +++ b/device_request_handler.go @@ -0,0 +1,71 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright © 2015-2021 Aeneas Rekkas + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @author Aeneas Rekkas + * @copyright 2015-2021 Aeneas Rekkas + * @license Apache-2.0 + * + */ + +package fosite + +import ( + "context" + "net/http" + "strings" + + "github.com/ory/fosite/i18n" + "github.com/ory/x/errorsx" +) + +func (f *Fosite) NewDeviceRequest(ctx context.Context, req *http.Request) (DeviceRequester, error) { + request := NewDeviceRequest() + request.Lang = i18n.GetLangFromRequest(f.Config.GetMessageCatalog(ctx), req) + + if err := req.ParseForm(); err != nil { + return nil, errorsx.WithStack(ErrInvalidRequest.WithHint("Unable to parse HTTP body, make sure to send a properly formatted form request body.").WithWrap(err).WithDebug(err.Error())) + } + request.Form = req.PostForm + + client, err := f.Store.GetClient(ctx, request.GetRequestForm().Get("client_id")) + if err != nil { + return nil, errorsx.WithStack(ErrInvalidClient.WithHint("The requested OAuth 2.0 Client does not exist.").WithWrap(err).WithDebug(err.Error())) + } + request.Client = client + + if !client.GetGrantTypes().Has(string(GrantTypeDeviceCode)) { + return nil, errorsx.WithStack(ErrInvalidGrant.WithHint("The requested OAuth 2.0 Client does not have the 'urn:ietf:params:oauth:grant-type:device_code' grant.")) + } + + if err := f.validateDeviceScope(ctx, req, request); err != nil { + return nil, err + } + + return request, nil +} + +func (f *Fosite) validateDeviceScope(ctx context.Context, req *http.Request, request *DeviceRequest) error { + scope := RemoveEmpty(strings.Split(request.Form.Get("scope"), " ")) + for _, permission := range scope { + if !f.Config.GetScopeStrategy(ctx)(request.Client.GetScopes(), permission) { + return errorsx.WithStack(ErrInvalidScope.WithHintf("The OAuth 2.0 Client is not allowed to request scope '%s'.", permission)) + } + } + request.SetRequestedScopes(scope) + return nil +} diff --git a/device_request_handler_test.go b/device_request_handler_test.go new file mode 100644 index 000000000..6d6f69fe4 --- /dev/null +++ b/device_request_handler_test.go @@ -0,0 +1,138 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package fosite_test + +import ( + "context" + "fmt" + "net/http" + "net/url" + "testing" + + "github.com/golang/mock/gomock" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + . "github.com/ory/fosite" + . "github.com/ory/fosite/internal" +) + +func TestNewDeviceRequest(t *testing.T) { + var store *MockStorage + for k, c := range []struct { + desc string + conf *Fosite + r *http.Request + query url.Values + expectedError error + mock func() + expect *DeviceUserRequest + }{ + /* empty request */ + { + desc: "empty request fails", + conf: &Fosite{Store: store, Config: &Config{ScopeStrategy: ExactScopeStrategy, AudienceMatchingStrategy: DefaultAudienceMatchingStrategy}}, + expectedError: ErrInvalidClient, + mock: func() { + store.EXPECT().GetClient(gomock.Any(), gomock.Any()).Return(nil, errors.New("foo")) + }, + }, + /* invalid client */ + { + desc: "invalid client fails", + conf: &Fosite{Store: store, Config: &Config{ScopeStrategy: ExactScopeStrategy, AudienceMatchingStrategy: DefaultAudienceMatchingStrategy}}, + r: &http.Request{ + PostForm: url.Values{ + "client_id": {"1234"}, + "scope": {"foo bar"}, + }, + }, + expectedError: ErrInvalidClient, + mock: func() { + store.EXPECT().GetClient(gomock.Any(), gomock.Any()).Return(nil, errors.New("foo")) + }, + }, + /* fails because scope not given */ + { + desc: "should fail because client does not have scope baz", + conf: &Fosite{Store: store, Config: &Config{ScopeStrategy: ExactScopeStrategy, AudienceMatchingStrategy: DefaultAudienceMatchingStrategy}}, + r: &http.Request{ + PostForm: url.Values{ + "client_id": {"1234"}, + "scope": {"foo bar baz"}, + }, + }, + mock: func() { + store.EXPECT().GetClient(gomock.Any(), "1234").Return(&DefaultClient{ + GrantTypes: []string{"urn:ietf:params:oauth:grant-type:device_code"}, + Scopes: []string{"foo", "bar"}, + }, nil) + }, + expectedError: ErrInvalidScope, + }, + /* success case */ + { + desc: "should pass", + conf: &Fosite{Store: store, Config: &Config{ScopeStrategy: ExactScopeStrategy, AudienceMatchingStrategy: DefaultAudienceMatchingStrategy}}, + r: &http.Request{ + PostForm: url.Values{ + "client_id": {"1234"}, + "scope": {"foo bar"}, + }, + }, + mock: func() { + store.EXPECT().GetClient(gomock.Any(), "1234").Return(&DefaultClient{ + Scopes: []string{"foo", "bar"}, + GrantTypes: []string{"urn:ietf:params:oauth:grant-type:device_code"}, + }, nil) + }, + expect: &DeviceUserRequest{ + Request: Request{ + Client: &DefaultClient{ + Scopes: []string{"foo", "bar"}, + }, + RequestedScope: []string{"foo", "bar"}, + }, + }, + }, + /* should fail because doesn't have proper grant */ + { + desc: "should pass", + conf: &Fosite{Store: store, Config: &Config{ScopeStrategy: ExactScopeStrategy, AudienceMatchingStrategy: DefaultAudienceMatchingStrategy}}, + r: &http.Request{ + PostForm: url.Values{ + "client_id": {"1234"}, + "scope": {"foo bar"}, + }, + }, + mock: func() { + store.EXPECT().GetClient(gomock.Any(), "1234").Return(&DefaultClient{ + Scopes: []string{"foo", "bar"}, + }, nil) + }, + expectedError: ErrInvalidGrant, + }, + } { + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + ctrl := gomock.NewController(t) + store = NewMockStorage(ctrl) + defer ctrl.Finish() + + c.mock() + if c.r == nil { + c.r = &http.Request{Header: http.Header{}} + } + + c.conf.Store = store + ar, err := c.conf.NewDeviceRequest(context.Background(), c.r) + if c.expectedError != nil { + assert.EqualError(t, err, c.expectedError.Error()) + } else { + require.NoError(t, err) + assert.NotNil(t, ar.GetRequestedAt()) + } + }) + } +} diff --git a/device_response.go b/device_response.go new file mode 100644 index 000000000..b9b9d655e --- /dev/null +++ b/device_response.go @@ -0,0 +1,97 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package fosite + +import ( + "encoding/json" + "io" + "net/http" +) + +type deviceResponse struct { + Header http.Header + DeviceCode string `json:"device_code"` + UserCode string `json:"user_code"` + VerificationURI string `json:"verification_uri"` + VerificationURIComplete string `json:"verification_uri_complete,omitempty"` + ExpiresIn int64 `json:"expires_in"` + Interval int `json:"interval,omitempty"` +} + +type DeviceResponse struct { + deviceResponse +} + +func NewDeviceResponse() *DeviceResponse { + return &DeviceResponse{} +} + +func (d *DeviceResponse) GetDeviceCode() string { + return d.deviceResponse.DeviceCode +} + +// SetDeviceCode returns the response's user code +func (d *DeviceResponse) SetDeviceCode(code string) { + d.deviceResponse.DeviceCode = code +} + +func (d *DeviceResponse) GetUserCode() string { + return d.deviceResponse.UserCode +} + +func (d *DeviceResponse) SetUserCode(code string) { + d.deviceResponse.UserCode = code +} + +// GetVerificationURI returns the response's verification uri +func (d *DeviceResponse) GetVerificationURI() string { + return d.deviceResponse.VerificationURI +} + +func (d *DeviceResponse) SetVerificationURI(uri string) { + d.deviceResponse.VerificationURI = uri +} + +// GetVerificationURIComplete returns the response's complete verification uri if set +func (d *DeviceResponse) GetVerificationURIComplete() string { + return d.deviceResponse.VerificationURIComplete +} + +func (d *DeviceResponse) SetVerificationURIComplete(uri string) { + d.deviceResponse.VerificationURIComplete = uri +} + +// GetExpiresIn returns the response's device code and user code lifetime in seconds if set +func (d *DeviceResponse) GetExpiresIn() int64 { + return d.deviceResponse.ExpiresIn +} + +func (d *DeviceResponse) SetExpiresIn(seconds int64) { + d.deviceResponse.ExpiresIn = seconds +} + +// GetInterval returns the response's polling interval if set +func (d *DeviceResponse) GetInterval() int { + return d.deviceResponse.Interval +} + +func (d *DeviceResponse) SetInterval(seconds int) { + d.deviceResponse.Interval = seconds +} + +func (a *DeviceResponse) GetHeader() http.Header { + return a.deviceResponse.Header +} + +func (a *DeviceResponse) AddHeader(key, value string) { + a.deviceResponse.Header.Add(key, value) +} + +func (d *DeviceResponse) FromJson(r io.Reader) error { + return json.NewDecoder(r).Decode(&d.deviceResponse) +} + +func (d *DeviceResponse) ToJson(rw io.Writer) error { + return json.NewEncoder(rw).Encode(&d.deviceResponse) +} diff --git a/device_response_writer.go b/device_response_writer.go new file mode 100644 index 000000000..1cf9cda04 --- /dev/null +++ b/device_response_writer.go @@ -0,0 +1,20 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package fosite + +import ( + "context" +) + +func (f *Fosite) NewDeviceResponse(ctx context.Context, r DeviceRequester) (DeviceResponder, error) { + var resp = &DeviceResponse{} + + for _, h := range f.Config.GetDeviceEndpointHandlers(ctx) { + if err := h.HandleDeviceEndpointRequest(ctx, r, resp); err != nil { + return nil, err + } + } + + return resp, nil +} diff --git a/device_response_writer_test.go b/device_response_writer_test.go new file mode 100644 index 000000000..88c23222f --- /dev/null +++ b/device_response_writer_test.go @@ -0,0 +1,76 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package fosite_test + +import ( + "context" + "testing" + + "github.com/golang/mock/gomock" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + + . "github.com/ory/fosite" + . "github.com/ory/fosite/internal" +) + +func TestNewDeviceResponse(t *testing.T) { + ctrl := gomock.NewController(t) + handlers := []*MockDeviceEndpointHandler{NewMockDeviceEndpointHandler(ctrl)} + dar := NewMockDeviceUserRequester(ctrl) + defer ctrl.Finish() + + ctx := context.Background() + oauth2 := &Fosite{Config: &Config{DeviceEndpointHandlers: DeviceEndpointHandlers{handlers[0]}}} + duo := &Fosite{Config: &Config{DeviceEndpointHandlers: DeviceEndpointHandlers{handlers[0], handlers[0]}}} + dar.EXPECT().SetSession(gomock.Eq(new(DefaultSession))).AnyTimes() + fooErr := errors.New("foo") + for k, c := range []struct { + isErr bool + mock func() + expectErr error + }{ + { + mock: func() { + handlers[0].EXPECT().HandleDeviceEndpointRequest(gomock.Any(), gomock.Any(), gomock.Any()).Return(fooErr) + }, + isErr: true, + expectErr: fooErr, + }, + { + mock: func() { + handlers[0].EXPECT().HandleDeviceEndpointRequest(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + }, + isErr: false, + }, + { + mock: func() { + oauth2 = duo + handlers[0].EXPECT().HandleDeviceEndpointRequest(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + handlers[0].EXPECT().HandleDeviceEndpointRequest(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + }, + isErr: false, + }, + { + mock: func() { + oauth2 = duo + handlers[0].EXPECT().HandleDeviceEndpointRequest(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + handlers[0].EXPECT().HandleDeviceEndpointRequest(gomock.Any(), gomock.Any(), gomock.Any()).Return(fooErr) + }, + isErr: true, + expectErr: fooErr, + }, + } { + c.mock() + responder, err := oauth2.NewDeviceResponse(ctx, dar) + assert.Equal(t, c.isErr, err != nil, "%d: %s", k, err) + if err != nil { + assert.Equal(t, c.expectErr, err, "%d: %s", k, err) + assert.Nil(t, responder, "%d", k) + } else { + assert.NotNil(t, responder, "%d", k) + } + t.Logf("Passed test case %d", k) + } +} diff --git a/device_user_request.go b/device_user_request.go new file mode 100644 index 000000000..e99529b8d --- /dev/null +++ b/device_user_request.go @@ -0,0 +1,24 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package fosite + +// DeviceUserRequest is an implementation of DeviceUserRequester +type DeviceUserRequest struct { + signature string + Request +} + +func (d *DeviceUserRequest) GetDeviceCodeSignature() string { + return d.signature +} + +func (d *DeviceUserRequest) SetDeviceCodeSignature(signature string) { + d.signature = signature +} + +func NewDeviceUserRequest() *DeviceUserRequest { + return &DeviceUserRequest{ + Request: *NewRequest(), + } +} diff --git a/device_user_request_handler.go b/device_user_request_handler.go new file mode 100644 index 000000000..e294e400c --- /dev/null +++ b/device_user_request_handler.go @@ -0,0 +1,41 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package fosite + +import ( + "context" + "net/http" + + "github.com/ory/fosite/i18n" + "github.com/ory/x/errorsx" + "github.com/ory/x/otelx" + "go.opentelemetry.io/otel/trace" +) + +func (f *Fosite) NewDeviceUserRequest(ctx context.Context, r *http.Request) (_ DeviceUserRequester, err error) { + ctx, span := trace.SpanFromContext(ctx).TracerProvider().Tracer("github.com/ory/fosite").Start(ctx, "Fosite.NewDeviceUserRequest") + defer otelx.End(span, &err) + + return f.newDeviceUserRequest(ctx, r) +} + +func (f *Fosite) newDeviceUserRequest(ctx context.Context, r *http.Request) (DeviceUserRequester, error) { + request := NewDeviceUserRequest() + request.Lang = i18n.GetLangFromRequest(f.Config.GetMessageCatalog(ctx), r) + request.Form = r.URL.Query() + + if r.URL.Query().Has("device_verifier") { + client, err := f.Store.GetClient(ctx, r.URL.Query().Get("client_id")) + if err != nil { + return nil, errorsx.WithStack(ErrInvalidClient.WithHint("The requested OAuth 2.0 Client does not exist.").WithWrap(err).WithDebug(err.Error())) + } + request.Client = client + + if !client.GetGrantTypes().Has(string(GrantTypeDeviceCode)) { + return nil, errorsx.WithStack(ErrInvalidGrant.WithHint("The requested OAuth 2.0 Client does not have the 'urn:ietf:params:oauth:grant-type:device_code' grant.")) + } + } + + return request, nil +} diff --git a/device_user_request_handler_test.go b/device_user_request_handler_test.go new file mode 100644 index 000000000..df937a16c --- /dev/null +++ b/device_user_request_handler_test.go @@ -0,0 +1,137 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package fosite_test + +import ( + "context" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "testing" + + "github.com/golang/mock/gomock" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + . "github.com/ory/fosite" + . "github.com/ory/fosite/internal" +) + +func TestNewDeviceUserRequest(t *testing.T) { + var store *MockStorage + for k, c := range []struct { + desc string + conf *Fosite + r *http.Request + query url.Values + form url.Values + expectedError error + mock func() + expect *DeviceUserRequest + }{ + /* invalid client */ + { + desc: "invalid client fails", + conf: &Fosite{Store: store, Config: &Config{ScopeStrategy: ExactScopeStrategy, AudienceMatchingStrategy: DefaultAudienceMatchingStrategy}}, + query: url.Values{"device_verifier": []string{"BBBB"}}, + expectedError: ErrInvalidClient, + mock: func() { + store.EXPECT().GetClient(gomock.Any(), gomock.Any()).Return(nil, errors.New("foo")) + }, + }, + /* success case */ + { + desc: "empty request should pass", + conf: &Fosite{Store: store, Config: &Config{ScopeStrategy: ExactScopeStrategy, AudienceMatchingStrategy: DefaultAudienceMatchingStrategy}}, + r: &http.Request{}, + mock: func() {}, + expect: &DeviceUserRequest{ + Request: Request{}, + }, + }, + { + desc: "should pass", + conf: &Fosite{Store: store, Config: &Config{ScopeStrategy: ExactScopeStrategy, AudienceMatchingStrategy: DefaultAudienceMatchingStrategy}}, + query: url.Values{ + "device_verifier": {"AAAA"}, + "client_id": {"1234"}, + }, + mock: func() { + store.EXPECT().GetClient(gomock.Any(), "1234").Return(&DefaultClient{ + GrantTypes: []string{"urn:ietf:params:oauth:grant-type:device_code"}, + }, nil) + }, + expect: &DeviceUserRequest{ + Request: Request{ + Client: &DefaultClient{ + GrantTypes: []string{"urn:ietf:params:oauth:grant-type:device_code"}, + }, + }, + }, + }, + { + desc: "should pass (body)", + conf: &Fosite{Store: store, Config: &Config{ScopeStrategy: ExactScopeStrategy, AudienceMatchingStrategy: DefaultAudienceMatchingStrategy}}, + form: url.Values{ + "device_verifier": {"AAAA"}, + "client_id": {"1234"}, + }, + mock: func() { + store.EXPECT().GetClient(gomock.Any(), "1234").Return(&DefaultClient{ + GrantTypes: []string{"urn:ietf:params:oauth:grant-type:device_code"}, + }, nil) + }, + expect: &DeviceUserRequest{ + Request: Request{ + Client: &DefaultClient{ + GrantTypes: []string{"urn:ietf:params:oauth:grant-type:device_code"}, + }, + }, + }, + }, + { + desc: "should fail client doesn't have device grant", + conf: &Fosite{Store: store, Config: &Config{ScopeStrategy: ExactScopeStrategy, AudienceMatchingStrategy: DefaultAudienceMatchingStrategy}}, + query: url.Values{ + "device_verifier": {"AAAA"}, + "client_id": {"1234"}, + }, + mock: func() { + store.EXPECT().GetClient(gomock.Any(), "1234").Return(&DefaultClient{}, nil) + }, + expectedError: ErrInvalidGrant, + }, + } { + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + ctrl := gomock.NewController(t) + store = NewMockStorage(ctrl) + defer ctrl.Finish() + + c.mock() + if c.r == nil { + c.r = &http.Request{Header: http.Header{}} + if c.query != nil { + c.r.URL = &url.URL{RawQuery: c.query.Encode()} + } + if c.form != nil { + c.r.Method = "POST" + c.r.Header.Add("Content-Type", "application/x-www-form-urlencoded") + c.r.Body = io.NopCloser(strings.NewReader(c.form.Encode())) + } + } + + c.conf.Store = store + ar, err := c.conf.NewDeviceUserRequest(context.Background(), c.r) + if c.expectedError != nil { + assert.EqualError(t, err, c.expectedError.Error()) + } else { + require.NoError(t, err) + assert.NotNil(t, ar.GetRequestedAt()) + } + }) + } +} diff --git a/device_user_request_test.go b/device_user_request_test.go new file mode 100644 index 000000000..473c1a561 --- /dev/null +++ b/device_user_request_test.go @@ -0,0 +1,60 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package fosite + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestDeviceUserRequest(t *testing.T) { + for k, c := range []struct { + ar *DeviceUserRequest + }{ + { + ar: NewDeviceUserRequest(), + }, + { + ar: &DeviceUserRequest{}, + }, + { + ar: &DeviceUserRequest{ + Request: Request{ + Client: &DefaultClient{RedirectURIs: []string{""}}, + }, + }, + }, + { + ar: &DeviceUserRequest{ + signature: "AAAA", + Request: Request{ + Client: &DefaultClient{RedirectURIs: []string{""}}, + }, + }, + }, + { + ar: &DeviceUserRequest{ + Request: Request{ + Client: &DefaultClient{RedirectURIs: []string{"https://foobar.com/cb"}}, + RequestedAt: time.Now().UTC(), + RequestedScope: []string{"foo", "bar"}, + }, + }, + }, + } { + assert.Equal(t, c.ar.Client, c.ar.GetClient(), "%d", k) + assert.Equal(t, c.ar.signature, c.ar.GetDeviceCodeSignature(), "%d", k) + assert.Equal(t, c.ar.RequestedAt, c.ar.GetRequestedAt(), "%d", k) + assert.Equal(t, c.ar.RequestedScope, c.ar.GetRequestedScopes(), "%d", k) + + c.ar.GrantScope("foo") + c.ar.SetSession(&DefaultSession{}) + c.ar.SetRequestedScopes([]string{"foo"}) + assert.True(t, c.ar.GetGrantedScopes().Has("foo")) + assert.True(t, c.ar.GetRequestedScopes().Has("foo")) + assert.Equal(t, &DefaultSession{}, c.ar.GetSession()) + } +} diff --git a/device_user_response.go b/device_user_response.go new file mode 100644 index 000000000..00c4165aa --- /dev/null +++ b/device_user_response.go @@ -0,0 +1,22 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package fosite + +import "net/http" + +type DeviceUserResponse struct { + Header http.Header +} + +func NewDeviceUserResponse() *DeviceUserResponse { + return &DeviceUserResponse{} +} + +func (a *DeviceUserResponse) GetHeader() http.Header { + return a.Header +} + +func (a *DeviceUserResponse) AddHeader(key, value string) { + a.Header.Add(key, value) +} diff --git a/device_user_response_writer.go b/device_user_response_writer.go new file mode 100644 index 000000000..4c9c370b5 --- /dev/null +++ b/device_user_response_writer.go @@ -0,0 +1,21 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package fosite + +import ( + "context" +) + +func (f *Fosite) NewDeviceUserResponse(ctx context.Context, dur DeviceUserRequester, session Session) (DeviceUserResponder, error) { + var resp = &DeviceUserResponse{} + + dur.SetSession(session) + for _, h := range f.Config.GetDeviceUserEndpointHandlers(ctx) { + if err := h.HandleDeviceUserEndpointRequest(ctx, dur, resp); err != nil { + return nil, err + } + } + + return resp, nil +} diff --git a/device_user_writer.go b/device_user_writer.go new file mode 100644 index 000000000..0c75cfc0e --- /dev/null +++ b/device_user_writer.go @@ -0,0 +1,37 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright © 2015-2021 Aeneas Rekkas + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @author Aeneas Rekkas + * @copyright 2015-2021 Aeneas Rekkas + * @license Apache-2.0 + * + */ + +package fosite + +import ( + "context" + "net/http" +) + +// Once the user has approved the grant he will be redirected on his interactive device +// to a webpage (usally hosted in hydra-ui) to understand that he was connected successfully +// and that he can close this tab safely and return to his non-interactive device; +func (f *Fosite) WriteDeviceUserResponse(ctx context.Context, r *http.Request, rw http.ResponseWriter, requester DeviceUserRequester, responder DeviceUserResponder) { + http.Redirect(rw, r, f.Config.GetDeviceDone(ctx), http.StatusSeeOther) +} diff --git a/device_write.go b/device_write.go new file mode 100644 index 000000000..0e8fa77b5 --- /dev/null +++ b/device_write.go @@ -0,0 +1,37 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package fosite + +import ( + "context" + "net/http" +) + +// TODO: Do documentation + +func (f *Fosite) WriteDeviceResponse(ctx context.Context, rw http.ResponseWriter, requester DeviceRequester, responder DeviceResponder) { + // Set custom headers, e.g. "X-MySuperCoolCustomHeader" or "X-DONT-CACHE-ME"... + wh := rw.Header() + rh := responder.GetHeader() + for k := range rh { + wh.Set(k, rh.Get(k)) + } + + rw.Header().Set("Content-Type", "application/json;charset=UTF-8") + rw.Header().Set("Cache-Control", "no-store") + rw.Header().Set("Pragma", "no-cache") + + deviceResponse := &DeviceResponse{ + deviceResponse{ + DeviceCode: responder.GetDeviceCode(), + UserCode: responder.GetUserCode(), + VerificationURI: responder.GetVerificationURI(), + VerificationURIComplete: responder.GetVerificationURIComplete(), + ExpiresIn: responder.GetExpiresIn(), + Interval: responder.GetInterval(), + }, + } + + _ = deviceResponse.ToJson(rw) +} diff --git a/device_write_test.go b/device_write_test.go new file mode 100644 index 000000000..0ed418cb7 --- /dev/null +++ b/device_write_test.go @@ -0,0 +1,55 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package fosite_test + +import ( + "context" + "net/http/httptest" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + . "github.com/ory/fosite" +) + +func TestWriteDeviceUserResponse(t *testing.T) { + oauth2 := &Fosite{Config: &Config{ + DeviceAndUserCodeLifespan: time.Minute, + DeviceAuthTokenPollingInterval: time.Minute, + DeviceVerificationURL: "http://ory.sh", + }} + + rw := httptest.NewRecorder() + ar := &Request{} + resp := &DeviceResponse{} + resp.SetUserCode("AAAA") + resp.SetDeviceCode("BBBB") + resp.SetInterval(int( + oauth2.Config.GetDeviceAuthTokenPollingInterval(context.TODO()).Round(time.Second).Seconds(), + )) + resp.SetExpiresIn(int64( + time.Now().Round(time.Second).Add(oauth2.Config.GetDeviceAndUserCodeLifespan(context.TODO())).Second(), + )) + resp.SetVerificationURI(oauth2.Config.GetDeviceVerificationURL(context.TODO())) + resp.SetVerificationURIComplete( + oauth2.Config.GetDeviceVerificationURL(context.TODO()) + "?user_code=" + resp.GetUserCode(), + ) + + oauth2.WriteDeviceResponse(context.Background(), rw, ar, resp) + + assert.Equal(t, 200, rw.Code) + + wroteDeviceResponse := DeviceResponse{} + err := wroteDeviceResponse.FromJson(rw.Body) + require.NoError(t, err) + + assert.Equal(t, resp.GetUserCode(), wroteDeviceResponse.UserCode) + assert.Equal(t, resp.GetDeviceCode(), wroteDeviceResponse.DeviceCode) + assert.Equal(t, resp.GetVerificationURI(), wroteDeviceResponse.VerificationURI) + assert.Equal(t, resp.GetVerificationURIComplete(), wroteDeviceResponse.VerificationURIComplete) + assert.Equal(t, resp.GetInterval(), wroteDeviceResponse.Interval) + assert.Equal(t, resp.GetExpiresIn(), wroteDeviceResponse.ExpiresIn) +} diff --git a/errors.go b/errors.go index bf6c2b42c..f276e1d33 100644 --- a/errors.go +++ b/errors.go @@ -22,7 +22,11 @@ import ( var ( // ErrInvalidatedAuthorizeCode is an error indicating that an authorization code has been // used previously. - ErrInvalidatedAuthorizeCode = errors.New("Authorization code has ben invalidated") + ErrInvalidatedAuthorizeCode = errors.New("Authorization code has been invalidated") + // ErrInvalidatedDeviceCode is an error indicating that a device code has been used previously. + ErrInvalidatedDeviceCode = errors.New("Device code has been invalidated") + // ErrInvalidatedUserCode is an error indicating that a user code has been used previously. + ErrInvalidatedUserCode = errors.New("user code has been invalidated") // ErrSerializationFailure is an error indicating that the transactional capable storage could not guarantee // consistency of Update & Delete operations on the same rows between multiple sessions. ErrSerializationFailure = errors.New("The request could not be completed due to concurrent access") @@ -202,6 +206,22 @@ var ( ErrorField: errJTIKnownName, CodeField: http.StatusBadRequest, } + ErrAuthorizationPending = &RFC6749Error{ + DescriptionField: "The authorization request is still pending as the end user hasn't yet completed the user-interaction steps.", + ErrorField: errAuthorizationPending, + CodeField: http.StatusBadRequest, + } + ErrPollingRateLimited = &RFC6749Error{ + DescriptionField: "The authorization request was rate-limited to prevent system overload.", + HintField: "Ensure that you don't call the token endpoint sooner than the polling interval", + ErrorField: errPollingIntervalRateLimited, + CodeField: http.StatusTooManyRequests, + } + ErrDeviceExpiredToken = &RFC6749Error{ + DescriptionField: "The device_code has expired, and the device authorization session has concluded.", + ErrorField: errDeviceExpiredToken, + CodeField: http.StatusBadRequest, + } ) const ( @@ -239,6 +259,9 @@ const ( errRequestURINotSupportedName = "request_uri_not_supported" errRegistrationNotSupportedName = "registration_not_supported" errJTIKnownName = "jti_known" + errAuthorizationPending = "authorization_pending" + errPollingIntervalRateLimited = "polling_interval_rate_limited" + errDeviceExpiredToken = "expired_token" ) type ( diff --git a/fosite.go b/fosite.go index e84c964e2..e5d30fb77 100644 --- a/fosite.go +++ b/fosite.go @@ -82,6 +82,34 @@ func (a *PushedAuthorizeEndpointHandlers) Append(h PushedAuthorizeEndpointHandle *a = append(*a, h) } +// DeviceEndpointHandlers is a list of DeviceEndpointHandler +type DeviceEndpointHandlers []DeviceEndpointHandler + +// Append adds an DeviceEndpointHandlers to this list. Ignores duplicates based on reflect.TypeOf. +func (a *DeviceEndpointHandlers) Append(h DeviceEndpointHandler) { + for _, this := range *a { + if reflect.TypeOf(this) == reflect.TypeOf(h) { + return + } + } + + *a = append(*a, h) +} + +// DeviceUserEndpointHandlers is a list of DeviceUserEndpointHandler +type DeviceUserEndpointHandlers []DeviceUserEndpointHandler + +// Append adds an DeviceUserEndpointHandlers to this list. Ignores duplicates based on reflect.TypeOf. +func (a *DeviceUserEndpointHandlers) Append(h DeviceUserEndpointHandler) { + for _, this := range *a { + if reflect.TypeOf(this) == reflect.TypeOf(h) { + return + } + } + + *a = append(*a, h) +} + var _ OAuth2Provider = (*Fosite)(nil) type Configurator interface { @@ -108,6 +136,7 @@ type Configurator interface { RefreshTokenLifespanProvider VerifiableCredentialsNonceLifespanProvider AuthorizeCodeLifespanProvider + DeviceAndUserCodeLifespanProvider TokenEntropyProvider RotatedGlobalSecretsProvider GlobalSecretProvider @@ -132,6 +161,10 @@ type Configurator interface { TokenIntrospectionHandlersProvider RevocationHandlersProvider UseLegacyErrorFormatProvider + DeviceEndpointHandlersProvider + DeviceUserEndpointHandlersProvider + DeviceProvider + DeviceUserProvider } func NewOAuth2Provider(s Storage, c Configurator) *Fosite { diff --git a/fosite_test.go b/fosite_test.go index 9b87919f5..b3f406bb1 100644 --- a/fosite_test.go +++ b/fosite_test.go @@ -16,23 +16,23 @@ import ( ) func TestAuthorizeEndpointHandlers(t *testing.T) { - h := &oauth2.AuthorizeExplicitGrantHandler{} + h := &oauth2.AuthorizeExplicitGrantAuthHandler{} hs := AuthorizeEndpointHandlers{} hs.Append(h) hs.Append(h) - hs.Append(&oauth2.AuthorizeExplicitGrantHandler{}) + hs.Append(&oauth2.AuthorizeExplicitGrantAuthHandler{}) assert.Len(t, hs, 1) assert.Equal(t, hs[0], h) } func TestTokenEndpointHandlers(t *testing.T) { - h := &oauth2.AuthorizeExplicitGrantHandler{} + h := &oauth2.GenericCodeTokenEndpointHandler{} hs := TokenEndpointHandlers{} hs.Append(h) hs.Append(h) // do some crazy type things and make sure dupe detection works - var f interface{} = &oauth2.AuthorizeExplicitGrantHandler{} - hs.Append(&oauth2.AuthorizeExplicitGrantHandler{}) + var f interface{} = &oauth2.GenericCodeTokenEndpointHandler{} + hs.Append(&oauth2.GenericCodeTokenEndpointHandler{}) hs.Append(f.(TokenEndpointHandler)) require.Len(t, hs, 1) assert.Equal(t, hs[0], h) diff --git a/generate-mocks.sh b/generate-mocks.sh index d4dded4ea..c56a5b781 100755 --- a/generate-mocks.sh +++ b/generate-mocks.sh @@ -6,7 +6,10 @@ mockgen -package internal -destination internal/transactional.go github.com/ory/ mockgen -package internal -destination internal/oauth2_storage.go github.com/ory/fosite/handler/oauth2 CoreStorage mockgen -package internal -destination internal/oauth2_strategy.go github.com/ory/fosite/handler/oauth2 CoreStrategy mockgen -package internal -destination internal/authorize_code_storage.go github.com/ory/fosite/handler/oauth2 AuthorizeCodeStorage +mockgen -package internal -destination internal/device_code_storage.go github.com/ory/fosite/handler/rfc8628 DeviceCodeStorage +mockgen -package internal -destination internal/user_code_storage.go github.com/ory/fosite/handler/rfc8628 UserCodeStorage mockgen -package internal -destination internal/oauth2_auth_jwt_storage.go github.com/ory/fosite/handler/rfc7523 RFC7523KeyStorage +mockgen -package internal -destination internal/oauth2_auth_device_storage.go github.com/ory/fosite/handler/rfc8628 RFC8628CodeStorage mockgen -package internal -destination internal/access_token_storage.go github.com/ory/fosite/handler/oauth2 AccessTokenStorage mockgen -package internal -destination internal/refresh_token_strategy.go github.com/ory/fosite/handler/oauth2 RefreshTokenStorage mockgen -package internal -destination internal/oauth2_client_storage.go github.com/ory/fosite/handler/oauth2 ClientCredentialsGrantStorage @@ -19,6 +22,8 @@ mockgen -package internal -destination internal/authorize_code_strategy.go githu mockgen -package internal -destination internal/id_token_strategy.go github.com/ory/fosite/handler/openid OpenIDConnectTokenStrategy mockgen -package internal -destination internal/pkce_storage_strategy.go github.com/ory/fosite/handler/pkce PKCERequestStorage mockgen -package internal -destination internal/authorize_handler.go github.com/ory/fosite AuthorizeEndpointHandler +mockgen -package internal -destination internal/device_handler.go github.com/ory/fosite DeviceEndpointHandler +mockgen -package internal -destination internal/device_user_handler.go github.com/ory/fosite DeviceUserEndpointHandler mockgen -package internal -destination internal/revoke_handler.go github.com/ory/fosite RevocationHandler mockgen -package internal -destination internal/token_handler.go github.com/ory/fosite TokenEndpointHandler mockgen -package internal -destination internal/introspector.go github.com/ory/fosite TokenIntrospector @@ -28,5 +33,9 @@ mockgen -package internal -destination internal/access_request.go github.com/ory mockgen -package internal -destination internal/access_response.go github.com/ory/fosite AccessResponder mockgen -package internal -destination internal/authorize_request.go github.com/ory/fosite AuthorizeRequester mockgen -package internal -destination internal/authorize_response.go github.com/ory/fosite AuthorizeResponder +mockgen -package internal -destination internal/device_user_request.go github.com/ory/fosite DeviceUserRequester +mockgen -package internal -destination internal/device_user_response.go github.com/ory/fosite DeviceUserResponder +mockgen -package internal -destination internal/device_request.go github.com/ory/fosite DeviceRequester +mockgen -package internal -destination internal/device_response.go github.com/ory/fosite DeviceResponder goimports -w internal/ \ No newline at end of file diff --git a/generate.go b/generate.go index 46dbe0e71..43e211ce8 100644 --- a/generate.go +++ b/generate.go @@ -9,6 +9,8 @@ package fosite //go:generate go run github.com/golang/mock/mockgen -package internal -destination internal/oauth2_storage.go github.com/ory/fosite/handler/oauth2 CoreStorage //go:generate go run github.com/golang/mock/mockgen -package internal -destination internal/oauth2_strategy.go github.com/ory/fosite/handler/oauth2 CoreStrategy //go:generate go run github.com/golang/mock/mockgen -package internal -destination internal/authorize_code_storage.go github.com/ory/fosite/handler/oauth2 AuthorizeCodeStorage +//go:generate go run github.com/golang/mock/mockgen -package internal -destination internal/device_code_storage.go github.com/ory/fosite/handler/oauth2 DeviceCodeStorage +//go:generate go run github.com/golang/mock/mockgen -package internal -destination internal/user_code_storage.go github.com/ory/fosite/handler/oauth2 UserCodeStorage //go:generate go run github.com/golang/mock/mockgen -package internal -destination internal/oauth2_auth_jwt_storage.go github.com/ory/fosite/handler/rfc7523 RFC7523KeyStorage //go:generate go run github.com/golang/mock/mockgen -package internal -destination internal/access_token_storage.go github.com/ory/fosite/handler/oauth2 AccessTokenStorage //go:generate go run github.com/golang/mock/mockgen -package internal -destination internal/refresh_token_strategy.go github.com/ory/fosite/handler/oauth2 RefreshTokenStorage @@ -22,6 +24,8 @@ package fosite //go:generate go run github.com/golang/mock/mockgen -package internal -destination internal/id_token_strategy.go github.com/ory/fosite/handler/openid OpenIDConnectTokenStrategy //go:generate go run github.com/golang/mock/mockgen -package internal -destination internal/pkce_storage_strategy.go github.com/ory/fosite/handler/pkce PKCERequestStorage //go:generate go run github.com/golang/mock/mockgen -package internal -destination internal/authorize_handler.go github.com/ory/fosite AuthorizeEndpointHandler +//go:generate go run github.com/golang/mock/mockgen -package internal -destination internal/device_handler.go github.com/ory/fosite DeviceEndpointHandler +//go:generate go run github.com/golang/mock/mockgen -package internal -destination internal/device_user_handler.go github.com/ory/fosite DeviceUserEndpointHandler //go:generate go run github.com/golang/mock/mockgen -package internal -destination internal/revoke_handler.go github.com/ory/fosite RevocationHandler //go:generate go run github.com/golang/mock/mockgen -package internal -destination internal/token_handler.go github.com/ory/fosite TokenEndpointHandler //go:generate go run github.com/golang/mock/mockgen -package internal -destination internal/introspector.go github.com/ory/fosite TokenIntrospector @@ -31,3 +35,7 @@ package fosite //go:generate go run github.com/golang/mock/mockgen -package internal -destination internal/access_response.go github.com/ory/fosite AccessResponder //go:generate go run github.com/golang/mock/mockgen -package internal -destination internal/authorize_request.go github.com/ory/fosite AuthorizeRequester //go:generate go run github.com/golang/mock/mockgen -package internal -destination internal/authorize_response.go github.com/ory/fosite AuthorizeResponder +//go:generate go run github.com/golang/mock/mockgen -package internal -destination internal/device_user_request.go github.com/ory/fosite DeviceUserRequester +//go:generate go run github.com/golang/mock/mockgen -package internal -destination internal/device_user_response.go github.com/ory/fosite DeviceUserResponder +//go:generate go run github.com/golang/mock/mockgen -package internal -destination internal/device_request.go github.com/ory/fosite DeviceRequester +//go:generate go run github.com/golang/mock/mockgen -package internal -destination internal/device_response.go github.com/ory/fosite DeviceResponder diff --git a/go.mod b/go.mod index ad75e99a3..1fed8f122 100644 --- a/go.mod +++ b/go.mod @@ -11,9 +11,9 @@ require ( github.com/cristalhq/jwt/v4 v4.0.2 github.com/dgraph-io/ristretto v0.1.1 github.com/ecordell/optgen v0.0.9 - github.com/go-jose/go-jose/v3 v3.0.0 + github.com/go-jose/go-jose/v3 v3.0.1 github.com/golang/mock v1.6.0 - github.com/google/uuid v1.3.0 + github.com/google/uuid v1.3.1 github.com/gorilla/mux v1.8.0 github.com/gorilla/websocket v1.5.0 github.com/hashicorp/go-retryablehttp v0.7.4 @@ -23,16 +23,18 @@ require ( github.com/oleiade/reflections v1.0.1 github.com/ory/go-acc v0.2.9-0.20230103102148-6b1c9a70dbbe github.com/ory/go-convenience v0.1.0 - github.com/ory/x v0.0.575 + github.com/ory/x v0.0.609 github.com/parnurzeal/gorequest v0.2.15 + github.com/patrickmn/go-cache v2.1.0+incompatible github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.8.4 github.com/tidwall/gjson v1.14.3 - go.opentelemetry.io/otel/trace v1.16.0 - golang.org/x/crypto v0.11.0 - golang.org/x/net v0.13.0 - golang.org/x/oauth2 v0.10.0 - golang.org/x/text v0.11.0 + go.opentelemetry.io/otel/trace v1.21.0 + golang.org/x/crypto v0.15.0 + golang.org/x/net v0.18.0 + golang.org/x/oauth2 v0.14.0 + golang.org/x/text v0.14.0 + golang.org/x/time v0.4.0 ) require ( @@ -44,21 +46,21 @@ require ( github.com/dustin/go-humanize v1.0.1 // indirect github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819 // indirect github.com/fatih/structtag v1.2.0 // indirect - github.com/felixge/httpsnoop v1.0.3 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect - github.com/go-logr/logr v1.2.4 // indirect + github.com/go-logr/logr v1.3.0 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/gobuffalo/pop/v6 v6.0.8 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/glog v1.1.1 // indirect + github.com/golang/glog v1.1.2 // indirect github.com/golang/protobuf v1.5.3 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.2 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/moul/http2curl v0.0.0-20170919181001-9ac6cf4d929b // indirect - github.com/openzipkin/zipkin-go v0.4.1 // indirect + github.com/openzipkin/zipkin-go v0.4.2 // indirect github.com/pelletier/go-toml/v2 v2.0.9 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/seatgeek/logrus-gelf-formatter v0.0.0-20210414080842-5b05eb8ff761 // indirect @@ -73,28 +75,27 @@ require ( github.com/subosito/gotenv v1.4.2 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.42.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0 // indirect - go.opentelemetry.io/contrib/propagators/b3 v1.17.0 // indirect - go.opentelemetry.io/contrib/propagators/jaeger v1.17.0 // indirect - go.opentelemetry.io/contrib/samplers/jaegerremote v0.11.0 // indirect - go.opentelemetry.io/otel v1.16.0 // indirect - go.opentelemetry.io/otel/exporters/jaeger v1.16.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.16.0 // indirect - go.opentelemetry.io/otel/exporters/zipkin v1.16.0 // indirect - go.opentelemetry.io/otel/metric v1.16.0 // indirect - go.opentelemetry.io/otel/sdk v1.16.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect + go.opentelemetry.io/contrib/propagators/b3 v1.21.0 // indirect + go.opentelemetry.io/contrib/propagators/jaeger v1.21.1 // indirect + go.opentelemetry.io/contrib/samplers/jaegerremote v0.15.1 // indirect + go.opentelemetry.io/otel v1.21.0 // indirect + go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0 // indirect + go.opentelemetry.io/otel/exporters/zipkin v1.21.0 // indirect + go.opentelemetry.io/otel/metric v1.21.0 // indirect + go.opentelemetry.io/otel/sdk v1.21.0 // indirect go.opentelemetry.io/proto/otlp v1.0.0 // indirect - golang.org/x/mod v0.12.0 // indirect - golang.org/x/sys v0.10.0 // indirect - golang.org/x/tools v0.11.1 // indirect - google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20230731193218-e0aa005b6bdf // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20230731193218-e0aa005b6bdf // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20230731193218-e0aa005b6bdf // indirect - google.golang.org/grpc v1.57.0 // indirect + golang.org/x/mod v0.14.0 // indirect + golang.org/x/sys v0.14.0 // indirect + golang.org/x/tools v0.15.0 // indirect + google.golang.org/appengine v1.6.8 // indirect + google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 // indirect + google.golang.org/grpc v1.59.0 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index c23397f58..fc2f9d8e9 100644 --- a/go.sum +++ b/go.sum @@ -92,21 +92,21 @@ github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYF github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= -github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= -github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo= -github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= +github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA= +github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= -github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= @@ -132,8 +132,8 @@ github.com/gofrs/uuid v4.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRx github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.1.1 h1:jxpi2eWoU84wbX9iIEyAeeoac3FLuifZpY9tcNUD9kw= -github.com/golang/glog v1.1.1/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ= +github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo= +github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -161,6 +161,7 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= @@ -175,7 +176,7 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -191,8 +192,8 @@ github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= @@ -203,8 +204,8 @@ github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.2 h1:dygLcbEBA+t/P7ck6a8AkXv6juQ4cK0RHBoh32jxhHM= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.2/go.mod h1:Ap9RLCIJVtgQg1/BBgVEfypOAySvvlcpcVQkSzJCH4Y= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1 h1:6UKoz5ujsI55KNpsJH3UwCq3T8kKbZwNZBNPuTTje8U= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1/go.mod h1:YvJ2f6MplWDhfxiUC3KpyTy76kYUZA4W3pTv/wdKQ9Y= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= @@ -314,18 +315,20 @@ github.com/moul/http2curl v0.0.0-20170919181001-9ac6cf4d929b/go.mod h1:8UbvGypXm github.com/nyaruka/phonenumbers v1.1.1 h1:fyoZmpLN2VCmAnc51XcrNOUVP2wT1ZzQl348ggIaXII= github.com/oleiade/reflections v1.0.1 h1:D1XO3LVEYroYskEsoSiGItp9RUxG6jWnCVvrqH0HHQM= github.com/oleiade/reflections v1.0.1/go.mod h1:rdFxbxq4QXVZWj0F+e9jqjDkc7dbp97vkRixKo2JR60= -github.com/openzipkin/zipkin-go v0.4.1 h1:kNd/ST2yLLWhaWrkgchya40TJabe8Hioj9udfPcEO5A= -github.com/openzipkin/zipkin-go v0.4.1/go.mod h1:qY0VqDSN1pOBN94dBc6w2GJlWLiovAyg7Qt6/I9HecM= +github.com/openzipkin/zipkin-go v0.4.2 h1:zjqfqHjUpPmB3c1GlCvvgsM1G4LkvqQbBDueDOCg/jA= +github.com/openzipkin/zipkin-go v0.4.2/go.mod h1:ZeVkFjuuBiSy13y8vpSDCjMi9GoI3hPpCJSBx/EYFhY= github.com/ory/go-acc v0.2.9-0.20230103102148-6b1c9a70dbbe h1:rvu4obdvqR0fkSIJ8IfgzKOWwZ5kOT2UNfLq81Qk7rc= github.com/ory/go-acc v0.2.9-0.20230103102148-6b1c9a70dbbe/go.mod h1:z4n3u6as84LbV4YmgjHhnwtccQqzf4cZlSk9f1FhygI= github.com/ory/go-convenience v0.1.0 h1:zouLKfF2GoSGnJwGq+PE/nJAE6dj2Zj5QlTgmMTsTS8= github.com/ory/go-convenience v0.1.0/go.mod h1:uEY/a60PL5c12nYz4V5cHY03IBmwIAEm8TWB0yn9KNs= -github.com/ory/herodot v0.10.3-0.20230626083119-d7e5192f0d88 h1:J0CIFKdpUeqKbVMw7pQ1qLtUnflRM1JWAcOEq7Hp4yg= +github.com/ory/herodot v0.9.13 h1:cN/Z4eOkErl/9W7hDIDLb79IO/bfsH+8yscBjRpB4IU= github.com/ory/jsonschema/v3 v3.0.7 h1:GQ9qfZDiJqs4l2d3p56dozCChvejQFZyLKGHYzDzOSo= -github.com/ory/x v0.0.575 h1:LvOeR+YlJ6/JtvIJvSwMoDBY/i3GACUe7HpWXHGNUTA= -github.com/ory/x v0.0.575/go.mod h1:aeJFTlvDLGYSABzPS3z5SeLcYC52Ek7uGZiuYGcTMSU= +github.com/ory/x v0.0.609 h1:M92c+SyYtjAbyGF4kXvAkPDPq+4NugbHAvx7tGmm+dY= +github.com/ory/x v0.0.609/go.mod h1:Wtu0ZYwP1NEhChLJpSy3NEHnUfOgwNMFiena+hHhmuM= github.com/parnurzeal/gorequest v0.2.15 h1:oPjDCsF5IkD4gUk6vIgsxYNaSgvAnIh1EJeROn3HdJU= github.com/parnurzeal/gorequest v0.2.15/go.mod h1:3Kh2QUMJoqw3icWAecsyzkpY7UzRfDhbRdTjtNwNiUE= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0= github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= @@ -338,8 +341,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= @@ -413,34 +416,32 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.42.0 h1:0vzgiFDsCh/jxRCR1xcRrtMoeCu2itXz/PsXst5P8rI= -go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.42.0/go.mod h1:y0vOY2OKFMOTvwxKfurStPayUUKGHlNeVqNneHmFXr0= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0 h1:pginetY7+onl4qN1vl0xW/V/v6OBZ0vVdH+esuJgvmM= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0/go.mod h1:XiYsayHc36K3EByOO6nbAXnAWbrUxdjUROCEeeROOH8= -go.opentelemetry.io/contrib/propagators/b3 v1.17.0 h1:ImOVvHnku8jijXqkwCSyYKRDt2YrnGXD4BbhcpfbfJo= -go.opentelemetry.io/contrib/propagators/b3 v1.17.0/go.mod h1:IkfUfMpKWmynvvE0264trz0sf32NRTZL4nuAN9AbWRc= -go.opentelemetry.io/contrib/propagators/jaeger v1.17.0 h1:Zbpbmwav32Ea5jSotpmkWEl3a6Xvd4tw/3xxGO1i05Y= -go.opentelemetry.io/contrib/propagators/jaeger v1.17.0/go.mod h1:tcTUAlmO8nuInPDSBVfG+CP6Mzjy5+gNV4mPxMbL0IA= -go.opentelemetry.io/contrib/samplers/jaegerremote v0.11.0 h1:P3JkQvs0s4Ww3hPb+jWFW9N6A0ioew7WwGTyqwgeofs= -go.opentelemetry.io/contrib/samplers/jaegerremote v0.11.0/go.mod h1:U+s0mJMfMC2gicc4WEgZ50JSR+5DhOIjcvFOCVAe8/U= -go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s= -go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4= -go.opentelemetry.io/otel/exporters/jaeger v1.16.0 h1:YhxxmXZ011C0aDZKoNw+juVWAmEfv/0W2XBOv9aHTaA= -go.opentelemetry.io/otel/exporters/jaeger v1.16.0/go.mod h1:grYbBo/5afWlPpdPZYhyn78Bk04hnvxn2+hvxQhKIQM= -go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0 h1:t4ZwRPU+emrcvM2e9DHd0Fsf0JTPVcbfa/BhTDF03d0= -go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0/go.mod h1:vLarbg68dH2Wa77g71zmKQqlQ8+8Rq3GRG31uc0WcWI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0 h1:cbsD4cUcviQGXdw8+bo5x2wazq10SKz8hEbtCRPcU78= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0/go.mod h1:JgXSGah17croqhJfhByOLVY719k1emAXC8MVhCIJlRs= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.16.0 h1:iqjq9LAB8aK++sKVcELezzn655JnBNdsDhghU4G/So8= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.16.0/go.mod h1:hGXzO5bhhSHZnKvrDaXB82Y9DRFour0Nz/KrBh7reWw= -go.opentelemetry.io/otel/exporters/zipkin v1.16.0 h1:WdMSH6vIJ+myJfr/HB/pjsYoJWQP0Wz/iJ1haNO5hX4= -go.opentelemetry.io/otel/exporters/zipkin v1.16.0/go.mod h1:QjDOKdylighHJBc7pf4Vo6fdhtiEJEqww/3Df8TOWjo= -go.opentelemetry.io/otel/metric v1.16.0 h1:RbrpwVG1Hfv85LgnZ7+txXioPDoh6EdbZHo26Q3hqOo= -go.opentelemetry.io/otel/metric v1.16.0/go.mod h1:QE47cpOmkwipPiefDwo2wDzwJrlfxxNYodqc4xnGCo4= -go.opentelemetry.io/otel/sdk v1.16.0 h1:Z1Ok1YsijYL0CSJpHt4cS3wDDh7p572grzNrBMiMWgE= -go.opentelemetry.io/otel/sdk v1.16.0/go.mod h1:tMsIuKXuuIWPBAOrH+eHtvhTL+SntFtXF9QD68aP6p4= -go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs= -go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1 h1:gbhw/u49SS3gkPWiYweQNJGm/uJN5GkI/FrosxSHT7A= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1/go.mod h1:GnOaBaFQ2we3b9AGWJpsBa7v1S5RlQzlC3O7dRMxZhM= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo= +go.opentelemetry.io/contrib/propagators/b3 v1.21.0 h1:uGdgDPNzwQWRwCXJgw/7h29JaRqcq9B87Iv4hJDKAZw= +go.opentelemetry.io/contrib/propagators/b3 v1.21.0/go.mod h1:D9GQXvVGT2pzyTfp1QBOnD1rzKEWzKjjwu5q2mslCUI= +go.opentelemetry.io/contrib/propagators/jaeger v1.21.1 h1:f4beMGDKiVzg9IcX7/VuWVy+oGdjx3dNJ72YehmtY5k= +go.opentelemetry.io/contrib/propagators/jaeger v1.21.1/go.mod h1:U9jhkEl8d1LL+QXY7q3kneJWJugiN3kZJV2OWz3hkBY= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.15.1 h1:Qb+5A+JbIjXwO7l4HkRUhgIn4Bzz0GNS2q+qdmSx+0c= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.15.1/go.mod h1:G4vNCm7fRk0kjZ6pGNLo5SpLxAUvOfSrcaegnT8TPck= +go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc= +go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= +go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4= +go.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 h1:cl5P5/GIfFh4t6xyruOgJP5QiA1pw4fYYdv6nc6CBWw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0/go.mod h1:zgBdWWAu7oEEMC06MMKc5NLbA/1YDXV1sMpSqEeLQLg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0 h1:digkEZCJWobwBqMwC0cwCq8/wkkRy/OowZg5OArWZrM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0/go.mod h1:/OpE/y70qVkndM0TrxT4KBoN3RsFZP0QaofcfYrj76I= +go.opentelemetry.io/otel/exporters/zipkin v1.21.0 h1:D+Gv6lSfrFBWmQYyxKjDd0Zuld9SRXpIrEsKZvE4DO4= +go.opentelemetry.io/otel/exporters/zipkin v1.21.0/go.mod h1:83oMKR6DzmHisFOW3I+yIMGZUTjxiWaiBI8M8+TU5zE= +go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4= +go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= +go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8= +go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= +go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc= +go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= @@ -468,8 +469,8 @@ golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= -golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= +golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -507,8 +508,8 @@ golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -548,8 +549,8 @@ golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfS golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= -golang.org/x/net v0.13.0 h1:Nvo8UFsZ8X3BhAC9699Z1j7XQ3rsZnUUm7jfBEk1ueY= -golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= +golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= +golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -559,8 +560,8 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8= -golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI= +golang.org/x/oauth2 v0.14.0 h1:P0Vrf/2538nmC0H+pEQ3MNFRRnVR7RlqyVw+bvm26z0= +golang.org/x/oauth2 v0.14.0/go.mod h1:lAtNWgaWfL4cm7j2OV8TxGi9Qb7ECORx8DktCY74OwM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -575,7 +576,7 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -629,8 +630,8 @@ golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= -golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -645,13 +646,16 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= -golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.4.0 h1:Z81tqI5ddIoXDPvVQ7/7CC9TnLM7ubaFG2qXYd5BbYY= +golang.org/x/time v0.4.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -711,8 +715,8 @@ golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4= -golang.org/x/tools v0.11.1 h1:ojD5zOW8+7dOGzdnNgersm8aPfcDjhMp12UfG93NIMc= -golang.org/x/tools v0.11.1/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8= +golang.org/x/tools v0.15.0 h1:zdAyfUGbYmuVokhzVmghFl2ZJh5QhcfebBgmVPFYA+8= +golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -744,8 +748,9 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -782,12 +787,12 @@ google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20230731193218-e0aa005b6bdf h1:v5Cf4E9+6tawYrs/grq1q1hFpGtzlGFzgWHqwt6NFiU= -google.golang.org/genproto v0.0.0-20230731193218-e0aa005b6bdf/go.mod h1:oH/ZOT02u4kWEp7oYBGYFFkCdKS/uYR9Z7+0/xuuFp8= -google.golang.org/genproto/googleapis/api v0.0.0-20230731193218-e0aa005b6bdf h1:xkVZ5FdZJF4U82Q/JS+DcZA83s/GRVL+QrFMlexk9Yo= -google.golang.org/genproto/googleapis/api v0.0.0-20230731193218-e0aa005b6bdf/go.mod h1:5DZzOUPCLYL3mNkQ0ms0F3EuUNZ7py1Bqeq6sxzI7/Q= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230731193218-e0aa005b6bdf h1:guOdSPaeFgN+jEJwTo1dQ71hdBm+yKSCCKuTRkJzcVo= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230731193218-e0aa005b6bdf/go.mod h1:zBEcrKX2ZOcEkHWxBPAIvYUWOKKMIhYcmNiUIu2ji3I= +google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 h1:wpZ8pe2x1Q3f2KyT5f8oP/fa9rHAKgFPr/HZdNuS+PQ= +google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:J7XzRzVy1+IPwWHZUzoD0IccYZIrXILAQpc+Qy9CMhY= +google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 h1:JpwMPBpFN3uKhdaekDpiNlImDdkUAyiJ6ez/uxGaUSo= +google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:0xJLfVdJqpAPl8tDg1ujOCGzx6LFLttXT5NhllGOXY4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 h1:Jyp0Hsi0bmHXG6k9eATXoYtjd6e2UzZ1SCn/wIupY14= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:oQ5rr10WTTMvP4A36n8JpR1OrO1BEiV4f78CneXZxkA= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -804,8 +809,8 @@ google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.57.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw= -google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo= +google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= +google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= diff --git a/handler.go b/handler.go index 2b9d0b412..d0a91497f 100644 --- a/handler.go +++ b/handler.go @@ -66,3 +66,21 @@ type PushedAuthorizeEndpointHandler interface { // the pushed authorize request, he must return nil and NOT modify session nor responder neither requester. HandlePushedAuthorizeEndpointRequest(ctx context.Context, requester AuthorizeRequester, responder PushedAuthorizeResponder) error } + +type DeviceEndpointHandler interface { + // HandleDeviceEndpointRequest handles a device authorize endpoint request. To extend the handler's capabilities, the http request + // is passed along, if further information retrieval is required. If the handler feels that he is not responsible for + // the device authorize request, he must return nil and NOT modify session nor responder neither requester. + // + // The following spec is a good example of what HandleDeviceUserRequest should do. + // * https://tools.ietf.org/html/rfc8628#section-3.2 + HandleDeviceEndpointRequest(ctx context.Context, requester DeviceRequester, responder DeviceResponder) error +} + +type DeviceUserEndpointHandler interface { + // HandleDeviceUserEndpointRequest handles a device authorize endpoint request. + // To extend the handler's capabilities, the http request is passed along, if further + // information retrieval is required. If the handler feels that he is not responsible for + // the authorize request, he must return nil and NOT modify session nor responder neither requester. + HandleDeviceUserEndpointRequest(ctx context.Context, requester DeviceUserRequester, responder DeviceUserResponder) error +} diff --git a/handler/.DS_Store b/handler/.DS_Store new file mode 100644 index 000000000..bded983cf Binary files /dev/null and b/handler/.DS_Store differ diff --git a/handler/oauth2/flow_authorize_code_auth.go b/handler/oauth2/flow_authorize_code_auth.go index 6bf437c96..f08b5d469 100644 --- a/handler/oauth2/flow_authorize_code_auth.go +++ b/handler/oauth2/flow_authorize_code_auth.go @@ -14,38 +14,32 @@ import ( "github.com/ory/fosite" ) -var _ fosite.AuthorizeEndpointHandler = (*AuthorizeExplicitGrantHandler)(nil) -var _ fosite.TokenEndpointHandler = (*AuthorizeExplicitGrantHandler)(nil) +var _ fosite.AuthorizeEndpointHandler = (*AuthorizeExplicitGrantAuthHandler)(nil) -// AuthorizeExplicitGrantHandler is a response handler for the Authorize Code grant using the explicit grant type +// AuthorizeExplicitGrantAuthHandler is a response handler for the Authorize Code grant using the explicit grant type // as defined in https://tools.ietf.org/html/rfc6749#section-4.1 -type AuthorizeExplicitGrantHandler struct { - AccessTokenStrategy AccessTokenStrategy - RefreshTokenStrategy RefreshTokenStrategy - AuthorizeCodeStrategy AuthorizeCodeStrategy - CoreStorage CoreStorage - TokenRevocationStorage TokenRevocationStorage - Config interface { +type AuthorizeExplicitGrantAuthHandler struct { + AuthorizeCodeStrategy AuthorizeCodeStrategy + AuthorizeCodeStorage AuthorizeCodeStorage + + Config interface { fosite.AuthorizeCodeLifespanProvider - fosite.AccessTokenLifespanProvider - fosite.RefreshTokenLifespanProvider fosite.ScopeStrategyProvider fosite.AudienceStrategyProvider fosite.RedirectSecureCheckerProvider - fosite.RefreshTokenScopesProvider fosite.OmitRedirectScopeParamProvider fosite.SanitationAllowedProvider } } -func (c *AuthorizeExplicitGrantHandler) secureChecker(ctx context.Context) func(context.Context, *url.URL) bool { +func (c *AuthorizeExplicitGrantAuthHandler) 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 *AuthorizeExplicitGrantHandler) HandleAuthorizeEndpointRequest(ctx context.Context, ar fosite.AuthorizeRequester, resp fosite.AuthorizeResponder) error { +func (c *AuthorizeExplicitGrantAuthHandler) HandleAuthorizeEndpointRequest(ctx context.Context, ar fosite.AuthorizeRequester, resp fosite.AuthorizeResponder) error { // This let's us define multiple response types, for example open id connect's id_token if !ar.GetResponseTypes().ExactOne("code") { return nil @@ -76,14 +70,14 @@ func (c *AuthorizeExplicitGrantHandler) HandleAuthorizeEndpointRequest(ctx conte return c.IssueAuthorizeCode(ctx, ar, resp) } -func (c *AuthorizeExplicitGrantHandler) IssueAuthorizeCode(ctx context.Context, ar fosite.AuthorizeRequester, resp fosite.AuthorizeResponder) error { +func (c *AuthorizeExplicitGrantAuthHandler) IssueAuthorizeCode(ctx context.Context, ar fosite.AuthorizeRequester, resp fosite.AuthorizeResponder) error { code, signature, err := c.AuthorizeCodeStrategy.GenerateAuthorizeCode(ctx, ar) if err != nil { return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) } ar.GetSession().SetExpiresAt(fosite.AuthorizeCode, time.Now().UTC().Add(c.Config.GetAuthorizeCodeLifespan(ctx))) - if err := c.CoreStorage.CreateAuthorizeCodeSession(ctx, signature, ar.Sanitize(c.GetSanitationWhiteList(ctx))); err != nil { + if err := c.AuthorizeCodeStorage.CreateAuthorizeCodeSession(ctx, signature, ar.Sanitize(c.GetSanitationWhiteList(ctx))); err != nil { return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) } @@ -97,7 +91,7 @@ func (c *AuthorizeExplicitGrantHandler) IssueAuthorizeCode(ctx context.Context, return nil } -func (c *AuthorizeExplicitGrantHandler) GetSanitationWhiteList(ctx context.Context) []string { +func (c *AuthorizeExplicitGrantAuthHandler) GetSanitationWhiteList(ctx context.Context) []string { if allowedList := c.Config.GetSanitationWhiteList(ctx); len(allowedList) > 0 { return allowedList } diff --git a/handler/oauth2/flow_authorize_code_auth_test.go b/handler/oauth2/flow_authorize_code_auth_test.go index 3dc73fa19..4bee85659 100644 --- a/handler/oauth2/flow_authorize_code_auth_test.go +++ b/handler/oauth2/flow_authorize_code_auth_test.go @@ -28,16 +28,16 @@ func TestAuthorizeCode_HandleAuthorizeEndpointRequest(t *testing.T) { } { t.Run("strategy="+k, func(t *testing.T) { store := storage.NewMemoryStore() - handler := AuthorizeExplicitGrantHandler{ - CoreStorage: store, + handler := AuthorizeExplicitGrantAuthHandler{ AuthorizeCodeStrategy: strategy, + AuthorizeCodeStorage: store, Config: &fosite.Config{ AudienceMatchingStrategy: fosite.DefaultAudienceMatchingStrategy, ScopeStrategy: fosite.HierarchicScopeStrategy, }, } for _, c := range []struct { - handler AuthorizeExplicitGrantHandler + handler AuthorizeExplicitGrantAuthHandler areq *fosite.AuthorizeRequest description string expectErr error @@ -122,9 +122,9 @@ func TestAuthorizeCode_HandleAuthorizeEndpointRequest(t *testing.T) { }, }, { - handler: AuthorizeExplicitGrantHandler{ - CoreStorage: store, + handler: AuthorizeExplicitGrantAuthHandler{ AuthorizeCodeStrategy: strategy, + AuthorizeCodeStorage: store, Config: &fosite.Config{ ScopeStrategy: fosite.HierarchicScopeStrategy, AudienceMatchingStrategy: fosite.DefaultAudienceMatchingStrategy, diff --git a/handler/oauth2/flow_authorize_code_token.go b/handler/oauth2/flow_authorize_code_token.go index 3289cfcbf..20213f15a 100644 --- a/handler/oauth2/flow_authorize_code_token.go +++ b/handler/oauth2/flow_authorize_code_token.go @@ -5,197 +5,54 @@ package oauth2 import ( "context" - "time" "github.com/ory/x/errorsx" - "github.com/ory/fosite/storage" - - "github.com/pkg/errors" - "github.com/ory/fosite" ) -// HandleTokenEndpointRequest implements -// * https://tools.ietf.org/html/rfc6749#section-4.1.3 (everything) -func (c *AuthorizeExplicitGrantHandler) HandleTokenEndpointRequest(ctx context.Context, request fosite.AccessRequester) error { - if !c.CanHandleTokenEndpointRequest(ctx, request) { - return errorsx.WithStack(errorsx.WithStack(fosite.ErrUnknownRequest)) - } - - if !request.GetClient().GetGrantTypes().Has("authorization_code") { - return errorsx.WithStack(fosite.ErrUnauthorizedClient.WithHint("The OAuth 2.0 Client is not allowed to use authorization grant \"authorization_code\".")) - } - - code := request.GetRequestForm().Get("code") - signature := c.AuthorizeCodeStrategy.AuthorizeCodeSignature(ctx, code) - authorizeRequest, err := c.CoreStorage.GetAuthorizeCodeSession(ctx, signature, request.GetSession()) - if errors.Is(err, fosite.ErrInvalidatedAuthorizeCode) { - if authorizeRequest == nil { - return fosite.ErrServerError. - WithHint("Misconfigured code lead to an error that prohibited the OAuth 2.0 Framework from processing this request."). - WithDebug("GetAuthorizeCodeSession must return a value for \"fosite.Requester\" when returning \"ErrInvalidatedAuthorizeCode\".") - } - - // If an authorize code is used twice, we revoke all refresh and access tokens associated with this request. - reqID := authorizeRequest.GetID() - hint := "The authorization code has already been used." - debug := "" - if revErr := c.TokenRevocationStorage.RevokeAccessToken(ctx, reqID); revErr != nil { - hint += " Additionally, an error occurred during processing the access token revocation." - debug += "Revocation of access_token lead to error " + revErr.Error() + "." - } - if revErr := c.TokenRevocationStorage.RevokeRefreshToken(ctx, reqID); revErr != nil { - hint += " Additionally, an error occurred during processing the refresh token revocation." - debug += "Revocation of refresh_token lead to error " + revErr.Error() + "." - } - return errorsx.WithStack(fosite.ErrInvalidGrant.WithHint(hint).WithDebug(debug)) - } else if err != nil && errors.Is(err, fosite.ErrNotFound) { - return errorsx.WithStack(fosite.ErrInvalidGrant.WithWrap(err).WithDebug(err.Error())) - } else if err != nil { - return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) - } - - // The authorization server MUST verify that the authorization code is valid - // This needs to happen after store retrieval for the session to be hydrated properly - if err := c.AuthorizeCodeStrategy.ValidateAuthorizeCode(ctx, request, code); err != nil { - return errorsx.WithStack(fosite.ErrInvalidGrant.WithWrap(err).WithDebug(err.Error())) - } - - // Override scopes - request.SetRequestedScopes(authorizeRequest.GetRequestedScopes()) - - // Override audiences - request.SetRequestedAudience(authorizeRequest.GetRequestedAudience()) - - // The authorization server MUST ensure that the authorization code was issued to the authenticated - // confidential client, or if the client is public, ensure that the - // code was issued to "client_id" in the request, - if authorizeRequest.GetClient().GetID() != request.GetClient().GetID() { - return errorsx.WithStack(fosite.ErrInvalidGrant.WithHint("The OAuth 2.0 Client ID from this request does not match the one from the authorize request.")) - } - - // ensure that the "redirect_uri" parameter is present if the - // "redirect_uri" parameter was included in the initial authorization - // request as described in Section 4.1.1, and if included ensure that - // their values are identical. - forcedRedirectURI := authorizeRequest.GetRequestForm().Get("redirect_uri") - if forcedRedirectURI != "" && forcedRedirectURI != request.GetRequestForm().Get("redirect_uri") { - return errorsx.WithStack(fosite.ErrInvalidGrant.WithHint("The \"redirect_uri\" from this request does not match the one from the authorize request.")) - } +// AuthorizeExplicitGrantTokenHandler is a response handler for the Authorize Code grant using the explicit grant type +// as defined in https://tools.ietf.org/html/rfc6749#section-4.1 +type AuthorizeExplicitGrantTokenHandler struct { + AuthorizeCodeStrategy AuthorizeCodeStrategy + AuthorizeCodeStorage AuthorizeCodeStorage +} - // Checking of POST client_id skipped, because: - // If the client type is confidential or the client was issued client - // credentials (or assigned other authentication requirements), the - // client MUST authenticate with the authorization server as described - // in Section 3.2.1. - request.SetSession(authorizeRequest.GetSession()) - request.SetID(authorizeRequest.GetID()) +type AuthorizeExplicitTokenEndpointHandler struct { + GenericCodeTokenEndpointHandler +} - atLifespan := fosite.GetEffectiveLifespan(request.GetClient(), fosite.GrantTypeAuthorizationCode, fosite.AccessToken, c.Config.GetAccessTokenLifespan(ctx)) - request.GetSession().SetExpiresAt(fosite.AccessToken, time.Now().UTC().Add(atLifespan).Round(time.Second)) +var _ CodeTokenEndpointHandler = (*AuthorizeExplicitGrantTokenHandler)(nil) +var _ fosite.TokenEndpointHandler = (*AuthorizeExplicitTokenEndpointHandler)(nil) - rtLifespan := fosite.GetEffectiveLifespan(request.GetClient(), fosite.GrantTypeAuthorizationCode, fosite.RefreshToken, c.Config.GetRefreshTokenLifespan(ctx)) - if rtLifespan > -1 { - request.GetSession().SetExpiresAt(fosite.RefreshToken, time.Now().UTC().Add(rtLifespan).Round(time.Second)) +func (c *AuthorizeExplicitGrantTokenHandler) ValidateGrantTypes(ctx context.Context, requester fosite.AccessRequester) error { + if !requester.GetClient().GetGrantTypes().Has("authorization_code") { + return errorsx.WithStack(fosite.ErrUnauthorizedClient.WithHint("The OAuth 2.0 Client is not allowed to use authorization grant \"authorization_code\".")) } return nil } -func canIssueRefreshToken(ctx context.Context, c *AuthorizeExplicitGrantHandler, request fosite.Requester) bool { - scope := c.Config.GetRefreshTokenScopes(ctx) - // Require one of the refresh token scopes, if set. - if len(scope) > 0 && !request.GetGrantedScopes().HasOneOf(scope...) { - return false - } - // Do not issue a refresh token to clients that cannot use the refresh token grant type. - if !request.GetClient().GetGrantTypes().Has("refresh_token") { - return false - } - return true +func (c *AuthorizeExplicitGrantTokenHandler) ValidateCode(ctx context.Context, request fosite.AccessRequester, code string) error { + return c.AuthorizeCodeStrategy.ValidateAuthorizeCode(ctx, request, code) } -func (c *AuthorizeExplicitGrantHandler) PopulateTokenEndpointResponse(ctx context.Context, requester fosite.AccessRequester, responder fosite.AccessResponder) (err error) { - if !c.CanHandleTokenEndpointRequest(ctx, requester) { - return errorsx.WithStack(fosite.ErrUnknownRequest) - } - +func (c *AuthorizeExplicitGrantTokenHandler) GetCodeAndSession(ctx context.Context, requester fosite.AccessRequester) (string, string, fosite.Requester, error) { code := requester.GetRequestForm().Get("code") signature := c.AuthorizeCodeStrategy.AuthorizeCodeSignature(ctx, code) - authorizeRequest, err := c.CoreStorage.GetAuthorizeCodeSession(ctx, signature, requester.GetSession()) - if err != nil { - return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) - } else if err := c.AuthorizeCodeStrategy.ValidateAuthorizeCode(ctx, requester, code); err != nil { - // This needs to happen after store retrieval for the session to be hydrated properly - return errorsx.WithStack(fosite.ErrInvalidRequest.WithWrap(err).WithDebug(err.Error())) - } - - for _, scope := range authorizeRequest.GetGrantedScopes() { - requester.GrantScope(scope) - } - - for _, audience := range authorizeRequest.GetGrantedAudience() { - requester.GrantAudience(audience) - } - - access, accessSignature, err := c.AccessTokenStrategy.GenerateAccessToken(ctx, requester) - if err != nil { - return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) - } - - var refresh, refreshSignature string - if canIssueRefreshToken(ctx, c, authorizeRequest) { - refresh, refreshSignature, err = c.RefreshTokenStrategy.GenerateRefreshToken(ctx, requester) - if err != nil { - return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) - } - } - - ctx, err = storage.MaybeBeginTx(ctx, c.CoreStorage) - if err != nil { - return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) - } - defer func() { - if err != nil { - if rollBackTxnErr := storage.MaybeRollbackTx(ctx, c.CoreStorage); rollBackTxnErr != nil { - err = errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebugf("error: %s; rollback error: %s", err, rollBackTxnErr)) - } - } - }() - - if err = c.CoreStorage.InvalidateAuthorizeCodeSession(ctx, signature); err != nil { - return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) - } else if err = c.CoreStorage.CreateAccessTokenSession(ctx, accessSignature, requester.Sanitize([]string{})); err != nil { - return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) - } else if refreshSignature != "" { - if err = c.CoreStorage.CreateRefreshTokenSession(ctx, refreshSignature, requester.Sanitize([]string{})); err != nil { - return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) - } - } - - responder.SetAccessToken(access) - responder.SetTokenType("bearer") - atLifespan := fosite.GetEffectiveLifespan(requester.GetClient(), fosite.GrantTypeAuthorizationCode, fosite.AccessToken, c.Config.GetAccessTokenLifespan(ctx)) - responder.SetExpiresIn(getExpiresIn(requester, fosite.AccessToken, atLifespan, time.Now().UTC())) - responder.SetScopes(requester.GetGrantedScopes()) - if refresh != "" { - responder.SetExtra("refresh_token", refresh) - } - - if err = storage.MaybeCommitTx(ctx, c.CoreStorage); err != nil { - return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) - } + req, err := c.AuthorizeCodeStorage.GetAuthorizeCodeSession(ctx, signature, requester.GetSession()) + return code, signature, req, err +} - return nil +func (c *AuthorizeExplicitGrantTokenHandler) InvalidateSession(ctx context.Context, signature string) error { + return c.AuthorizeCodeStorage.InvalidateAuthorizeCodeSession(ctx, signature) } -func (c *AuthorizeExplicitGrantHandler) CanSkipClientAuth(ctx context.Context, requester fosite.AccessRequester) bool { +// implement TokenEndpointHandler +func (c *AuthorizeExplicitGrantTokenHandler) CanSkipClientAuth(ctx context.Context, requester fosite.AccessRequester) bool { return false } -func (c *AuthorizeExplicitGrantHandler) CanHandleTokenEndpointRequest(ctx context.Context, requester fosite.AccessRequester) bool { - // grant_type REQUIRED. - // Value MUST be set to "authorization_code" +func (c *AuthorizeExplicitGrantTokenHandler) CanHandleTokenEndpointRequest(ctx context.Context, requester fosite.AccessRequester) bool { return requester.GetGrantTypes().ExactOne("authorization_code") } diff --git a/handler/oauth2/flow_authorize_code_token_test.go b/handler/oauth2/flow_authorize_code_token_test.go index 02992171f..1c8e6bb79 100644 --- a/handler/oauth2/flow_authorize_code_token_test.go +++ b/handler/oauth2/flow_authorize_code_token_test.go @@ -30,7 +30,7 @@ func TestAuthorizeCode_PopulateTokenEndpointResponse(t *testing.T) { t.Run("strategy="+k, func(t *testing.T) { store := storage.NewMemoryStore() - var h AuthorizeExplicitGrantHandler + var h GenericCodeTokenEndpointHandler for _, c := range []struct { areq *fosite.AccessRequest description string @@ -209,12 +209,15 @@ func TestAuthorizeCode_PopulateTokenEndpointResponse(t *testing.T) { AccessTokenLifespan: time.Minute, RefreshTokenScopes: []string{"offline"}, } - h = AuthorizeExplicitGrantHandler{ - CoreStorage: store, - AuthorizeCodeStrategy: strategy, - AccessTokenStrategy: strategy, - RefreshTokenStrategy: strategy, - Config: config, + h = GenericCodeTokenEndpointHandler{ + CodeTokenEndpointHandler: &AuthorizeExplicitGrantTokenHandler{ + AuthorizeCodeStrategy: strategy, + AuthorizeCodeStorage: store, + }, + AccessTokenStrategy: strategy, + RefreshTokenStrategy: strategy, + CoreStorage: store, + Config: config, } if c.setup != nil { @@ -245,16 +248,18 @@ func TestAuthorizeCode_HandleTokenEndpointRequest(t *testing.T) { } { t.Run("strategy="+k, func(t *testing.T) { store := storage.NewMemoryStore() - - h := AuthorizeExplicitGrantHandler{ - CoreStorage: store, - AuthorizeCodeStrategy: &hmacshaStrategy, - TokenRevocationStorage: store, - Config: &fosite.Config{ - ScopeStrategy: fosite.HierarchicScopeStrategy, - AudienceMatchingStrategy: fosite.DefaultAudienceMatchingStrategy, - AuthorizeCodeLifespan: time.Minute, + config := &fosite.Config{ + ScopeStrategy: fosite.HierarchicScopeStrategy, + AudienceMatchingStrategy: fosite.DefaultAudienceMatchingStrategy, + AuthorizeCodeLifespan: time.Minute, + } + h := GenericCodeTokenEndpointHandler{ + CodeTokenEndpointHandler: &AuthorizeExplicitGrantTokenHandler{ + AuthorizeCodeStorage: store, + AuthorizeCodeStrategy: &hmacshaStrategy, }, + TokenRevocationStorage: store, + Config: config, } for i, c := range []struct { areq *fosite.AccessRequest @@ -445,6 +450,7 @@ func TestAuthorizeCode_HandleTokenEndpointRequest(t *testing.T) { func TestAuthorizeCodeTransactional_HandleTokenEndpointRequest(t *testing.T) { var mockTransactional *internal.MockTransactional var mockCoreStore *internal.MockCoreStorage + var mockAuthorizeStore *internal.MockAuthorizeCodeStorage strategy := hmacshaStrategy request := &fosite.AccessRequest{ GrantTypes: fosite.Arguments{"authorization_code"}, @@ -469,6 +475,11 @@ func TestAuthorizeCodeTransactional_HandleTokenEndpointRequest(t *testing.T) { CoreStorage } + type authorizeTransactionalStore struct { + storage.Transactional + AuthorizeCodeStorage + } + for _, testCase := range []struct { description string setup func() @@ -477,7 +488,7 @@ func TestAuthorizeCodeTransactional_HandleTokenEndpointRequest(t *testing.T) { { description: "transaction should be committed successfully if no errors occur", setup: func() { - mockCoreStore. + mockAuthorizeStore. EXPECT(). GetAuthorizeCodeSession(gomock.Any(), gomock.Any(), gomock.Any()). Return(request, nil). @@ -486,7 +497,7 @@ func TestAuthorizeCodeTransactional_HandleTokenEndpointRequest(t *testing.T) { EXPECT(). BeginTX(propagatedContext). Return(propagatedContext, nil) - mockCoreStore. + mockAuthorizeStore. EXPECT(). InvalidateAuthorizeCodeSession(gomock.Any(), gomock.Any()). Return(nil). @@ -511,7 +522,7 @@ func TestAuthorizeCodeTransactional_HandleTokenEndpointRequest(t *testing.T) { { description: "transaction should be rolled back if `InvalidateAuthorizeCodeSession` returns an error", setup: func() { - mockCoreStore. + mockAuthorizeStore. EXPECT(). GetAuthorizeCodeSession(gomock.Any(), gomock.Any(), gomock.Any()). Return(request, nil). @@ -520,7 +531,7 @@ func TestAuthorizeCodeTransactional_HandleTokenEndpointRequest(t *testing.T) { EXPECT(). BeginTX(propagatedContext). Return(propagatedContext, nil) - mockCoreStore. + mockAuthorizeStore. EXPECT(). InvalidateAuthorizeCodeSession(gomock.Any(), gomock.Any()). Return(errors.New("Whoops, a nasty database error occurred!")). @@ -536,7 +547,7 @@ func TestAuthorizeCodeTransactional_HandleTokenEndpointRequest(t *testing.T) { { description: "transaction should be rolled back if `CreateAccessTokenSession` returns an error", setup: func() { - mockCoreStore. + mockAuthorizeStore. EXPECT(). GetAuthorizeCodeSession(gomock.Any(), gomock.Any(), gomock.Any()). Return(request, nil). @@ -545,7 +556,7 @@ func TestAuthorizeCodeTransactional_HandleTokenEndpointRequest(t *testing.T) { EXPECT(). BeginTX(propagatedContext). Return(propagatedContext, nil) - mockCoreStore. + mockAuthorizeStore. EXPECT(). InvalidateAuthorizeCodeSession(gomock.Any(), gomock.Any()). Return(nil). @@ -566,7 +577,7 @@ func TestAuthorizeCodeTransactional_HandleTokenEndpointRequest(t *testing.T) { { description: "should result in a server error if transaction cannot be created", setup: func() { - mockCoreStore. + mockAuthorizeStore. EXPECT(). GetAuthorizeCodeSession(gomock.Any(), gomock.Any(), gomock.Any()). Return(request, nil). @@ -581,7 +592,7 @@ func TestAuthorizeCodeTransactional_HandleTokenEndpointRequest(t *testing.T) { { description: "should result in a server error if transaction cannot be rolled back", setup: func() { - mockCoreStore. + mockAuthorizeStore. EXPECT(). GetAuthorizeCodeSession(gomock.Any(), gomock.Any(), gomock.Any()). Return(request, nil). @@ -590,7 +601,7 @@ func TestAuthorizeCodeTransactional_HandleTokenEndpointRequest(t *testing.T) { EXPECT(). BeginTX(propagatedContext). Return(propagatedContext, nil) - mockCoreStore. + mockAuthorizeStore. EXPECT(). InvalidateAuthorizeCodeSession(gomock.Any(), gomock.Any()). Return(errors.New("Whoops, a nasty database error occurred!")). @@ -606,7 +617,7 @@ func TestAuthorizeCodeTransactional_HandleTokenEndpointRequest(t *testing.T) { { description: "should result in a server error if transaction cannot be committed", setup: func() { - mockCoreStore. + mockAuthorizeStore. EXPECT(). GetAuthorizeCodeSession(gomock.Any(), gomock.Any(), gomock.Any()). Return(request, nil). @@ -615,7 +626,7 @@ func TestAuthorizeCodeTransactional_HandleTokenEndpointRequest(t *testing.T) { EXPECT(). BeginTX(propagatedContext). Return(propagatedContext, nil) - mockCoreStore. + mockAuthorizeStore. EXPECT(). InvalidateAuthorizeCodeSession(gomock.Any(), gomock.Any()). Return(nil). @@ -650,21 +661,28 @@ func TestAuthorizeCodeTransactional_HandleTokenEndpointRequest(t *testing.T) { mockTransactional = internal.NewMockTransactional(ctrl) mockCoreStore = internal.NewMockCoreStorage(ctrl) + mockAuthorizeStore = internal.NewMockAuthorizeCodeStorage(ctrl) testCase.setup() - - handler := AuthorizeExplicitGrantHandler{ + config := &fosite.Config{ + ScopeStrategy: fosite.HierarchicScopeStrategy, + AudienceMatchingStrategy: fosite.DefaultAudienceMatchingStrategy, + AuthorizeCodeLifespan: time.Minute, + } + handler := GenericCodeTokenEndpointHandler{ + CodeTokenEndpointHandler: &AuthorizeExplicitGrantTokenHandler{ + AuthorizeCodeStrategy: &strategy, + AuthorizeCodeStorage: authorizeTransactionalStore{ + mockTransactional, + mockAuthorizeStore, + }, + }, CoreStorage: transactionalStore{ mockTransactional, mockCoreStore, }, - AccessTokenStrategy: &strategy, - RefreshTokenStrategy: &strategy, - AuthorizeCodeStrategy: &strategy, - Config: &fosite.Config{ - ScopeStrategy: fosite.HierarchicScopeStrategy, - AudienceMatchingStrategy: fosite.DefaultAudienceMatchingStrategy, - AuthorizeCodeLifespan: time.Minute, - }, + AccessTokenStrategy: &strategy, + RefreshTokenStrategy: &strategy, + Config: config, } if err := handler.PopulateTokenEndpointResponse(propagatedContext, request, response); testCase.expectError != nil { diff --git a/handler/oauth2/flow_generic_code_token.go b/handler/oauth2/flow_generic_code_token.go new file mode 100644 index 000000000..22e92190f --- /dev/null +++ b/handler/oauth2/flow_generic_code_token.go @@ -0,0 +1,224 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oauth2 + +import ( + "context" + "time" + + "github.com/ory/x/errorsx" + + "github.com/ory/fosite/storage" + + "github.com/pkg/errors" + + "github.com/ory/fosite" +) + +type CodeTokenEndpointHandler interface { + ValidateGrantTypes(ctx context.Context, requester fosite.AccessRequester) error + ValidateCode(ctx context.Context, request fosite.AccessRequester, code string) error + GetCodeAndSession(ctx context.Context, request fosite.AccessRequester) (code string, signature string, authorizeRequest fosite.Requester, err error) + InvalidateSession(ctx context.Context, signature string) error + CanSkipClientAuth(ctx context.Context, requester fosite.AccessRequester) bool + CanHandleTokenEndpointRequest(ctx context.Context, requester fosite.AccessRequester) bool +} + +type GenericCodeTokenEndpointHandler struct { + CodeTokenEndpointHandler + + AccessTokenStrategy AccessTokenStrategy + RefreshTokenStrategy RefreshTokenStrategy + CoreStorage CoreStorage + TokenRevocationStorage TokenRevocationStorage + Config interface { + fosite.AccessTokenLifespanProvider + fosite.RefreshTokenLifespanProvider + fosite.RefreshTokenScopesProvider + } +} + +var _ fosite.TokenEndpointHandler = (*GenericCodeTokenEndpointHandler)(nil) + +func (c *GenericCodeTokenEndpointHandler) HandleTokenEndpointRequest(ctx context.Context, request fosite.AccessRequester) error { + if !c.CanHandleTokenEndpointRequest(ctx, request) { + return errorsx.WithStack(errorsx.WithStack(fosite.ErrUnknownRequest)) + } + + if err := c.ValidateGrantTypes(ctx, request); err != nil { + return err + } + + code, _, authorizeRequest, err := c.GetCodeAndSession(ctx, request) + if errors.Is(err, fosite.ErrInvalidatedAuthorizeCode) || errors.Is(err, fosite.ErrInvalidatedDeviceCode) { + if authorizeRequest == nil { + return fosite.ErrServerError. + WithHint("Misconfigured code lead to an error that prohibited the OAuth 2.0 Framework from processing this request."). + WithDebug("getCodeSession must return a value for \"fosite.Requester\" when returning \"ErrInvalidatedAuthorizeCode\" or \"ErrInvalidatedDeviceCode\".") + } + + // If an authorize code is used twice, we revoke all refresh and access tokens associated with this request. + reqID := authorizeRequest.GetID() + hint := "The authorization code has already been used." + debug := "" + if revErr := c.TokenRevocationStorage.RevokeAccessToken(ctx, reqID); revErr != nil { + hint += " Additionally, an error occurred during processing the access token revocation." + debug += "Revocation of access_token lead to error " + revErr.Error() + "." + } + if revErr := c.TokenRevocationStorage.RevokeRefreshToken(ctx, reqID); revErr != nil { + hint += " Additionally, an error occurred during processing the refresh token revocation." + debug += "Revocation of refresh_token lead to error " + revErr.Error() + "." + } + return errorsx.WithStack(fosite.ErrInvalidGrant.WithHint(hint).WithDebug(debug)) + } else if errors.Is(err, fosite.ErrAuthorizationPending) { + // Don't print a stacktrace as it spams logs + return err + } else if errors.Is(err, fosite.ErrPollingRateLimited) { + return errorsx.WithStack(err) + } else if err != nil && errors.Is(err, fosite.ErrNotFound) { + return errorsx.WithStack(fosite.ErrInvalidGrant.WithWrap(err).WithDebug(err.Error())) + } else if err != nil { + return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) + } + + err = c.ValidateCode(ctx, request, code) + if err != nil { + return errorsx.WithStack(err) + } + + // Override scopes + request.SetRequestedScopes(authorizeRequest.GetRequestedScopes()) + + // Override audiences + request.SetRequestedAudience(authorizeRequest.GetRequestedAudience()) + + // The authorization server MUST ensure that the authorization code was issued to the authenticated + // confidential client, or if the client is public, ensure that the + // code was issued to "client_id" in the request, + if authorizeRequest.GetClient().GetID() != request.GetClient().GetID() { + return errorsx.WithStack(fosite.ErrInvalidGrant.WithHint("The OAuth 2.0 Client ID from this request does not match the one from the authorize request.")) + } + + // ensure that the "redirect_uri" parameter is present if the + // "redirect_uri" parameter was included in the initial authorization + // request as described in Section 4.1.1, and if included ensure that + // their values are identical. + forcedRedirectURI := authorizeRequest.GetRequestForm().Get("redirect_uri") + if forcedRedirectURI != "" && forcedRedirectURI != request.GetRequestForm().Get("redirect_uri") { + return errorsx.WithStack(fosite.ErrInvalidGrant.WithHint("The \"redirect_uri\" from this request does not match the one from the authorize request.")) + } + + // Checking of POST client_id skipped, because: + // If the client type is confidential or the client was issued client + // credentials (or assigned other authentication requirements), the + // client MUST authenticate with the authorization server as described + // in Section 3.2.1. + request.SetSession(authorizeRequest.GetSession()) + request.SetID(authorizeRequest.GetID()) + + atLifespan := fosite.GetEffectiveLifespan(request.GetClient(), fosite.GrantTypeAuthorizationCode, fosite.AccessToken, c.Config.GetAccessTokenLifespan(ctx)) + request.GetSession().SetExpiresAt(fosite.AccessToken, time.Now().UTC().Add(atLifespan).Round(time.Second)) + + rtLifespan := fosite.GetEffectiveLifespan(request.GetClient(), fosite.GrantTypeAuthorizationCode, fosite.RefreshToken, c.Config.GetRefreshTokenLifespan(ctx)) + if rtLifespan > -1 { + request.GetSession().SetExpiresAt(fosite.RefreshToken, time.Now().UTC().Add(rtLifespan).Round(time.Second)) + } + + return nil +} + +func (c *GenericCodeTokenEndpointHandler) PopulateTokenEndpointResponse(ctx context.Context, requester fosite.AccessRequester, responder fosite.AccessResponder) (err error) { + if !c.CanHandleTokenEndpointRequest(ctx, requester) { + return errorsx.WithStack(fosite.ErrUnknownRequest) + } + + code, signature, authorizeRequest, err := c.GetCodeAndSession(ctx, requester) + if err != nil { + return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) + } else if err := c.ValidateCode(ctx, requester, code); err != nil { + // This needs to happen after store retrieval for the session to be hydrated properly + return errorsx.WithStack(fosite.ErrInvalidRequest.WithWrap(err).WithDebug(err.Error())) + } + + for _, scope := range authorizeRequest.GetGrantedScopes() { + requester.GrantScope(scope) + } + + for _, audience := range authorizeRequest.GetGrantedAudience() { + requester.GrantAudience(audience) + } + + access, accessSignature, err := c.AccessTokenStrategy.GenerateAccessToken(ctx, requester) + if err != nil { + return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) + } + + var refresh, refreshSignature string + if c.canIssueRefreshToken(ctx, authorizeRequest) { + refresh, refreshSignature, err = c.RefreshTokenStrategy.GenerateRefreshToken(ctx, requester) + if err != nil { + return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) + } + } + + ctx, err = storage.MaybeBeginTx(ctx, c.CoreStorage) + if err != nil { + return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) + } + defer func() { + if err != nil { + if rollBackTxnErr := storage.MaybeRollbackTx(ctx, c.CoreStorage); rollBackTxnErr != nil { + err = errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebugf("error: %s; rollback error: %s", err, rollBackTxnErr)) + } + } + }() + + if err = c.InvalidateSession(ctx, signature); err != nil { + return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) + } + + if err = c.CoreStorage.CreateAccessTokenSession(ctx, accessSignature, requester.Sanitize([]string{})); err != nil { + return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) + } else if refreshSignature != "" { + if err = c.CoreStorage.CreateRefreshTokenSession(ctx, refreshSignature, requester.Sanitize([]string{})); err != nil { + return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) + } + } + + responder.SetAccessToken(access) + responder.SetTokenType("bearer") + atLifespan := fosite.GetEffectiveLifespan(requester.GetClient(), fosite.GrantTypeAuthorizationCode, fosite.AccessToken, c.Config.GetAccessTokenLifespan(ctx)) + responder.SetExpiresIn(getExpiresIn(requester, fosite.AccessToken, atLifespan, time.Now().UTC())) + responder.SetScopes(requester.GetGrantedScopes()) + if refresh != "" { + responder.SetExtra("refresh_token", refresh) + } + + if err = storage.MaybeCommitTx(ctx, c.CoreStorage); err != nil { + return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) + } + + return nil +} + +func (c *GenericCodeTokenEndpointHandler) canIssueRefreshToken(ctx context.Context, request fosite.Requester) bool { + scope := c.Config.GetRefreshTokenScopes(ctx) + // Require one of the refresh token scopes, if set. + if len(scope) > 0 && !request.GetGrantedScopes().HasOneOf(scope...) { + return false + } + // Do not issue a refresh token to clients that cannot use the refresh token grant type. + if !request.GetClient().GetGrantTypes().Has("refresh_token") { + return false + } + return true +} + +func (c *GenericCodeTokenEndpointHandler) CanSkipClientAuth(ctx context.Context, requester fosite.AccessRequester) bool { + return c.CodeTokenEndpointHandler.CanSkipClientAuth(ctx, requester) +} + +func (c *GenericCodeTokenEndpointHandler) CanHandleTokenEndpointRequest(ctx context.Context, requester fosite.AccessRequester) bool { + return c.CodeTokenEndpointHandler.CanHandleTokenEndpointRequest(ctx, requester) +} diff --git a/handler/oauth2/storage.go b/handler/oauth2/storage.go index 1d49ea3de..db1a9323e 100644 --- a/handler/oauth2/storage.go +++ b/handler/oauth2/storage.go @@ -10,7 +10,6 @@ import ( ) type CoreStorage interface { - AuthorizeCodeStorage AccessTokenStorage RefreshTokenStorage } diff --git a/handler/openid/flow_device_auth.go b/handler/openid/flow_device_auth.go new file mode 100644 index 000000000..56c4b0acc --- /dev/null +++ b/handler/openid/flow_device_auth.go @@ -0,0 +1,49 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package openid + +import ( + "context" + + "github.com/ory/x/errorsx" + + "github.com/ory/fosite" +) + +type OpenIDConnectDeviceHandler struct { + OpenIDConnectRequestStorage OpenIDConnectRequestStorage + OpenIDConnectRequestValidator *OpenIDConnectRequestValidator + + Config interface { + fosite.IDTokenLifespanProvider + } + + *IDTokenHandleHelper +} + +func (c *OpenIDConnectDeviceHandler) HandleDeviceUserEndpointRequest(ctx context.Context, ar fosite.DeviceUserRequester, resp fosite.DeviceUserResponder) error { + if !(ar.GetGrantedScopes().Has("openid")) { + return nil + } + + if !ar.GetClient().GetGrantTypes().Has(string(fosite.GrantTypeDeviceCode)) { + return nil + } + + if len(ar.GetDeviceCodeSignature()) == 0 { + return errorsx.WithStack(fosite.ErrMisconfiguration.WithDebug("The device code has not been issued yet, indicating a broken code configuration.")) + } + + if err := c.OpenIDConnectRequestValidator.ValidatePrompt(ctx, ar); err != nil { + return err + } + + if err := c.OpenIDConnectRequestStorage.CreateOpenIDConnectSession(ctx, ar.GetDeviceCodeSignature(), ar.Sanitize(oidcParameters)); err != nil { + return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) + } + + // there is no need to check for https, because it has already been checked by core.explicit + + return nil +} diff --git a/handler/openid/flow_device_token.go b/handler/openid/flow_device_token.go new file mode 100644 index 000000000..081d26f35 --- /dev/null +++ b/handler/openid/flow_device_token.go @@ -0,0 +1,81 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package openid + +import ( + "context" + "strings" + + "github.com/ory/x/errorsx" + + "github.com/pkg/errors" + + "github.com/ory/fosite" +) + +func (c *OpenIDConnectDeviceHandler) HandleTokenEndpointRequest(ctx context.Context, request fosite.AccessRequester) error { + return errorsx.WithStack(fosite.ErrUnknownRequest) +} + +func (OpenIDConnectDeviceHandler) getDeviceCodeSignature(token string) string { + split := strings.Split(token, ".") + + if len(split) != 2 { + return "" + } + + return split[1] +} + +func (c *OpenIDConnectDeviceHandler) PopulateTokenEndpointResponse(ctx context.Context, requester fosite.AccessRequester, responder fosite.AccessResponder) error { + if !c.CanHandleTokenEndpointRequest(ctx, requester) { + return errorsx.WithStack(fosite.ErrUnknownRequest) + } + + deviceCodeSignature := c.getDeviceCodeSignature(requester.GetRequestForm().Get("device_code")) + authorize, err := c.OpenIDConnectRequestStorage.GetOpenIDConnectSession(ctx, deviceCodeSignature, requester) + if errors.Is(err, ErrNoSessionFound) { + return errorsx.WithStack(fosite.ErrUnknownRequest.WithWrap(err).WithDebug(err.Error())) + } else if err != nil { + return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) + } + + if !authorize.GetGrantedScopes().Has("openid") { + return errorsx.WithStack(fosite.ErrMisconfiguration.WithDebug("An OpenID Connect session was found but the openid scope is missing, probably due to a broken code configuration.")) + } + + if !requester.GetClient().GetGrantTypes().Has(string(fosite.GrantTypeDeviceCode)) { + return errorsx.WithStack(fosite.ErrUnauthorizedClient.WithHint("The OAuth 2.0 Client is not allowed to use the authorization grant \"urn:ietf:params:oauth:grant-type:device_code\".")) + } + + sess, ok := authorize.GetSession().(Session) + if !ok { + return errorsx.WithStack(fosite.ErrServerError.WithDebug("Failed to generate id token because session must be of type fosite/handler/openid.Session.")) + } + + claims := sess.IDTokenClaims() + if claims.Subject == "" { + return errorsx.WithStack(fosite.ErrServerError.WithDebug("Failed to generate id token because subject is an empty string.")) + } + + claims.AccessTokenHash = c.GetAccessTokenHash(ctx, requester, responder) + + // The response type `id_token` is only required when performing the implicit or hybrid flow, see: + // https://openid.net/specs/openid-connect-registration-1_0.html + // + // if !requester.GetClient().GetResponseTypes().Has("id_token") { + // return errorsx.WithStack(fosite.ErrInvalidGrant.WithDebug("The client is not allowed to use response type id_token")) + // } + + idTokenLifespan := fosite.GetEffectiveLifespan(requester.GetClient(), fosite.GrantTypeAuthorizationCode, fosite.IDToken, c.Config.GetIDTokenLifespan(ctx)) + return c.IssueExplicitIDToken(ctx, idTokenLifespan, authorize, responder) +} + +func (c *OpenIDConnectDeviceHandler) CanSkipClientAuth(ctx context.Context, requester fosite.AccessRequester) bool { + return false +} + +func (c *OpenIDConnectDeviceHandler) CanHandleTokenEndpointRequest(ctx context.Context, requester fosite.AccessRequester) bool { + return requester.GetGrantTypes().ExactOne(string(fosite.GrantTypeDeviceCode)) +} diff --git a/handler/openid/flow_hybrid.go b/handler/openid/flow_hybrid.go index ce43496e6..adc254e65 100644 --- a/handler/openid/flow_hybrid.go +++ b/handler/openid/flow_hybrid.go @@ -16,7 +16,7 @@ import ( type OpenIDConnectHybridHandler struct { AuthorizeImplicitGrantTypeHandler *oauth2.AuthorizeImplicitGrantTypeHandler - AuthorizeExplicitGrantHandler *oauth2.AuthorizeExplicitGrantHandler + AuthorizeExplicitGrantAuthHandler *oauth2.AuthorizeExplicitGrantAuthHandler IDTokenHandleHelper *IDTokenHandleHelper OpenIDConnectRequestValidator *OpenIDConnectRequestValidator OpenIDConnectRequestStorage OpenIDConnectRequestStorage @@ -85,7 +85,7 @@ func (c *OpenIDConnectHybridHandler) HandleAuthorizeEndpointRequest(ctx context. return errorsx.WithStack(fosite.ErrInvalidGrant.WithHint("The OAuth 2.0 Client is not allowed to use authorization grant 'authorization_code'.")) } - code, signature, err := c.AuthorizeExplicitGrantHandler.AuthorizeCodeStrategy.GenerateAuthorizeCode(ctx, ar) + code, signature, err := c.AuthorizeExplicitGrantAuthHandler.AuthorizeCodeStrategy.GenerateAuthorizeCode(ctx, ar) if err != nil { return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) } @@ -98,8 +98,8 @@ func (c *OpenIDConnectHybridHandler) HandleAuthorizeEndpointRequest(ctx context. // } // This is required because we must limit the authorize code lifespan. - ar.GetSession().SetExpiresAt(fosite.AuthorizeCode, time.Now().UTC().Add(c.AuthorizeExplicitGrantHandler.Config.GetAuthorizeCodeLifespan(ctx)).Round(time.Second)) - if err := c.AuthorizeExplicitGrantHandler.CoreStorage.CreateAuthorizeCodeSession(ctx, signature, ar.Sanitize(c.AuthorizeExplicitGrantHandler.GetSanitationWhiteList(ctx))); err != nil { + ar.GetSession().SetExpiresAt(fosite.AuthorizeCode, time.Now().UTC().Add(c.AuthorizeExplicitGrantAuthHandler.Config.GetAuthorizeCodeLifespan(ctx)).Round(time.Second)) + if err := c.AuthorizeExplicitGrantAuthHandler.AuthorizeCodeStorage.CreateAuthorizeCodeSession(ctx, signature, ar.Sanitize(c.AuthorizeExplicitGrantAuthHandler.GetSanitationWhiteList(ctx))); err != nil { return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) } diff --git a/handler/openid/flow_hybrid_test.go b/handler/openid/flow_hybrid_test.go index c42b97f19..a9d2a64cf 100644 --- a/handler/openid/flow_hybrid_test.go +++ b/handler/openid/flow_hybrid_test.go @@ -64,11 +64,11 @@ func makeOpenIDConnectHybridHandler(minParameterEntropy int) OpenIDConnectHybrid AuthorizeCodeLifespan: time.Hour, RefreshTokenLifespan: time.Hour, } + store := storage.NewMemoryStore() return OpenIDConnectHybridHandler{ - AuthorizeExplicitGrantHandler: &oauth2.AuthorizeExplicitGrantHandler{ + AuthorizeExplicitGrantAuthHandler: &oauth2.AuthorizeExplicitGrantAuthHandler{ AuthorizeCodeStrategy: hmacStrategy, - AccessTokenStrategy: hmacStrategy, - CoreStorage: storage.NewMemoryStore(), + AuthorizeCodeStorage: store, Config: config, }, AuthorizeImplicitGrantTypeHandler: &oauth2.AuthorizeImplicitGrantTypeHandler{ diff --git a/handler/openid/validator.go b/handler/openid/validator.go index 5208d039c..224084caf 100644 --- a/handler/openid/validator.go +++ b/handler/openid/validator.go @@ -37,33 +37,35 @@ func NewOpenIDConnectRequestValidator(strategy jwt.Signer, config openIDConnectR } } -func (v *OpenIDConnectRequestValidator) ValidatePrompt(ctx context.Context, req fosite.AuthorizeRequester) error { +func (v *OpenIDConnectRequestValidator) ValidatePrompt(ctx context.Context, req fosite.Requester) error { // prompt is case sensitive! requiredPrompt := fosite.RemoveEmpty(strings.Split(req.GetRequestForm().Get("prompt"), " ")) - if req.GetClient().IsPublic() { - // Threat: Malicious Client Obtains Existing Authorization by Fraud - // https://tools.ietf.org/html/rfc6819#section-4.2.3 - // - // Authorization servers should not automatically process repeat - // authorizations to public clients unless the client is validated - // using a pre-registered redirect URI - - // Client Impersonation - // https://tools.ietf.org/html/rfc8252#section-8.6# - // - // As stated in Section 10.2 of OAuth 2.0 [RFC6749], the authorization - // server SHOULD NOT process authorization requests automatically - // without user consent or interaction, except when the identity of the - // client can be assured. This includes the case where the user has - // previously approved an authorization request for a given client id -- - // unless the identity of the client can be proven, the request SHOULD - // be processed as if no previous request had been approved. - - checker := v.Config.GetRedirectSecureChecker(ctx) - if stringslice.Has(requiredPrompt, "none") { - if !checker(ctx, req.GetRedirectURI()) { - return errorsx.WithStack(fosite.ErrConsentRequired.WithHint("OAuth 2.0 Client is marked public and redirect uri is not considered secure (https missing), but \"prompt=none\" was requested.")) + if ar, ok := req.(fosite.AuthorizeRequester); ok { + if req.GetClient().IsPublic() { + // Threat: Malicious Client Obtains Existing Authorization by Fraud + // https://tools.ietf.org/html/rfc6819#section-4.2.3 + // + // Authorization servers should not automatically process repeat + // authorizations to public clients unless the client is validated + // using a pre-registered redirect URI + + // Client Impersonation + // https://tools.ietf.org/html/rfc8252#section-8.6# + // + // As stated in Section 10.2 of OAuth 2.0 [RFC6749], the authorization + // server SHOULD NOT process authorization requests automatically + // without user consent or interaction, except when the identity of the + // client can be assured. This includes the case where the user has + // previously approved an authorization request for a given client id -- + // unless the identity of the client can be proven, the request SHOULD + // be processed as if no previous request had been approved. + + checker := v.Config.GetRedirectSecureChecker(ctx) + if stringslice.Has(requiredPrompt, "none") { + if !checker(ctx, ar.GetRedirectURI()) { + return errorsx.WithStack(fosite.ErrConsentRequired.WithHint("OAuth 2.0 Client is marked public and redirect uri is not considered secure (https missing), but \"prompt=none\" was requested.")) + } } } } diff --git a/handler/openid/validator_test.go b/handler/openid/validator_test.go index a36d47826..3ae72a026 100644 --- a/handler/openid/validator_test.go +++ b/handler/openid/validator_test.go @@ -268,6 +268,252 @@ func TestValidatePrompt(t *testing.T) { } } +func TestDeviceValidatePrompt(t *testing.T) { + config := &fosite.Config{ + MinParameterEntropy: fosite.MinParameterEntropy, + } + var j = &DefaultStrategy{ + Signer: &jwt.DefaultSigner{ + GetPrivateKey: func(_ context.Context) (interface{}, error) { + return key, nil + }}, + Config: &fosite.Config{ + MinParameterEntropy: fosite.MinParameterEntropy, + }, + } + + v := NewOpenIDConnectRequestValidator(j, config) + + var genIDToken = func(c jwt.IDTokenClaims) string { + s, _, err := j.Generate(context.TODO(), c.ToMapClaims(), jwt.NewHeaders()) + require.NoError(t, err) + return s + } + + for k, tc := range []struct { + d string + prompt string + isPublic bool + expectErr bool + idTokenHint string + s *DefaultSession + }{ + { + d: "should fail because prompt=none should not work together with public clients and http non-localhost", + prompt: "none", + isPublic: true, + expectErr: false, + s: &DefaultSession{ + Subject: "foo", + Claims: &jwt.IDTokenClaims{ + Subject: "foo", + RequestedAt: time.Now().UTC(), + AuthTime: time.Now().UTC().Add(-time.Minute), + }, + }, + }, + { + d: "should pass because prompt=none works for public clients and http localhost", + prompt: "none", + isPublic: true, + expectErr: false, + s: &DefaultSession{ + Subject: "foo", + Claims: &jwt.IDTokenClaims{ + Subject: "foo", + RequestedAt: time.Now().UTC(), + AuthTime: time.Now().UTC().Add(-time.Minute), + }, + }, + }, + { + d: "should pass", + prompt: "none", + isPublic: true, + expectErr: false, + s: &DefaultSession{ + Subject: "foo", + Claims: &jwt.IDTokenClaims{ + Subject: "foo", + RequestedAt: time.Now().UTC(), + AuthTime: time.Now().UTC().Add(-time.Minute), + }, + }, + }, + { + d: "should fail because prompt=none requires an auth time being set", + prompt: "none", + isPublic: false, + expectErr: true, + s: &DefaultSession{ + Subject: "foo", + Claims: &jwt.IDTokenClaims{ + Subject: "foo", + RequestedAt: time.Now().UTC(), + }, + }, + }, + { + d: "should fail because prompt=none and auth time is recent (after requested at)", + prompt: "none", + isPublic: false, + expectErr: true, + s: &DefaultSession{ + Subject: "foo", + Claims: &jwt.IDTokenClaims{ + Subject: "foo", + RequestedAt: time.Now().UTC().Add(-time.Minute), + AuthTime: time.Now().UTC(), + }, + }, + }, + { + d: "should pass because prompt=none and auth time is in the past (before requested at)", + prompt: "none", + isPublic: false, + expectErr: false, + s: &DefaultSession{ + Subject: "foo", + Claims: &jwt.IDTokenClaims{ + Subject: "foo", + RequestedAt: time.Now().UTC(), + AuthTime: time.Now().UTC().Add(-time.Minute), + }, + }, + }, + { + d: "should fail because prompt=none can not be used together with other prompts", + prompt: "none login", + isPublic: false, + expectErr: true, + s: &DefaultSession{ + Subject: "foo", + Claims: &jwt.IDTokenClaims{ + Subject: "foo", + RequestedAt: time.Now().UTC(), + AuthTime: time.Now().UTC(), + }, + }, + }, + { + d: "should fail because prompt=foo is an unknown value", + prompt: "foo", + isPublic: false, + expectErr: true, + s: &DefaultSession{ + Subject: "foo", + Claims: &jwt.IDTokenClaims{ + Subject: "foo", + RequestedAt: time.Now().UTC(), + AuthTime: time.Now().UTC(), + }, + }, + }, + { + d: "should pass because requesting consent and login works with public clients", + prompt: "login consent", + isPublic: true, + expectErr: false, + s: &DefaultSession{ + Subject: "foo", + Claims: &jwt.IDTokenClaims{ + Subject: "foo", + RequestedAt: time.Now().UTC().Add(-time.Second * 5), + AuthTime: time.Now().UTC().Add(-time.Second), + }, + }, + }, + { + d: "should pass because requesting consent and login works with confidential clients", + prompt: "login consent", + isPublic: false, + expectErr: false, + s: &DefaultSession{ + Subject: "foo", + Claims: &jwt.IDTokenClaims{ + Subject: "foo", + RequestedAt: time.Now().UTC().Add(-time.Second * 5), + AuthTime: time.Now().UTC().Add(-time.Second), + }, + }, + }, + { + d: "should fail subject from ID token does not match subject from session", + prompt: "login", + isPublic: false, + expectErr: true, + s: &DefaultSession{ + Subject: "foo", + Claims: &jwt.IDTokenClaims{ + Subject: "foo", + RequestedAt: time.Now().UTC(), + AuthTime: time.Now().UTC().Add(-time.Second), + }, + }, + idTokenHint: genIDToken(jwt.IDTokenClaims{ + Subject: "bar", + RequestedAt: time.Now(), + ExpiresAt: time.Now().Add(time.Hour), + }), + }, + { + d: "should pass subject from ID token matches subject from session", + prompt: "", + isPublic: false, + expectErr: false, + s: &DefaultSession{ + Subject: "foo", + Claims: &jwt.IDTokenClaims{ + Subject: "foo", + RequestedAt: time.Now().UTC(), + AuthTime: time.Now().UTC().Add(-time.Second), + }, + }, + idTokenHint: genIDToken(jwt.IDTokenClaims{ + Subject: "foo", + RequestedAt: time.Now(), + ExpiresAt: time.Now().Add(time.Hour), + }), + }, + { + d: "should pass subject from ID token matches subject from session even though id token is expired", + prompt: "", + isPublic: false, + expectErr: false, + s: &DefaultSession{ + Subject: "foo", + Claims: &jwt.IDTokenClaims{ + Subject: "foo", + RequestedAt: time.Now().UTC(), + AuthTime: time.Now().UTC().Add(-time.Second), + ExpiresAt: time.Now().UTC().Add(-time.Second), + }, + }, + idTokenHint: genIDToken(jwt.IDTokenClaims{ + Subject: "foo", + RequestedAt: time.Now(), + ExpiresAt: time.Now().Add(time.Hour), + }), + }, + } { + t.Run(fmt.Sprintf("case=%d/description=%s", k, tc.d), func(t *testing.T) { + t.Logf("%s", tc.idTokenHint) + err := v.ValidatePrompt(context.TODO(), &fosite.DeviceUserRequest{ + Request: fosite.Request{ + Form: url.Values{"prompt": {tc.prompt}, "id_token_hint": {tc.idTokenHint}}, + Client: &fosite.DefaultClient{Public: tc.isPublic}, + Session: tc.s, + }, + }) + if tc.expectErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + func parse(u string) *url.URL { o, _ := url.Parse(u) return o diff --git a/handler/rfc8628/auth_handler.go b/handler/rfc8628/auth_handler.go new file mode 100644 index 000000000..af3fc00db --- /dev/null +++ b/handler/rfc8628/auth_handler.go @@ -0,0 +1,61 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package rfc8628 + +import ( + "context" + "time" + + "github.com/ory/fosite" + "github.com/ory/x/errorsx" +) + +type DeviceAuthHandler struct { + Storage RFC8628CodeStorage + Strategy RFC8628CodeStrategy + Config interface { + fosite.DeviceProvider + fosite.DeviceAndUserCodeLifespanProvider + } +} + +// DeviceAuthorizationHandler is a response handler for the Device Authorisation Grant as +// defined in https://tools.ietf.org/html/rfc8628#section-3.1 +func (d *DeviceAuthHandler) HandleDeviceEndpointRequest(ctx context.Context, dar fosite.DeviceRequester, resp fosite.DeviceResponder) error { + deviceCode, deviceCodeSignature, err := d.Strategy.GenerateDeviceCode(ctx) + if err != nil { + return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) + } + + userCode, userCodeSignature, err := d.Strategy.GenerateUserCode(ctx) + if err != nil { + return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) + } + + // Save the real request_id + requestId := dar.GetID() + + // Store the User Code session (this has no real data other that the uer and device code), can be converted into a 'full' session after user auth + dar.GetSession().SetExpiresAt(fosite.AuthorizeCode, time.Now().UTC().Add(d.Config.GetDeviceAndUserCodeLifespan(ctx))) + if err := d.Storage.CreateDeviceCodeSession(ctx, deviceCodeSignature, dar.Sanitize(nil)); err != nil { + return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) + } + + // Fake the RequestId field to store the DeviceCodeSignature for easy handling + dar.SetID(deviceCodeSignature) + dar.GetSession().SetExpiresAt(fosite.UserCode, time.Now().UTC().Add(d.Config.GetDeviceAndUserCodeLifespan(ctx)).Round(time.Second)) + if err := d.Storage.CreateUserCodeSession(ctx, userCodeSignature, dar.Sanitize(nil)); err != nil { + return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) + } + dar.SetID(requestId) + + // Populate the response fields + resp.SetDeviceCode(deviceCode) + resp.SetUserCode(userCode) + resp.SetVerificationURI(d.Config.GetDeviceVerificationURL(ctx)) + resp.SetVerificationURIComplete(d.Config.GetDeviceVerificationURL(ctx) + "?user_code=" + userCode) + resp.SetExpiresIn(int64(time.Until(dar.GetSession().GetExpiresAt(fosite.UserCode)).Seconds())) + resp.SetInterval(int(d.Config.GetDeviceAuthTokenPollingInterval(ctx).Seconds())) + return nil +} diff --git a/handler/rfc8628/auth_handler_test.go b/handler/rfc8628/auth_handler_test.go new file mode 100644 index 000000000..ef59b97f1 --- /dev/null +++ b/handler/rfc8628/auth_handler_test.go @@ -0,0 +1,50 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package rfc8628_test + +import ( + "context" + "testing" + "time" + + "github.com/golang/mock/gomock" + "github.com/ory/fosite" + . "github.com/ory/fosite/handler/rfc8628" + "github.com/ory/fosite/storage" + "github.com/stretchr/testify/assert" +) + +func Test_HandleDeviceEndpointRequest(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + store := storage.NewMemoryStore() + handler := DeviceAuthHandler{ + Storage: store, + Strategy: &hmacshaStrategy, + Config: &fosite.Config{ + DeviceAndUserCodeLifespan: time.Minute * 10, + DeviceAuthTokenPollingInterval: time.Second * 5, + DeviceVerificationURL: "www.test.com", + AccessTokenLifespan: time.Hour, + RefreshTokenLifespan: time.Hour, + ScopeStrategy: fosite.HierarchicScopeStrategy, + AudienceMatchingStrategy: fosite.DefaultAudienceMatchingStrategy, + RefreshTokenScopes: []string{"offline"}, + }, + } + + req := &fosite.Request{ + Session: &fosite.DefaultSession{}, + } + resp := &fosite.DeviceResponse{} + + handler.HandleDeviceEndpointRequest(context.TODO(), req, resp) + + assert.NotEmpty(t, resp.GetDeviceCode()) + assert.NotEmpty(t, resp.GetUserCode()) + assert.Equal(t, len(resp.GetUserCode()), 8) + assert.Contains(t, resp.GetDeviceCode(), "ory_dc_") + assert.Contains(t, resp.GetDeviceCode(), ".") + assert.Equal(t, resp.GetVerificationURI(), "www.test.com") +} diff --git a/handler/rfc8628/storage.go b/handler/rfc8628/storage.go new file mode 100644 index 000000000..1bc22c853 --- /dev/null +++ b/handler/rfc8628/storage.go @@ -0,0 +1,52 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package rfc8628 + +import ( + "context" + + "github.com/ory/fosite" +) + +type RFC8628CodeStorage interface { + DeviceCodeStorage + UserCodeStorage +} + +type DeviceCodeStorage interface { + // CreateDeviceCodeSession stores the device request for a given device code. + CreateDeviceCodeSession(ctx context.Context, signature string, request fosite.Requester) (err error) + + // UpdateDeviceCodeSession udpate in store the device code session for a given device code. + UpdateDeviceCodeSession(ctx context.Context, signature string, request fosite.Requester) (err error) + + // GetDeviceCodeSession hydrates the session based on the given device code and returns the device request. + // If the device code has been invalidated with `InvalidateDeviceCodeSession`, this + // method should return the ErrInvalidatedDeviceCode error. + // + // Make sure to also return the fosite.Requester value when returning the fosite.ErrInvalidatedDeviceCode error! + GetDeviceCodeSession(ctx context.Context, signature string, session fosite.Session) (request fosite.Requester, err error) + + // InvalidateDeviceCodeSession is called when an device code is being used. The state of the user + // code should be set to invalid and consecutive requests to GetDeviceCodeSession should return the + // ErrInvalidatedDeviceCode error. + InvalidateDeviceCodeSession(ctx context.Context, signature string) (err error) +} + +type UserCodeStorage interface { + // CreateUserCodeSession stores the device request for a given user code. + CreateUserCodeSession(ctx context.Context, signature string, request fosite.Requester) (err error) + + // GetUserCodeSession hydrates the session based on the given user code and returns the device request. + // If the user code has been invalidated with `InvalidateUserCodeSession`, this + // method should return the ErrInvalidatedUserCode error. + // + // Make sure to also return the fosite.Requester value when returning the fosite.ErrInvalidatedUserCode error! + GetUserCodeSession(ctx context.Context, signature string, session fosite.Session) (request fosite.Requester, err error) + + // InvalidateUserCodeSession is called when an user code is being used. The state of the user + // code should be set to invalid and consecutive requests to GetUserCodeSession should return the + // ErrInvalidatedUserCode error. + InvalidateUserCodeSession(ctx context.Context, signature string) (err error) +} diff --git a/handler/rfc8628/strategy.go b/handler/rfc8628/strategy.go new file mode 100644 index 000000000..3b0df7a71 --- /dev/null +++ b/handler/rfc8628/strategy.go @@ -0,0 +1,32 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package rfc8628 + +import ( + "context" + + "github.com/ory/fosite" +) + +type RFC8628CodeStrategy interface { + DeviceRateLimitStrategy + DeviceCodeStrategy + UserCodeStrategy +} + +type DeviceRateLimitStrategy interface { + ShouldRateLimit(ctx context.Context, code string) bool +} + +type DeviceCodeStrategy interface { + DeviceCodeSignature(ctx context.Context, code string) (signature string, err error) + GenerateDeviceCode(ctx context.Context) (code string, signature string, err error) + ValidateDeviceCode(ctx context.Context, r fosite.Requester, code string) (err error) +} + +type UserCodeStrategy interface { + UserCodeSignature(ctx context.Context, code string) (signature string, err error) + GenerateUserCode(ctx context.Context) (code string, signature string, err error) + ValidateUserCode(ctx context.Context, r fosite.Requester, code string) (err error) +} diff --git a/handler/rfc8628/strategy_hmacsha.go b/handler/rfc8628/strategy_hmacsha.go new file mode 100644 index 000000000..b45d7f8a1 --- /dev/null +++ b/handler/rfc8628/strategy_hmacsha.go @@ -0,0 +1,104 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package rfc8628 + +import ( + "context" + "strconv" + "strings" + "time" + + "github.com/ory/x/errorsx" + "github.com/ory/x/randx" + "github.com/patrickmn/go-cache" + "golang.org/x/time/rate" + + "github.com/ory/fosite" + enigma "github.com/ory/fosite/token/hmac" +) + +type DefaultDeviceStrategy struct { + Enigma *enigma.HMACStrategy + RateLimiterCache *cache.Cache + Config interface { + fosite.DeviceProvider + fosite.DeviceAndUserCodeLifespanProvider + } +} + +var _ RFC8628CodeStrategy = (*DefaultDeviceStrategy)(nil) + +func (h *DefaultDeviceStrategy) GenerateUserCode(ctx context.Context) (token string, signature string, err error) { + seq, err := randx.RuneSequence(8, []rune(randx.AlphaUpperNoVowels)) + if err != nil { + return "", "", err + } + userCode := string(seq) + signUserCode, signErr := h.UserCodeSignature(ctx, userCode) + if signErr != nil { + return "", "", err + } + return userCode, signUserCode, nil +} + +func (h *DefaultDeviceStrategy) UserCodeSignature(ctx context.Context, token string) (signature string, err error) { + return h.Enigma.GenerateHMACForString(ctx, token) +} + +func (h *DefaultDeviceStrategy) ValidateUserCode(ctx context.Context, r fosite.Requester, code string) (err error) { + var exp = r.GetSession().GetExpiresAt(fosite.UserCode) + if exp.IsZero() && r.GetRequestedAt().Add(h.Config.GetDeviceAndUserCodeLifespan(ctx)).Before(time.Now().UTC()) { + return errorsx.WithStack(fosite.ErrDeviceExpiredToken.WithHintf("User code expired at '%s'.", r.GetRequestedAt().Add(h.Config.GetDeviceAndUserCodeLifespan(ctx)))) + } + if !exp.IsZero() && exp.Before(time.Now().UTC()) { + return errorsx.WithStack(fosite.ErrDeviceExpiredToken.WithHintf("User code expired at '%s'.", exp)) + } + return nil +} + +func (h *DefaultDeviceStrategy) GenerateDeviceCode(ctx context.Context) (token string, signature string, err error) { + token, sig, err := h.Enigma.Generate(ctx) + if err != nil { + return "", "", err + } + + return "ory_dc_" + token, sig, nil +} + +func (h *DefaultDeviceStrategy) DeviceCodeSignature(ctx context.Context, token string) (signature string, err error) { + return h.Enigma.Signature(token), nil +} + +func (h *DefaultDeviceStrategy) ValidateDeviceCode(ctx context.Context, r fosite.Requester, code string) (err error) { + var exp = r.GetSession().GetExpiresAt(fosite.DeviceCode) + if exp.IsZero() && r.GetRequestedAt().Add(h.Config.GetDeviceAndUserCodeLifespan(ctx)).Before(time.Now().UTC()) { + return errorsx.WithStack(fosite.ErrDeviceExpiredToken.WithHintf("Device code expired at '%s'.", r.GetRequestedAt().Add(h.Config.GetDeviceAndUserCodeLifespan(ctx)))) + } + + if !exp.IsZero() && exp.Before(time.Now().UTC()) { + return errorsx.WithStack(fosite.ErrDeviceExpiredToken.WithHintf("Device code expired at '%s'.", exp)) + } + + return h.Enigma.Validate(ctx, strings.TrimPrefix(code, "ory_dc_")) +} + +func (t *DefaultDeviceStrategy) ShouldRateLimit(context context.Context, code string) bool { + key := code + "_limiter" + + if x, found := t.RateLimiterCache.Get(key); found { + return !x.(*rate.Limiter).Allow() + } + + rateLimiter := rate.NewLimiter( + rate.Every( + t.Config.GetDeviceAuthTokenPollingInterval(context), + ), + 1, + ) + + print(time.Now().String() + " -> " + strconv.FormatBool(!rateLimiter.Allow()) + "\n") + + t.RateLimiterCache.Set(key, rateLimiter, cache.DefaultExpiration) + return false +} diff --git a/handler/rfc8628/strategy_hmacsha_test.go b/handler/rfc8628/strategy_hmacsha_test.go new file mode 100644 index 000000000..8181921c3 --- /dev/null +++ b/handler/rfc8628/strategy_hmacsha_test.go @@ -0,0 +1,168 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package rfc8628_test + +import ( + "context" + "fmt" + "regexp" + "strings" + "testing" + "time" + + "github.com/patrickmn/go-cache" + "github.com/stretchr/testify/assert" + + "github.com/ory/fosite" + . "github.com/ory/fosite/handler/rfc8628" + "github.com/ory/fosite/token/hmac" +) + +var hmacshaStrategy = DefaultDeviceStrategy{ + Enigma: &hmac.HMACStrategy{Config: &fosite.Config{GlobalSecret: []byte("foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobar")}}, + RateLimiterCache: cache.New(24*time.Minute, 2*24*time.Minute), + Config: &fosite.Config{ + AccessTokenLifespan: time.Minute * 24, + AuthorizeCodeLifespan: time.Minute * 24, + DeviceAndUserCodeLifespan: time.Minute * 24, + DeviceAuthTokenPollingInterval: 400 * time.Millisecond, + }, +} + +var hmacExpiredCase = fosite.Request{ + Client: &fosite.DefaultClient{ + Secret: []byte("foobarfoobarfoobarfoobar"), + }, + Session: &fosite.DefaultSession{ + ExpiresAt: map[fosite.TokenType]time.Time{ + fosite.UserCode: time.Now().UTC().Add(-time.Hour), + fosite.DeviceCode: time.Now().UTC().Add(-time.Hour), + }, + }, +} + +var hmacValidCase = fosite.Request{ + Client: &fosite.DefaultClient{ + Secret: []byte("foobarfoobarfoobarfoobar"), + }, + Session: &fosite.DefaultSession{ + ExpiresAt: map[fosite.TokenType]time.Time{ + fosite.UserCode: time.Now().UTC().Add(time.Hour), + fosite.DeviceCode: time.Now().UTC().Add(time.Hour), + }, + }, +} + +var hmacValidZeroTimeRefreshCase = fosite.Request{ + Client: &fosite.DefaultClient{ + Secret: []byte("foobarfoobarfoobarfoobar"), + }, + RequestedAt: time.Now().UTC().Add(-time.Hour * 48), + Session: &fosite.DefaultSession{ + ExpiresAt: map[fosite.TokenType]time.Time{ + fosite.UserCode: {}, + fosite.DeviceCode: {}, + }, + }, +} + +func TestHMACUserCode(t *testing.T) { + for k, c := range []struct { + r fosite.Request + pass bool + }{ + { + r: hmacValidCase, + pass: true, + }, + { + r: hmacExpiredCase, + pass: false, + }, + { + r: hmacValidZeroTimeRefreshCase, + pass: false, + }, + } { + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + userCode, signature, err := hmacshaStrategy.GenerateUserCode(context.TODO()) + assert.NoError(t, err) + regex := regexp.MustCompile("[BCDFGHJKLMNPQRSTVWXZ]{8}") + assert.Equal(t, len(regex.FindString(userCode)), len(userCode)) + + err = hmacshaStrategy.ValidateUserCode(context.TODO(), &c.r, userCode) + if c.pass { + assert.NoError(t, err) + validate, _ := hmacshaStrategy.Enigma.GenerateHMACForString(context.TODO(), userCode) + assert.Equal(t, signature, validate) + testSign, err := hmacshaStrategy.UserCodeSignature(context.TODO(), userCode) + assert.NoError(t, err) + assert.Equal(t, testSign, signature) + } else { + assert.Error(t, err) + } + }) + } +} + +func TestHMACDeviceCode(t *testing.T) { + for k, c := range []struct { + r fosite.Request + pass bool + }{ + { + r: hmacValidCase, + pass: true, + }, + { + r: hmacExpiredCase, + pass: false, + }, + { + r: hmacValidZeroTimeRefreshCase, + pass: false, + }, + } { + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + token, signature, err := hmacshaStrategy.GenerateDeviceCode(context.TODO()) + assert.NoError(t, err) + assert.Equal(t, strings.Split(token, ".")[1], signature) + assert.Contains(t, token, "ory_dc_") + + for k, token := range []string{ + token, + strings.TrimPrefix(token, "ory_dc_"), + } { + t.Run(fmt.Sprintf("prefix=%v", k == 0), func(t *testing.T) { + err = hmacshaStrategy.ValidateDeviceCode(context.TODO(), &c.r, token) + if c.pass { + assert.NoError(t, err) + validate := hmacshaStrategy.Enigma.Signature(token) + assert.Equal(t, signature, validate) + testSign, err := hmacshaStrategy.DeviceCodeSignature(context.TODO(), token) + assert.NoError(t, err) + assert.Equal(t, testSign, signature) + } else { + assert.Error(t, err) + } + }) + } + }) + } +} + +func TestRateLimit(t *testing.T) { + t.Run("ratelimit no-wait", func(t *testing.T) { + hmacshaStrategy.RateLimiterCache.Flush() + assert.False(t, hmacshaStrategy.ShouldRateLimit(context.TODO(), "AAA")) + assert.True(t, hmacshaStrategy.ShouldRateLimit(context.TODO(), "AAA")) + }) + + t.Run("ratelimit wait", func(t *testing.T) { + hmacshaStrategy.RateLimiterCache.Flush() + assert.False(t, hmacshaStrategy.ShouldRateLimit(context.TODO(), "AAA")) + time.Sleep(500 * time.Millisecond) + assert.False(t, hmacshaStrategy.ShouldRateLimit(context.TODO(), "AAA")) + }) +} diff --git a/handler/rfc8628/token_endpoint_handler.go b/handler/rfc8628/token_endpoint_handler.go new file mode 100644 index 000000000..e1f581f92 --- /dev/null +++ b/handler/rfc8628/token_endpoint_handler.go @@ -0,0 +1,69 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package rfc8628 + +import ( + "context" + + "github.com/ory/fosite/handler/oauth2" + "github.com/ory/x/errorsx" + + "github.com/ory/fosite" +) + +// DeviceHandler is a token response handler for the Device Code introduced in the Device Authorize Grant +// as defined in https://www.rfc-editor.org/rfc/rfc8628 +type DeviceHandler struct { + DeviceRateLimitStrategy DeviceRateLimitStrategy + DeviceStrategy DeviceCodeStrategy + DeviceStorage DeviceCodeStorage +} + +type DeviceCodeTokenEndpointHandler struct { + oauth2.GenericCodeTokenEndpointHandler +} + +var _ oauth2.CodeTokenEndpointHandler = (*DeviceHandler)(nil) +var _ fosite.TokenEndpointHandler = (*DeviceCodeTokenEndpointHandler)(nil) + +func (c *DeviceHandler) ValidateGrantTypes(ctx context.Context, requester fosite.AccessRequester) error { + if !requester.GetClient().GetGrantTypes().Has(string(fosite.GrantTypeDeviceCode)) { + return errorsx.WithStack(fosite.ErrUnauthorizedClient.WithHint("The OAuth 2.0 Client is not allowed to use authorization grant \"urn:ietf:params:oauth:grant-type:device_code\".")) + } + + return nil +} + +func (c *DeviceHandler) ValidateCode(ctx context.Context, request fosite.AccessRequester, code string) error { + return c.DeviceStrategy.ValidateDeviceCode(ctx, request, code) +} + +func (c *DeviceHandler) GetCodeAndSession(ctx context.Context, requester fosite.AccessRequester) (code string, signature string, request fosite.Requester, err error) { + code = requester.GetRequestForm().Get("device_code") + + if c.DeviceRateLimitStrategy.ShouldRateLimit(ctx, code) { + return "", "", nil, fosite.ErrPollingRateLimited + } + + signature, err = c.DeviceStrategy.DeviceCodeSignature(ctx, code) + if err != nil { + return "", "", nil, errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) + } + + req, err := c.DeviceStorage.GetDeviceCodeSession(ctx, signature, requester.GetSession()) + return code, signature, req, err +} + +func (c *DeviceHandler) InvalidateSession(ctx context.Context, signature string) error { + return c.DeviceStorage.InvalidateDeviceCodeSession(ctx, signature) +} + +// implement TokenEndpointHandler +func (c *DeviceHandler) CanSkipClientAuth(ctx context.Context, requester fosite.AccessRequester) bool { + return requester.GetGrantTypes().ExactOne(string(fosite.GrantTypeDeviceCode)) +} + +func (c *DeviceHandler) CanHandleTokenEndpointRequest(ctx context.Context, requester fosite.AccessRequester) bool { + return requester.GetGrantTypes().ExactOne(string(fosite.GrantTypeDeviceCode)) +} diff --git a/handler/rfc8628/token_endpoint_handler_test.go b/handler/rfc8628/token_endpoint_handler_test.go new file mode 100644 index 000000000..fa0452e99 --- /dev/null +++ b/handler/rfc8628/token_endpoint_handler_test.go @@ -0,0 +1,674 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package rfc8628 + +import ( + "context" + "fmt" + "net/url" + "testing" //"time" + + "github.com/golang/mock/gomock" + + "github.com/ory/fosite/handler/oauth2" + "github.com/ory/fosite/internal" + "github.com/ory/fosite/token/hmac" + + //"github.com/golang/mock/gomock" + "time" + + "github.com/ory/fosite" //"github.com/ory/fosite/internal" + "github.com/ory/fosite/storage" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var hmacshaStrategy = oauth2.HMACSHAStrategy{ + Enigma: &hmac.HMACStrategy{Config: &fosite.Config{GlobalSecret: []byte("foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobar")}}, + Config: &fosite.Config{ + AccessTokenLifespan: time.Hour * 24, + AuthorizeCodeLifespan: time.Hour * 24, + }, +} + +var RFC8628HMACSHAStrategy = DefaultDeviceStrategy{ + Enigma: &hmac.HMACStrategy{Config: &fosite.Config{GlobalSecret: []byte("foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobar")}}, + Config: &fosite.Config{ + DeviceAndUserCodeLifespan: time.Hour * 24, + }, +} + +func TestDeviceUserCode_PopulateTokenEndpointResponse(t *testing.T) { + for k, strategy := range map[string]struct { + oauth2.CoreStrategy + RFC8628CodeStrategy + }{ + "hmac": {&hmacshaStrategy, &RFC8628HMACSHAStrategy}, + } { + t.Run("strategy="+k, func(t *testing.T) { + store := storage.NewMemoryStore() + + var h oauth2.GenericCodeTokenEndpointHandler + for _, c := range []struct { + areq *fosite.AccessRequest + description string + setup func(t *testing.T, areq *fosite.AccessRequest, config *fosite.Config) + check func(t *testing.T, aresp *fosite.AccessResponse) + expectErr error + }{ + { + areq: &fosite.AccessRequest{ + GrantTypes: fosite.Arguments{"123"}, + }, + description: "should fail because not responsible", + expectErr: fosite.ErrUnknownRequest, + }, + { + areq: &fosite.AccessRequest{ + GrantTypes: fosite.Arguments{"urn:ietf:params:oauth:grant-type:device_code"}, + Request: fosite.Request{ + Form: url.Values{}, + Client: &fosite.DefaultClient{ + GrantTypes: fosite.Arguments{"urn:ietf:params:oauth:grant-type:device_code"}, + }, + Session: &fosite.DefaultSession{}, + RequestedAt: time.Now().UTC(), + }, + }, + description: "should fail because device code not found", + setup: func(t *testing.T, areq *fosite.AccessRequest, config *fosite.Config) { + code, _, err := strategy.GenerateDeviceCode(context.TODO()) + require.NoError(t, err) + areq.Form.Set("device_code", code) + }, + expectErr: fosite.ErrServerError, + }, + { + areq: &fosite.AccessRequest{ + GrantTypes: fosite.Arguments{"urn:ietf:params:oauth:grant-type:device_code"}, + Request: fosite.Request{ + Form: url.Values{}, + Client: &fosite.DefaultClient{ + GrantTypes: fosite.Arguments{"urn:ietf:params:oauth:grant-type:device_code", "refresh_token"}, + }, + GrantedScope: fosite.Arguments{"foo", "offline"}, + Session: &fosite.DefaultSession{}, + RequestedAt: time.Now().UTC(), + }, + }, + setup: func(t *testing.T, areq *fosite.AccessRequest, config *fosite.Config) { + code, sig, err := strategy.GenerateDeviceCode(context.TODO()) + require.NoError(t, err) + areq.Form.Add("device_code", code) + + require.NoError(t, store.CreateDeviceCodeSession(context.TODO(), sig, areq)) + }, + description: "should pass with offline scope and refresh token", + check: func(t *testing.T, aresp *fosite.AccessResponse) { + assert.NotEmpty(t, aresp.AccessToken) + assert.Equal(t, "bearer", aresp.TokenType) + assert.NotEmpty(t, aresp.GetExtra("refresh_token")) + assert.NotEmpty(t, aresp.GetExtra("expires_in")) + assert.Equal(t, "foo offline", aresp.GetExtra("scope")) + }, + }, + { + areq: &fosite.AccessRequest{ + GrantTypes: fosite.Arguments{"urn:ietf:params:oauth:grant-type:device_code"}, + Request: fosite.Request{ + Form: url.Values{}, + Client: &fosite.DefaultClient{ + GrantTypes: fosite.Arguments{"urn:ietf:params:oauth:grant-type:device_code", "refresh_token"}, + }, + GrantedScope: fosite.Arguments{"foo"}, + Session: &fosite.DefaultSession{}, + RequestedAt: time.Now().UTC(), + }, + }, + setup: func(t *testing.T, areq *fosite.AccessRequest, config *fosite.Config) { + config.RefreshTokenScopes = []string{} + code, sig, err := strategy.GenerateDeviceCode(context.TODO()) + require.NoError(t, err) + areq.Form.Add("device_code", code) + + require.NoError(t, store.CreateDeviceCodeSession(context.TODO(), sig, areq)) + }, + description: "should pass with refresh token always provided", + check: func(t *testing.T, aresp *fosite.AccessResponse) { + assert.NotEmpty(t, aresp.AccessToken) + assert.Equal(t, "bearer", aresp.TokenType) + assert.NotEmpty(t, aresp.GetExtra("refresh_token")) + assert.NotEmpty(t, aresp.GetExtra("expires_in")) + assert.Equal(t, "foo", aresp.GetExtra("scope")) + }, + }, + { + areq: &fosite.AccessRequest{ + GrantTypes: fosite.Arguments{"urn:ietf:params:oauth:grant-type:device_code"}, + Request: fosite.Request{ + Form: url.Values{}, + Client: &fosite.DefaultClient{ + GrantTypes: fosite.Arguments{"urn:ietf:params:oauth:grant-type:device_code"}, + }, + GrantedScope: fosite.Arguments{}, + Session: &fosite.DefaultSession{}, + RequestedAt: time.Now().UTC(), + }, + }, + setup: func(t *testing.T, areq *fosite.AccessRequest, config *fosite.Config) { + config.RefreshTokenScopes = []string{} + code, sig, err := strategy.GenerateDeviceCode(context.TODO()) + require.NoError(t, err) + areq.Form.Add("device_code", code) + + require.NoError(t, store.CreateDeviceCodeSession(context.TODO(), sig, areq)) + }, + description: "should pass with no refresh token", + check: func(t *testing.T, aresp *fosite.AccessResponse) { + assert.NotEmpty(t, aresp.AccessToken) + assert.Equal(t, "bearer", aresp.TokenType) + assert.Empty(t, aresp.GetExtra("refresh_token")) + assert.NotEmpty(t, aresp.GetExtra("expires_in")) + assert.Empty(t, aresp.GetExtra("scope")) + }, + }, + { + areq: &fosite.AccessRequest{ + GrantTypes: fosite.Arguments{"urn:ietf:params:oauth:grant-type:device_code"}, + Request: fosite.Request{ + Form: url.Values{}, + Client: &fosite.DefaultClient{ + GrantTypes: fosite.Arguments{"urn:ietf:params:oauth:grant-type:device_code"}, + }, + GrantedScope: fosite.Arguments{"foo"}, + Session: &fosite.DefaultSession{}, + RequestedAt: time.Now().UTC(), + }, + }, + setup: func(t *testing.T, areq *fosite.AccessRequest, config *fosite.Config) { + code, sig, err := strategy.GenerateDeviceCode(context.TODO()) + require.NoError(t, err) + areq.Form.Add("device_code", code) + + require.NoError(t, store.CreateDeviceCodeSession(context.TODO(), sig, areq)) + }, + description: "should not have refresh token", + check: func(t *testing.T, aresp *fosite.AccessResponse) { + assert.NotEmpty(t, aresp.AccessToken) + assert.Equal(t, "bearer", aresp.TokenType) + assert.Empty(t, aresp.GetExtra("refresh_token")) + assert.NotEmpty(t, aresp.GetExtra("expires_in")) + assert.Equal(t, "foo", aresp.GetExtra("scope")) + }, + }, + } { + t.Run("case="+c.description, func(t *testing.T) { + config := &fosite.Config{ + ScopeStrategy: fosite.HierarchicScopeStrategy, + AudienceMatchingStrategy: fosite.DefaultAudienceMatchingStrategy, + AccessTokenLifespan: time.Minute, + RefreshTokenScopes: []string{"offline"}, + } + h = oauth2.GenericCodeTokenEndpointHandler{ + CodeTokenEndpointHandler: &DeviceHandler{ + DeviceStrategy: strategy, + DeviceStorage: store, + }, + AccessTokenStrategy: strategy.CoreStrategy, + RefreshTokenStrategy: strategy.CoreStrategy, + Config: config, + CoreStorage: store, + TokenRevocationStorage: store, + } + + if c.setup != nil { + c.setup(t, c.areq, config) + } + + aresp := fosite.NewAccessResponse() + err := h.PopulateTokenEndpointResponse(context.TODO(), c.areq, aresp) + + if c.expectErr != nil { + require.EqualError(t, err, c.expectErr.Error(), "%+v", err) + } else { + require.NoError(t, err, "%+v", err) + } + + if c.check != nil { + c.check(t, aresp) + } + }) + } + }) + } +} + +func TestDeviceUserCode_HandleTokenEndpointRequest(t *testing.T) { + for k, strategy := range map[string]struct { + oauth2.CoreStrategy + RFC8628CodeStrategy + }{ + "hmac": {&hmacshaStrategy, &RFC8628HMACSHAStrategy}, + } { + t.Run("strategy="+k, func(t *testing.T) { + store := storage.NewMemoryStore() + + h := oauth2.GenericCodeTokenEndpointHandler{ + CodeTokenEndpointHandler: &DeviceHandler{ + DeviceStrategy: strategy.RFC8628CodeStrategy, + DeviceStorage: store, + }, + CoreStorage: store, + AccessTokenStrategy: strategy.CoreStrategy, + RefreshTokenStrategy: strategy.CoreStrategy, + Config: &fosite.Config{ + ScopeStrategy: fosite.HierarchicScopeStrategy, + AudienceMatchingStrategy: fosite.DefaultAudienceMatchingStrategy, + AuthorizeCodeLifespan: time.Minute, + }, + } + for i, c := range []struct { + areq *fosite.AccessRequest + authreq *fosite.DeviceUserRequest + description string + setup func(t *testing.T, areq *fosite.AccessRequest, authreq *fosite.DeviceUserRequest) + check func(t *testing.T, areq *fosite.AccessRequest, authreq *fosite.DeviceUserRequest) + expectErr error + }{ + { + areq: &fosite.AccessRequest{ + GrantTypes: fosite.Arguments{"12345678"}, + }, + description: "should fail because not responsible", + expectErr: fosite.ErrUnknownRequest, + }, + { + areq: &fosite.AccessRequest{ + GrantTypes: fosite.Arguments{"urn:ietf:params:oauth:grant-type:device_code"}, + Request: fosite.Request{ + Client: &fosite.DefaultClient{ID: "foo", GrantTypes: []string{""}}, + Session: &fosite.DefaultSession{}, + RequestedAt: time.Now().UTC(), + }, + }, + description: "should fail because client is not granted this grant type", + expectErr: fosite.ErrUnauthorizedClient, + }, + { + areq: &fosite.AccessRequest{ + GrantTypes: fosite.Arguments{"urn:ietf:params:oauth:grant-type:device_code"}, + Request: fosite.Request{ + Client: &fosite.DefaultClient{GrantTypes: []string{"urn:ietf:params:oauth:grant-type:device_code"}}, + Session: &fosite.DefaultSession{}, + RequestedAt: time.Now().UTC(), + }, + }, + description: "should fail because device code could not be retrieved", + setup: func(t *testing.T, areq *fosite.AccessRequest, authreq *fosite.DeviceUserRequest) { + deviceCode, _, err := strategy.GenerateDeviceCode(context.TODO()) + require.NoError(t, err) + areq.Form = url.Values{"device_code": {deviceCode}} + }, + expectErr: fosite.ErrInvalidGrant, + }, + { + areq: &fosite.AccessRequest{ + GrantTypes: fosite.Arguments{"urn:ietf:params:oauth:grant-type:device_code"}, + Request: fosite.Request{ + Form: url.Values{"device_code": {"AAAA"}}, + Client: &fosite.DefaultClient{GrantTypes: []string{"urn:ietf:params:oauth:grant-type:device_code"}}, + Session: &fosite.DefaultSession{}, + RequestedAt: time.Now().UTC(), + }, + }, + description: "should fail because device code validation failed", + expectErr: fosite.ErrInvalidGrant, + }, + { + areq: &fosite.AccessRequest{ + GrantTypes: fosite.Arguments{"urn:ietf:params:oauth:grant-type:device_code"}, + Request: fosite.Request{ + Client: &fosite.DefaultClient{ID: "foo", GrantTypes: []string{"urn:ietf:params:oauth:grant-type:device_code"}}, + Session: &fosite.DefaultSession{}, + RequestedAt: time.Now().UTC(), + }, + }, + authreq: &fosite.DeviceUserRequest{ + Request: fosite.Request{ + Client: &fosite.DefaultClient{ID: "bar"}, + RequestedScope: fosite.Arguments{"a", "b"}, + }, + }, + description: "should fail because client mismatch", + setup: func(t *testing.T, areq *fosite.AccessRequest, authreq *fosite.DeviceUserRequest) { + token, signature, err := strategy.GenerateDeviceCode(context.TODO()) + require.NoError(t, err) + areq.Form = url.Values{"device_code": {token}} + + require.NoError(t, store.CreateDeviceCodeSession(context.TODO(), signature, authreq)) + }, + expectErr: fosite.ErrInvalidGrant, + }, + { + areq: &fosite.AccessRequest{ + GrantTypes: fosite.Arguments{"urn:ietf:params:oauth:grant-type:device_code"}, + Request: fosite.Request{ + Client: &fosite.DefaultClient{ID: "foo", GrantTypes: []string{"urn:ietf:params:oauth:grant-type:device_code"}}, + Session: &fosite.DefaultSession{}, + RequestedAt: time.Now().UTC(), + }, + }, + authreq: &fosite.DeviceUserRequest{ + Request: fosite.Request{ + Client: &fosite.DefaultClient{ID: "foo", GrantTypes: []string{"urn:ietf:params:oauth:grant-type:device_code"}}, + Session: &fosite.DefaultSession{}, + RequestedScope: fosite.Arguments{"a", "b"}, + RequestedAt: time.Now().UTC(), + }, + }, + description: "should pass", + setup: func(t *testing.T, areq *fosite.AccessRequest, authreq *fosite.DeviceUserRequest) { + token, signature, err := strategy.GenerateDeviceCode(context.TODO()) + require.NoError(t, err) + + areq.Form = url.Values{"device_code": {token}} + require.NoError(t, store.CreateDeviceCodeSession(context.TODO(), signature, authreq)) + }, + }, + { + areq: &fosite.AccessRequest{ + GrantTypes: fosite.Arguments{"urn:ietf:params:oauth:grant-type:device_code"}, + Request: fosite.Request{ + Form: url.Values{}, + Client: &fosite.DefaultClient{ + GrantTypes: fosite.Arguments{"urn:ietf:params:oauth:grant-type:device_code"}, + }, + GrantedScope: fosite.Arguments{"foo", "offline"}, + Session: &fosite.DefaultSession{}, + RequestedAt: time.Now().UTC(), + }, + }, + check: func(t *testing.T, areq *fosite.AccessRequest, authreq *fosite.DeviceUserRequest) { + assert.Equal(t, time.Now().Add(time.Minute).UTC().Round(time.Second), areq.GetSession().GetExpiresAt(fosite.AccessToken)) + assert.Equal(t, time.Now().Add(time.Minute).UTC().Round(time.Second), areq.GetSession().GetExpiresAt(fosite.RefreshToken)) + }, + setup: func(t *testing.T, areq *fosite.AccessRequest, authreq *fosite.DeviceUserRequest) { + code, sig, err := strategy.GenerateDeviceCode(context.TODO()) + require.NoError(t, err) + areq.Form.Add("device_code", code) + areq.GetSession().SetExpiresAt(fosite.DeviceCode, time.Now().Add(-time.Hour).UTC().Round(time.Second)) + require.NoError(t, store.CreateDeviceCodeSession(context.TODO(), sig, areq)) + }, + description: "should fail because device code has expired", + expectErr: fosite.ErrDeviceExpiredToken, + }, + } { + t.Run(fmt.Sprintf("case=%d/description=%s", i, c.description), func(t *testing.T) { + if c.setup != nil { + c.setup(t, c.areq, c.authreq) + } + + t.Logf("Processing %+v", c.areq.Client) + + err := h.HandleTokenEndpointRequest(context.Background(), c.areq) + if c.expectErr != nil { + require.EqualError(t, err, c.expectErr.Error(), "%+v", err) + } else { + require.NoError(t, err, "%+v", err) + if c.check != nil { + c.check(t, c.areq, c.authreq) + } + } + }) + } + }) + } +} + +func TestDeviceUserCodeTransactional_HandleTokenEndpointRequest(t *testing.T) { + var mockTransactional *internal.MockTransactional + var mockCoreStore *internal.MockCoreStorage + var mockDeviceStore *internal.MockRFC8628CodeStorage + strategy := hmacshaStrategy + deviceStrategy := RFC8628HMACSHAStrategy + request := &fosite.AccessRequest{ + GrantTypes: fosite.Arguments{"urn:ietf:params:oauth:grant-type:device_code"}, + Request: fosite.Request{ + Client: &fosite.DefaultClient{ + GrantTypes: fosite.Arguments{"urn:ietf:params:oauth:grant-type:device_code", "refresh_token"}, + }, + GrantedScope: fosite.Arguments{"offline"}, + Session: &fosite.DefaultSession{}, + RequestedAt: time.Now().UTC(), + }, + } + token, _, err := deviceStrategy.GenerateDeviceCode(context.TODO()) + require.NoError(t, err) + request.Form = url.Values{"device_code": {token}} + response := fosite.NewAccessResponse() + propagatedContext := context.Background() + + // some storage implementation that has support for transactions, notice the embedded type `storage.Transactional` + type coreTransactionalStore struct { + storage.Transactional + oauth2.CoreStorage + } + + type deviceTransactionalStore struct { + storage.Transactional + RFC8628CodeStorage + } + + for _, testCase := range []struct { + description string + setup func() + expectError error + }{ + { + description: "transaction should be committed successfully if no errors occur", + setup: func() { + mockDeviceStore. + EXPECT(). + GetDeviceCodeSession(gomock.Any(), gomock.Any(), gomock.Any()). + Return(request, nil). + Times(1) + mockTransactional. + EXPECT(). + BeginTX(propagatedContext). + Return(propagatedContext, nil) + mockDeviceStore. + EXPECT(). + InvalidateDeviceCodeSession(gomock.Any(), gomock.Any()). + Return(nil). + Times(1) + mockCoreStore. + EXPECT(). + CreateAccessTokenSession(propagatedContext, gomock.Any(), gomock.Any()). + Return(nil). + Times(1) + mockCoreStore. + EXPECT(). + CreateRefreshTokenSession(propagatedContext, gomock.Any(), gomock.Any()). + Return(nil). + Times(1) + mockTransactional. + EXPECT(). + Commit(propagatedContext). + Return(nil). + Times(1) + }, + }, + { + description: "transaction should be rolled back if `InvalidateDeviceCodeSession` returns an error", + setup: func() { + mockDeviceStore. + EXPECT(). + GetDeviceCodeSession(gomock.Any(), gomock.Any(), gomock.Any()). + Return(request, nil). + Times(1) + mockTransactional. + EXPECT(). + BeginTX(propagatedContext). + Return(propagatedContext, nil) + mockDeviceStore. + EXPECT(). + InvalidateDeviceCodeSession(gomock.Any(), gomock.Any()). + Return(errors.New("Whoops, a nasty database error occurred!")). + Times(1) + mockTransactional. + EXPECT(). + Rollback(propagatedContext). + Return(nil). + Times(1) + }, + expectError: fosite.ErrServerError, + }, + { + description: "transaction should be rolled back if `CreateAccessTokenSession` returns an error", + setup: func() { + mockDeviceStore. + EXPECT(). + GetDeviceCodeSession(gomock.Any(), gomock.Any(), gomock.Any()). + Return(request, nil). + Times(1) + mockTransactional. + EXPECT(). + BeginTX(propagatedContext). + Return(propagatedContext, nil) + mockDeviceStore. + EXPECT(). + InvalidateDeviceCodeSession(gomock.Any(), gomock.Any()). + Return(nil). + Times(1) + mockCoreStore. + EXPECT(). + CreateAccessTokenSession(propagatedContext, gomock.Any(), gomock.Any()). + Return(errors.New("Whoops, a nasty database error occurred!")). + Times(1) + mockTransactional. + EXPECT(). + Rollback(propagatedContext). + Return(nil). + Times(1) + }, + expectError: fosite.ErrServerError, + }, + { + description: "should result in a server error if transaction cannot be created", + setup: func() { + mockDeviceStore. + EXPECT(). + GetDeviceCodeSession(gomock.Any(), gomock.Any(), gomock.Any()). + Return(request, nil). + Times(1) + mockTransactional. + EXPECT(). + BeginTX(propagatedContext). + Return(nil, errors.New("Whoops, unable to create transaction!")) + }, + expectError: fosite.ErrServerError, + }, + { + description: "should result in a server error if transaction cannot be rolled back", + setup: func() { + mockDeviceStore. + EXPECT(). + GetDeviceCodeSession(gomock.Any(), gomock.Any(), gomock.Any()). + Return(request, nil). + Times(1) + mockTransactional. + EXPECT(). + BeginTX(propagatedContext). + Return(propagatedContext, nil) + mockDeviceStore. + EXPECT(). + InvalidateDeviceCodeSession(gomock.Any(), gomock.Any()). + Return(errors.New("Whoops, a nasty database error occurred!")). + Times(1) + mockTransactional. + EXPECT(). + Rollback(propagatedContext). + Return(errors.New("Whoops, unable to rollback transaction!")). + Times(1) + }, + expectError: fosite.ErrServerError, + }, + { + description: "should result in a server error if transaction cannot be committed", + setup: func() { + mockDeviceStore. + EXPECT(). + GetDeviceCodeSession(gomock.Any(), gomock.Any(), gomock.Any()). + Return(request, nil). + Times(1) + mockTransactional. + EXPECT(). + BeginTX(propagatedContext). + Return(propagatedContext, nil) + mockDeviceStore. + EXPECT(). + InvalidateDeviceCodeSession(gomock.Any(), gomock.Any()). + Return(nil). + Times(1) + mockCoreStore. + EXPECT(). + CreateAccessTokenSession(propagatedContext, gomock.Any(), gomock.Any()). + Return(nil). + Times(1) + mockCoreStore. + EXPECT(). + CreateRefreshTokenSession(propagatedContext, gomock.Any(), gomock.Any()). + Return(nil). + Times(1) + mockTransactional. + EXPECT(). + Commit(propagatedContext). + Return(errors.New("Whoops, unable to commit transaction!")). + Times(1) + mockTransactional. + EXPECT(). + Rollback(propagatedContext). + Return(nil). + Times(1) + }, + expectError: fosite.ErrServerError, + }, + } { + t.Run(fmt.Sprintf("scenario=%s", testCase.description), func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockTransactional = internal.NewMockTransactional(ctrl) + mockCoreStore = internal.NewMockCoreStorage(ctrl) + mockDeviceStore = internal.NewMockRFC8628CodeStorage(ctrl) + testCase.setup() + + handler := oauth2.GenericCodeTokenEndpointHandler{ + CodeTokenEndpointHandler: &DeviceHandler{ + DeviceStrategy: &deviceStrategy, + DeviceStorage: deviceTransactionalStore{ + mockTransactional, + mockDeviceStore, + }, + }, + CoreStorage: coreTransactionalStore{ + mockTransactional, + mockCoreStore, + }, + AccessTokenStrategy: &strategy, + RefreshTokenStrategy: &strategy, + Config: &fosite.Config{ + ScopeStrategy: fosite.HierarchicScopeStrategy, + AudienceMatchingStrategy: fosite.DefaultAudienceMatchingStrategy, + DeviceAndUserCodeLifespan: time.Minute, + }, + } + + if err := handler.PopulateTokenEndpointResponse(propagatedContext, request, response); testCase.expectError != nil { + assert.EqualError(t, err, testCase.expectError.Error()) + } + }) + } +} diff --git a/integration/authorize_code_grant_public_client_pkce_test.go b/integration/authorize_code_grant_public_client_pkce_test.go index c3a735513..bc274fe45 100644 --- a/integration/authorize_code_grant_public_client_pkce_test.go +++ b/integration/authorize_code_grant_public_client_pkce_test.go @@ -32,7 +32,7 @@ func runAuthorizeCodeGrantWithPublicClientAndPKCETest(t *testing.T, strategy int c := new(fosite.Config) c.EnforcePKCE = true c.EnablePKCEPlainChallengeMethod = true - f := compose.Compose(c, fositeStore, strategy, compose.OAuth2AuthorizeExplicitFactory, compose.OAuth2PKCEFactory, compose.OAuth2TokenIntrospectionFactory) + f := compose.Compose(c, fositeStore, strategy, compose.OAuth2AuthorizeExplicitAuthFactory, compose.OAuth2AuthorizeExplicitTokenFactory, compose.OAuth2PKCEFactory, compose.OAuth2TokenIntrospectionFactory) ts := mockServer(t, f, &fosite.DefaultSession{}) defer ts.Close() diff --git a/integration/authorize_code_grant_public_client_test.go b/integration/authorize_code_grant_public_client_test.go index 4706311e8..291771977 100644 --- a/integration/authorize_code_grant_public_client_test.go +++ b/integration/authorize_code_grant_public_client_test.go @@ -27,7 +27,7 @@ func TestAuthorizeCodeFlowWithPublicClient(t *testing.T) { } func runAuthorizeCodeGrantWithPublicClientTest(t *testing.T, strategy interface{}) { - f := compose.Compose(new(fosite.Config), fositeStore, strategy, compose.OAuth2AuthorizeExplicitFactory, compose.OAuth2TokenIntrospectionFactory) + f := compose.Compose(new(fosite.Config), fositeStore, strategy, compose.OAuth2AuthorizeExplicitAuthFactory, compose.OAuth2AuthorizeExplicitTokenFactory, compose.OAuth2TokenIntrospectionFactory) ts := mockServer(t, f, &fosite.DefaultSession{Subject: "foo-sub"}) defer ts.Close() diff --git a/integration/authorize_code_grant_test.go b/integration/authorize_code_grant_test.go index caa716de0..afdb1295d 100644 --- a/integration/authorize_code_grant_test.go +++ b/integration/authorize_code_grant_test.go @@ -38,7 +38,7 @@ func TestAuthorizeCodeFlowDupeCode(t *testing.T) { } func runAuthorizeCodeGrantTest(t *testing.T, strategy interface{}) { - f := compose.Compose(new(fosite.Config), fositeStore, strategy, compose.OAuth2AuthorizeExplicitFactory, compose.OAuth2TokenIntrospectionFactory) + f := compose.Compose(new(fosite.Config), fositeStore, strategy, compose.OAuth2AuthorizeExplicitAuthFactory, compose.OAuth2AuthorizeExplicitTokenFactory, compose.OAuth2TokenIntrospectionFactory) ts := mockServer(t, f, &openid.DefaultSession{Subject: "foo-sub"}) defer ts.Close() @@ -148,7 +148,7 @@ func runAuthorizeCodeGrantTest(t *testing.T, strategy interface{}) { } func runAuthorizeCodeGrantDupeCodeTest(t *testing.T, strategy interface{}) { - f := compose.Compose(new(fosite.Config), fositeStore, strategy, compose.OAuth2AuthorizeExplicitFactory, compose.OAuth2TokenIntrospectionFactory) + f := compose.Compose(new(fosite.Config), fositeStore, strategy, compose.OAuth2AuthorizeExplicitAuthFactory, compose.OAuth2AuthorizeExplicitTokenFactory, compose.OAuth2TokenIntrospectionFactory) ts := mockServer(t, f, &fosite.DefaultSession{}) defer ts.Close() diff --git a/integration/helper_setup_test.go b/integration/helper_setup_test.go index 493424037..a938a252c 100644 --- a/integration/helper_setup_test.go +++ b/integration/helper_setup_test.go @@ -23,6 +23,7 @@ import ( "github.com/ory/fosite" "github.com/ory/fosite/handler/oauth2" "github.com/ory/fosite/handler/openid" + "github.com/ory/fosite/handler/rfc8628" "github.com/ory/fosite/integration/clients" "github.com/ory/fosite/storage" "github.com/ory/fosite/token/hmac" @@ -123,6 +124,8 @@ var accessTokenLifespan = time.Hour var authCodeLifespan = time.Minute +var deviceAndUserCodeLifespan = time.Hour + func createIssuerPublicKey(issuer, subject, keyID string, key crypto.PublicKey, scopes []string) storage.IssuerPublicKeys { return storage.IssuerPublicKeys{ Issuer: issuer, @@ -172,15 +175,32 @@ func newJWTBearerAppClient(ts *httptest.Server) *clients.JWTBearer { return clients.NewJWTBearer(ts.URL + tokenRelativePath) } -var hmacStrategy = &oauth2.HMACSHAStrategy{ - Enigma: &hmac.HMACStrategy{ +type GlobaStrategy struct { + oauth2.CoreStrategy + rfc8628.RFC8628CodeStrategy +} + +var hmacStrategy = &GlobaStrategy{ + &oauth2.HMACSHAStrategy{ + Enigma: &hmac.HMACStrategy{ + Config: &fosite.Config{ + GlobalSecret: []byte("some-super-cool-secret-that-nobody-knows"), + }, + }, Config: &fosite.Config{ - GlobalSecret: []byte("some-super-cool-secret-that-nobody-knows"), + AccessTokenLifespan: accessTokenLifespan, + AuthorizeCodeLifespan: authCodeLifespan, }, }, - Config: &fosite.Config{ - AccessTokenLifespan: accessTokenLifespan, - AuthorizeCodeLifespan: authCodeLifespan, + &rfc8628.DefaultDeviceStrategy{ + Enigma: &hmac.HMACStrategy{ + Config: &fosite.Config{ + GlobalSecret: []byte("some-super-cool-secret-that-nobody-knows"), + }, + }, + Config: &fosite.Config{ + DeviceAndUserCodeLifespan: deviceAndUserCodeLifespan, + }, }, } @@ -191,8 +211,18 @@ var jwtStrategy = &oauth2.DefaultJWTStrategy{ return defaultRSAKey, nil }, }, - Config: &fosite.Config{}, - HMACSHAStrategy: hmacStrategy, + Config: &fosite.Config{}, + HMACSHAStrategy: &oauth2.HMACSHAStrategy{ + Enigma: &hmac.HMACStrategy{ + Config: &fosite.Config{ + GlobalSecret: []byte("some-super-cool-secret-that-nobody-knows"), + }, + }, + Config: &fosite.Config{ + AccessTokenLifespan: accessTokenLifespan, + AuthorizeCodeLifespan: authCodeLifespan, + }, + }, } func mockServer(t *testing.T, f fosite.OAuth2Provider, session fosite.Session) *httptest.Server { diff --git a/integration/pushed_authorize_code_grant_test.go b/integration/pushed_authorize_code_grant_test.go index a50fbf8fc..2d010f186 100644 --- a/integration/pushed_authorize_code_grant_test.go +++ b/integration/pushed_authorize_code_grant_test.go @@ -30,7 +30,7 @@ func TestPushedAuthorizeCodeFlow(t *testing.T) { } func runPushedAuthorizeCodeGrantTest(t *testing.T, strategy interface{}) { - f := compose.Compose(new(fosite.Config), fositeStore, strategy, compose.OAuth2AuthorizeExplicitFactory, compose.OAuth2TokenIntrospectionFactory, compose.PushedAuthorizeHandlerFactory) + f := compose.Compose(new(fosite.Config), fositeStore, strategy, compose.OAuth2AuthorizeExplicitAuthFactory, compose.OAuth2AuthorizeExplicitTokenFactory, compose.OAuth2TokenIntrospectionFactory, compose.PushedAuthorizeHandlerFactory) ts := mockServer(t, f, &fosite.DefaultSession{Subject: "foo-sub"}) defer ts.Close() diff --git a/internal/device_authorize_handler.go b/internal/device_authorize_handler.go new file mode 100644 index 000000000..ebe2ad150 --- /dev/null +++ b/internal/device_authorize_handler.go @@ -0,0 +1,53 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/ory/fosite (interfaces: DeviceUserEndpointHandler) + +// Package internal is a generated GoMock package. +package internal + +import ( + context "context" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + fosite "github.com/ory/fosite" +) + +// MockDeviceUserEndpointHandler is a mock of DeviceUserEndpointHandler interface. +type MockDeviceUserEndpointHandler struct { + ctrl *gomock.Controller + recorder *MockDeviceUserEndpointHandlerMockRecorder +} + +// MockDeviceUserEndpointHandlerMockRecorder is the mock recorder for MockDeviceUserEndpointHandler. +type MockDeviceUserEndpointHandlerMockRecorder struct { + mock *MockDeviceUserEndpointHandler +} + +// NewMockDeviceUserEndpointHandler creates a new mock instance. +func NewMockDeviceUserEndpointHandler(ctrl *gomock.Controller) *MockDeviceUserEndpointHandler { + mock := &MockDeviceUserEndpointHandler{ctrl: ctrl} + mock.recorder = &MockDeviceUserEndpointHandlerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDeviceUserEndpointHandler) EXPECT() *MockDeviceUserEndpointHandlerMockRecorder { + return m.recorder +} + +// HandleDeviceUserEndpointRequest mocks base method. +func (m *MockDeviceUserEndpointHandler) HandleDeviceUserEndpointRequest(arg0 context.Context, arg1 fosite.DeviceUserRequester, arg2 fosite.DeviceUserResponder) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HandleDeviceUserEndpointRequest", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// HandleDeviceUserEndpointRequest indicates an expected call of HandleDeviceUserEndpointRequest. +func (mr *MockDeviceUserEndpointHandlerMockRecorder) HandleDeviceUserEndpointRequest(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandleDeviceUserEndpointRequest", reflect.TypeOf((*MockDeviceUserEndpointHandler)(nil).HandleDeviceUserEndpointRequest), arg0, arg1, arg2) +} diff --git a/internal/device_authorize_request.go b/internal/device_authorize_request.go new file mode 100644 index 000000000..ce01302de --- /dev/null +++ b/internal/device_authorize_request.go @@ -0,0 +1,302 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/ory/fosite (interfaces: DeviceUserRequester) + +// Package internal is a generated GoMock package. +package internal + +import ( + url "net/url" + reflect "reflect" + time "time" + + gomock "github.com/golang/mock/gomock" + fosite "github.com/ory/fosite" +) + +// MockDeviceUserRequester is a mock of DeviceUserRequester interface. +type MockDeviceUserRequester struct { + ctrl *gomock.Controller + recorder *MockDeviceUserRequesterMockRecorder +} + +// MockDeviceUserRequesterMockRecorder is the mock recorder for MockDeviceUserRequester. +type MockDeviceUserRequesterMockRecorder struct { + mock *MockDeviceUserRequester +} + +// NewMockDeviceUserRequester creates a new mock instance. +func NewMockDeviceUserRequester(ctrl *gomock.Controller) *MockDeviceUserRequester { + mock := &MockDeviceUserRequester{ctrl: ctrl} + mock.recorder = &MockDeviceUserRequesterMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDeviceUserRequester) EXPECT() *MockDeviceUserRequesterMockRecorder { + return m.recorder +} + +// AppendRequestedScope mocks base method. +func (m *MockDeviceUserRequester) AppendRequestedScope(arg0 string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "AppendRequestedScope", arg0) +} + +// AppendRequestedScope indicates an expected call of AppendRequestedScope. +func (mr *MockDeviceUserRequesterMockRecorder) AppendRequestedScope(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AppendRequestedScope", reflect.TypeOf((*MockDeviceUserRequester)(nil).AppendRequestedScope), arg0) +} + +// GetClient mocks base method. +func (m *MockDeviceUserRequester) GetClient() fosite.Client { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetClient") + ret0, _ := ret[0].(fosite.Client) + return ret0 +} + +// GetClient indicates an expected call of GetClient. +func (mr *MockDeviceUserRequesterMockRecorder) GetClient() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClient", reflect.TypeOf((*MockDeviceUserRequester)(nil).GetClient)) +} + +// GetDeviceCodeSignature mocks base method. +func (m *MockDeviceUserRequester) GetDeviceCodeSignature() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetDeviceCodeSignature") + ret0, _ := ret[0].(string) + return ret0 +} + +// GetDeviceCodeSignature indicates an expected call of GetDeviceCodeSignature. +func (mr *MockDeviceUserRequesterMockRecorder) GetDeviceCodeSignature() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDeviceCodeSignature", reflect.TypeOf((*MockDeviceUserRequester)(nil).GetDeviceCodeSignature)) +} + +// GetGrantedAudience mocks base method. +func (m *MockDeviceUserRequester) GetGrantedAudience() fosite.Arguments { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetGrantedAudience") + ret0, _ := ret[0].(fosite.Arguments) + return ret0 +} + +// GetGrantedAudience indicates an expected call of GetGrantedAudience. +func (mr *MockDeviceUserRequesterMockRecorder) GetGrantedAudience() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGrantedAudience", reflect.TypeOf((*MockDeviceUserRequester)(nil).GetGrantedAudience)) +} + +// GetGrantedScopes mocks base method. +func (m *MockDeviceUserRequester) GetGrantedScopes() fosite.Arguments { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetGrantedScopes") + ret0, _ := ret[0].(fosite.Arguments) + return ret0 +} + +// GetGrantedScopes indicates an expected call of GetGrantedScopes. +func (mr *MockDeviceUserRequesterMockRecorder) GetGrantedScopes() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGrantedScopes", reflect.TypeOf((*MockDeviceUserRequester)(nil).GetGrantedScopes)) +} + +// GetID mocks base method. +func (m *MockDeviceUserRequester) GetID() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetID") + ret0, _ := ret[0].(string) + return ret0 +} + +// GetID indicates an expected call of GetID. +func (mr *MockDeviceUserRequesterMockRecorder) GetID() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetID", reflect.TypeOf((*MockDeviceUserRequester)(nil).GetID)) +} + +// GetRequestForm mocks base method. +func (m *MockDeviceUserRequester) GetRequestForm() url.Values { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetRequestForm") + ret0, _ := ret[0].(url.Values) + return ret0 +} + +// GetRequestForm indicates an expected call of GetRequestForm. +func (mr *MockDeviceUserRequesterMockRecorder) GetRequestForm() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRequestForm", reflect.TypeOf((*MockDeviceUserRequester)(nil).GetRequestForm)) +} + +// GetRequestedAt mocks base method. +func (m *MockDeviceUserRequester) GetRequestedAt() time.Time { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetRequestedAt") + ret0, _ := ret[0].(time.Time) + return ret0 +} + +// GetRequestedAt indicates an expected call of GetRequestedAt. +func (mr *MockDeviceUserRequesterMockRecorder) GetRequestedAt() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRequestedAt", reflect.TypeOf((*MockDeviceUserRequester)(nil).GetRequestedAt)) +} + +// GetRequestedAudience mocks base method. +func (m *MockDeviceUserRequester) GetRequestedAudience() fosite.Arguments { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetRequestedAudience") + ret0, _ := ret[0].(fosite.Arguments) + return ret0 +} + +// GetRequestedAudience indicates an expected call of GetRequestedAudience. +func (mr *MockDeviceUserRequesterMockRecorder) GetRequestedAudience() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRequestedAudience", reflect.TypeOf((*MockDeviceUserRequester)(nil).GetRequestedAudience)) +} + +// GetRequestedScopes mocks base method. +func (m *MockDeviceUserRequester) GetRequestedScopes() fosite.Arguments { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetRequestedScopes") + ret0, _ := ret[0].(fosite.Arguments) + return ret0 +} + +// GetRequestedScopes indicates an expected call of GetRequestedScopes. +func (mr *MockDeviceUserRequesterMockRecorder) GetRequestedScopes() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRequestedScopes", reflect.TypeOf((*MockDeviceUserRequester)(nil).GetRequestedScopes)) +} + +// GetSession mocks base method. +func (m *MockDeviceUserRequester) GetSession() fosite.Session { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSession") + ret0, _ := ret[0].(fosite.Session) + return ret0 +} + +// GetSession indicates an expected call of GetSession. +func (mr *MockDeviceUserRequesterMockRecorder) GetSession() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSession", reflect.TypeOf((*MockDeviceUserRequester)(nil).GetSession)) +} + +// GrantAudience mocks base method. +func (m *MockDeviceUserRequester) GrantAudience(arg0 string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "GrantAudience", arg0) +} + +// GrantAudience indicates an expected call of GrantAudience. +func (mr *MockDeviceUserRequesterMockRecorder) GrantAudience(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GrantAudience", reflect.TypeOf((*MockDeviceUserRequester)(nil).GrantAudience), arg0) +} + +// GrantScope mocks base method. +func (m *MockDeviceUserRequester) GrantScope(arg0 string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "GrantScope", arg0) +} + +// GrantScope indicates an expected call of GrantScope. +func (mr *MockDeviceUserRequesterMockRecorder) GrantScope(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GrantScope", reflect.TypeOf((*MockDeviceUserRequester)(nil).GrantScope), arg0) +} + +// Merge mocks base method. +func (m *MockDeviceUserRequester) Merge(arg0 fosite.Requester) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Merge", arg0) +} + +// Merge indicates an expected call of Merge. +func (mr *MockDeviceUserRequesterMockRecorder) Merge(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Merge", reflect.TypeOf((*MockDeviceUserRequester)(nil).Merge), arg0) +} + +// Sanitize mocks base method. +func (m *MockDeviceUserRequester) Sanitize(arg0 []string) fosite.Requester { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Sanitize", arg0) + ret0, _ := ret[0].(fosite.Requester) + return ret0 +} + +// Sanitize indicates an expected call of Sanitize. +func (mr *MockDeviceUserRequesterMockRecorder) Sanitize(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Sanitize", reflect.TypeOf((*MockDeviceUserRequester)(nil).Sanitize), arg0) +} + +// SetDeviceCodeSignature mocks base method. +func (m *MockDeviceUserRequester) SetDeviceCodeSignature(arg0 string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetDeviceCodeSignature", arg0) +} + +// SetDeviceCodeSignature indicates an expected call of SetDeviceCodeSignature. +func (mr *MockDeviceUserRequesterMockRecorder) SetDeviceCodeSignature(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDeviceCodeSignature", reflect.TypeOf((*MockDeviceUserRequester)(nil).SetDeviceCodeSignature), arg0) +} + +// SetID mocks base method. +func (m *MockDeviceUserRequester) SetID(arg0 string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetID", arg0) +} + +// SetID indicates an expected call of SetID. +func (mr *MockDeviceUserRequesterMockRecorder) SetID(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetID", reflect.TypeOf((*MockDeviceUserRequester)(nil).SetID), arg0) +} + +// SetRequestedAudience mocks base method. +func (m *MockDeviceUserRequester) SetRequestedAudience(arg0 fosite.Arguments) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetRequestedAudience", arg0) +} + +// SetRequestedAudience indicates an expected call of SetRequestedAudience. +func (mr *MockDeviceUserRequesterMockRecorder) SetRequestedAudience(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetRequestedAudience", reflect.TypeOf((*MockDeviceUserRequester)(nil).SetRequestedAudience), arg0) +} + +// SetRequestedScopes mocks base method. +func (m *MockDeviceUserRequester) SetRequestedScopes(arg0 fosite.Arguments) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetRequestedScopes", arg0) +} + +// SetRequestedScopes indicates an expected call of SetRequestedScopes. +func (mr *MockDeviceUserRequesterMockRecorder) SetRequestedScopes(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetRequestedScopes", reflect.TypeOf((*MockDeviceUserRequester)(nil).SetRequestedScopes), arg0) +} + +// SetSession mocks base method. +func (m *MockDeviceUserRequester) SetSession(arg0 fosite.Session) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetSession", arg0) +} + +// SetSession indicates an expected call of SetSession. +func (mr *MockDeviceUserRequesterMockRecorder) SetSession(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetSession", reflect.TypeOf((*MockDeviceUserRequester)(nil).SetSession), arg0) +} diff --git a/internal/device_authorize_response.go b/internal/device_authorize_response.go new file mode 100644 index 000000000..9a77bcdbb --- /dev/null +++ b/internal/device_authorize_response.go @@ -0,0 +1,64 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/ory/fosite (interfaces: DeviceUserResponder) + +// Package internal is a generated GoMock package. +package internal + +import ( + http "net/http" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" +) + +// MockDeviceUserResponder is a mock of DeviceUserResponder interface. +type MockDeviceUserResponder struct { + ctrl *gomock.Controller + recorder *MockDeviceUserResponderMockRecorder +} + +// MockDeviceUserResponderMockRecorder is the mock recorder for MockDeviceUserResponder. +type MockDeviceUserResponderMockRecorder struct { + mock *MockDeviceUserResponder +} + +// NewMockDeviceUserResponder creates a new mock instance. +func NewMockDeviceUserResponder(ctrl *gomock.Controller) *MockDeviceUserResponder { + mock := &MockDeviceUserResponder{ctrl: ctrl} + mock.recorder = &MockDeviceUserResponderMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDeviceUserResponder) EXPECT() *MockDeviceUserResponderMockRecorder { + return m.recorder +} + +// AddHeader mocks base method. +func (m *MockDeviceUserResponder) AddHeader(arg0, arg1 string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "AddHeader", arg0, arg1) +} + +// AddHeader indicates an expected call of AddHeader. +func (mr *MockDeviceUserResponderMockRecorder) AddHeader(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddHeader", reflect.TypeOf((*MockDeviceUserResponder)(nil).AddHeader), arg0, arg1) +} + +// GetHeader mocks base method. +func (m *MockDeviceUserResponder) GetHeader() http.Header { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetHeader") + ret0, _ := ret[0].(http.Header) + return ret0 +} + +// GetHeader indicates an expected call of GetHeader. +func (mr *MockDeviceUserResponderMockRecorder) GetHeader() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetHeader", reflect.TypeOf((*MockDeviceUserResponder)(nil).GetHeader)) +} diff --git a/internal/device_code_storage.go b/internal/device_code_storage.go new file mode 100644 index 000000000..3c337e555 --- /dev/null +++ b/internal/device_code_storage.go @@ -0,0 +1,96 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/ory/fosite/handler/rfc8628 (interfaces: DeviceCodeStorage) + +// Package internal is a generated GoMock package. +package internal + +import ( + context "context" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + fosite "github.com/ory/fosite" +) + +// MockDeviceCodeStorage is a mock of DeviceCodeStorage interface. +type MockDeviceCodeStorage struct { + ctrl *gomock.Controller + recorder *MockDeviceCodeStorageMockRecorder +} + +// MockDeviceCodeStorageMockRecorder is the mock recorder for MockDeviceCodeStorage. +type MockDeviceCodeStorageMockRecorder struct { + mock *MockDeviceCodeStorage +} + +// NewMockDeviceCodeStorage creates a new mock instance. +func NewMockDeviceCodeStorage(ctrl *gomock.Controller) *MockDeviceCodeStorage { + mock := &MockDeviceCodeStorage{ctrl: ctrl} + mock.recorder = &MockDeviceCodeStorageMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDeviceCodeStorage) EXPECT() *MockDeviceCodeStorageMockRecorder { + return m.recorder +} + +// CreateDeviceCodeSession mocks base method. +func (m *MockDeviceCodeStorage) CreateDeviceCodeSession(arg0 context.Context, arg1 string, arg2 fosite.Requester) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateDeviceCodeSession", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// CreateDeviceCodeSession indicates an expected call of CreateDeviceCodeSession. +func (mr *MockDeviceCodeStorageMockRecorder) CreateDeviceCodeSession(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateDeviceCodeSession", reflect.TypeOf((*MockDeviceCodeStorage)(nil).CreateDeviceCodeSession), arg0, arg1, arg2) +} + +// GetDeviceCodeSession mocks base method. +func (m *MockDeviceCodeStorage) GetDeviceCodeSession(arg0 context.Context, arg1 string, arg2 fosite.Session) (fosite.Requester, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetDeviceCodeSession", arg0, arg1, arg2) + ret0, _ := ret[0].(fosite.Requester) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetDeviceCodeSession indicates an expected call of GetDeviceCodeSession. +func (mr *MockDeviceCodeStorageMockRecorder) GetDeviceCodeSession(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDeviceCodeSession", reflect.TypeOf((*MockDeviceCodeStorage)(nil).GetDeviceCodeSession), arg0, arg1, arg2) +} + +// InvalidateDeviceCodeSession mocks base method. +func (m *MockDeviceCodeStorage) InvalidateDeviceCodeSession(arg0 context.Context, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InvalidateDeviceCodeSession", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// InvalidateDeviceCodeSession indicates an expected call of InvalidateDeviceCodeSession. +func (mr *MockDeviceCodeStorageMockRecorder) InvalidateDeviceCodeSession(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InvalidateDeviceCodeSession", reflect.TypeOf((*MockDeviceCodeStorage)(nil).InvalidateDeviceCodeSession), arg0, arg1) +} + +// UpdateDeviceCodeSession mocks base method. +func (m *MockDeviceCodeStorage) UpdateDeviceCodeSession(arg0 context.Context, arg1 string, arg2 fosite.Requester) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateDeviceCodeSession", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateDeviceCodeSession indicates an expected call of UpdateDeviceCodeSession. +func (mr *MockDeviceCodeStorageMockRecorder) UpdateDeviceCodeSession(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateDeviceCodeSession", reflect.TypeOf((*MockDeviceCodeStorage)(nil).UpdateDeviceCodeSession), arg0, arg1, arg2) +} diff --git a/internal/device_handler.go b/internal/device_handler.go new file mode 100644 index 000000000..28ba82336 --- /dev/null +++ b/internal/device_handler.go @@ -0,0 +1,53 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/ory/fosite (interfaces: DeviceEndpointHandler) + +// Package internal is a generated GoMock package. +package internal + +import ( + context "context" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + fosite "github.com/ory/fosite" +) + +// MockDeviceEndpointHandler is a mock of DeviceEndpointHandler interface. +type MockDeviceEndpointHandler struct { + ctrl *gomock.Controller + recorder *MockDeviceEndpointHandlerMockRecorder +} + +// MockDeviceEndpointHandlerMockRecorder is the mock recorder for MockDeviceEndpointHandler. +type MockDeviceEndpointHandlerMockRecorder struct { + mock *MockDeviceEndpointHandler +} + +// NewMockDeviceEndpointHandler creates a new mock instance. +func NewMockDeviceEndpointHandler(ctrl *gomock.Controller) *MockDeviceEndpointHandler { + mock := &MockDeviceEndpointHandler{ctrl: ctrl} + mock.recorder = &MockDeviceEndpointHandlerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDeviceEndpointHandler) EXPECT() *MockDeviceEndpointHandlerMockRecorder { + return m.recorder +} + +// HandleDeviceEndpointRequest mocks base method. +func (m *MockDeviceEndpointHandler) HandleDeviceEndpointRequest(arg0 context.Context, arg1 fosite.DeviceRequester, arg2 fosite.DeviceResponder) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HandleDeviceEndpointRequest", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// HandleDeviceEndpointRequest indicates an expected call of HandleDeviceEndpointRequest. +func (mr *MockDeviceEndpointHandlerMockRecorder) HandleDeviceEndpointRequest(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandleDeviceEndpointRequest", reflect.TypeOf((*MockDeviceEndpointHandler)(nil).HandleDeviceEndpointRequest), arg0, arg1, arg2) +} diff --git a/internal/device_request.go b/internal/device_request.go new file mode 100644 index 000000000..35ffb6365 --- /dev/null +++ b/internal/device_request.go @@ -0,0 +1,276 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/ory/fosite (interfaces: DeviceRequester) + +// Package internal is a generated GoMock package. +package internal + +import ( + url "net/url" + reflect "reflect" + time "time" + + gomock "github.com/golang/mock/gomock" + fosite "github.com/ory/fosite" +) + +// MockDeviceRequester is a mock of DeviceRequester interface. +type MockDeviceRequester struct { + ctrl *gomock.Controller + recorder *MockDeviceRequesterMockRecorder +} + +// MockDeviceRequesterMockRecorder is the mock recorder for MockDeviceRequester. +type MockDeviceRequesterMockRecorder struct { + mock *MockDeviceRequester +} + +// NewMockDeviceRequester creates a new mock instance. +func NewMockDeviceRequester(ctrl *gomock.Controller) *MockDeviceRequester { + mock := &MockDeviceRequester{ctrl: ctrl} + mock.recorder = &MockDeviceRequesterMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDeviceRequester) EXPECT() *MockDeviceRequesterMockRecorder { + return m.recorder +} + +// AppendRequestedScope mocks base method. +func (m *MockDeviceRequester) AppendRequestedScope(arg0 string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "AppendRequestedScope", arg0) +} + +// AppendRequestedScope indicates an expected call of AppendRequestedScope. +func (mr *MockDeviceRequesterMockRecorder) AppendRequestedScope(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AppendRequestedScope", reflect.TypeOf((*MockDeviceRequester)(nil).AppendRequestedScope), arg0) +} + +// GetClient mocks base method. +func (m *MockDeviceRequester) GetClient() fosite.Client { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetClient") + ret0, _ := ret[0].(fosite.Client) + return ret0 +} + +// GetClient indicates an expected call of GetClient. +func (mr *MockDeviceRequesterMockRecorder) GetClient() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClient", reflect.TypeOf((*MockDeviceRequester)(nil).GetClient)) +} + +// GetGrantedAudience mocks base method. +func (m *MockDeviceRequester) GetGrantedAudience() fosite.Arguments { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetGrantedAudience") + ret0, _ := ret[0].(fosite.Arguments) + return ret0 +} + +// GetGrantedAudience indicates an expected call of GetGrantedAudience. +func (mr *MockDeviceRequesterMockRecorder) GetGrantedAudience() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGrantedAudience", reflect.TypeOf((*MockDeviceRequester)(nil).GetGrantedAudience)) +} + +// GetGrantedScopes mocks base method. +func (m *MockDeviceRequester) GetGrantedScopes() fosite.Arguments { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetGrantedScopes") + ret0, _ := ret[0].(fosite.Arguments) + return ret0 +} + +// GetGrantedScopes indicates an expected call of GetGrantedScopes. +func (mr *MockDeviceRequesterMockRecorder) GetGrantedScopes() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGrantedScopes", reflect.TypeOf((*MockDeviceRequester)(nil).GetGrantedScopes)) +} + +// GetID mocks base method. +func (m *MockDeviceRequester) GetID() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetID") + ret0, _ := ret[0].(string) + return ret0 +} + +// GetID indicates an expected call of GetID. +func (mr *MockDeviceRequesterMockRecorder) GetID() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetID", reflect.TypeOf((*MockDeviceRequester)(nil).GetID)) +} + +// GetRequestForm mocks base method. +func (m *MockDeviceRequester) GetRequestForm() url.Values { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetRequestForm") + ret0, _ := ret[0].(url.Values) + return ret0 +} + +// GetRequestForm indicates an expected call of GetRequestForm. +func (mr *MockDeviceRequesterMockRecorder) GetRequestForm() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRequestForm", reflect.TypeOf((*MockDeviceRequester)(nil).GetRequestForm)) +} + +// GetRequestedAt mocks base method. +func (m *MockDeviceRequester) GetRequestedAt() time.Time { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetRequestedAt") + ret0, _ := ret[0].(time.Time) + return ret0 +} + +// GetRequestedAt indicates an expected call of GetRequestedAt. +func (mr *MockDeviceRequesterMockRecorder) GetRequestedAt() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRequestedAt", reflect.TypeOf((*MockDeviceRequester)(nil).GetRequestedAt)) +} + +// GetRequestedAudience mocks base method. +func (m *MockDeviceRequester) GetRequestedAudience() fosite.Arguments { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetRequestedAudience") + ret0, _ := ret[0].(fosite.Arguments) + return ret0 +} + +// GetRequestedAudience indicates an expected call of GetRequestedAudience. +func (mr *MockDeviceRequesterMockRecorder) GetRequestedAudience() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRequestedAudience", reflect.TypeOf((*MockDeviceRequester)(nil).GetRequestedAudience)) +} + +// GetRequestedScopes mocks base method. +func (m *MockDeviceRequester) GetRequestedScopes() fosite.Arguments { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetRequestedScopes") + ret0, _ := ret[0].(fosite.Arguments) + return ret0 +} + +// GetRequestedScopes indicates an expected call of GetRequestedScopes. +func (mr *MockDeviceRequesterMockRecorder) GetRequestedScopes() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRequestedScopes", reflect.TypeOf((*MockDeviceRequester)(nil).GetRequestedScopes)) +} + +// GetSession mocks base method. +func (m *MockDeviceRequester) GetSession() fosite.Session { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSession") + ret0, _ := ret[0].(fosite.Session) + return ret0 +} + +// GetSession indicates an expected call of GetSession. +func (mr *MockDeviceRequesterMockRecorder) GetSession() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSession", reflect.TypeOf((*MockDeviceRequester)(nil).GetSession)) +} + +// GrantAudience mocks base method. +func (m *MockDeviceRequester) GrantAudience(arg0 string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "GrantAudience", arg0) +} + +// GrantAudience indicates an expected call of GrantAudience. +func (mr *MockDeviceRequesterMockRecorder) GrantAudience(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GrantAudience", reflect.TypeOf((*MockDeviceRequester)(nil).GrantAudience), arg0) +} + +// GrantScope mocks base method. +func (m *MockDeviceRequester) GrantScope(arg0 string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "GrantScope", arg0) +} + +// GrantScope indicates an expected call of GrantScope. +func (mr *MockDeviceRequesterMockRecorder) GrantScope(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GrantScope", reflect.TypeOf((*MockDeviceRequester)(nil).GrantScope), arg0) +} + +// Merge mocks base method. +func (m *MockDeviceRequester) Merge(arg0 fosite.Requester) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Merge", arg0) +} + +// Merge indicates an expected call of Merge. +func (mr *MockDeviceRequesterMockRecorder) Merge(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Merge", reflect.TypeOf((*MockDeviceRequester)(nil).Merge), arg0) +} + +// Sanitize mocks base method. +func (m *MockDeviceRequester) Sanitize(arg0 []string) fosite.Requester { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Sanitize", arg0) + ret0, _ := ret[0].(fosite.Requester) + return ret0 +} + +// Sanitize indicates an expected call of Sanitize. +func (mr *MockDeviceRequesterMockRecorder) Sanitize(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Sanitize", reflect.TypeOf((*MockDeviceRequester)(nil).Sanitize), arg0) +} + +// SetID mocks base method. +func (m *MockDeviceRequester) SetID(arg0 string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetID", arg0) +} + +// SetID indicates an expected call of SetID. +func (mr *MockDeviceRequesterMockRecorder) SetID(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetID", reflect.TypeOf((*MockDeviceRequester)(nil).SetID), arg0) +} + +// SetRequestedAudience mocks base method. +func (m *MockDeviceRequester) SetRequestedAudience(arg0 fosite.Arguments) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetRequestedAudience", arg0) +} + +// SetRequestedAudience indicates an expected call of SetRequestedAudience. +func (mr *MockDeviceRequesterMockRecorder) SetRequestedAudience(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetRequestedAudience", reflect.TypeOf((*MockDeviceRequester)(nil).SetRequestedAudience), arg0) +} + +// SetRequestedScopes mocks base method. +func (m *MockDeviceRequester) SetRequestedScopes(arg0 fosite.Arguments) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetRequestedScopes", arg0) +} + +// SetRequestedScopes indicates an expected call of SetRequestedScopes. +func (mr *MockDeviceRequesterMockRecorder) SetRequestedScopes(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetRequestedScopes", reflect.TypeOf((*MockDeviceRequester)(nil).SetRequestedScopes), arg0) +} + +// SetSession mocks base method. +func (m *MockDeviceRequester) SetSession(arg0 fosite.Session) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetSession", arg0) +} + +// SetSession indicates an expected call of SetSession. +func (mr *MockDeviceRequesterMockRecorder) SetSession(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetSession", reflect.TypeOf((*MockDeviceRequester)(nil).SetSession), arg0) +} diff --git a/internal/device_response.go b/internal/device_response.go new file mode 100644 index 000000000..889ed5ec9 --- /dev/null +++ b/internal/device_response.go @@ -0,0 +1,220 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/ory/fosite (interfaces: DeviceResponder) + +// Package internal is a generated GoMock package. +package internal + +import ( + http "net/http" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" +) + +// MockDeviceResponder is a mock of DeviceResponder interface. +type MockDeviceResponder struct { + ctrl *gomock.Controller + recorder *MockDeviceResponderMockRecorder +} + +// MockDeviceResponderMockRecorder is the mock recorder for MockDeviceResponder. +type MockDeviceResponderMockRecorder struct { + mock *MockDeviceResponder +} + +// NewMockDeviceResponder creates a new mock instance. +func NewMockDeviceResponder(ctrl *gomock.Controller) *MockDeviceResponder { + mock := &MockDeviceResponder{ctrl: ctrl} + mock.recorder = &MockDeviceResponderMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDeviceResponder) EXPECT() *MockDeviceResponderMockRecorder { + return m.recorder +} + +// AddHeader mocks base method. +func (m *MockDeviceResponder) AddHeader(arg0, arg1 string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "AddHeader", arg0, arg1) +} + +// AddHeader indicates an expected call of AddHeader. +func (mr *MockDeviceResponderMockRecorder) AddHeader(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddHeader", reflect.TypeOf((*MockDeviceResponder)(nil).AddHeader), arg0, arg1) +} + +// GetDeviceCode mocks base method. +func (m *MockDeviceResponder) GetDeviceCode() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetDeviceCode") + ret0, _ := ret[0].(string) + return ret0 +} + +// GetDeviceCode indicates an expected call of GetDeviceCode. +func (mr *MockDeviceResponderMockRecorder) GetDeviceCode() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDeviceCode", reflect.TypeOf((*MockDeviceResponder)(nil).GetDeviceCode)) +} + +// GetExpiresIn mocks base method. +func (m *MockDeviceResponder) GetExpiresIn() int64 { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetExpiresIn") + ret0, _ := ret[0].(int64) + return ret0 +} + +// GetExpiresIn indicates an expected call of GetExpiresIn. +func (mr *MockDeviceResponderMockRecorder) GetExpiresIn() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetExpiresIn", reflect.TypeOf((*MockDeviceResponder)(nil).GetExpiresIn)) +} + +// GetHeader mocks base method. +func (m *MockDeviceResponder) GetHeader() http.Header { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetHeader") + ret0, _ := ret[0].(http.Header) + return ret0 +} + +// GetHeader indicates an expected call of GetHeader. +func (mr *MockDeviceResponderMockRecorder) GetHeader() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetHeader", reflect.TypeOf((*MockDeviceResponder)(nil).GetHeader)) +} + +// GetInterval mocks base method. +func (m *MockDeviceResponder) GetInterval() int { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetInterval") + ret0, _ := ret[0].(int) + return ret0 +} + +// GetInterval indicates an expected call of GetInterval. +func (mr *MockDeviceResponderMockRecorder) GetInterval() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInterval", reflect.TypeOf((*MockDeviceResponder)(nil).GetInterval)) +} + +// GetUserCode mocks base method. +func (m *MockDeviceResponder) GetUserCode() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUserCode") + ret0, _ := ret[0].(string) + return ret0 +} + +// GetUserCode indicates an expected call of GetUserCode. +func (mr *MockDeviceResponderMockRecorder) GetUserCode() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserCode", reflect.TypeOf((*MockDeviceResponder)(nil).GetUserCode)) +} + +// GetVerificationURI mocks base method. +func (m *MockDeviceResponder) GetVerificationURI() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetVerificationURI") + ret0, _ := ret[0].(string) + return ret0 +} + +// GetVerificationURI indicates an expected call of GetVerificationURI. +func (mr *MockDeviceResponderMockRecorder) GetVerificationURI() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetVerificationURI", reflect.TypeOf((*MockDeviceResponder)(nil).GetVerificationURI)) +} + +// GetVerificationURIComplete mocks base method. +func (m *MockDeviceResponder) GetVerificationURIComplete() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetVerificationURIComplete") + ret0, _ := ret[0].(string) + return ret0 +} + +// GetVerificationURIComplete indicates an expected call of GetVerificationURIComplete. +func (mr *MockDeviceResponderMockRecorder) GetVerificationURIComplete() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetVerificationURIComplete", reflect.TypeOf((*MockDeviceResponder)(nil).GetVerificationURIComplete)) +} + +// SetDeviceCode mocks base method. +func (m *MockDeviceResponder) SetDeviceCode(arg0 string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetDeviceCode", arg0) +} + +// SetDeviceCode indicates an expected call of SetDeviceCode. +func (mr *MockDeviceResponderMockRecorder) SetDeviceCode(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDeviceCode", reflect.TypeOf((*MockDeviceResponder)(nil).SetDeviceCode), arg0) +} + +// SetExpiresIn mocks base method. +func (m *MockDeviceResponder) SetExpiresIn(arg0 int64) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetExpiresIn", arg0) +} + +// SetExpiresIn indicates an expected call of SetExpiresIn. +func (mr *MockDeviceResponderMockRecorder) SetExpiresIn(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetExpiresIn", reflect.TypeOf((*MockDeviceResponder)(nil).SetExpiresIn), arg0) +} + +// SetInterval mocks base method. +func (m *MockDeviceResponder) SetInterval(arg0 int) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetInterval", arg0) +} + +// SetInterval indicates an expected call of SetInterval. +func (mr *MockDeviceResponderMockRecorder) SetInterval(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetInterval", reflect.TypeOf((*MockDeviceResponder)(nil).SetInterval), arg0) +} + +// SetUserCode mocks base method. +func (m *MockDeviceResponder) SetUserCode(arg0 string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetUserCode", arg0) +} + +// SetUserCode indicates an expected call of SetUserCode. +func (mr *MockDeviceResponderMockRecorder) SetUserCode(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetUserCode", reflect.TypeOf((*MockDeviceResponder)(nil).SetUserCode), arg0) +} + +// SetVerificationURI mocks base method. +func (m *MockDeviceResponder) SetVerificationURI(arg0 string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetVerificationURI", arg0) +} + +// SetVerificationURI indicates an expected call of SetVerificationURI. +func (mr *MockDeviceResponderMockRecorder) SetVerificationURI(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetVerificationURI", reflect.TypeOf((*MockDeviceResponder)(nil).SetVerificationURI), arg0) +} + +// SetVerificationURIComplete mocks base method. +func (m *MockDeviceResponder) SetVerificationURIComplete(arg0 string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetVerificationURIComplete", arg0) +} + +// SetVerificationURIComplete indicates an expected call of SetVerificationURIComplete. +func (mr *MockDeviceResponderMockRecorder) SetVerificationURIComplete(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetVerificationURIComplete", reflect.TypeOf((*MockDeviceResponder)(nil).SetVerificationURIComplete), arg0) +} diff --git a/internal/oauth2_auth_device_storage.go b/internal/oauth2_auth_device_storage.go new file mode 100644 index 000000000..84f54c4a4 --- /dev/null +++ b/internal/oauth2_auth_device_storage.go @@ -0,0 +1,139 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/ory/fosite/handler/rfc8628 (interfaces: RFC8628CodeStorage) + +// Package internal is a generated GoMock package. +package internal + +import ( + context "context" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + fosite "github.com/ory/fosite" +) + +// MockRFC8628CodeStorage is a mock of RFC8628CodeStorage interface. +type MockRFC8628CodeStorage struct { + ctrl *gomock.Controller + recorder *MockRFC8628CodeStorageMockRecorder +} + +// MockRFC8628CodeStorageMockRecorder is the mock recorder for MockRFC8628CodeStorage. +type MockRFC8628CodeStorageMockRecorder struct { + mock *MockRFC8628CodeStorage +} + +// NewMockRFC8628CodeStorage creates a new mock instance. +func NewMockRFC8628CodeStorage(ctrl *gomock.Controller) *MockRFC8628CodeStorage { + mock := &MockRFC8628CodeStorage{ctrl: ctrl} + mock.recorder = &MockRFC8628CodeStorageMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRFC8628CodeStorage) EXPECT() *MockRFC8628CodeStorageMockRecorder { + return m.recorder +} + +// CreateDeviceCodeSession mocks base method. +func (m *MockRFC8628CodeStorage) CreateDeviceCodeSession(arg0 context.Context, arg1 string, arg2 fosite.Requester) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateDeviceCodeSession", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// CreateDeviceCodeSession indicates an expected call of CreateDeviceCodeSession. +func (mr *MockRFC8628CodeStorageMockRecorder) CreateDeviceCodeSession(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateDeviceCodeSession", reflect.TypeOf((*MockRFC8628CodeStorage)(nil).CreateDeviceCodeSession), arg0, arg1, arg2) +} + +// CreateUserCodeSession mocks base method. +func (m *MockRFC8628CodeStorage) CreateUserCodeSession(arg0 context.Context, arg1 string, arg2 fosite.Requester) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateUserCodeSession", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// CreateUserCodeSession indicates an expected call of CreateUserCodeSession. +func (mr *MockRFC8628CodeStorageMockRecorder) CreateUserCodeSession(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUserCodeSession", reflect.TypeOf((*MockRFC8628CodeStorage)(nil).CreateUserCodeSession), arg0, arg1, arg2) +} + +// GetDeviceCodeSession mocks base method. +func (m *MockRFC8628CodeStorage) GetDeviceCodeSession(arg0 context.Context, arg1 string, arg2 fosite.Session) (fosite.Requester, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetDeviceCodeSession", arg0, arg1, arg2) + ret0, _ := ret[0].(fosite.Requester) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetDeviceCodeSession indicates an expected call of GetDeviceCodeSession. +func (mr *MockRFC8628CodeStorageMockRecorder) GetDeviceCodeSession(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDeviceCodeSession", reflect.TypeOf((*MockRFC8628CodeStorage)(nil).GetDeviceCodeSession), arg0, arg1, arg2) +} + +// GetUserCodeSession mocks base method. +func (m *MockRFC8628CodeStorage) GetUserCodeSession(arg0 context.Context, arg1 string, arg2 fosite.Session) (fosite.Requester, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUserCodeSession", arg0, arg1, arg2) + ret0, _ := ret[0].(fosite.Requester) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUserCodeSession indicates an expected call of GetUserCodeSession. +func (mr *MockRFC8628CodeStorageMockRecorder) GetUserCodeSession(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserCodeSession", reflect.TypeOf((*MockRFC8628CodeStorage)(nil).GetUserCodeSession), arg0, arg1, arg2) +} + +// InvalidateDeviceCodeSession mocks base method. +func (m *MockRFC8628CodeStorage) InvalidateDeviceCodeSession(arg0 context.Context, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InvalidateDeviceCodeSession", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// InvalidateDeviceCodeSession indicates an expected call of InvalidateDeviceCodeSession. +func (mr *MockRFC8628CodeStorageMockRecorder) InvalidateDeviceCodeSession(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InvalidateDeviceCodeSession", reflect.TypeOf((*MockRFC8628CodeStorage)(nil).InvalidateDeviceCodeSession), arg0, arg1) +} + +// InvalidateUserCodeSession mocks base method. +func (m *MockRFC8628CodeStorage) InvalidateUserCodeSession(arg0 context.Context, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InvalidateUserCodeSession", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// InvalidateUserCodeSession indicates an expected call of InvalidateUserCodeSession. +func (mr *MockRFC8628CodeStorageMockRecorder) InvalidateUserCodeSession(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InvalidateUserCodeSession", reflect.TypeOf((*MockRFC8628CodeStorage)(nil).InvalidateUserCodeSession), arg0, arg1) +} + +// UpdateDeviceCodeSession mocks base method. +func (m *MockRFC8628CodeStorage) UpdateDeviceCodeSession(arg0 context.Context, arg1 string, arg2 fosite.Requester) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateDeviceCodeSession", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateDeviceCodeSession indicates an expected call of UpdateDeviceCodeSession. +func (mr *MockRFC8628CodeStorageMockRecorder) UpdateDeviceCodeSession(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateDeviceCodeSession", reflect.TypeOf((*MockRFC8628CodeStorage)(nil).UpdateDeviceCodeSession), arg0, arg1, arg2) +} diff --git a/internal/user_code_storage.go b/internal/user_code_storage.go new file mode 100644 index 000000000..70c2748d6 --- /dev/null +++ b/internal/user_code_storage.go @@ -0,0 +1,82 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/ory/fosite/handler/rfc8628 (interfaces: UserCodeStorage) + +// Package internal is a generated GoMock package. +package internal + +import ( + context "context" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + fosite "github.com/ory/fosite" +) + +// MockUserCodeStorage is a mock of UserCodeStorage interface. +type MockUserCodeStorage struct { + ctrl *gomock.Controller + recorder *MockUserCodeStorageMockRecorder +} + +// MockUserCodeStorageMockRecorder is the mock recorder for MockUserCodeStorage. +type MockUserCodeStorageMockRecorder struct { + mock *MockUserCodeStorage +} + +// NewMockUserCodeStorage creates a new mock instance. +func NewMockUserCodeStorage(ctrl *gomock.Controller) *MockUserCodeStorage { + mock := &MockUserCodeStorage{ctrl: ctrl} + mock.recorder = &MockUserCodeStorageMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockUserCodeStorage) EXPECT() *MockUserCodeStorageMockRecorder { + return m.recorder +} + +// CreateUserCodeSession mocks base method. +func (m *MockUserCodeStorage) CreateUserCodeSession(arg0 context.Context, arg1 string, arg2 fosite.Requester) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateUserCodeSession", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// CreateUserCodeSession indicates an expected call of CreateUserCodeSession. +func (mr *MockUserCodeStorageMockRecorder) CreateUserCodeSession(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUserCodeSession", reflect.TypeOf((*MockUserCodeStorage)(nil).CreateUserCodeSession), arg0, arg1, arg2) +} + +// GetUserCodeSession mocks base method. +func (m *MockUserCodeStorage) GetUserCodeSession(arg0 context.Context, arg1 string, arg2 fosite.Session) (fosite.Requester, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUserCodeSession", arg0, arg1, arg2) + ret0, _ := ret[0].(fosite.Requester) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUserCodeSession indicates an expected call of GetUserCodeSession. +func (mr *MockUserCodeStorageMockRecorder) GetUserCodeSession(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserCodeSession", reflect.TypeOf((*MockUserCodeStorage)(nil).GetUserCodeSession), arg0, arg1, arg2) +} + +// InvalidateUserCodeSession mocks base method. +func (m *MockUserCodeStorage) InvalidateUserCodeSession(arg0 context.Context, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InvalidateUserCodeSession", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// InvalidateUserCodeSession indicates an expected call of InvalidateUserCodeSession. +func (mr *MockUserCodeStorageMockRecorder) InvalidateUserCodeSession(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InvalidateUserCodeSession", reflect.TypeOf((*MockUserCodeStorage)(nil).InvalidateUserCodeSession), arg0, arg1) +} diff --git a/oauth2.go b/oauth2.go index c25abf65a..5b129dd63 100644 --- a/oauth2.go +++ b/oauth2.go @@ -23,6 +23,8 @@ const ( RefreshToken TokenType = "refresh_token" AuthorizeCode TokenType = "authorize_code" IDToken TokenType = "id_token" + DeviceCode TokenType = "device_code" + UserCode TokenType = "user_code" // PushedAuthorizeRequestContext represents the PAR context object PushedAuthorizeRequestContext TokenType = "par_context" @@ -31,7 +33,8 @@ const ( GrantTypeAuthorizationCode GrantType = "authorization_code" GrantTypePassword GrantType = "password" GrantTypeClientCredentials GrantType = "client_credentials" - GrantTypeJWTBearer GrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer" //nolint:gosec // this is not a hardcoded credential + GrantTypeJWTBearer GrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer" //nolint:gosec // this is not a hardcoded credential + GrantTypeDeviceCode GrantType = "urn:ietf:params:oauth:grant-type:device_code" //nolint:gosec // this is not a hardcoded credential BearerAccessToken string = "bearer" ) @@ -100,6 +103,48 @@ type OAuth2Provider interface { // * https://tools.ietf.org/html/rfc6749#section-3.1.2.2 (everything MUST be implemented) WriteAuthorizeResponse(ctx context.Context, rw http.ResponseWriter, requester AuthorizeRequester, responder AuthorizeResponder) + // NewDeviceUserRequest returns an DeviceUserRequest. + // This endpoint is a loose implementation of the Authorize endpoint + // but instead of a code, it's a user_code that is generated by the device endpoint + NewDeviceUserRequest(ctx context.Context, req *http.Request) (DeviceUserRequester, error) + + // NewDeviceUserResponse + // This endpoint is a loose implementation of the Authorize endpoint + // but instead of a code, it's a user_code that is generated by the device endpoint + NewDeviceUserResponse(ctx context.Context, requester DeviceUserRequester, session Session) (DeviceUserResponder, error) + + // WriteDeviceUserResponse + // Once the user is authorized, it is being redirect to the login page; + WriteDeviceUserResponse(ctx context.Context, r *http.Request, rw http.ResponseWriter, requester DeviceUserRequester, responder DeviceUserResponder) + + // NewDeviceRequest validate the OAuth 2.0 Device Authorization Flow Request + // + // The following specs must be considered in any implementation of this method: + // * https://www.rfc-editor.org/rfc/rfc8628#section-3.1 (everything MUST be implemented) + // Parameters sent without a value MUST be treated as if they were + // omitted from the request. The authorization server MUST ignore + // unrecognized request parameters. Request and response parameters + // MUST NOT be included more than once. + NewDeviceRequest(ctx context.Context, req *http.Request) (DeviceRequester, error) + + // NewDeviceResponse persists the DeviceCodeSession and UserCodeSession in the store + // + // The following specs must be considered in any implementation of this method: + // * https://www.rfc-editor.org/rfc/rfc8628#section-3.2 (everything MUST be implemented) + // In response, the authorization server generates a unique device + // verification code and an end-user code that are valid for a limited + // time + NewDeviceResponse(ctx context.Context, requester DeviceRequester) (DeviceResponder, error) + + // WriteDeviceResponse return to the user both codes and + // some configuration informations in a JSON formated manner + // + // The following specs must be considered in any implementation of this method: + // * https://www.rfc-editor.org/rfc/rfc8628#section-3.2 (everything MUST be implemented) + // Response is a HTTP response body using the + // "application/json" format [RFC8259] with a 200 (OK) status code. + WriteDeviceResponse(ctx context.Context, rw http.ResponseWriter, requester DeviceRequester, responder DeviceResponder) + // NewAccessRequest creates a new access request object and validates // various parameters. // @@ -253,6 +298,27 @@ type AccessRequester interface { Requester } +// DeviceRequestThrottler is a device api to throttle request +type DeviceRequestThrottler interface { + ShouldRateLimit(deviceCode string, client Client) bool +} + +// DeviceRequester is an device endpoint's request context. +type DeviceRequester interface { + Requester +} + +// DeviceUserRequester is an device authorize endpoint's request context. +type DeviceUserRequester interface { + // SetDeviceCodeSignature set the device code signature + SetDeviceCodeSignature(signature string) + + // GetDeviceCodeSignature returns the device code signature + GetDeviceCodeSignature() string + + Requester +} + // AuthorizeRequester is an authorize endpoint's request context. type AuthorizeRequester interface { // GetResponseTypes returns the requested response types @@ -287,6 +353,14 @@ type AuthorizeRequester interface { Requester } +type Responder interface { + // GetHeader returns the response's header + GetHeader() (header http.Header) + + // AddHeader adds an header key value pair to the response + AddHeader(key, value string) +} + // AccessResponder is a token endpoint's response. type AccessResponder interface { // SetExtra sets a key value pair for the access response. @@ -320,17 +394,13 @@ type AuthorizeResponder interface { // GetCode returns the response's authorize code if set. GetCode() string - // GetHeader returns the response's header - GetHeader() (header http.Header) - - // AddHeader adds an header key value pair to the response - AddHeader(key, value string) - // GetParameters returns the response's parameters GetParameters() (query url.Values) // AddParameter adds key value pair to the response AddParameter(key, value string) + + Responder } // PushedAuthorizeResponder is the response object for PAR @@ -344,12 +414,6 @@ type PushedAuthorizeResponder interface { // SetExpiresIn sets the expires_in SetExpiresIn(seconds int) - // GetHeader returns the response's header - GetHeader() (header http.Header) - - // AddHeader adds an header key value pair to the response - AddHeader(key, value string) - // SetExtra sets a key value pair for the response. SetExtra(key string, value interface{}) @@ -358,6 +422,8 @@ type PushedAuthorizeResponder interface { // ToMap converts the response to a map. ToMap() map[string]interface{} + + Responder } // G11NContext is the globalization context @@ -365,3 +431,29 @@ type G11NContext interface { // GetLang returns the current language in the context GetLang() language.Tag } + +type DeviceUserResponder interface { + Responder +} + +type DeviceResponder interface { + GetDeviceCode() string + SetDeviceCode(code string) + + GetUserCode() string + SetUserCode(code string) + + GetVerificationURI() string + SetVerificationURI(uri string) + + GetVerificationURIComplete() string + SetVerificationURIComplete(uri string) + + GetExpiresIn() int64 + SetExpiresIn(seconds int64) + + GetInterval() int + SetInterval(seconds int) + + Responder +} diff --git a/storage/memory.go b/storage/memory.go index 3c6a5ad45..641a53bd7 100644 --- a/storage/memory.go +++ b/storage/memory.go @@ -41,12 +41,16 @@ type MemoryStore struct { IDSessions map[string]fosite.Requester AccessTokens map[string]fosite.Requester RefreshTokens map[string]StoreRefreshToken + DeviceCodes map[string]fosite.Requester + UserCodes map[string]fosite.Requester PKCES map[string]fosite.Requester Users map[string]MemoryUserRelation BlacklistedJTIs map[string]time.Time // In-memory request ID to token signatures AccessTokenRequestIDs map[string]string RefreshTokenRequestIDs map[string]string + DeviceCodesRequestIDs map[string]string + UserCodesRequestIDs map[string]string // Public keys to check signature in auth grant jwt assertion. IssuerPublicKeys map[string]IssuerPublicKeys PARSessions map[string]fosite.AuthorizeRequester @@ -56,11 +60,15 @@ type MemoryStore struct { idSessionsMutex sync.RWMutex accessTokensMutex sync.RWMutex refreshTokensMutex sync.RWMutex + userCodesMutex sync.RWMutex + deviceCodesMutex sync.RWMutex pkcesMutex sync.RWMutex usersMutex sync.RWMutex blacklistedJTIsMutex sync.RWMutex accessTokenRequestIDsMutex sync.RWMutex refreshTokenRequestIDsMutex sync.RWMutex + userCodesRequestIDsMutex sync.RWMutex + deviceCodesRequestIDsMutex sync.RWMutex issuerPublicKeysMutex sync.RWMutex parSessionsMutex sync.RWMutex } @@ -72,10 +80,14 @@ func NewMemoryStore() *MemoryStore { IDSessions: make(map[string]fosite.Requester), AccessTokens: make(map[string]fosite.Requester), RefreshTokens: make(map[string]StoreRefreshToken), + DeviceCodes: make(map[string]fosite.Requester), + UserCodes: make(map[string]fosite.Requester), PKCES: make(map[string]fosite.Requester), Users: make(map[string]MemoryUserRelation), AccessTokenRequestIDs: make(map[string]string), RefreshTokenRequestIDs: make(map[string]string), + DeviceCodesRequestIDs: make(map[string]string), + UserCodesRequestIDs: make(map[string]string), BlacklistedJTIs: make(map[string]time.Time), IssuerPublicKeys: make(map[string]IssuerPublicKeys), PARSessions: make(map[string]fosite.AuthorizeRequester), @@ -139,8 +151,12 @@ func NewExampleStore() *MemoryStore { AccessTokens: map[string]fosite.Requester{}, RefreshTokens: map[string]StoreRefreshToken{}, PKCES: map[string]fosite.Requester{}, + DeviceCodes: make(map[string]fosite.Requester), + UserCodes: make(map[string]fosite.Requester), AccessTokenRequestIDs: map[string]string{}, RefreshTokenRequestIDs: map[string]string{}, + DeviceCodesRequestIDs: make(map[string]string), + UserCodesRequestIDs: make(map[string]string), IssuerPublicKeys: map[string]IssuerPublicKeys{}, PARSessions: map[string]fosite.AuthorizeRequester{}, } @@ -497,3 +513,81 @@ func (s *MemoryStore) DeletePARSession(ctx context.Context, requestURI string) ( delete(s.PARSessions, requestURI) return nil } + +func (s *MemoryStore) CreateDeviceCodeSession(_ context.Context, signature string, req fosite.Requester) error { + // We first lock accessTokenRequestIDsMutex and then accessTokensMutex because this is the same order + // locking happens in RevokeAccessToken and using the same order prevents deadlocks. + s.deviceCodesRequestIDsMutex.Lock() + defer s.deviceCodesRequestIDsMutex.Unlock() + s.deviceCodesMutex.Lock() + defer s.deviceCodesMutex.Unlock() + + s.DeviceCodes[signature] = req + s.DeviceCodesRequestIDs[req.GetID()] = signature + return nil +} + +func (s *MemoryStore) UpdateDeviceCodeSession(_ context.Context, signature string, req fosite.Requester) error { + s.deviceCodesRequestIDsMutex.Lock() + defer s.deviceCodesRequestIDsMutex.Unlock() + s.deviceCodesMutex.Lock() + defer s.deviceCodesMutex.Unlock() + + // Only update if exist + if _, exists := s.DeviceCodes[signature]; exists { + s.DeviceCodes[signature] = req + s.DeviceCodesRequestIDs[req.GetID()] = signature + } + return nil +} + +func (s *MemoryStore) GetDeviceCodeSession(_ context.Context, signature string, _ fosite.Session) (fosite.Requester, error) { + s.deviceCodesMutex.RLock() + defer s.deviceCodesMutex.RUnlock() + + rel, ok := s.DeviceCodes[signature] + if !ok { + return nil, fosite.ErrNotFound + } + return rel, nil +} + +func (s *MemoryStore) InvalidateDeviceCodeSession(_ context.Context, code string) error { + s.deviceCodesMutex.Lock() + defer s.deviceCodesMutex.Unlock() + + delete(s.DeviceCodes, code) + return nil +} + +func (s *MemoryStore) CreateUserCodeSession(_ context.Context, signature string, req fosite.Requester) error { + // We first lock accessTokenRequestIDsMutex and then accessTokensMutex because this is the same order + // locking happens in RevokeAccessToken and using the same order prevents deadlocks. + s.accessTokenRequestIDsMutex.Lock() + defer s.accessTokenRequestIDsMutex.Unlock() + s.accessTokensMutex.Lock() + defer s.accessTokensMutex.Unlock() + + s.AccessTokens[signature] = req + s.AccessTokenRequestIDs[req.GetID()] = signature + return nil +} + +func (s *MemoryStore) GetUserCodeSession(_ context.Context, signature string, _ fosite.Session) (fosite.Requester, error) { + s.accessTokensMutex.RLock() + defer s.accessTokensMutex.RUnlock() + + rel, ok := s.AccessTokens[signature] + if !ok { + return nil, fosite.ErrNotFound + } + return rel, nil +} + +func (s *MemoryStore) InvalidateUserCodeSession(_ context.Context, code string) error { + s.userCodesMutex.Lock() + defer s.userCodesMutex.Unlock() + + delete(s.UserCodes, code) + return nil +} diff --git a/token/hmac/hmacsha.go b/token/hmac/hmacsha.go index 84db7c529..86875f6b1 100644 --- a/token/hmac/hmacsha.go +++ b/token/hmac/hmacsha.go @@ -170,6 +170,26 @@ func (c *HMACStrategy) Signature(token string) string { return split[1] } +func (c *HMACStrategy) GenerateHMACForString(ctx context.Context, text string) (string, error) { + var signingKey [32]byte + + secrets, err := c.Config.GetGlobalSecret(ctx) + if err != nil { + return "", err + } + + if len(secrets) < minimumSecretLength { + return "", errors.Errorf("secret for signing HMAC-SHA512/256 is expected to be 32 byte long, got %d byte", len(secrets)) + } + copy(signingKey[:], secrets) + + bytes := []byte(text) + hashBytes := c.generateHMAC(ctx, bytes, &signingKey) + + b64 := base64.RawURLEncoding.EncodeToString(hashBytes) + return b64, nil +} + func (c *HMACStrategy) generateHMAC(ctx context.Context, data []byte, key *[32]byte) []byte { hasher := c.Config.GetHMACHasher(ctx) if hasher == nil { diff --git a/token/hmac/hmacsha_test.go b/token/hmac/hmacsha_test.go index 0df788302..fb3e60945 100644 --- a/token/hmac/hmacsha_test.go +++ b/token/hmac/hmacsha_test.go @@ -133,3 +133,33 @@ func TestCustomHMAC(t *testing.T) { require.NoError(t, sha512.Validate(context.Background(), token512)) require.EqualError(t, def.Validate(context.Background(), token512), fosite.ErrTokenSignatureMismatch.Error()) } + +func TestGenerateFromString(t *testing.T) { + cg := HMACStrategy{Config: &fosite.Config{ + GlobalSecret: []byte("1234567890123456789012345678901234567890")}, + } + for _, c := range []struct { + text string + hash string + }{ + { + text: "", + hash: "-n7EqD-bXkY3yYMH-ctEAGV8XLkU7Y6Bo6pbyT1agGA", + }, + { + text: " ", + hash: "zXJvonHTNSOOGj_QKl4RpIX_zXgD2YfXUfwuDKaTTIg", + }, + { + text: "Test", + hash: "TMeEaHS-cDC2nijiesCNtsOyBqHHtzWqAcWvceQT50g", + }, + { + text: "AnotherTest1234", + hash: "zHYDOZGjzhVjx5r8RlBhpnJemX5JxEEBUjVT01n3IFM", + }, + } { + hash, _ := cg.GenerateHMACForString(context.Background(), c.text) + assert.Equal(t, c.hash, hash) + } +}