From ce02ff38002906cfeb9dcf073be76ed06f2a2040 Mon Sep 17 00:00:00 2001 From: BumbleBee Date: Sun, 4 Sep 2022 21:58:35 +0100 Subject: [PATCH 01/49] Device grant flow (migrate to master) --- compose/compose.go | 34 +- compose/compose_oauth2.go | 22 ++ compose/compose_openid.go | 20 +- compose/compose_pkce.go | 11 + compose/compose_strategy.go | 1 + config.go | 13 + config_default.go | 34 ++ device_authorize_request_handler.go | 44 +++ device_authorize_response.go | 71 ++++ device_authorize_response_writer.go | 19 + device_authorize_writer.go | 28 ++ errors.go | 10 + fosite.go | 17 + handler.go | 10 + handler/oauth2/device_authorization.go | 60 +++ handler/oauth2/device_authorization_test.go | 52 +++ .../oauth2/flow_authorize_code_auth_test.go | 2 +- .../oauth2/flow_authorize_code_token_test.go | 2 +- handler/oauth2/flow_device_code_auth.go | 61 +++ handler/oauth2/flow_device_code_auth_test.go | 200 ++++++++++ handler/oauth2/flow_device_code_token.go | 163 ++++++++ handler/oauth2/flow_device_code_token_test.go | 350 ++++++++++++++++++ handler/oauth2/storage.go | 13 + handler/oauth2/strategy.go | 14 + handler/oauth2/strategy_hmacsha.go | 56 +++ handler/oauth2/strategy_hmacsha_test.go | 5 +- handler/oauth2/strategy_jwt.go | 24 ++ handler/openid/flow_device_auth.go | 61 +++ handler/openid/flow_device_token.go | 75 ++++ handler/openid/flow_explicit_token.go | 2 + handler/pkce/handler_device.go | 228 ++++++++++++ internal/access_request.go | 1 - internal/access_response.go | 1 - internal/access_token_storage.go | 1 - internal/access_token_strategy.go | 1 - internal/authorize_code_storage.go | 1 - internal/authorize_code_strategy.go | 1 - internal/authorize_handler.go | 1 - internal/authorize_request.go | 1 - internal/client.go | 28 -- internal/id_token_strategy.go | 1 - internal/introspector.go | 1 - internal/oauth2_client_storage.go | 1 - internal/oauth2_owner_storage.go | 1 - internal/oauth2_revoke_storage.go | 1 - internal/oauth2_storage.go | 87 ++++- internal/oauth2_strategy.go | 87 ++++- internal/openid_id_token_storage.go | 1 - internal/pkce_storage_strategy.go | 1 - internal/refresh_token_strategy.go | 1 - internal/request.go | 1 - internal/revoke_handler.go | 1 - internal/storage.go | 1 - internal/token_handler.go | 1 - oauth2.go | 27 ++ storage/memory.go | 80 ++++ token/hmac/hmacsha.go | 11 + token/hmac/hmacsha_test.go | 30 ++ 58 files changed, 2002 insertions(+), 70 deletions(-) create mode 100644 device_authorize_request_handler.go create mode 100644 device_authorize_response.go create mode 100644 device_authorize_response_writer.go create mode 100644 device_authorize_writer.go create mode 100644 handler/oauth2/device_authorization.go create mode 100644 handler/oauth2/device_authorization_test.go create mode 100644 handler/oauth2/flow_device_code_auth.go create mode 100644 handler/oauth2/flow_device_code_auth_test.go create mode 100644 handler/oauth2/flow_device_code_token.go create mode 100644 handler/oauth2/flow_device_code_token_test.go create mode 100644 handler/openid/flow_device_auth.go create mode 100644 handler/openid/flow_device_token.go create mode 100644 handler/pkce/handler_device.go diff --git a/compose/compose.go b/compose/compose.go index 683564537..372c8d3c9 100644 --- a/compose/compose.go +++ b/compose/compose.go @@ -32,24 +32,24 @@ type Factory func(config fosite.Configurator, storage interface{}, strategy inte // Compose takes a config, a storage, a strategy and handlers to instantiate an OAuth2Provider: // -// import "github.com/ory/fosite/compose" +// import "github.com/ory/fosite/compose" // -// // var storage = new(MyFositeStorage) -// var config = Config { -// AccessTokenLifespan: time.Minute * 30, -// // check Config for further configuration options -// } +// // var storage = new(MyFositeStorage) +// var config = Config { +// AccessTokenLifespan: time.Minute * 30, +// // check Config for further configuration options +// } // -// var strategy = NewOAuth2HMACStrategy(config) +// var strategy = NewOAuth2HMACStrategy(config) // -// var oauth2Provider = Compose( -// config, -// storage, -// strategy, -// NewOAuth2AuthorizeExplicitHandler, -// OAuth2ClientCredentialsGrantFactory, -// // for a complete list refer to the docs of this package -// ) +// var oauth2Provider = Compose( +// config, +// storage, +// strategy, +// NewOAuth2AuthorizeExplicitHandler, +// OAuth2ClientCredentialsGrantFactory, +// // for a complete list refer to the docs of this package +// ) // // Compose makes use of interface{} types in order to be able to handle a all types of stores, strategies and handlers. func Compose(config *fosite.Config, storage interface{}, strategy interface{}, factories ...Factory) fosite.OAuth2Provider { @@ -91,8 +91,10 @@ func ComposeAllEnabled(config *fosite.Config, storage interface{}, key interface }, OAuth2AuthorizeExplicitFactory, OAuth2AuthorizeImplicitFactory, + OAuth2AuthorizeDeviceFactory, OAuth2ClientCredentialsGrantFactory, OAuth2RefreshTokenGrantFactory, + OAuth2DeviceAuthorizeFactory, OAuth2ResourceOwnerPasswordCredentialsFactory, RFC7523AssertionGrantFactory, @@ -100,11 +102,13 @@ func ComposeAllEnabled(config *fosite.Config, storage interface{}, key interface OpenIDConnectImplicitFactory, OpenIDConnectHybridFactory, OpenIDConnectRefreshFactory, + OpenIDConnectDeviceFactory, OAuth2TokenIntrospectionFactory, OAuth2TokenRevocationFactory, OAuth2PKCEFactory, PushedAuthorizeHandlerFactory, + OAuth2DevicePKCEFactory, ) } diff --git a/compose/compose_oauth2.go b/compose/compose_oauth2.go index f80316f76..174377c70 100644 --- a/compose/compose_oauth2.go +++ b/compose/compose_oauth2.go @@ -125,3 +125,25 @@ func OAuth2StatelessJWTIntrospectionFactory(config fosite.Configurator, storage Config: config, } } + +func OAuth2AuthorizeDeviceFactory(config fosite.Configurator, storage interface{}, strategy interface{}) interface{} { + return &oauth2.AuthorizeDeviceGrantTypeHandler{ + DeviceCodeStrategy: strategy.(oauth2.DeviceCodeStrategy), + UserCodeStrategy: strategy.(oauth2.UserCodeStrategy), + AccessTokenStrategy: strategy.(oauth2.AccessTokenStrategy), + RefreshTokenStrategy: strategy.(oauth2.RefreshTokenStrategy), + AuthorizeCodeStrategy: strategy.(oauth2.AuthorizeCodeStrategy), + CoreStorage: storage.(oauth2.CoreStorage), + Config: config, + } +} + +func OAuth2DeviceAuthorizeFactory(config fosite.Configurator, storage interface{}, strategy interface{}) interface{} { + return &oauth2.DeviceAuthorizationHandler{ + DeviceCodeStorage: storage.(oauth2.DeviceCodeStorage), + UserCodeStorage: storage.(oauth2.UserCodeStorage), + DeviceCodeStrategy: strategy.(oauth2.DeviceCodeStrategy), + UserCodeStrategy: strategy.(oauth2.UserCodeStrategy), + Config: config, + } +} diff --git a/compose/compose_openid.go b/compose/compose_openid.go index 4ced36a55..ca2bfc1bb 100644 --- a/compose/compose_openid.go +++ b/compose/compose_openid.go @@ -62,7 +62,8 @@ func OpenIDConnectImplicitFactory(config fosite.Configurator, storage interface{ AuthorizeImplicitGrantTypeHandler: &oauth2.AuthorizeImplicitGrantTypeHandler{ AccessTokenStrategy: strategy.(oauth2.AccessTokenStrategy), AccessTokenStorage: storage.(oauth2.AccessTokenStorage), - Config: config, + + Config: config, }, Config: config, IDTokenHandleHelper: &openid.IDTokenHandleHelper{ @@ -97,3 +98,20 @@ 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{ + CoreStorage: storage.(oauth2.CoreStorage), + DeviceCodeStrategy: strategy.(oauth2.DeviceCodeStrategy), + UserCodeStrategy: strategy.(oauth2.UserCodeStrategy), + 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_pkce.go b/compose/compose_pkce.go index b5d2ddc33..4dfc003d0 100644 --- a/compose/compose_pkce.go +++ b/compose/compose_pkce.go @@ -35,3 +35,14 @@ func OAuth2PKCEFactory(config fosite.Configurator, storage interface{}, strategy Config: config, } } + +func OAuth2DevicePKCEFactory(config fosite.Configurator, storage interface{}, strategy interface{}) interface{} { + return &pkce.HandlerDevice{ + CoreStorage: storage.(oauth2.CoreStorage), + DeviceCodeStrategy: strategy.(oauth2.DeviceCodeStrategy), + UserCodeStrategy: strategy.(oauth2.UserCodeStrategy), + AuthorizeCodeStrategy: strategy.(oauth2.AuthorizeCodeStrategy), + Storage: storage.(pkce.PKCERequestStorage), + Config: config, + } +} diff --git a/compose/compose_strategy.go b/compose/compose_strategy.go index dda18f41a..b31c42816 100644 --- a/compose/compose_strategy.go +++ b/compose/compose_strategy.go @@ -45,6 +45,7 @@ type HMACSHAStrategyConfigurator interface { fosite.GlobalSecretProvider fosite.RotatedGlobalSecretsProvider fosite.HMACHashingProvider + fosite.DeviceAndUserCodeLifespanProvider } func NewOAuth2HMACStrategy(config HMACSHAStrategyConfigurator) *oauth2.HMACSHAStrategy { diff --git a/config.go b/config.go index 1edc5bc5f..5d4d31e4c 100644 --- a/config.go +++ b/config.go @@ -19,6 +19,15 @@ type AuthorizeCodeLifespanProvider interface { GetAuthorizeCodeLifespan(ctx context.Context) time.Duration } +type DeviceAndUserCodeLifespanProvider interface { + GetDeviceAndUserCodeLifespan(ctx context.Context) time.Duration +} + +type DeviceUriProvider 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. @@ -272,6 +281,10 @@ type PushedAuthorizeRequestHandlersProvider interface { GetPushedAuthorizeEndpointHandlers(ctx context.Context) PushedAuthorizeEndpointHandlers } +type DeviceAuthorizeEndpointHandlersProvider interface { + GetDeviceAuthorizeEndpointHandlers(ctx context.Context) DeviceAuthorizeEndpointHandlers +} + // 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 9ff7dc941..1da029d7a 100644 --- a/config_default.go +++ b/config_default.go @@ -80,6 +80,7 @@ var ( _ RevocationHandlersProvider = (*Config)(nil) _ PushedAuthorizeRequestHandlersProvider = (*Config)(nil) _ PushedAuthorizeRequestConfigProvider = (*Config)(nil) + _ DeviceAuthorizeEndpointHandlersProvider = (*Config)(nil) ) type Config struct { @@ -93,6 +94,15 @@ 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 + // IDTokenLifespan sets the default id token lifetime. Defaults to one hour. IDTokenLifespan time.Duration @@ -212,6 +222,8 @@ type Config struct { // PushedAuthorizeEndpointHandlers is a list of handlers that are called before the PAR endpoint is served. PushedAuthorizeEndpointHandlers PushedAuthorizeEndpointHandlers + DeviceAuthorizeEndpointHandlers DeviceAuthorizeEndpointHandlers + // GlobalSecret is the global secret used to sign and verify signatures. GlobalSecret []byte @@ -260,6 +272,10 @@ func (c *Config) GetTokenIntrospectionHandlers(ctx context.Context) TokenIntrosp return c.TokenIntrospectionHandlers } +func (c *Config) GetDeviceAuthorizeEndpointHandlers(ctx context.Context) DeviceAuthorizeEndpointHandlers { + return c.DeviceAuthorizeEndpointHandlers +} + func (c *Config) GetRevocationHandlers(ctx context.Context) RevocationHandlers { return c.RevocationHandlers } @@ -378,6 +394,13 @@ func (c *Config) GetAuthorizeCodeLifespan(_ context.Context) time.Duration { return c.AuthorizeCodeLifespan } +func (c *Config) GetDeviceAndUserCodeLifespan(_ context.Context) time.Duration { + if c.AuthorizeCodeLifespan == 0 { + return time.Minute * 10 + } + return c.DeviceAndUserCodeLifespan +} + // GeIDTokenLifespan 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 { @@ -506,3 +529,14 @@ func (c *Config) GetPushedAuthorizeContextLifespan(ctx context.Context) time.Dur func (c *Config) EnforcePushedAuthorize(ctx context.Context) bool { return c.IsPushedAuthorizeEnforced } + +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 * 10 + } + return c.DeviceAuthTokenPollingInterval +} diff --git a/device_authorize_request_handler.go b/device_authorize_request_handler.go new file mode 100644 index 000000000..efc981094 --- /dev/null +++ b/device_authorize_request_handler.go @@ -0,0 +1,44 @@ +package fosite + +import ( + "context" + "net/http" + "strings" + + "github.com/ory/fosite/i18n" + "github.com/ory/x/errorsx" +) + +func (f *Fosite) NewDeviceAuthorizeRequest(ctx context.Context, req *http.Request) (Requester, error) { + + request := NewRequest() + request.Lang = i18n.GetLangFromRequest(f.Config.GetMessageCatalog(ctx), req) + + if err := req.ParseMultipartForm(1 << 20); err != nil && err != http.ErrNotMultipart { + return request, 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 request, errorsx.WithStack(ErrInvalidClient.WithHint("The requested OAuth 2.0 Client does not exist.").WithWrap(err).WithDebug(err.Error())) + } + request.Client = client + + if err := f.validateDeviceAuthorizeScope(ctx, request); err != nil { + return request, err + } + + return request, nil +} + +func (f *Fosite) validateDeviceAuthorizeScope(ctx context.Context, request *Request) 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_authorize_response.go b/device_authorize_response.go new file mode 100644 index 000000000..14697c08f --- /dev/null +++ b/device_authorize_response.go @@ -0,0 +1,71 @@ +package fosite + +import "context" + +type DeviceAuthorizeResponse struct { + context context.Context + deviceCode string + userCode string + verificationURI string + verificationURIComplete string + interval int + expiresIn int64 +} + +// GetDeviceCode returns the response's device code +func NewDeviceAuthorizeResponse() *DeviceAuthorizeResponse { + return &DeviceAuthorizeResponse{} +} + +func (d *DeviceAuthorizeResponse) GetDeviceCode() string { + return d.deviceCode +} + +// GetUserCode returns the response's user code +func (d *DeviceAuthorizeResponse) SetDeviceCode(code string) { + d.deviceCode = code +} + +func (d *DeviceAuthorizeResponse) GetUserCode() string { + return d.userCode +} + +func (d *DeviceAuthorizeResponse) SetUserCode(code string) { + d.userCode = code +} + +// GetVerificationURI returns the response's verification uri +func (d *DeviceAuthorizeResponse) GetVerificationURI() string { + return d.verificationURI +} + +func (d *DeviceAuthorizeResponse) SetVerificationURI(uri string) { + d.verificationURI = uri +} + +// GetVerificationURIComplete returns the response's complete verification uri if set +func (d *DeviceAuthorizeResponse) GetVerificationURIComplete() string { + return d.verificationURIComplete +} + +func (d *DeviceAuthorizeResponse) SetVerificationURIComplete(uri string) { + d.verificationURIComplete = uri +} + +// GetExpiresIn returns the response's device code and user code lifetime in seconds if set +func (d *DeviceAuthorizeResponse) GetExpiresIn() int64 { + return d.expiresIn +} + +func (d *DeviceAuthorizeResponse) SetExpiresIn(seconds int64) { + d.expiresIn = seconds +} + +// GetInterval returns the response's polling interval if set +func (d *DeviceAuthorizeResponse) GetInterval() int { + return d.interval +} + +func (d *DeviceAuthorizeResponse) SetInterval(seconds int) { + d.interval = seconds +} diff --git a/device_authorize_response_writer.go b/device_authorize_response_writer.go new file mode 100644 index 000000000..81cff1fc1 --- /dev/null +++ b/device_authorize_response_writer.go @@ -0,0 +1,19 @@ +package fosite + +import ( + "context" + "fmt" +) + +func (f *Fosite) NewDeviceAuthorizeResponse(ctx context.Context, r Requester) (DeviceAuthorizeResponder, error) { + var resp = NewDeviceAuthorizeResponse() + + for _, h := range f.Config.GetDeviceAuthorizeEndpointHandlers(ctx) { + fmt.Println("NewDeviceAuthorizeResponse +++") + if err := h.HandleDeviceAuthorizeEndpointRequest(ctx, r, resp); err != nil { + return nil, err + } + } + + return resp, nil +} diff --git a/device_authorize_writer.go b/device_authorize_writer.go new file mode 100644 index 000000000..68b41da18 --- /dev/null +++ b/device_authorize_writer.go @@ -0,0 +1,28 @@ +package fosite + +import ( + "encoding/json" + "net/http" +) + +func (f *Fosite) WriteDeviceAuthorizeResponse(rw http.ResponseWriter, r Requester, resp DeviceAuthorizeResponder) { + rw.Header().Set("Content-Type", "application/json;charset=UTF-8") + rw.Header().Set("Cache-Control", "no-store") + rw.Header().Set("Pragma", "no-cache") + + _ = json.NewEncoder(rw).Encode(struct { + 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"` + }{ + DeviceCode: resp.GetDeviceCode(), + UserCode: resp.GetUserCode(), + VerificationURI: resp.GetVerificationURI(), + VerificationURIComplete: resp.GetVerificationURIComplete(), + ExpiresIn: resp.GetExpiresIn(), + Interval: resp.GetInterval(), + }) +} diff --git a/errors.go b/errors.go index abf001272..4da5ed5b3 100644 --- a/errors.go +++ b/errors.go @@ -42,6 +42,10 @@ var ( // ErrInvalidatedAuthorizeCode is an error indicating that an authorization code has been // used previously. ErrInvalidatedAuthorizeCode = errors.New("Authorization code has ben 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") @@ -221,6 +225,11 @@ 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.StatusForbidden, + } ) const ( @@ -258,6 +267,7 @@ const ( errRequestURINotSupportedName = "request_uri_not_supported" errRegistrationNotSupportedName = "registration_not_supported" errJTIKnownName = "jti_known" + errAuthorizationPending = "authorization_pending" ) type ( diff --git a/fosite.go b/fosite.go index 285a6be3f..db1f512aa 100644 --- a/fosite.go +++ b/fosite.go @@ -100,6 +100,20 @@ func (a *PushedAuthorizeEndpointHandlers) Append(h PushedAuthorizeEndpointHandle *a = append(*a, h) } +// DeviceAuthorizeEndpointHandler is a list of DeviceAuthorizeEndpointHandler +type DeviceAuthorizeEndpointHandlers []DeviceAuthorizeEndpointHandler + +// Append adds an AuthorizeEndpointHandler to this list. Ignores duplicates based on reflect.TypeOf. +func (a *DeviceAuthorizeEndpointHandlers) Append(h DeviceAuthorizeEndpointHandler) { + for _, this := range *a { + if reflect.TypeOf(this) == reflect.TypeOf(h) { + return + } + } + + *a = append(*a, h) +} + var _ OAuth2Provider = (*Fosite)(nil) type Configurator interface { @@ -125,6 +139,7 @@ type Configurator interface { AccessTokenLifespanProvider RefreshTokenLifespanProvider AuthorizeCodeLifespanProvider + DeviceAndUserCodeLifespanProvider TokenEntropyProvider RotatedGlobalSecretsProvider GlobalSecretProvider @@ -149,6 +164,8 @@ type Configurator interface { TokenIntrospectionHandlersProvider RevocationHandlersProvider UseLegacyErrorFormatProvider + DeviceAuthorizeEndpointHandlersProvider + DeviceUriProvider } func NewOAuth2Provider(s Storage, c Configurator) *Fosite { diff --git a/handler.go b/handler.go index f1319a610..0432e6036 100644 --- a/handler.go +++ b/handler.go @@ -84,3 +84,13 @@ 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 DeviceAuthorizeEndpointHandler interface { + // HandleDeviceAuthorizeRequest 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 HandleDeviceAuthorizeRequest should do. + // * https://tools.ietf.org/html/rfc8628#section-3.2 + HandleDeviceAuthorizeEndpointRequest(ctx context.Context, requester Requester, responder DeviceAuthorizeResponder) error +} diff --git a/handler/oauth2/device_authorization.go b/handler/oauth2/device_authorization.go new file mode 100644 index 000000000..39d33c8d8 --- /dev/null +++ b/handler/oauth2/device_authorization.go @@ -0,0 +1,60 @@ +package oauth2 + +import ( + "context" + "fmt" + "time" + + "github.com/ory/fosite" + "github.com/ory/x/errorsx" +) + +// DeviceAuthorizationHandler is a response handler for the Device Authorisation Grant as +// defined in https://tools.ietf.org/html/rfc8628#section-3.1 +type DeviceAuthorizationHandler struct { + DeviceCodeStorage DeviceCodeStorage + UserCodeStorage UserCodeStorage + DeviceCodeStrategy DeviceCodeStrategy + UserCodeStrategy UserCodeStrategy + Config fosite.Configurator +} + +func (d *DeviceAuthorizationHandler) HandleDeviceAuthorizeEndpointRequest(ctx context.Context, dar fosite.Requester, resp fosite.DeviceAuthorizeResponder) error { + fmt.Println("DeviceAuthorizationHandler :: HandleDeviceAuthorizeEndpointRequest ++") + deviceCode, err := d.DeviceCodeStrategy.GenerateDeviceCode() + if err != nil { + return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) + } + + userCode, err := d.UserCodeStrategy.GenerateUserCode() + if err != nil { + return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) + } + + fmt.Println("DeviceAuthorizationHandler :: HandleDeviceAuthorizeEndpointRequest +++") + + userCodeSignature := d.UserCodeStrategy.UserCodeSignature(ctx, userCode) + deviceCodeSignature := d.DeviceCodeStrategy.DeviceCodeSignature(ctx, deviceCode) + + // Set User Code expiry time + dar.GetSession().SetExpiresAt(fosite.UserCode, time.Now().UTC().Add(d.Config.GetDeviceAndUserCodeLifespan(ctx)).Round(time.Second)) + dar.SetID(deviceCodeSignature) + + fmt.Println("DeviceAuthorizationHandler :: HandleDeviceAuthorizeEndpointRequest ++++") + + // 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 + if err := d.UserCodeStorage.CreateUserCodeSession(ctx, userCodeSignature, dar); err != nil { + return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) + } + + fmt.Println("DeviceAuthorizationHandler :: HandleDeviceAuthorizeEndpointRequest +++++") + + // 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/oauth2/device_authorization_test.go b/handler/oauth2/device_authorization_test.go new file mode 100644 index 000000000..fe6395ee1 --- /dev/null +++ b/handler/oauth2/device_authorization_test.go @@ -0,0 +1,52 @@ +package oauth2 + +import ( + "testing" + "time" + + "github.com/golang/mock/gomock" + "github.com/ory/fosite" + "github.com/ory/fosite/storage" + "github.com/stretchr/testify/assert" +) + +func Test_HandleDeviceAuthorizeEndpointRequest(t *testing.T) { + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + deviceStore := storage.NewMemoryStore() + userStore := storage.NewMemoryStore() + handler := DeviceAuthorizationHandler{ + DeviceCodeStorage: deviceStore, + UserCodeStorage: userStore, + DeviceCodeStrategy: hmacshaStrategy, + UserCodeStrategy: hmacshaStrategy, + Config: &fosite.Config{ + DeviceAndUserCodeLifespan: time.Minute * 10, + DeviceAuthTokenPollingInterval: time.Second * 10, + DeviceVerificationURL: "www.test.com", + AccessTokenLifespan: time.Hour, + RefreshTokenLifespan: time.Hour, + ScopeStrategy: fosite.HierarchicScopeStrategy, + AudienceMatchingStrategy: fosite.DefaultAudienceMatchingStrategy, + RefreshTokenScopes: []string{"offline"}, + }, + } + + req := &fosite.AuthorizeRequest{ + ResponseTypes: fosite.Arguments{"code"}, + Request: fosite.Request{ + Session: &fosite.DefaultSession{}, + }, + } + resp := fosite.NewDeviceAuthorizeResponse() + + handler.HandleDeviceAuthorizeEndpointRequest(nil, req, resp) + + assert.NotEmpty(t, resp.GetDeviceCode()) + assert.NotEmpty(t, resp.GetUserCode()) + assert.Equal(t, len(resp.GetUserCode()), 8) + assert.Equal(t, len(resp.GetDeviceCode()), 100) + assert.Equal(t, resp.GetVerificationURI(), "www.test.com") + +} diff --git a/handler/oauth2/flow_authorize_code_auth_test.go b/handler/oauth2/flow_authorize_code_auth_test.go index be0f1cc8d..c95fd5321 100644 --- a/handler/oauth2/flow_authorize_code_auth_test.go +++ b/handler/oauth2/flow_authorize_code_auth_test.go @@ -40,7 +40,7 @@ func parseUrl(uu string) *url.URL { } func TestAuthorizeCode_HandleAuthorizeEndpointRequest(t *testing.T) { - for k, strategy := range map[string]CoreStrategy{ + for k, strategy := range map[string]AuthorizeCodeStrategy{ "hmac": &hmacshaStrategy, } { t.Run("strategy="+k, func(t *testing.T) { diff --git a/handler/oauth2/flow_authorize_code_token_test.go b/handler/oauth2/flow_authorize_code_token_test.go index 326d617c9..3bc754669 100644 --- a/handler/oauth2/flow_authorize_code_token_test.go +++ b/handler/oauth2/flow_authorize_code_token_test.go @@ -258,7 +258,7 @@ func TestAuthorizeCode_PopulateTokenEndpointResponse(t *testing.T) { } func TestAuthorizeCode_HandleTokenEndpointRequest(t *testing.T) { - for k, strategy := range map[string]CoreStrategy{ + for k, strategy := range map[string]AuthorizeCodeStrategy{ "hmac": &hmacshaStrategy, } { t.Run("strategy="+k, func(t *testing.T) { diff --git a/handler/oauth2/flow_device_code_auth.go b/handler/oauth2/flow_device_code_auth.go new file mode 100644 index 000000000..6ebac1b19 --- /dev/null +++ b/handler/oauth2/flow_device_code_auth.go @@ -0,0 +1,61 @@ +package oauth2 + +import ( + "context" + "fmt" + + "github.com/ory/fosite" + "github.com/ory/x/errorsx" +) + +type AuthorizeDeviceGrantTypeHandler struct { + CoreStorage CoreStorage + DeviceCodeStrategy DeviceCodeStrategy + UserCodeStrategy UserCodeStrategy + AccessTokenStrategy AccessTokenStrategy + RefreshTokenStrategy RefreshTokenStrategy + AuthorizeCodeStrategy AuthorizeCodeStrategy + Config fosite.Configurator +} + +func (c *AuthorizeDeviceGrantTypeHandler) HandleAuthorizeEndpointRequest(ctx context.Context, ar fosite.AuthorizeRequester, resp fosite.AuthorizeResponder) error { + + if !ar.GetResponseTypes().ExactOne("device_code") { + return nil + } + + if !ar.GetClient().GetGrantTypes().Has("urn:ietf:params:oauth:grant-type:device_code") { + return nil + } + + resp.AddParameter("state", ar.GetState()) + + userCode := ar.GetRequestForm().Get("user_code") + userCodeSignature := c.UserCodeStrategy.UserCodeSignature(ctx, userCode) + + session, err := c.CoreStorage.GetUserCodeSession(ctx, userCodeSignature, fosite.NewRequest().Session) + if err != nil { + return err + } + + fmt.Println("SUBJECT : " + ar.GetSession().GetSubject()) + + if session.GetClient().GetID() != ar.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.")) + } + + /* + expires := session.GetSession().GetExpiresAt(fosite.UserCode) + if time.Now().UTC().After(expires) { + return errorsx.WithStack(fosite.ErrTokenExpired) + }*/ + + // session.GetID() is the HMAC signature of the device code generated in the inital request + err = c.CoreStorage.CreateDeviceCodeSession(ctx, session.GetID(), ar) + if err != nil { + return errorsx.WithStack(err) + } + + ar.SetResponseTypeHandled("device_code") + return nil +} diff --git a/handler/oauth2/flow_device_code_auth_test.go b/handler/oauth2/flow_device_code_auth_test.go new file mode 100644 index 000000000..842b4c035 --- /dev/null +++ b/handler/oauth2/flow_device_code_auth_test.go @@ -0,0 +1,200 @@ +package oauth2 + +import ( + "context" + "net/url" + "testing" + "time" + + "github.com/ory/fosite" + "github.com/ory/fosite/storage" + "github.com/stretchr/testify/require" +) + +func TestAuthorizeCode_HandleDeviceAuthorizeEndpointRequest(t *testing.T) { + + for k, strategy := range map[string]CoreStrategy{ + "hmac": &hmacshaStrategy, + } { + t.Run("strategy="+k, func(t *testing.T) { + store := storage.NewMemoryStore() + handler := AuthorizeDeviceGrantTypeHandler{ + CoreStorage: store, + DeviceCodeStrategy: hmacshaStrategy, + UserCodeStrategy: hmacshaStrategy, + AccessTokenStrategy: strategy, + RefreshTokenStrategy: strategy, + AuthorizeCodeStrategy: strategy, + Config: &fosite.Config{ + DeviceAndUserCodeLifespan: time.Minute * 10, + DeviceAuthTokenPollingInterval: time.Second * 10, + DeviceVerificationURL: "localhost", + AccessTokenLifespan: time.Hour, + RefreshTokenLifespan: time.Hour, + ScopeStrategy: fosite.HierarchicScopeStrategy, + AudienceMatchingStrategy: fosite.DefaultAudienceMatchingStrategy, + RefreshTokenScopes: []string{"offline"}, + }, + } + for _, c := range []struct { + handler AuthorizeDeviceGrantTypeHandler + areq *fosite.AuthorizeRequest + breq *fosite.AuthorizeRequest + expire time.Duration + description string + expectErr error + expect func(t *testing.T, areq *fosite.AuthorizeRequest, aresp *fosite.AuthorizeResponse) + }{ + { + handler: handler, + areq: &fosite.AuthorizeRequest{ + ResponseTypes: fosite.Arguments{""}, + Request: *fosite.NewRequest(), + }, + breq: &fosite.AuthorizeRequest{ + ResponseTypes: fosite.Arguments{""}, + Request: *fosite.NewRequest(), + }, + description: "should pass because not responsible for handling an empty response type", + expire: time.Minute * 10, + }, + { + handler: handler, + areq: &fosite.AuthorizeRequest{ + ResponseTypes: fosite.Arguments{"foo"}, + Request: *fosite.NewRequest(), + }, + breq: &fosite.AuthorizeRequest{ + ResponseTypes: fosite.Arguments{""}, + Request: *fosite.NewRequest(), + }, + description: "should pass because not responsible for handling an invalid response type", + expire: time.Minute * 10, + }, + { + handler: handler, + areq: &fosite.AuthorizeRequest{ + ResponseTypes: fosite.Arguments{"device_code"}, + Request: fosite.Request{ + Client: &fosite.DefaultClient{ + ID: "Default", + GrantTypes: fosite.Arguments{"code"}, + }, + }, + }, + breq: &fosite.AuthorizeRequest{ + ResponseTypes: fosite.Arguments{"device_code"}, + Request: fosite.Request{ + Client: &fosite.DefaultClient{ + ID: "Default", + GrantTypes: fosite.Arguments{"code"}, + }, + }, + }, + description: "should pass because not responsible for handling an invalid grant type", + expire: time.Minute * 10, + }, + { + handler: handler, + areq: &fosite.AuthorizeRequest{ + ResponseTypes: fosite.Arguments{"device_code"}, + Request: fosite.Request{ + Client: &fosite.DefaultClient{ + ID: "Default", + GrantTypes: fosite.Arguments{"urn:ietf:params:oauth:grant-type:device_code"}, + }, + Form: url.Values{"user_code": {"ABC123"}}, + }, + }, + breq: &fosite.AuthorizeRequest{ + ResponseTypes: fosite.Arguments{"device_code"}, + Request: fosite.Request{ + Client: &fosite.DefaultClient{ + ID: "Default", + GrantTypes: fosite.Arguments{"urn:ietf:params:oauth:grant-type:device_code"}, + }, + Form: url.Values{"user_code": {"ABC123"}}, + }, + }, + description: "should pass as session and request have matching client id", + expire: time.Minute * 10, + }, + { + handler: handler, + areq: &fosite.AuthorizeRequest{ + ResponseTypes: fosite.Arguments{"device_code"}, + Request: fosite.Request{ + Client: &fosite.DefaultClient{ + ID: "Default", + GrantTypes: fosite.Arguments{"urn:ietf:params:oauth:grant-type:device_code"}, + }, + Form: url.Values{"user_code": {"ABC123"}}, + }, + }, + breq: &fosite.AuthorizeRequest{ + ResponseTypes: fosite.Arguments{"device_code"}, + Request: fosite.Request{ + Client: &fosite.DefaultClient{ + ID: "Broken", + GrantTypes: fosite.Arguments{"urn:ietf:params:oauth:grant-type:device_code"}, + }, + Form: url.Values{"user_code": {"ABC123"}}, + }, + }, + description: "should fail due to a missmatch in session and request ClientID", + expire: time.Minute * 10, + expectErr: fosite.ErrInvalidGrant, + }, + { + handler: handler, + areq: &fosite.AuthorizeRequest{ + ResponseTypes: fosite.Arguments{"device_code"}, + Request: fosite.Request{ + Client: &fosite.DefaultClient{ + ID: "Default", + GrantTypes: fosite.Arguments{"urn:ietf:params:oauth:grant-type:device_code"}, + }, + Form: url.Values{"user_code": {"ABC123"}}, + }, + }, + breq: &fosite.AuthorizeRequest{ + ResponseTypes: fosite.Arguments{"device_code"}, + Request: fosite.Request{ + Client: &fosite.DefaultClient{ + ID: "Default", + GrantTypes: fosite.Arguments{"urn:ietf:params:oauth:grant-type:device_code"}, + }, + Form: url.Values{"user_code": {"ABC123"}}, + }, + }, + description: "should fail due to expired user session", + expire: -(time.Minute * 10), + //expectErr: fosite.ErrTokenExpired, + }, + } { + t.Run("case="+c.description, func(t *testing.T) { + + c.areq.SetID("ID1") + c.areq.Session = &fosite.DefaultSession{Subject: "A"} + c.breq.Session = &fosite.DefaultSession{Subject: "A"} + expireAt := time.Now().UTC().Add(c.expire) + c.areq.Session.SetExpiresAt(fosite.UserCode, expireAt) + userCodeSig := hmacshaStrategy.UserCodeSignature(context.Background(), c.areq.Form.Get("user_code")) + store.CreateUserCodeSession(nil, userCodeSig, c.areq) + + aresp := fosite.NewAuthorizeResponse() + err := c.handler.HandleAuthorizeEndpointRequest(nil, c.breq, aresp) + if c.expectErr != nil { + require.EqualError(t, err, c.expectErr.Error()) + } else { + require.NoError(t, err) + } + + if c.expect != nil { + c.expect(t, c.areq, aresp) + } + }) + } + }) + } +} diff --git a/handler/oauth2/flow_device_code_token.go b/handler/oauth2/flow_device_code_token.go new file mode 100644 index 000000000..b349b8de8 --- /dev/null +++ b/handler/oauth2/flow_device_code_token.go @@ -0,0 +1,163 @@ +package oauth2 + +import ( + "context" + "fmt" + "time" + + "github.com/ory/fosite" + "github.com/ory/fosite/storage" + "github.com/ory/x/errorsx" +) + +const deviceCodeGrantType = "urn:ietf:params:oauth:grant-type:device_code" + +func (d *AuthorizeDeviceGrantTypeHandler) HandleTokenEndpointRequest(ctx context.Context, requester fosite.AccessRequester) error { + + if !d.CanHandleTokenEndpointRequest(ctx, requester) { + return errorsx.WithStack(errorsx.WithStack(fosite.ErrUnknownRequest)) + } + + if !requester.GetClient().GetGrantTypes().Has(deviceCodeGrantType) { + return errorsx.WithStack(fosite.ErrUnauthorizedClient.WithHint("The OAuth 2.0 Client is not allowed to use authorization grant \"" + deviceCodeGrantType + "\".")) + } + + code := requester.GetRequestForm().Get("device_code") + if code == "" { + return errorsx.WithStack(errorsx.WithStack(fosite.ErrUnknownRequest.WithHint("device_code missing form body"))) + } + codeSignature := d.DeviceCodeStrategy.DeviceCodeSignature(ctx, code) + + // Get the device code session to validate based on HMAC of the device code supplied + session, err := d.CoreStorage.GetDeviceCodeSession(ctx, codeSignature, requester.GetSession()) + + if err != nil { + return errorsx.WithStack(fosite.ErrAuthorizationPending) + } + + requester.SetRequestedScopes(session.GetRequestedScopes()) + requester.SetRequestedAudience(session.GetRequestedAudience()) + + if requester.GetClient().GetID() != session.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.")) + } + + //expires := session.GetSession().GetExpiresAt(fosite.UserCode) + //if time.Now().UTC().After(expires) { + // return errorsx.WithStack(fosite.ErrTokenExpired) + //} + + //requester.SetSession(session.GetSession()) + //requester.SetID(session.GetID()) + + atLifespan := fosite.GetEffectiveLifespan(requester.GetClient(), fosite.GrantTypeAuthorizationCode, fosite.AccessToken, d.Config.GetAccessTokenLifespan(ctx)) + requester.GetSession().SetExpiresAt(fosite.AccessToken, time.Now().UTC().Add(atLifespan).Round(time.Second)) + + rtLifespan := fosite.GetEffectiveLifespan(requester.GetClient(), fosite.GrantTypeAuthorizationCode, fosite.RefreshToken, d.Config.GetRefreshTokenLifespan(ctx)) + if rtLifespan > -1 { + requester.GetSession().SetExpiresAt(fosite.RefreshToken, time.Now().UTC().Add(rtLifespan).Round(time.Second)) + } + + return nil +} + +func (d *AuthorizeDeviceGrantTypeHandler) CanSkipClientAuth(ctx context.Context, requester fosite.AccessRequester) bool { + return true +} + +func (d *AuthorizeDeviceGrantTypeHandler) CanHandleTokenEndpointRequest(ctx context.Context, requester fosite.AccessRequester) bool { + fmt.Println("CanHandleTokenEndpointRequest OAUTH") + return requester.GetGrantTypes().ExactOne(deviceCodeGrantType) +} + +func (d *AuthorizeDeviceGrantTypeHandler) PopulateTokenEndpointResponse(ctx context.Context, requester fosite.AccessRequester, responder fosite.AccessResponder) error { + + if !d.CanHandleTokenEndpointRequest(ctx, requester) { + return errorsx.WithStack(fosite.ErrUnknownRequest) + } + + code := requester.GetRequestForm().Get("device_code") + if code == "" { + return errorsx.WithStack(errorsx.WithStack(fosite.ErrUnknownRequest.WithHint("device_code missing form body"))) + } + codeSignature := d.DeviceCodeStrategy.DeviceCodeSignature(ctx, code) + + if err := d.DeviceCodeStrategy.ValidateDeviceCode(ctx, requester, code); err != nil { + // This needs to happen after store retrieval for the session to be hydrated properly + return err + } + + // Get the device code session ready for exchange to auth / refresh / oidc sessions + session, err := d.CoreStorage.GetDeviceCodeSession(ctx, codeSignature, requester.GetSession()) + + if err != nil { + return errorsx.WithStack(fosite.ErrInvalidRequest.WithWrap(err).WithDebug(err.Error())) + } + + for _, scope := range session.GetGrantedScopes() { + requester.GrantScope(scope) + } + + for _, audience := range session.GetGrantedAudience() { + requester.GrantAudience(audience) + } + + access, accessSignature, err := d.AccessTokenStrategy.GenerateAccessToken(ctx, requester) + if err != nil { + return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) + } + + var refresh, refreshSignature string + + if d.canIssueRefreshToken(ctx, requester) { + refresh, refreshSignature, err = d.RefreshTokenStrategy.GenerateRefreshToken(ctx, requester) + if err != nil { + return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) + } + } + + ctx, err = storage.MaybeBeginTx(ctx, d.CoreStorage) + if err != nil { + return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) + } + defer func() { + if err != nil { + if rollBackTxnErr := storage.MaybeRollbackTx(ctx, d.CoreStorage); rollBackTxnErr != nil { + err = errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebugf("error: %s; rollback error: %s", err, rollBackTxnErr)) + } + } + }() + + if err = d.CoreStorage.DeleteDeviceCodeSession(ctx, code); err != nil { + return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) + } else if err = d.CoreStorage.CreateAccessTokenSession(ctx, accessSignature, requester.Sanitize([]string{})); err != nil { + return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) + } else if refreshSignature != "" { + if err = d.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, d.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, d.CoreStorage); err != nil { + return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) + } + + return nil +} + +func (c *AuthorizeDeviceGrantTypeHandler) canIssueRefreshToken(ctx context.Context, request fosite.Requester) bool { + // Require one of the refresh token scopes, if set. + if len(c.Config.GetRefreshTokenScopes(ctx)) > 0 && !request.GetGrantedScopes().HasOneOf(c.Config.GetRefreshTokenScopes(ctx)...) { + return false + } + return true +} diff --git a/handler/oauth2/flow_device_code_token_test.go b/handler/oauth2/flow_device_code_token_test.go new file mode 100644 index 000000000..cb23f6243 --- /dev/null +++ b/handler/oauth2/flow_device_code_token_test.go @@ -0,0 +1,350 @@ +package oauth2 + +import ( + "context" + "net/url" + "testing" + "time" + + "github.com/ory/fosite" + "github.com/ory/fosite/storage" + "github.com/stretchr/testify/require" +) + +func TestAuthorizeCode_HandleDeviceTokenEndpointRequest(t *testing.T) { + + for k, strategy := range map[string]CoreStrategy{ + "hmac": &hmacshaStrategy, + } { + t.Run("strategy="+k, func(t *testing.T) { + store := storage.NewMemoryStore() + handler := AuthorizeDeviceGrantTypeHandler{ + CoreStorage: store, + DeviceCodeStrategy: hmacshaStrategy, + UserCodeStrategy: hmacshaStrategy, + AccessTokenStrategy: strategy, + RefreshTokenStrategy: strategy, + AuthorizeCodeStrategy: strategy, + Config: &fosite.Config{ + DeviceAndUserCodeLifespan: time.Minute * 10, + DeviceAuthTokenPollingInterval: time.Second * 10, + DeviceVerificationURL: "localhost", + AccessTokenLifespan: time.Hour, + RefreshTokenLifespan: time.Hour, + ScopeStrategy: fosite.HierarchicScopeStrategy, + AudienceMatchingStrategy: fosite.DefaultAudienceMatchingStrategy, + RefreshTokenScopes: []string{"offline"}, + }, + } + for _, c := range []struct { + handler AuthorizeDeviceGrantTypeHandler + areq *fosite.AccessRequest + breq *fosite.AccessRequest + createDeviceSession bool + expire time.Duration + description string + expectErr error + expect func(t *testing.T, areq *fosite.AccessRequest) + }{ + { + handler: handler, + areq: &fosite.AccessRequest{ + GrantTypes: fosite.Arguments{"authorization_code"}, + Request: fosite.Request{ + Client: &fosite.DefaultClient{ID: "foo", GrantTypes: []string{""}}, + Session: &fosite.DefaultSession{Subject: "A"}, + RequestedAt: time.Now().UTC(), + }, + }, + breq: &fosite.AccessRequest{ + GrantTypes: fosite.Arguments{"authorization_code"}, + Request: fosite.Request{ + Client: &fosite.DefaultClient{ID: "foo", GrantTypes: []string{""}}, + Session: &fosite.DefaultSession{Subject: "A"}, + RequestedAt: time.Now().UTC(), + }, + }, + description: "Should fail due to wrong grant type", + expectErr: fosite.ErrUnknownRequest, + createDeviceSession: false, + expire: time.Minute * 10, + }, + { + handler: handler, + 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{Subject: "A"}, + RequestedAt: time.Now().UTC(), + }, + }, + breq: &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{Subject: "A"}, + RequestedAt: time.Now().UTC(), + }, + }, + description: "Should fail due to no device_code supplied", + expectErr: fosite.ErrUnauthorizedClient, + createDeviceSession: false, + expire: time.Minute * 10, + }, + { + handler: handler, + 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{Subject: "A"}, + RequestedAt: time.Now().UTC(), + Form: url.Values{"device_code": {"ABC1234"}}, + }, + }, + breq: &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{Subject: "A"}, + RequestedAt: time.Now().UTC(), + Form: url.Values{"device_code": {"ABC1234"}}, + }, + }, + description: "Should fail due to no user_code session available", + expectErr: fosite.ErrUnauthorizedClient, + createDeviceSession: false, + expire: time.Minute * 10, + }, + { + handler: handler, + 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{Subject: "A"}, + RequestedAt: time.Now().UTC(), + Form: url.Values{"device_code": {"ABC1234"}}, + }, + }, + breq: &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{Subject: "A"}, + RequestedAt: time.Now().UTC(), + Form: url.Values{"device_code": {"ABC1234"}}, + }, + }, + description: "Should pass as device_code form data and session are available", + createDeviceSession: true, + expire: time.Minute * 10, + }, + { + handler: handler, + 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{Subject: "A"}, + RequestedAt: time.Now().UTC(), + Form: url.Values{"device_code": {"ABC1234"}}, + }, + }, + breq: &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{Subject: "A"}, + RequestedAt: time.Now().UTC(), + Form: url.Values{"device_code": {"ABC1234"}}, + }, + }, + description: "Should fail as session expired", + createDeviceSession: true, + expire: -(time.Minute * 10), + expectErr: fosite.ErrUnauthorizedClient, + }, + { + handler: handler, + 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{Subject: "A"}, + RequestedAt: time.Now().UTC(), + Form: url.Values{"device_code": {"ABC1234"}}, + }, + }, + breq: &fosite.AccessRequest{ + GrantTypes: fosite.Arguments{"urn:ietf:params:oauth:grant-type:device_code"}, + Request: fosite.Request{ + Client: &fosite.DefaultClient{ID: "bar", GrantTypes: []string{""}}, + Session: &fosite.DefaultSession{Subject: "A"}, + RequestedAt: time.Now().UTC(), + Form: url.Values{"device_code": {"ABC1234"}}, + }, + }, + description: "Should fail as session and request clients do not match", + createDeviceSession: true, + expire: time.Minute * 10, + expectErr: fosite.ErrUnauthorizedClient, + }, + } { + t.Run("case="+c.description, func(t *testing.T) { + + if c.createDeviceSession { + c.areq.SetID("ID1") + c.areq.Session = &fosite.DefaultSession{} + expireAt := time.Now().UTC().Add(c.expire) + c.areq.Session.SetExpiresAt(fosite.UserCode, expireAt) + deviceSignature := hmacshaStrategy.DeviceCodeSignature(context.Background(), c.areq.Form.Get("device_code")) + store.CreateDeviceCodeSession(nil, deviceSignature, c.areq) + } + + err := c.handler.HandleTokenEndpointRequest(nil, c.breq) + if c.expectErr != nil { + require.EqualError(t, err, c.expectErr.Error()) + } else { + require.NoError(t, err) + } + + if c.expect != nil { + c.expect(t, c.areq) + } + }) + } + }) + } +} + +func TestAuthorizeCode_PopulateDeviceTokenEndpointResponse(t *testing.T) { + + for k, strategy := range map[string]CoreStrategy{ + "hmac": &hmacshaStrategy, + } { + t.Run("strategy="+k, func(t *testing.T) { + store := storage.NewMemoryStore() + handler := AuthorizeDeviceGrantTypeHandler{ + CoreStorage: store, + DeviceCodeStrategy: hmacshaStrategy, + UserCodeStrategy: hmacshaStrategy, + AccessTokenStrategy: strategy, + RefreshTokenStrategy: strategy, + AuthorizeCodeStrategy: strategy, + Config: &fosite.Config{ + DeviceAndUserCodeLifespan: time.Minute * 10, + DeviceAuthTokenPollingInterval: time.Second * 10, + DeviceVerificationURL: "localhost", + AccessTokenLifespan: time.Hour, + RefreshTokenLifespan: time.Hour, + ScopeStrategy: fosite.HierarchicScopeStrategy, + AudienceMatchingStrategy: fosite.DefaultAudienceMatchingStrategy, + RefreshTokenScopes: []string{"offline"}, + }, + } + for _, c := range []struct { + handler AuthorizeDeviceGrantTypeHandler + areq *fosite.AccessRequest + createDeviceSession bool + description string + expectErr error + expect func(t *testing.T, areq *fosite.AccessRequest) + }{ + { + handler: handler, + areq: &fosite.AccessRequest{ + GrantTypes: fosite.Arguments{"authorization_code"}, + Request: fosite.Request{ + Client: &fosite.DefaultClient{ID: "foo", GrantTypes: []string{""}}, + Session: &fosite.DefaultSession{}, + RequestedAt: time.Now().UTC(), + }, + }, + description: "Should fail due to wrong grant type", + expectErr: fosite.ErrUnknownRequest, + createDeviceSession: false, + }, { + handler: handler, + 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 due to no device_code supplied", + expectErr: fosite.ErrUnknownRequest, + createDeviceSession: false, + }, + { + handler: handler, + 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(), + Form: url.Values{"device_code": {"ABC1234"}}, + }, + }, + description: "Should fail due to no user_code session available", + expectErr: fosite.ErrInvalidRequest, + createDeviceSession: false, + }, + { + handler: handler, + 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(), + GrantedScope: fosite.Arguments{"openid", "offline"}, + GrantedAudience: fosite.Arguments{"www.websitesite.com"}, + Form: url.Values{"device_code": {"ABC1234"}}, + }, + }, + description: "Should pass as device_code form data and session are available", + createDeviceSession: true, + }, + } { + t.Run("case="+c.description, func(t *testing.T) { + + c.areq.GetSession().SetExpiresAt(fosite.UserCode, time.Now().Add(time.Minute*5)) + if c.createDeviceSession { + c.areq.SetID("ID1") + deviceSig := hmacshaStrategy.DeviceCodeSignature(context.TODO(), c.areq.Form.Get("device_code")) + store.CreateDeviceCodeSession(nil, deviceSig, c.areq) + } + + resp := fosite.NewAccessResponse() + err := c.handler.PopulateTokenEndpointResponse(nil, c.areq, resp) + if c.expectErr != nil { + require.EqualError(t, err, c.expectErr.Error()) + } else { + require.NoError(t, err) + } + + accessToken := resp.GetAccessToken() + refreshToken := resp.GetExtra("refresh_token") + + // Make sure we only create tokens if we have a device session available + if c.createDeviceSession { + require.NotEmpty(t, accessToken) + require.NotEmpty(t, refreshToken) + } else { + require.Empty(t, accessToken) + require.Empty(t, refreshToken) + } + + if c.expect != nil { + c.expect(t, c.areq) + } + }) + } + }) + } +} diff --git a/handler/oauth2/storage.go b/handler/oauth2/storage.go index 550211993..a72002564 100644 --- a/handler/oauth2/storage.go +++ b/handler/oauth2/storage.go @@ -31,6 +31,8 @@ type CoreStorage interface { AuthorizeCodeStorage AccessTokenStorage RefreshTokenStorage + DeviceCodeStorage + UserCodeStorage } // AuthorizeCodeStorage handles storage requests related to authorization codes. @@ -66,3 +68,14 @@ type RefreshTokenStorage interface { DeleteRefreshTokenSession(ctx context.Context, signature string) (err error) } + +type DeviceCodeStorage interface { + CreateDeviceCodeSession(ctx context.Context, signature string, request fosite.Requester) (err error) + GetDeviceCodeSession(ctx context.Context, code string, session fosite.Session) (request fosite.Requester, err error) + DeleteDeviceCodeSession(ctx context.Context, code string) (err error) +} +type UserCodeStorage interface { + CreateUserCodeSession(ctx context.Context, signature string, request fosite.Requester) (err error) + GetUserCodeSession(ctx context.Context, code string, session fosite.Session) (request fosite.Requester, err error) + DeleteUserCodeSession(ctx context.Context, code string) (err error) +} diff --git a/handler/oauth2/strategy.go b/handler/oauth2/strategy.go index 8a469c8c4..26107f731 100644 --- a/handler/oauth2/strategy.go +++ b/handler/oauth2/strategy.go @@ -31,6 +31,8 @@ type CoreStrategy interface { AccessTokenStrategy RefreshTokenStrategy AuthorizeCodeStrategy + DeviceCodeStrategy + UserCodeStrategy } type AccessTokenStrategy interface { @@ -50,3 +52,15 @@ type AuthorizeCodeStrategy interface { GenerateAuthorizeCode(ctx context.Context, requester fosite.Requester) (token string, signature string, err error) ValidateAuthorizeCode(ctx context.Context, requester fosite.Requester, token string) (err error) } + +type DeviceCodeStrategy interface { + DeviceCodeSignature(context context.Context, code string) string + ValidateDeviceCode(context context.Context, r fosite.Requester, code string) (err error) + GenerateDeviceCode() (code string, err error) +} + +type UserCodeStrategy interface { + UserCodeSignature(context context.Context, code string) string + ValidateUserCode(context context.Context, r fosite.Requester, code string) (err error) + GenerateUserCode() (code string, err error) +} diff --git a/handler/oauth2/strategy_hmacsha.go b/handler/oauth2/strategy_hmacsha.go index ad51bf44a..6b722ad6a 100644 --- a/handler/oauth2/strategy_hmacsha.go +++ b/handler/oauth2/strategy_hmacsha.go @@ -23,7 +23,9 @@ package oauth2 import ( "context" + "crypto/rand" "fmt" + "math/big" "strings" "time" @@ -39,6 +41,7 @@ type HMACSHAStrategy struct { fosite.AccessTokenLifespanProvider fosite.RefreshTokenLifespanProvider fosite.AuthorizeCodeLifespanProvider + fosite.DeviceAndUserCodeLifespanProvider } prefix *string } @@ -138,3 +141,56 @@ func (h *HMACSHAStrategy) ValidateAuthorizeCode(ctx context.Context, r fosite.Re return h.Enigma.Validate(ctx, h.trimPrefix(token, "ac")) } + +func (h HMACSHAStrategy) generateRandomString(length int) (token string, err error) { + chars := [20]byte{'B', 'C', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'X', 'Z'} + chars_length := int64(len(chars)) + + code := make([]byte, length) + for i := 0; i < length; i++ { + num, err := rand.Int(rand.Reader, big.NewInt(chars_length)) + if err != nil { + return "", err + } + code[i] = chars[num.Int64()] + } + return string(code), nil +} + +func (h HMACSHAStrategy) GenerateUserCode() (token string, err error) { + return h.generateRandomString(8) +} + +func (h HMACSHAStrategy) UserCodeSignature(ctx context.Context, token string) string { + return h.Enigma.GenerateHMACForString(token, ctx) +} + +func (h HMACSHAStrategy) 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.ErrTokenExpired.WithHintf("Access token expired at '%s'.", r.GetRequestedAt().Add(h.Config.GetDeviceAndUserCodeLifespan(ctx)))) + } + if !exp.IsZero() && exp.Before(time.Now().UTC()) { + return errorsx.WithStack(fosite.ErrTokenExpired.WithHintf("Access token expired at '%s'.", exp)) + } + return nil +} + +func (h HMACSHAStrategy) GenerateDeviceCode() (token string, err error) { + return h.generateRandomString(100) +} + +func (h HMACSHAStrategy) DeviceCodeSignature(ctx context.Context, token string) string { + return h.Enigma.GenerateHMACForString(token, ctx) +} + +func (h HMACSHAStrategy) ValidateDeviceCode(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.ErrTokenExpired.WithHintf("1 Device code expired at '%s'.", r.GetRequestedAt().Add(h.Config.GetDeviceAndUserCodeLifespan(ctx)))) + } + if !exp.IsZero() && exp.Before(time.Now().UTC()) { + return errorsx.WithStack(fosite.ErrTokenExpired.WithHintf("2 Device code expired at '%s'.", exp)) + } + return nil +} diff --git a/handler/oauth2/strategy_hmacsha_test.go b/handler/oauth2/strategy_hmacsha_test.go index ee232e608..3a5c1653d 100644 --- a/handler/oauth2/strategy_hmacsha_test.go +++ b/handler/oauth2/strategy_hmacsha_test.go @@ -36,8 +36,9 @@ import ( var hmacshaStrategy = HMACSHAStrategy{ Enigma: &hmac.HMACStrategy{Config: &fosite.Config{GlobalSecret: []byte("foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobar")}}, Config: &fosite.Config{ - AccessTokenLifespan: time.Hour * 24, - AuthorizeCodeLifespan: time.Hour * 24, + AccessTokenLifespan: time.Hour * 24, + AuthorizeCodeLifespan: time.Hour * 24, + DeviceAndUserCodeLifespan: time.Hour * 24, }, } diff --git a/handler/oauth2/strategy_jwt.go b/handler/oauth2/strategy_jwt.go index 8ce1854c6..85bd20e00 100644 --- a/handler/oauth2/strategy_jwt.go +++ b/handler/oauth2/strategy_jwt.go @@ -149,3 +149,27 @@ func (h *DefaultJWTStrategy) generate(ctx context.Context, tokenType fosite.Toke return h.Signer.Generate(ctx, claims.ToMapClaims(), jwtSession.GetJWTHeader()) } } + +func (h DefaultJWTStrategy) DeviceCodeSignature(ctx context.Context, token string) string { + return h.HMACSHAStrategy.DeviceCodeSignature(ctx, token) +} + +func (h *DefaultJWTStrategy) GenerateDeviceCode() (token string, err error) { + return h.HMACSHAStrategy.GenerateDeviceCode() +} + +func (h *DefaultJWTStrategy) ValidateDeviceCode(context context.Context, r fosite.Requester, code string) (err error) { + return h.HMACSHAStrategy.ValidateDeviceCode(context, r, code) +} + +func (h DefaultJWTStrategy) UserCodeSignature(ctx context.Context, token string) string { + return h.HMACSHAStrategy.UserCodeSignature(ctx, token) +} + +func (h *DefaultJWTStrategy) GenerateUserCode() (token string, err error) { + return h.HMACSHAStrategy.GenerateUserCode() +} + +func (h *DefaultJWTStrategy) ValidateUserCode(context context.Context, r fosite.Requester, code string) (err error) { + return h.HMACSHAStrategy.ValidateUserCode(context, r, code) +} diff --git a/handler/openid/flow_device_auth.go b/handler/openid/flow_device_auth.go new file mode 100644 index 000000000..a038f698d --- /dev/null +++ b/handler/openid/flow_device_auth.go @@ -0,0 +1,61 @@ +package openid + +import ( + "context" + + "github.com/ory/fosite/handler/oauth2" + "github.com/ory/x/errorsx" + + "github.com/ory/fosite" +) + +type OpenIDConnectDeviceHandler struct { + CoreStorage oauth2.CoreStorage + DeviceCodeStrategy oauth2.DeviceCodeStrategy + UserCodeStrategy oauth2.UserCodeStrategy + + OpenIDConnectRequestStorage OpenIDConnectRequestStorage + OpenIDConnectRequestValidator *OpenIDConnectRequestValidator + + Config fosite.Configurator + + *IDTokenHandleHelper +} + +func (c *OpenIDConnectDeviceHandler) HandleAuthorizeEndpointRequest(ctx context.Context, ar fosite.AuthorizeRequester, resp fosite.AuthorizeResponder) error { + if !(ar.GetGrantedScopes().Has("openid") && ar.GetResponseTypes().ExactOne("device_code")) { + return nil + } + + if !ar.GetClient().GetGrantTypes().Has("urn:ietf:params:oauth:grant-type:device_code") { + return nil + } + + userCode := ar.GetRequestForm().Get("user_code") + userCodeSignature := c.UserCodeStrategy.UserCodeSignature(ctx, userCode) + + userSession, err := c.CoreStorage.GetUserCodeSession(ctx, userCodeSignature, fosite.NewRequest().Session) + if err != nil { + return errorsx.WithStack(fosite.ErrNotFound.WithDebug("User session not found.")) + } + + deviceSession, err := c.CoreStorage.GetDeviceCodeSession(ctx, userSession.GetID(), fosite.NewRequest().Session) + if err != nil { + return errorsx.WithStack(fosite.ErrNotFound.WithDebug("The devicve code has not been issued yet.")) + } + + if len(deviceSession.GetID()) == 0 { + return errorsx.WithStack(fosite.ErrMisconfiguration.WithDebug("The devicve code has not been issued yet, indicating a broken code configuration.")) + } + + if err := c.OpenIDConnectRequestValidator.ValidatePrompt(ctx, ar); err != nil { + return err + } + + // The device code is stored in the ID field of the requester, use this to build the OpenID session as the token endpoint will not know about the user_code + if err := c.OpenIDConnectRequestStorage.CreateOpenIDConnectSession(ctx, userSession.GetID(), ar.Sanitize(oidcParameters)); err != nil { + return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) + } + + return nil +} diff --git a/handler/openid/flow_device_token.go b/handler/openid/flow_device_token.go new file mode 100644 index 000000000..ecbb7c5d5 --- /dev/null +++ b/handler/openid/flow_device_token.go @@ -0,0 +1,75 @@ +package openid + +import ( + "context" + "fmt" + + "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 (c *OpenIDConnectDeviceHandler) PopulateTokenEndpointResponse(ctx context.Context, requester fosite.AccessRequester, responder fosite.AccessResponder) error { + + if !c.CanHandleTokenEndpointRequest(ctx, requester) { + return errorsx.WithStack(fosite.ErrUnknownRequest) + } + + code := requester.GetRequestForm().Get("device_code") + if code == "" { + return errorsx.WithStack(errorsx.WithStack(fosite.ErrUnknownRequest.WithHint("device_code missing form body"))) + } + codeSignature := c.DeviceCodeStrategy.DeviceCodeSignature(ctx, code) + + authorize, err := c.OpenIDConnectRequestStorage.GetOpenIDConnectSession(ctx, codeSignature, 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("urn:ietf:params:oauth:grant-type:device_code") { + 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 := requester.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 { + fmt.Println("CanHandleTokenEndpointRequest OIDC") + return requester.GetGrantTypes().ExactOne("urn:ietf:params:oauth:grant-type:device_code") +} diff --git a/handler/openid/flow_explicit_token.go b/handler/openid/flow_explicit_token.go index c7a16dcc5..2e19e1f54 100644 --- a/handler/openid/flow_explicit_token.go +++ b/handler/openid/flow_explicit_token.go @@ -23,6 +23,7 @@ package openid import ( "context" + "fmt" "github.com/ory/x/errorsx" @@ -83,5 +84,6 @@ func (c *OpenIDConnectExplicitHandler) CanSkipClientAuth(ctx context.Context, re } func (c *OpenIDConnectExplicitHandler) CanHandleTokenEndpointRequest(ctx context.Context, requester fosite.AccessRequester) bool { + fmt.Println("CanHandleTokenEndpointRequest EXPL TOKEN") return requester.GetGrantTypes().ExactOne("authorization_code") } diff --git a/handler/pkce/handler_device.go b/handler/pkce/handler_device.go new file mode 100644 index 000000000..82c81e083 --- /dev/null +++ b/handler/pkce/handler_device.go @@ -0,0 +1,228 @@ +package pkce + +import ( + "context" + "crypto/sha256" + "encoding/base64" + "fmt" + + "github.com/ory/x/errorsx" + + "github.com/pkg/errors" + + "github.com/ory/fosite" + "github.com/ory/fosite/handler/oauth2" +) + +type HandlerDevice struct { + CoreStorage oauth2.CoreStorage + DeviceCodeStrategy oauth2.DeviceCodeStrategy + UserCodeStrategy oauth2.UserCodeStrategy + AuthorizeCodeStrategy oauth2.AuthorizeCodeStrategy + Storage PKCERequestStorage + Config fosite.Configurator +} + +func (c *HandlerDevice) HandleDeviceAuthorizeEndpointRequest(ctx context.Context, ar fosite.Requester, resp fosite.DeviceAuthorizeResponder) error { + fmt.Println("HandlerDevice :: HandleDeviceAuthorizeEndpointRequest ++") + + if !ar.GetClient().GetGrantTypes().Has("urn:ietf:params:oauth:grant-type:device_code") { + return nil + } + + challenge := ar.GetRequestForm().Get("code_challenge") + method := ar.GetRequestForm().Get("code_challenge_method") + client := ar.GetClient() + userCode := resp.GetUserCode() + + userCodeSignature := c.UserCodeStrategy.UserCodeSignature(ctx, userCode) + + session, err := c.CoreStorage.GetUserCodeSession(ctx, userCodeSignature, fosite.NewRequest().Session) + if err != nil { + return err + } + + if err := c.validate(ctx, challenge, method, client); err != nil { + return err + } + + fmt.Println("PKCE ID : " + session.GetID()) + + if err := c.Storage.CreatePKCERequestSession(ctx, session.GetID(), ar.Sanitize([]string{ + "code_challenge", + "code_challenge_method", + })); err != nil { + return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) + } + + fmt.Println(resp) + + return nil +} + +func (c *HandlerDevice) validate(ctx context.Context, challenge string, method string, client fosite.Client) error { + if challenge == "" { + // If the server requires Proof Key for Code Exchange (PKCE) by OAuth + // clients and the client does not send the "code_challenge" in + // the request, the authorization endpoint MUST return the authorization + // error response with the "error" value set to "invalid_request". The + // "error_description" or the response of "error_uri" SHOULD explain the + // nature of error, e.g., code challenge required. + + if c.Config.GetEnforcePKCE(ctx) { + return errorsx.WithStack(fosite.ErrInvalidRequest. + WithHint("Clients must include a code_challenge when performing the authorize code flow, but it is missing."). + WithDebug("The server is configured in a way that enforces PKCE for clients.")) + } + if c.Config.GetEnforcePKCEForPublicClients(ctx) && client.IsPublic() { + return errorsx.WithStack(fosite.ErrInvalidRequest. + WithHint("This client must include a code_challenge when performing the authorize code flow, but it is missing."). + WithDebug("The server is configured in a way that enforces PKCE for this client.")) + } + return nil + } + + // If the server supporting PKCE does not support the requested + // transformation, the authorization endpoint MUST return the + // authorization error response with "error" value set to + // "invalid_request". The "error_description" or the response of + // "error_uri" SHOULD explain the nature of error, e.g., transform + // algorithm not supported. + switch method { + case "S256": + break + case "plain": + fallthrough + case "": + if !c.Config.GetEnablePKCEPlainChallengeMethod(ctx) { + return errorsx.WithStack(fosite.ErrInvalidRequest. + WithHint("Clients must use code_challenge_method=S256, plain is not allowed."). + WithDebug("The server is configured in a way that enforces PKCE S256 as challenge method for clients.")) + } + default: + return errorsx.WithStack(fosite.ErrInvalidRequest. + WithHint("The code_challenge_method is not supported, use S256 instead.")) + } + return nil +} + +func (c *HandlerDevice) HandleTokenEndpointRequest(ctx context.Context, request fosite.AccessRequester) error { + if !c.CanHandleTokenEndpointRequest(ctx, request) { + return errorsx.WithStack(fosite.ErrUnknownRequest) + } + + // code_verifier + // REQUIRED. Code verifier + // + // The "code_challenge_method" is bound to the Authorization Code when + // the Authorization Code is issued. That is the method that the token + // endpoint MUST use to verify the "code_verifier". + verifier := request.GetRequestForm().Get("code_verifier") + + code := request.GetRequestForm().Get("device_code") + if code == "" { + return errorsx.WithStack(errorsx.WithStack(fosite.ErrUnknownRequest.WithHint("device_code missing form body"))) + } + codeSignature := c.DeviceCodeStrategy.DeviceCodeSignature(ctx, code) + + fmt.Println("PKCE ID : " + codeSignature) + + authorizeRequest, err := c.Storage.GetPKCERequestSession(ctx, codeSignature, request.GetSession()) + if errors.Is(err, fosite.ErrNotFound) { + return errorsx.WithStack(fosite.ErrInvalidGrant.WithHint("Unable to find initial PKCE data tied to this request").WithWrap(err).WithDebug(err.Error())) + } else if err != nil { + return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) + } + + if err := c.Storage.DeletePKCERequestSession(ctx, codeSignature); err != nil { + return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) + } + + challenge := authorizeRequest.GetRequestForm().Get("code_challenge") + method := authorizeRequest.GetRequestForm().Get("code_challenge_method") + client := authorizeRequest.GetClient() + if err := c.validate(ctx, challenge, method, client); err != nil { + return err + } + + if !c.Config.GetEnforcePKCE(ctx) && challenge == "" && verifier == "" { + return nil + } + + // NOTE: The code verifier SHOULD have enough entropy to make it + // impractical to guess the value. It is RECOMMENDED that the output of + // a suitable random number generator be used to create a 32-octet + // sequence. The octet sequence is then base64url-encoded to produce a + // 43-octet URL safe string to use as the code verifier. + + // Validation + if len(verifier) < 43 { + return errorsx.WithStack(fosite.ErrInvalidGrant. + WithHint("The PKCE code verifier must be at least 43 characters.")) + } else if len(verifier) > 128 { + return errorsx.WithStack(fosite.ErrInvalidGrant. + WithHint("The PKCE code verifier can not be longer than 128 characters.")) + } else if verifierWrongFormat.MatchString(verifier) { + return errorsx.WithStack(fosite.ErrInvalidGrant. + WithHint("The PKCE code verifier must only contain [a-Z], [0-9], '-', '.', '_', '~'.")) + } + + // Upon receipt of the request at the token endpoint, the server + // verifies it by calculating the code challenge from the received + // "code_verifier" and comparing it with the previously associated + // "code_challenge", after first transforming it according to the + // "code_challenge_method" method specified by the client. + // + // If the "code_challenge_method" from Section 4.3 was "S256", the + // received "code_verifier" is hashed by SHA-256, base64url-encoded, and + // then compared to the "code_challenge", i.e.: + // + // BASE64URL-ENCODE(SHA256(ASCII(code_verifier))) == code_challenge + // + // If the "code_challenge_method" from Section 4.3 was "plain", they are + // compared directly, i.e.: + // + // code_verifier == code_challenge. + // + // If the values are equal, the token endpoint MUST continue processing + // as normal (as defined by OAuth 2.0 [RFC6749]). If the values are not + // equal, an error response indicating "invalid_grant" as described in + // Section 5.2 of [RFC6749] MUST be returned. + switch method { + case "S256": + hash := sha256.New() + if _, err := hash.Write([]byte(verifier)); err != nil { + return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) + } + + if base64.RawURLEncoding.EncodeToString(hash.Sum([]byte{})) != challenge { + return errorsx.WithStack(fosite.ErrInvalidGrant. + WithHint("The PKCE code challenge did not match the code verifier.")) + } + break + case "plain": + fallthrough + default: + if verifier != challenge { + return errorsx.WithStack(fosite.ErrInvalidGrant. + WithHint("The PKCE code challenge did not match the code verifier.")) + } + } + + return nil +} + +func (c *HandlerDevice) PopulateTokenEndpointResponse(ctx context.Context, requester fosite.AccessRequester, responder fosite.AccessResponder) error { + return nil +} + +func (c *HandlerDevice) CanSkipClientAuth(ctx context.Context, requester fosite.AccessRequester) bool { + return false +} + +func (c *HandlerDevice) CanHandleTokenEndpointRequest(ctx context.Context, requester fosite.AccessRequester) bool { + fmt.Println("CanHandleTokenEndpointRequest PKCE") + // grant_type REQUIRED. + // Value MUST be set to "authorization_code" + return requester.GetGrantTypes().ExactOne("urn:ietf:params:oauth:grant-type:device_code") +} diff --git a/internal/access_request.go b/internal/access_request.go index 7f408e080..149b920aa 100644 --- a/internal/access_request.go +++ b/internal/access_request.go @@ -10,7 +10,6 @@ import ( time "time" gomock "github.com/golang/mock/gomock" - fosite "github.com/ory/fosite" ) diff --git a/internal/access_response.go b/internal/access_response.go index 9c383cb79..cdd100769 100644 --- a/internal/access_response.go +++ b/internal/access_response.go @@ -9,7 +9,6 @@ import ( time "time" gomock "github.com/golang/mock/gomock" - fosite "github.com/ory/fosite" ) diff --git a/internal/access_token_storage.go b/internal/access_token_storage.go index a30404098..7d4d80549 100644 --- a/internal/access_token_storage.go +++ b/internal/access_token_storage.go @@ -9,7 +9,6 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" - fosite "github.com/ory/fosite" ) diff --git a/internal/access_token_strategy.go b/internal/access_token_strategy.go index a811e51cf..965404f64 100644 --- a/internal/access_token_strategy.go +++ b/internal/access_token_strategy.go @@ -9,7 +9,6 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" - fosite "github.com/ory/fosite" ) diff --git a/internal/authorize_code_storage.go b/internal/authorize_code_storage.go index ef3ab3b3c..75e62d4ac 100644 --- a/internal/authorize_code_storage.go +++ b/internal/authorize_code_storage.go @@ -9,7 +9,6 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" - fosite "github.com/ory/fosite" ) diff --git a/internal/authorize_code_strategy.go b/internal/authorize_code_strategy.go index cb826a23d..8baf3a2c4 100644 --- a/internal/authorize_code_strategy.go +++ b/internal/authorize_code_strategy.go @@ -9,7 +9,6 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" - fosite "github.com/ory/fosite" ) diff --git a/internal/authorize_handler.go b/internal/authorize_handler.go index 964fb7e4a..a660d7ebb 100644 --- a/internal/authorize_handler.go +++ b/internal/authorize_handler.go @@ -9,7 +9,6 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" - fosite "github.com/ory/fosite" ) diff --git a/internal/authorize_request.go b/internal/authorize_request.go index a643e04e8..e3d2f959b 100644 --- a/internal/authorize_request.go +++ b/internal/authorize_request.go @@ -10,7 +10,6 @@ import ( time "time" gomock "github.com/golang/mock/gomock" - fosite "github.com/ory/fosite" ) diff --git a/internal/client.go b/internal/client.go index 771660924..b2299dc02 100644 --- a/internal/client.go +++ b/internal/client.go @@ -6,10 +6,8 @@ package internal import ( reflect "reflect" - time "time" gomock "github.com/golang/mock/gomock" - fosite "github.com/ory/fosite" ) @@ -134,20 +132,6 @@ func (mr *MockClientMockRecorder) GetScopes() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetScopes", reflect.TypeOf((*MockClient)(nil).GetScopes)) } -// GetTokenLifespan mocks base method. -func (m *MockClient) GetTokenLifespan(arg0 fosite.GrantType, arg1 fosite.TokenType, arg2 time.Duration) time.Duration { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetTokenLifespan", arg0, arg1, arg2) - ret0, _ := ret[0].(time.Duration) - return ret0 -} - -// GetTokenLifespan indicates an expected call of GetTokenLifespan. -func (mr *MockClientMockRecorder) GetTokenLifespan(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTokenLifespan", reflect.TypeOf((*MockClient)(nil).GetTokenLifespan), arg0, arg1, arg2) -} - // IsPublic mocks base method. func (m *MockClient) IsPublic() bool { m.ctrl.T.Helper() @@ -161,15 +145,3 @@ func (mr *MockClientMockRecorder) IsPublic() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsPublic", reflect.TypeOf((*MockClient)(nil).IsPublic)) } - -// SetTokenLifespans mocks base method. -func (m *MockClient) SetTokenLifespans(arg0 map[fosite.TokenType]time.Duration) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "SetTokenLifespans", arg0) -} - -// SetTokenLifespans indicates an expected call of SetTokenLifespans. -func (mr *MockClientMockRecorder) SetTokenLifespans(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetTokenLifespans", reflect.TypeOf((*MockClient)(nil).SetTokenLifespans), arg0) -} diff --git a/internal/id_token_strategy.go b/internal/id_token_strategy.go index 2e36d1488..9ec9b8ce6 100644 --- a/internal/id_token_strategy.go +++ b/internal/id_token_strategy.go @@ -10,7 +10,6 @@ import ( time "time" gomock "github.com/golang/mock/gomock" - fosite "github.com/ory/fosite" ) diff --git a/internal/introspector.go b/internal/introspector.go index c47cf76e6..6744ec5f7 100644 --- a/internal/introspector.go +++ b/internal/introspector.go @@ -9,7 +9,6 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" - fosite "github.com/ory/fosite" ) diff --git a/internal/oauth2_client_storage.go b/internal/oauth2_client_storage.go index 54770c89d..bb15e6c53 100644 --- a/internal/oauth2_client_storage.go +++ b/internal/oauth2_client_storage.go @@ -9,7 +9,6 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" - fosite "github.com/ory/fosite" ) diff --git a/internal/oauth2_owner_storage.go b/internal/oauth2_owner_storage.go index ce53de4c8..37ace932f 100644 --- a/internal/oauth2_owner_storage.go +++ b/internal/oauth2_owner_storage.go @@ -9,7 +9,6 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" - fosite "github.com/ory/fosite" ) diff --git a/internal/oauth2_revoke_storage.go b/internal/oauth2_revoke_storage.go index 71f8e10a5..c4b28282a 100644 --- a/internal/oauth2_revoke_storage.go +++ b/internal/oauth2_revoke_storage.go @@ -9,7 +9,6 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" - fosite "github.com/ory/fosite" ) diff --git a/internal/oauth2_storage.go b/internal/oauth2_storage.go index d29457603..0b9789d53 100644 --- a/internal/oauth2_storage.go +++ b/internal/oauth2_storage.go @@ -9,7 +9,6 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" - fosite "github.com/ory/fosite" ) @@ -64,6 +63,20 @@ func (mr *MockCoreStorageMockRecorder) CreateAuthorizeCodeSession(arg0, arg1, ar return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateAuthorizeCodeSession", reflect.TypeOf((*MockCoreStorage)(nil).CreateAuthorizeCodeSession), arg0, arg1, arg2) } +// CreateDeviceCodeSession mocks base method. +func (m *MockCoreStorage) 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 *MockCoreStorageMockRecorder) CreateDeviceCodeSession(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateDeviceCodeSession", reflect.TypeOf((*MockCoreStorage)(nil).CreateDeviceCodeSession), arg0, arg1, arg2) +} + // CreateRefreshTokenSession mocks base method. func (m *MockCoreStorage) CreateRefreshTokenSession(arg0 context.Context, arg1 string, arg2 fosite.Requester) error { m.ctrl.T.Helper() @@ -78,6 +91,20 @@ func (mr *MockCoreStorageMockRecorder) CreateRefreshTokenSession(arg0, arg1, arg return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateRefreshTokenSession", reflect.TypeOf((*MockCoreStorage)(nil).CreateRefreshTokenSession), arg0, arg1, arg2) } +// CreateUserCodeSession mocks base method. +func (m *MockCoreStorage) 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 *MockCoreStorageMockRecorder) CreateUserCodeSession(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUserCodeSession", reflect.TypeOf((*MockCoreStorage)(nil).CreateUserCodeSession), arg0, arg1, arg2) +} + // DeleteAccessTokenSession mocks base method. func (m *MockCoreStorage) DeleteAccessTokenSession(arg0 context.Context, arg1 string) error { m.ctrl.T.Helper() @@ -92,6 +119,20 @@ func (mr *MockCoreStorageMockRecorder) DeleteAccessTokenSession(arg0, arg1 inter return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAccessTokenSession", reflect.TypeOf((*MockCoreStorage)(nil).DeleteAccessTokenSession), arg0, arg1) } +// DeleteDeviceCodeSession mocks base method. +func (m *MockCoreStorage) DeleteDeviceCodeSession(arg0 context.Context, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteDeviceCodeSession", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteDeviceCodeSession indicates an expected call of DeleteDeviceCodeSession. +func (mr *MockCoreStorageMockRecorder) DeleteDeviceCodeSession(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteDeviceCodeSession", reflect.TypeOf((*MockCoreStorage)(nil).DeleteDeviceCodeSession), arg0, arg1) +} + // DeleteRefreshTokenSession mocks base method. func (m *MockCoreStorage) DeleteRefreshTokenSession(arg0 context.Context, arg1 string) error { m.ctrl.T.Helper() @@ -106,6 +147,20 @@ func (mr *MockCoreStorageMockRecorder) DeleteRefreshTokenSession(arg0, arg1 inte return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteRefreshTokenSession", reflect.TypeOf((*MockCoreStorage)(nil).DeleteRefreshTokenSession), arg0, arg1) } +// DeleteUserCodeSession mocks base method. +func (m *MockCoreStorage) DeleteUserCodeSession(arg0 context.Context, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteUserCodeSession", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteUserCodeSession indicates an expected call of DeleteUserCodeSession. +func (mr *MockCoreStorageMockRecorder) DeleteUserCodeSession(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteUserCodeSession", reflect.TypeOf((*MockCoreStorage)(nil).DeleteUserCodeSession), arg0, arg1) +} + // GetAccessTokenSession mocks base method. func (m *MockCoreStorage) GetAccessTokenSession(arg0 context.Context, arg1 string, arg2 fosite.Session) (fosite.Requester, error) { m.ctrl.T.Helper() @@ -136,6 +191,21 @@ func (mr *MockCoreStorageMockRecorder) GetAuthorizeCodeSession(arg0, arg1, arg2 return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuthorizeCodeSession", reflect.TypeOf((*MockCoreStorage)(nil).GetAuthorizeCodeSession), arg0, arg1, arg2) } +// GetDeviceCodeSession mocks base method. +func (m *MockCoreStorage) 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 *MockCoreStorageMockRecorder) GetDeviceCodeSession(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDeviceCodeSession", reflect.TypeOf((*MockCoreStorage)(nil).GetDeviceCodeSession), arg0, arg1, arg2) +} + // GetRefreshTokenSession mocks base method. func (m *MockCoreStorage) GetRefreshTokenSession(arg0 context.Context, arg1 string, arg2 fosite.Session) (fosite.Requester, error) { m.ctrl.T.Helper() @@ -151,6 +221,21 @@ func (mr *MockCoreStorageMockRecorder) GetRefreshTokenSession(arg0, arg1, arg2 i return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRefreshTokenSession", reflect.TypeOf((*MockCoreStorage)(nil).GetRefreshTokenSession), arg0, arg1, arg2) } +// GetUserCodeSession mocks base method. +func (m *MockCoreStorage) 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 *MockCoreStorageMockRecorder) GetUserCodeSession(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserCodeSession", reflect.TypeOf((*MockCoreStorage)(nil).GetUserCodeSession), arg0, arg1, arg2) +} + // InvalidateAuthorizeCodeSession mocks base method. func (m *MockCoreStorage) InvalidateAuthorizeCodeSession(arg0 context.Context, arg1 string) error { m.ctrl.T.Helper() diff --git a/internal/oauth2_strategy.go b/internal/oauth2_strategy.go index 8934ff245..3a6bc859c 100644 --- a/internal/oauth2_strategy.go +++ b/internal/oauth2_strategy.go @@ -9,7 +9,6 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" - fosite "github.com/ory/fosite" ) @@ -64,6 +63,20 @@ func (mr *MockCoreStrategyMockRecorder) AuthorizeCodeSignature(arg0, arg1 interf return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthorizeCodeSignature", reflect.TypeOf((*MockCoreStrategy)(nil).AuthorizeCodeSignature), arg0, arg1) } +// DeviceCodeSignature mocks base method. +func (m *MockCoreStrategy) DeviceCodeSignature(arg0 context.Context, arg1 string) string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeviceCodeSignature", arg0, arg1) + ret0, _ := ret[0].(string) + return ret0 +} + +// DeviceCodeSignature indicates an expected call of DeviceCodeSignature. +func (mr *MockCoreStrategyMockRecorder) DeviceCodeSignature(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeviceCodeSignature", reflect.TypeOf((*MockCoreStrategy)(nil).DeviceCodeSignature), arg0, arg1) +} + // GenerateAccessToken mocks base method. func (m *MockCoreStrategy) GenerateAccessToken(arg0 context.Context, arg1 fosite.Requester) (string, string, error) { m.ctrl.T.Helper() @@ -96,6 +109,21 @@ func (mr *MockCoreStrategyMockRecorder) GenerateAuthorizeCode(arg0, arg1 interfa return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateAuthorizeCode", reflect.TypeOf((*MockCoreStrategy)(nil).GenerateAuthorizeCode), arg0, arg1) } +// GenerateDeviceCode mocks base method. +func (m *MockCoreStrategy) GenerateDeviceCode() (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GenerateDeviceCode") + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GenerateDeviceCode indicates an expected call of GenerateDeviceCode. +func (mr *MockCoreStrategyMockRecorder) GenerateDeviceCode() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateDeviceCode", reflect.TypeOf((*MockCoreStrategy)(nil).GenerateDeviceCode)) +} + // GenerateRefreshToken mocks base method. func (m *MockCoreStrategy) GenerateRefreshToken(arg0 context.Context, arg1 fosite.Requester) (string, string, error) { m.ctrl.T.Helper() @@ -112,6 +140,21 @@ func (mr *MockCoreStrategyMockRecorder) GenerateRefreshToken(arg0, arg1 interfac return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateRefreshToken", reflect.TypeOf((*MockCoreStrategy)(nil).GenerateRefreshToken), arg0, arg1) } +// GenerateUserCode mocks base method. +func (m *MockCoreStrategy) GenerateUserCode() (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GenerateUserCode") + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GenerateUserCode indicates an expected call of GenerateUserCode. +func (mr *MockCoreStrategyMockRecorder) GenerateUserCode() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateUserCode", reflect.TypeOf((*MockCoreStrategy)(nil).GenerateUserCode)) +} + // RefreshTokenSignature mocks base method. func (m *MockCoreStrategy) RefreshTokenSignature(arg0 context.Context, arg1 string) string { m.ctrl.T.Helper() @@ -126,6 +169,20 @@ func (mr *MockCoreStrategyMockRecorder) RefreshTokenSignature(arg0, arg1 interfa return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RefreshTokenSignature", reflect.TypeOf((*MockCoreStrategy)(nil).RefreshTokenSignature), arg0, arg1) } +// UserCodeSignature mocks base method. +func (m *MockCoreStrategy) UserCodeSignature(arg0 context.Context, arg1 string) string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UserCodeSignature", arg0, arg1) + ret0, _ := ret[0].(string) + return ret0 +} + +// UserCodeSignature indicates an expected call of UserCodeSignature. +func (mr *MockCoreStrategyMockRecorder) UserCodeSignature(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserCodeSignature", reflect.TypeOf((*MockCoreStrategy)(nil).UserCodeSignature), arg0, arg1) +} + // ValidateAccessToken mocks base method. func (m *MockCoreStrategy) ValidateAccessToken(arg0 context.Context, arg1 fosite.Requester, arg2 string) error { m.ctrl.T.Helper() @@ -154,6 +211,20 @@ func (mr *MockCoreStrategyMockRecorder) ValidateAuthorizeCode(arg0, arg1, arg2 i return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateAuthorizeCode", reflect.TypeOf((*MockCoreStrategy)(nil).ValidateAuthorizeCode), arg0, arg1, arg2) } +// ValidateDeviceCode mocks base method. +func (m *MockCoreStrategy) ValidateDeviceCode(arg0 context.Context, arg1 fosite.Requester, arg2 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ValidateDeviceCode", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// ValidateDeviceCode indicates an expected call of ValidateDeviceCode. +func (mr *MockCoreStrategyMockRecorder) ValidateDeviceCode(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateDeviceCode", reflect.TypeOf((*MockCoreStrategy)(nil).ValidateDeviceCode), arg0, arg1, arg2) +} + // ValidateRefreshToken mocks base method. func (m *MockCoreStrategy) ValidateRefreshToken(arg0 context.Context, arg1 fosite.Requester, arg2 string) error { m.ctrl.T.Helper() @@ -167,3 +238,17 @@ func (mr *MockCoreStrategyMockRecorder) ValidateRefreshToken(arg0, arg1, arg2 in mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateRefreshToken", reflect.TypeOf((*MockCoreStrategy)(nil).ValidateRefreshToken), arg0, arg1, arg2) } + +// ValidateUserCode mocks base method. +func (m *MockCoreStrategy) ValidateUserCode(arg0 context.Context, arg1 fosite.Requester, arg2 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ValidateUserCode", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// ValidateUserCode indicates an expected call of ValidateUserCode. +func (mr *MockCoreStrategyMockRecorder) ValidateUserCode(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateUserCode", reflect.TypeOf((*MockCoreStrategy)(nil).ValidateUserCode), arg0, arg1, arg2) +} diff --git a/internal/openid_id_token_storage.go b/internal/openid_id_token_storage.go index 9d33b855f..11f63c68d 100644 --- a/internal/openid_id_token_storage.go +++ b/internal/openid_id_token_storage.go @@ -9,7 +9,6 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" - fosite "github.com/ory/fosite" ) diff --git a/internal/pkce_storage_strategy.go b/internal/pkce_storage_strategy.go index 1ab44f9ad..0f25f14f4 100644 --- a/internal/pkce_storage_strategy.go +++ b/internal/pkce_storage_strategy.go @@ -9,7 +9,6 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" - fosite "github.com/ory/fosite" ) diff --git a/internal/refresh_token_strategy.go b/internal/refresh_token_strategy.go index d0e0422e5..99951c794 100644 --- a/internal/refresh_token_strategy.go +++ b/internal/refresh_token_strategy.go @@ -9,7 +9,6 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" - fosite "github.com/ory/fosite" ) diff --git a/internal/request.go b/internal/request.go index ce642cb0d..b833be8ba 100644 --- a/internal/request.go +++ b/internal/request.go @@ -10,7 +10,6 @@ import ( time "time" gomock "github.com/golang/mock/gomock" - fosite "github.com/ory/fosite" ) diff --git a/internal/revoke_handler.go b/internal/revoke_handler.go index 75418fc70..ed542c112 100644 --- a/internal/revoke_handler.go +++ b/internal/revoke_handler.go @@ -9,7 +9,6 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" - fosite "github.com/ory/fosite" ) diff --git a/internal/storage.go b/internal/storage.go index a17da5f7e..e7b934cc4 100644 --- a/internal/storage.go +++ b/internal/storage.go @@ -10,7 +10,6 @@ import ( time "time" gomock "github.com/golang/mock/gomock" - fosite "github.com/ory/fosite" ) diff --git a/internal/token_handler.go b/internal/token_handler.go index 5e31065d3..5ad46ac60 100644 --- a/internal/token_handler.go +++ b/internal/token_handler.go @@ -9,7 +9,6 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" - fosite "github.com/ory/fosite" ) diff --git a/oauth2.go b/oauth2.go index 66a539b06..bf03fbe22 100644 --- a/oauth2.go +++ b/oauth2.go @@ -41,6 +41,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" @@ -185,6 +187,11 @@ type OAuth2Provider interface { // WritePushedAuthorizeError writes the PAR error WritePushedAuthorizeError(ctx context.Context, rw http.ResponseWriter, ar AuthorizeRequester, err error) + + // ToDo add ietf docs + NewDeviceAuthorizeRequest(ctx context.Context, req *http.Request) (Requester, error) + NewDeviceAuthorizeResponse(ctx context.Context, requester Requester) (DeviceAuthorizeResponder, error) + WriteDeviceAuthorizeResponse(rw http.ResponseWriter, requester Requester, responder DeviceAuthorizeResponder) } // IntrospectionResponder is the response object that will be returned when token introspection was successful, @@ -383,3 +390,23 @@ type G11NContext interface { // GetLang returns the current language in the context GetLang() language.Tag } + +type DeviceAuthorizeResponder 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) +} diff --git a/storage/memory.go b/storage/memory.go index 41b4f86a3..7557984ab 100644 --- a/storage/memory.go +++ b/storage/memory.go @@ -59,12 +59,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 @@ -74,11 +78,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 } @@ -90,10 +98,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), @@ -157,8 +169,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{}, } } @@ -514,3 +530,67 @@ 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) 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) DeleteDeviceCodeSession(_ 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) DeleteUserCodeSession(_ 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 5910a5eb6..b6cd61da7 100644 --- a/token/hmac/hmacsha.go +++ b/token/hmac/hmacsha.go @@ -173,6 +173,17 @@ func (c *HMACStrategy) Signature(token string) string { return split[1] } +func (c *HMACStrategy) GenerateHMACForString(text string, ctx context.Context) string { + var signingKey [32]byte + copy(signingKey[:], c.Config.GetGlobalSecret(ctx)) + + bytes := []byte(text) + hashBytes := c.generateHMAC(ctx, bytes, &signingKey) + + b64 := base64.URLEncoding.EncodeToString(hashBytes) + return b64 +} + 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 23de6764a..2dcbc71b9 100644 --- a/token/hmac/hmacsha_test.go +++ b/token/hmac/hmacsha_test.go @@ -151,3 +151,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(c.text, context.Background()) + assert.Equal(t, c.hash, hash) + } +} From 99763604d8ab8391ff6f12a5c1e6ac064c00fc89 Mon Sep 17 00:00:00 2001 From: Romain Caire Date: Wed, 7 Sep 2022 13:26:24 +0200 Subject: [PATCH 02/49] Add own flow to the Device Grant --- compose/compose.go | 3 ++ device_authorize_request.go | 42 +++++++++++++++++++ device_authorize_request_handler.go | 52 ++++++++++++++++++++++-- device_authorize_response.go | 21 ++++++++++ device_authorize_response_writer.go | 21 ++++++++++ device_authorize_writer.go | 24 ++++++++++- errors.go | 2 +- handler/oauth2/device_authorization.go | 7 +--- handler/oauth2/flow_device_code_token.go | 32 +++++++-------- handler/oauth2/storage.go | 31 ++++++++++++-- handler/oauth2/strategy.go | 12 +++--- handler/oauth2/strategy_hmacsha.go | 24 ++++++----- handler/oauth2/strategy_jwt.go | 16 ++++---- handler/openid/flow_device_auth.go | 21 ++++++++++ handler/openid/flow_device_token.go | 21 ++++++++++ handler/openid/validator.go | 50 ++++++++++++----------- handler/pkce/handler_device.go | 21 ++++++++++ oauth2.go | 22 +++++++--- token/hmac/hmacsha.go | 2 +- token/hmac/hmacsha_test.go | 30 ++++++++++++++ 20 files changed, 368 insertions(+), 86 deletions(-) create mode 100644 device_authorize_request.go diff --git a/compose/compose.go b/compose/compose.go index 372c8d3c9..714b20752 100644 --- a/compose/compose.go +++ b/compose/compose.go @@ -71,6 +71,9 @@ func Compose(config *fosite.Config, storage interface{}, strategy interface{}, f if ph, ok := res.(fosite.PushedAuthorizeEndpointHandler); ok { config.PushedAuthorizeEndpointHandlers.Append(ph) } + if dah, ok := res.(fosite.DeviceAuthorizeEndpointHandler); ok { + config.DeviceAuthorizeEndpointHandlers.Append(dah) + } } return f diff --git a/device_authorize_request.go b/device_authorize_request.go new file mode 100644 index 000000000..75cd9d9d1 --- /dev/null +++ b/device_authorize_request.go @@ -0,0 +1,42 @@ +/* + * Copyright © 2015-2018 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-2018 Aeneas Rekkas + * @license Apache-2.0 + * + */ + +package fosite + +// DeviceAuthorizeRequest is an implementation of DeviceAuthorizeRequester +type DeviceAuthorizeRequest struct { + DeviceCodeSignature string + Request +} + +func (d *DeviceAuthorizeRequest) GetDeviceCodeSignature() string { + return d.DeviceCodeSignature +} + +func (d *DeviceAuthorizeRequest) SetDeviceCodeSignature(signature string) { + d.DeviceCodeSignature = signature +} + +func NewDeviceAuthorizeRequest() *DeviceAuthorizeRequest { + return &DeviceAuthorizeRequest{ + Request: *NewRequest(), + } +} diff --git a/device_authorize_request_handler.go b/device_authorize_request_handler.go index efc981094..ceeaf8297 100644 --- a/device_authorize_request_handler.go +++ b/device_authorize_request_handler.go @@ -1,3 +1,24 @@ +/* + * 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 Luke Stoward + * @copyright 2015-2021 Aeneas Rekkas + * @license Apache-2.0 + * + */ + package fosite import ( @@ -9,9 +30,32 @@ import ( "github.com/ory/x/errorsx" ) -func (f *Fosite) NewDeviceAuthorizeRequest(ctx context.Context, req *http.Request) (Requester, error) { +func (f *Fosite) NewDeviceAuthorizeGetRequest(ctx context.Context, r *http.Request) (DeviceAuthorizeRequester, error) { + request := NewDeviceAuthorizeRequest() + request.Lang = i18n.GetLangFromRequest(f.Config.GetMessageCatalog(ctx), r) + + if err := r.ParseMultipartForm(1 << 20); err != nil && err != http.ErrNotMultipart { + return request, 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 = r.Form + + if request.GetRequestForm().Has("device_verifier") { + client, err := f.Store.GetClient(ctx, request.GetRequestForm().Get("client_id")) + if err != nil { + return request, 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("urn:ietf:params:oauth:grant-type:device_code") { + return request, errorsx.WithStack(ErrInvalidClient.WithHint("The requested OAuth 2.0 Client does not have the 'urn:ietf:params:oauth:grant-type:device_code' grant.").WithWrap(err).WithDebug(err.Error())) + } + } + + return request, nil +} - request := NewRequest() +func (f *Fosite) NewDeviceAuthorizePostRequest(ctx context.Context, req *http.Request) (DeviceAuthorizeRequester, error) { + request := NewDeviceAuthorizeRequest() request.Lang = i18n.GetLangFromRequest(f.Config.GetMessageCatalog(ctx), req) if err := req.ParseMultipartForm(1 << 20); err != nil && err != http.ErrNotMultipart { @@ -25,14 +69,14 @@ func (f *Fosite) NewDeviceAuthorizeRequest(ctx context.Context, req *http.Reques } request.Client = client - if err := f.validateDeviceAuthorizeScope(ctx, request); err != nil { + if err := f.validateDeviceAuthorizeScope(ctx, req, request); err != nil { return request, err } return request, nil } -func (f *Fosite) validateDeviceAuthorizeScope(ctx context.Context, request *Request) error { +func (f *Fosite) validateDeviceAuthorizeScope(ctx context.Context, req *http.Request, request *DeviceAuthorizeRequest) error { scope := RemoveEmpty(strings.Split(request.Form.Get("scope"), " ")) for _, permission := range scope { if !f.Config.GetScopeStrategy(ctx)(request.Client.GetScopes(), permission) { diff --git a/device_authorize_response.go b/device_authorize_response.go index 14697c08f..648e14a42 100644 --- a/device_authorize_response.go +++ b/device_authorize_response.go @@ -1,3 +1,24 @@ +/* + * Copyright © 2015-2018 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-2018 Aeneas Rekkas + * @license Apache-2.0 + * + */ + package fosite import "context" diff --git a/device_authorize_response_writer.go b/device_authorize_response_writer.go index 81cff1fc1..1b4deb97c 100644 --- a/device_authorize_response_writer.go +++ b/device_authorize_response_writer.go @@ -1,3 +1,24 @@ +/* + * Copyright © 2015-2018 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-2018 Aeneas Rekkas + * @license Apache-2.0 + * + */ + package fosite import ( diff --git a/device_authorize_writer.go b/device_authorize_writer.go index 68b41da18..ce3082ac4 100644 --- a/device_authorize_writer.go +++ b/device_authorize_writer.go @@ -1,11 +1,33 @@ +/* + * 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 Luke Stoward + * @copyright 2015-2021 Aeneas Rekkas + * @license Apache-2.0 + * + */ + package fosite import ( + "context" "encoding/json" "net/http" ) -func (f *Fosite) WriteDeviceAuthorizeResponse(rw http.ResponseWriter, r Requester, resp DeviceAuthorizeResponder) { +func (f *Fosite) WriteDeviceAuthorizeResponse(ctx context.Context, rw http.ResponseWriter, r Requester, resp DeviceAuthorizeResponder) { rw.Header().Set("Content-Type", "application/json;charset=UTF-8") rw.Header().Set("Cache-Control", "no-store") rw.Header().Set("Pragma", "no-cache") diff --git a/errors.go b/errors.go index 4da5ed5b3..85f9a6426 100644 --- a/errors.go +++ b/errors.go @@ -41,7 +41,7 @@ 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. diff --git a/handler/oauth2/device_authorization.go b/handler/oauth2/device_authorization.go index 39d33c8d8..8c367a822 100644 --- a/handler/oauth2/device_authorization.go +++ b/handler/oauth2/device_authorization.go @@ -21,21 +21,18 @@ type DeviceAuthorizationHandler struct { func (d *DeviceAuthorizationHandler) HandleDeviceAuthorizeEndpointRequest(ctx context.Context, dar fosite.Requester, resp fosite.DeviceAuthorizeResponder) error { fmt.Println("DeviceAuthorizationHandler :: HandleDeviceAuthorizeEndpointRequest ++") - deviceCode, err := d.DeviceCodeStrategy.GenerateDeviceCode() + deviceCode, deviceCodeSignature, err := d.DeviceCodeStrategy.GenerateDeviceCode(ctx) if err != nil { return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) } - userCode, err := d.UserCodeStrategy.GenerateUserCode() + userCode, userCodeSignature, err := d.UserCodeStrategy.GenerateUserCode(ctx) if err != nil { return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) } fmt.Println("DeviceAuthorizationHandler :: HandleDeviceAuthorizeEndpointRequest +++") - userCodeSignature := d.UserCodeStrategy.UserCodeSignature(ctx, userCode) - deviceCodeSignature := d.DeviceCodeStrategy.DeviceCodeSignature(ctx, deviceCode) - // Set User Code expiry time dar.GetSession().SetExpiresAt(fosite.UserCode, time.Now().UTC().Add(d.Config.GetDeviceAndUserCodeLifespan(ctx)).Round(time.Second)) dar.SetID(deviceCodeSignature) diff --git a/handler/oauth2/flow_device_code_token.go b/handler/oauth2/flow_device_code_token.go index b349b8de8..5f0ab80f5 100644 --- a/handler/oauth2/flow_device_code_token.go +++ b/handler/oauth2/flow_device_code_token.go @@ -42,14 +42,6 @@ func (d *AuthorizeDeviceGrantTypeHandler) HandleTokenEndpointRequest(ctx context return errorsx.WithStack(fosite.ErrInvalidGrant.WithHint("The OAuth 2.0 Client ID from this request does not match the one from the authorize request.")) } - //expires := session.GetSession().GetExpiresAt(fosite.UserCode) - //if time.Now().UTC().After(expires) { - // return errorsx.WithStack(fosite.ErrTokenExpired) - //} - - //requester.SetSession(session.GetSession()) - //requester.SetID(session.GetID()) - atLifespan := fosite.GetEffectiveLifespan(requester.GetClient(), fosite.GrantTypeAuthorizationCode, fosite.AccessToken, d.Config.GetAccessTokenLifespan(ctx)) requester.GetSession().SetExpiresAt(fosite.AccessToken, time.Now().UTC().Add(atLifespan).Round(time.Second)) @@ -66,6 +58,8 @@ func (d *AuthorizeDeviceGrantTypeHandler) CanSkipClientAuth(ctx context.Context, } func (d *AuthorizeDeviceGrantTypeHandler) CanHandleTokenEndpointRequest(ctx context.Context, requester fosite.AccessRequester) bool { + // grant_type REQUIRED. + // Value MUST be set to "urn:ietf:params:oauth:grant-type:device_code" fmt.Println("CanHandleTokenEndpointRequest OAUTH") return requester.GetGrantTypes().ExactOne(deviceCodeGrantType) } @@ -80,7 +74,7 @@ func (d *AuthorizeDeviceGrantTypeHandler) PopulateTokenEndpointResponse(ctx cont if code == "" { return errorsx.WithStack(errorsx.WithStack(fosite.ErrUnknownRequest.WithHint("device_code missing form body"))) } - codeSignature := d.DeviceCodeStrategy.DeviceCodeSignature(ctx, code) + signature := d.DeviceCodeStrategy.DeviceCodeSignature(ctx, code) if err := d.DeviceCodeStrategy.ValidateDeviceCode(ctx, requester, code); err != nil { // This needs to happen after store retrieval for the session to be hydrated properly @@ -88,9 +82,11 @@ func (d *AuthorizeDeviceGrantTypeHandler) PopulateTokenEndpointResponse(ctx cont } // Get the device code session ready for exchange to auth / refresh / oidc sessions - session, err := d.CoreStorage.GetDeviceCodeSession(ctx, codeSignature, requester.GetSession()) - + session, err := d.CoreStorage.GetDeviceCodeSession(ctx, signature, requester.GetSession()) if err != nil { + return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) + } else if err := d.DeviceCodeStrategy.ValidateDeviceCode(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())) } @@ -108,8 +104,7 @@ func (d *AuthorizeDeviceGrantTypeHandler) PopulateTokenEndpointResponse(ctx cont } var refresh, refreshSignature string - - if d.canIssueRefreshToken(ctx, requester) { + if d.canIssueRefreshToken(ctx, d, session) { refresh, refreshSignature, err = d.RefreshTokenStrategy.GenerateRefreshToken(ctx, requester) if err != nil { return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) @@ -128,7 +123,7 @@ func (d *AuthorizeDeviceGrantTypeHandler) PopulateTokenEndpointResponse(ctx cont } }() - if err = d.CoreStorage.DeleteDeviceCodeSession(ctx, code); err != nil { + if err = d.CoreStorage.InvalidateDeviceCodeSession(ctx, signature); err != nil { return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) } else if err = d.CoreStorage.CreateAccessTokenSession(ctx, accessSignature, requester.Sanitize([]string{})); err != nil { return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) @@ -154,9 +149,14 @@ func (d *AuthorizeDeviceGrantTypeHandler) PopulateTokenEndpointResponse(ctx cont return nil } -func (c *AuthorizeDeviceGrantTypeHandler) canIssueRefreshToken(ctx context.Context, request fosite.Requester) bool { +func (c *AuthorizeDeviceGrantTypeHandler) canIssueRefreshToken(ctx context.Context, config *AuthorizeDeviceGrantTypeHandler, request fosite.Requester) bool { + scope := config.Config.GetRefreshTokenScopes(ctx) // Require one of the refresh token scopes, if set. - if len(c.Config.GetRefreshTokenScopes(ctx)) > 0 && !request.GetGrantedScopes().HasOneOf(c.Config.GetRefreshTokenScopes(ctx)...) { + 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 diff --git a/handler/oauth2/storage.go b/handler/oauth2/storage.go index a72002564..963d5ac42 100644 --- a/handler/oauth2/storage.go +++ b/handler/oauth2/storage.go @@ -70,12 +70,35 @@ type RefreshTokenStorage interface { } type DeviceCodeStorage interface { + // CreateDeviceCodeSession stores the device request for a given device code. CreateDeviceCodeSession(ctx context.Context, signature string, request fosite.Requester) (err error) - GetDeviceCodeSession(ctx context.Context, code string, session fosite.Session) (request fosite.Requester, err error) - DeleteDeviceCodeSession(ctx context.Context, code string) (err error) + + // GetDeviceCodeSession hydrates the session based on the given device code and returns the device request. + // If the user 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(ctx context.Context, code string, session fosite.Session) (request fosite.Requester, err error) - DeleteUserCodeSession(ctx context.Context, code string) (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/oauth2/strategy.go b/handler/oauth2/strategy.go index 26107f731..4cb88e7b8 100644 --- a/handler/oauth2/strategy.go +++ b/handler/oauth2/strategy.go @@ -54,13 +54,13 @@ type AuthorizeCodeStrategy interface { } type DeviceCodeStrategy interface { - DeviceCodeSignature(context context.Context, code string) string - ValidateDeviceCode(context context.Context, r fosite.Requester, code string) (err error) - GenerateDeviceCode() (code string, err error) + DeviceCodeSignature(ctx context.Context, code string) string + 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(context context.Context, code string) string - ValidateUserCode(context context.Context, r fosite.Requester, code string) (err error) - GenerateUserCode() (code string, err error) + UserCodeSignature(ctx context.Context, code string) string + 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/oauth2/strategy_hmacsha.go b/handler/oauth2/strategy_hmacsha.go index 6b722ad6a..f2a387ee2 100644 --- a/handler/oauth2/strategy_hmacsha.go +++ b/handler/oauth2/strategy_hmacsha.go @@ -142,7 +142,7 @@ func (h *HMACSHAStrategy) ValidateAuthorizeCode(ctx context.Context, r fosite.Re return h.Enigma.Validate(ctx, h.trimPrefix(token, "ac")) } -func (h HMACSHAStrategy) generateRandomString(length int) (token string, err error) { +func (h *HMACSHAStrategy) generateRandomString(length int) (token string, err error) { chars := [20]byte{'B', 'C', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'X', 'Z'} chars_length := int64(len(chars)) @@ -157,15 +157,16 @@ func (h HMACSHAStrategy) generateRandomString(length int) (token string, err err return string(code), nil } -func (h HMACSHAStrategy) GenerateUserCode() (token string, err error) { - return h.generateRandomString(8) +func (h *HMACSHAStrategy) GenerateUserCode(ctx context.Context) (token string, signature string, err error) { + userCode, err := h.generateRandomString(8) + return userCode, h.UserCodeSignature(ctx, userCode), err } -func (h HMACSHAStrategy) UserCodeSignature(ctx context.Context, token string) string { - return h.Enigma.GenerateHMACForString(token, ctx) +func (h *HMACSHAStrategy) UserCodeSignature(ctx context.Context, token string) string { + return h.Enigma.GenerateHMACForString(ctx, token) } -func (h HMACSHAStrategy) ValidateUserCode(ctx context.Context, r fosite.Requester, code string) (err error) { +func (h *HMACSHAStrategy) 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.ErrTokenExpired.WithHintf("Access token expired at '%s'.", r.GetRequestedAt().Add(h.Config.GetDeviceAndUserCodeLifespan(ctx)))) @@ -176,15 +177,16 @@ func (h HMACSHAStrategy) ValidateUserCode(ctx context.Context, r fosite.Requeste return nil } -func (h HMACSHAStrategy) GenerateDeviceCode() (token string, err error) { - return h.generateRandomString(100) +func (h *HMACSHAStrategy) GenerateDeviceCode(ctx context.Context) (token string, signature string, err error) { + deviceCode, err := h.generateRandomString(100) + return deviceCode, h.DeviceCodeSignature(ctx, deviceCode), err } -func (h HMACSHAStrategy) DeviceCodeSignature(ctx context.Context, token string) string { - return h.Enigma.GenerateHMACForString(token, ctx) +func (h *HMACSHAStrategy) DeviceCodeSignature(ctx context.Context, token string) string { + return h.Enigma.GenerateHMACForString(ctx, token) } -func (h HMACSHAStrategy) ValidateDeviceCode(ctx context.Context, r fosite.Requester, code string) (err error) { +func (h *HMACSHAStrategy) ValidateDeviceCode(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.ErrTokenExpired.WithHintf("1 Device code expired at '%s'.", r.GetRequestedAt().Add(h.Config.GetDeviceAndUserCodeLifespan(ctx)))) diff --git a/handler/oauth2/strategy_jwt.go b/handler/oauth2/strategy_jwt.go index 85bd20e00..2562ea6c4 100644 --- a/handler/oauth2/strategy_jwt.go +++ b/handler/oauth2/strategy_jwt.go @@ -154,22 +154,22 @@ func (h DefaultJWTStrategy) DeviceCodeSignature(ctx context.Context, token strin return h.HMACSHAStrategy.DeviceCodeSignature(ctx, token) } -func (h *DefaultJWTStrategy) GenerateDeviceCode() (token string, err error) { - return h.HMACSHAStrategy.GenerateDeviceCode() +func (h *DefaultJWTStrategy) GenerateDeviceCode(ctx context.Context) (token string, signature string, err error) { + return h.HMACSHAStrategy.GenerateDeviceCode(ctx) } -func (h *DefaultJWTStrategy) ValidateDeviceCode(context context.Context, r fosite.Requester, code string) (err error) { - return h.HMACSHAStrategy.ValidateDeviceCode(context, r, code) +func (h *DefaultJWTStrategy) ValidateDeviceCode(ctx context.Context, r fosite.Requester, code string) (err error) { + return h.HMACSHAStrategy.ValidateDeviceCode(ctx, r, code) } func (h DefaultJWTStrategy) UserCodeSignature(ctx context.Context, token string) string { return h.HMACSHAStrategy.UserCodeSignature(ctx, token) } -func (h *DefaultJWTStrategy) GenerateUserCode() (token string, err error) { - return h.HMACSHAStrategy.GenerateUserCode() +func (h *DefaultJWTStrategy) GenerateUserCode(ctx context.Context) (token string, signature string, err error) { + return h.HMACSHAStrategy.GenerateUserCode(ctx) } -func (h *DefaultJWTStrategy) ValidateUserCode(context context.Context, r fosite.Requester, code string) (err error) { - return h.HMACSHAStrategy.ValidateUserCode(context, r, code) +func (h *DefaultJWTStrategy) ValidateUserCode(ctx context.Context, r fosite.Requester, code string) (err error) { + return h.HMACSHAStrategy.ValidateUserCode(ctx, r, code) } diff --git a/handler/openid/flow_device_auth.go b/handler/openid/flow_device_auth.go index a038f698d..291faa633 100644 --- a/handler/openid/flow_device_auth.go +++ b/handler/openid/flow_device_auth.go @@ -1,3 +1,24 @@ +/* + * Copyright © 2015-2018 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-2018 Aeneas Rekkas + * @license Apache-2.0 + * + */ + package openid import ( diff --git a/handler/openid/flow_device_token.go b/handler/openid/flow_device_token.go index ecbb7c5d5..6b5d4621a 100644 --- a/handler/openid/flow_device_token.go +++ b/handler/openid/flow_device_token.go @@ -1,3 +1,24 @@ +/* + * Copyright © 2015-2018 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-2018 Aeneas Rekkas + * @license Apache-2.0 + * + */ + package openid import ( diff --git a/handler/openid/validator.go b/handler/openid/validator.go index 33229711a..a4d65cc9a 100644 --- a/handler/openid/validator.go +++ b/handler/openid/validator.go @@ -55,33 +55,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/pkce/handler_device.go b/handler/pkce/handler_device.go index 82c81e083..25cddfcb1 100644 --- a/handler/pkce/handler_device.go +++ b/handler/pkce/handler_device.go @@ -1,3 +1,24 @@ +/* + * Copyright © 2015-2018 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-2018 Aeneas Rekkas + * @license Apache-2.0 + * + */ + package pkce import ( diff --git a/oauth2.go b/oauth2.go index bf03fbe22..9a70d84e4 100644 --- a/oauth2.go +++ b/oauth2.go @@ -77,6 +77,12 @@ type OAuth2Provider interface { // * https://tools.ietf.org/html/rfc6749#section-3.1.2.2 (everything MUST be implemented) NewAuthorizeRequest(ctx context.Context, req *http.Request) (AuthorizeRequester, error) + // ToDo add ietf docs + NewDeviceAuthorizeGetRequest(ctx context.Context, req *http.Request) (DeviceAuthorizeRequester, error) + NewDeviceAuthorizePostRequest(ctx context.Context, req *http.Request) (DeviceAuthorizeRequester, error) + NewDeviceAuthorizeResponse(ctx context.Context, requester Requester) (DeviceAuthorizeResponder, error) + WriteDeviceAuthorizeResponse(ctx context.Context, rw http.ResponseWriter, requester Requester, responder DeviceAuthorizeResponder) + // NewAuthorizeResponse iterates through all response type handlers and returns their result or // ErrUnsupportedResponseType if none of the handler's were able to handle it. // @@ -187,11 +193,6 @@ type OAuth2Provider interface { // WritePushedAuthorizeError writes the PAR error WritePushedAuthorizeError(ctx context.Context, rw http.ResponseWriter, ar AuthorizeRequester, err error) - - // ToDo add ietf docs - NewDeviceAuthorizeRequest(ctx context.Context, req *http.Request) (Requester, error) - NewDeviceAuthorizeResponse(ctx context.Context, requester Requester) (DeviceAuthorizeResponder, error) - WriteDeviceAuthorizeResponse(rw http.ResponseWriter, requester Requester, responder DeviceAuthorizeResponder) } // IntrospectionResponder is the response object that will be returned when token introspection was successful, @@ -278,6 +279,17 @@ type AccessRequester interface { Requester } +// DeviceAuthorizeRequester is an device authorize endpoint's request context. +type DeviceAuthorizeRequester 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 diff --git a/token/hmac/hmacsha.go b/token/hmac/hmacsha.go index b6cd61da7..6c2123abc 100644 --- a/token/hmac/hmacsha.go +++ b/token/hmac/hmacsha.go @@ -173,7 +173,7 @@ func (c *HMACStrategy) Signature(token string) string { return split[1] } -func (c *HMACStrategy) GenerateHMACForString(text string, ctx context.Context) string { +func (c *HMACStrategy) GenerateHMACForString(ctx context.Context, text string) string { var signingKey [32]byte copy(signingKey[:], c.Config.GetGlobalSecret(ctx)) diff --git a/token/hmac/hmacsha_test.go b/token/hmac/hmacsha_test.go index 2dcbc71b9..b236ee290 100644 --- a/token/hmac/hmacsha_test.go +++ b/token/hmac/hmacsha_test.go @@ -78,6 +78,36 @@ func TestGenerate(t *testing.T) { } } +func TestGenerateFromString(t *testing.T) { + cg := HMACStrategy{ + 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(c.text) + assert.Equal(t, c.hash, hash) + } +} + func TestValidateSignatureRejects(t *testing.T) { var err error cg := HMACStrategy{ From ae0144cb29db694cdd600a5c558c4d6da2d14553 Mon Sep 17 00:00:00 2001 From: Romain Caire Date: Wed, 7 Sep 2022 15:17:47 +0200 Subject: [PATCH 03/49] Generate Mocks & MemoryStore Fix HMACPointer --- handler/oauth2/strategy_hmacsha.go | 38 ++++++++++---------- internal/oauth2_storage.go | 56 +++++++++++++++--------------- internal/oauth2_strategy.go | 26 +++++++------- storage/memory.go | 4 +-- 4 files changed, 63 insertions(+), 61 deletions(-) diff --git a/handler/oauth2/strategy_hmacsha.go b/handler/oauth2/strategy_hmacsha.go index f2a387ee2..a9c66e211 100644 --- a/handler/oauth2/strategy_hmacsha.go +++ b/handler/oauth2/strategy_hmacsha.go @@ -46,17 +46,17 @@ type HMACSHAStrategy struct { prefix *string } -func (h *HMACSHAStrategy) AccessTokenSignature(ctx context.Context, token string) string { +func (h HMACSHAStrategy) AccessTokenSignature(ctx context.Context, token string) string { return h.Enigma.Signature(token) } -func (h *HMACSHAStrategy) RefreshTokenSignature(ctx context.Context, token string) string { +func (h HMACSHAStrategy) RefreshTokenSignature(ctx context.Context, token string) string { return h.Enigma.Signature(token) } -func (h *HMACSHAStrategy) AuthorizeCodeSignature(ctx context.Context, token string) string { +func (h HMACSHAStrategy) AuthorizeCodeSignature(ctx context.Context, token string) string { return h.Enigma.Signature(token) } -func (h *HMACSHAStrategy) getPrefix(part string) string { +func (h HMACSHAStrategy) getPrefix(part string) string { if h.prefix == nil { prefix := "ory_%s_" h.prefix = &prefix @@ -67,15 +67,15 @@ func (h *HMACSHAStrategy) getPrefix(part string) string { return fmt.Sprintf(*h.prefix, part) } -func (h *HMACSHAStrategy) trimPrefix(token, part string) string { +func (h HMACSHAStrategy) trimPrefix(token, part string) string { return strings.TrimPrefix(token, h.getPrefix(part)) } -func (h *HMACSHAStrategy) setPrefix(token, part string) string { +func (h HMACSHAStrategy) setPrefix(token, part string) string { return h.getPrefix(part) + token } -func (h *HMACSHAStrategy) GenerateAccessToken(ctx context.Context, _ fosite.Requester) (token string, signature string, err error) { +func (h HMACSHAStrategy) GenerateAccessToken(ctx context.Context, _ fosite.Requester) (token string, signature string, err error) { token, sig, err := h.Enigma.Generate(ctx) if err != nil { return "", "", err @@ -84,7 +84,7 @@ func (h *HMACSHAStrategy) GenerateAccessToken(ctx context.Context, _ fosite.Requ return h.setPrefix(token, "at"), sig, nil } -func (h *HMACSHAStrategy) ValidateAccessToken(ctx context.Context, r fosite.Requester, token string) (err error) { +func (h HMACSHAStrategy) ValidateAccessToken(ctx context.Context, r fosite.Requester, token string) (err error) { var exp = r.GetSession().GetExpiresAt(fosite.AccessToken) if exp.IsZero() && r.GetRequestedAt().Add(h.Config.GetAccessTokenLifespan(ctx)).Before(time.Now().UTC()) { return errorsx.WithStack(fosite.ErrTokenExpired.WithHintf("Access token expired at '%s'.", r.GetRequestedAt().Add(h.Config.GetAccessTokenLifespan(ctx)))) @@ -97,7 +97,7 @@ func (h *HMACSHAStrategy) ValidateAccessToken(ctx context.Context, r fosite.Requ return h.Enigma.Validate(ctx, h.trimPrefix(token, "at")) } -func (h *HMACSHAStrategy) GenerateRefreshToken(ctx context.Context, _ fosite.Requester) (token string, signature string, err error) { +func (h HMACSHAStrategy) GenerateRefreshToken(ctx context.Context, _ fosite.Requester) (token string, signature string, err error) { token, sig, err := h.Enigma.Generate(ctx) if err != nil { return "", "", err @@ -106,7 +106,7 @@ func (h *HMACSHAStrategy) GenerateRefreshToken(ctx context.Context, _ fosite.Req return h.setPrefix(token, "rt"), sig, nil } -func (h *HMACSHAStrategy) ValidateRefreshToken(ctx context.Context, r fosite.Requester, token string) (err error) { +func (h HMACSHAStrategy) ValidateRefreshToken(ctx context.Context, r fosite.Requester, token string) (err error) { var exp = r.GetSession().GetExpiresAt(fosite.RefreshToken) if exp.IsZero() { // Unlimited lifetime @@ -120,7 +120,7 @@ func (h *HMACSHAStrategy) ValidateRefreshToken(ctx context.Context, r fosite.Req return h.Enigma.Validate(ctx, h.trimPrefix(token, "rt")) } -func (h *HMACSHAStrategy) GenerateAuthorizeCode(ctx context.Context, _ fosite.Requester) (token string, signature string, err error) { +func (h HMACSHAStrategy) GenerateAuthorizeCode(ctx context.Context, _ fosite.Requester) (token string, signature string, err error) { token, sig, err := h.Enigma.Generate(ctx) if err != nil { return "", "", err @@ -129,7 +129,7 @@ func (h *HMACSHAStrategy) GenerateAuthorizeCode(ctx context.Context, _ fosite.Re return h.setPrefix(token, "ac"), sig, nil } -func (h *HMACSHAStrategy) ValidateAuthorizeCode(ctx context.Context, r fosite.Requester, token string) (err error) { +func (h HMACSHAStrategy) ValidateAuthorizeCode(ctx context.Context, r fosite.Requester, token string) (err error) { var exp = r.GetSession().GetExpiresAt(fosite.AuthorizeCode) if exp.IsZero() && r.GetRequestedAt().Add(h.Config.GetAuthorizeCodeLifespan(ctx)).Before(time.Now().UTC()) { return errorsx.WithStack(fosite.ErrTokenExpired.WithHintf("Authorize code expired at '%s'.", r.GetRequestedAt().Add(h.Config.GetAuthorizeCodeLifespan(ctx)))) @@ -142,7 +142,7 @@ func (h *HMACSHAStrategy) ValidateAuthorizeCode(ctx context.Context, r fosite.Re return h.Enigma.Validate(ctx, h.trimPrefix(token, "ac")) } -func (h *HMACSHAStrategy) generateRandomString(length int) (token string, err error) { +func (h HMACSHAStrategy) generateRandomString(length int) (token string, err error) { chars := [20]byte{'B', 'C', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'X', 'Z'} chars_length := int64(len(chars)) @@ -157,16 +157,16 @@ func (h *HMACSHAStrategy) generateRandomString(length int) (token string, err er return string(code), nil } -func (h *HMACSHAStrategy) GenerateUserCode(ctx context.Context) (token string, signature string, err error) { +func (h HMACSHAStrategy) GenerateUserCode(ctx context.Context) (token string, signature string, err error) { userCode, err := h.generateRandomString(8) return userCode, h.UserCodeSignature(ctx, userCode), err } -func (h *HMACSHAStrategy) UserCodeSignature(ctx context.Context, token string) string { +func (h HMACSHAStrategy) UserCodeSignature(ctx context.Context, token string) string { return h.Enigma.GenerateHMACForString(ctx, token) } -func (h *HMACSHAStrategy) ValidateUserCode(ctx context.Context, r fosite.Requester, code string) (err error) { +func (h HMACSHAStrategy) 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.ErrTokenExpired.WithHintf("Access token expired at '%s'.", r.GetRequestedAt().Add(h.Config.GetDeviceAndUserCodeLifespan(ctx)))) @@ -177,16 +177,16 @@ func (h *HMACSHAStrategy) ValidateUserCode(ctx context.Context, r fosite.Request return nil } -func (h *HMACSHAStrategy) GenerateDeviceCode(ctx context.Context) (token string, signature string, err error) { +func (h HMACSHAStrategy) GenerateDeviceCode(ctx context.Context) (token string, signature string, err error) { deviceCode, err := h.generateRandomString(100) return deviceCode, h.DeviceCodeSignature(ctx, deviceCode), err } -func (h *HMACSHAStrategy) DeviceCodeSignature(ctx context.Context, token string) string { +func (h HMACSHAStrategy) DeviceCodeSignature(ctx context.Context, token string) string { return h.Enigma.GenerateHMACForString(ctx, token) } -func (h *HMACSHAStrategy) ValidateDeviceCode(ctx context.Context, r fosite.Requester, code string) (err error) { +func (h HMACSHAStrategy) ValidateDeviceCode(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.ErrTokenExpired.WithHintf("1 Device code expired at '%s'.", r.GetRequestedAt().Add(h.Config.GetDeviceAndUserCodeLifespan(ctx)))) diff --git a/internal/oauth2_storage.go b/internal/oauth2_storage.go index 0b9789d53..0b63462df 100644 --- a/internal/oauth2_storage.go +++ b/internal/oauth2_storage.go @@ -119,20 +119,6 @@ func (mr *MockCoreStorageMockRecorder) DeleteAccessTokenSession(arg0, arg1 inter return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAccessTokenSession", reflect.TypeOf((*MockCoreStorage)(nil).DeleteAccessTokenSession), arg0, arg1) } -// DeleteDeviceCodeSession mocks base method. -func (m *MockCoreStorage) DeleteDeviceCodeSession(arg0 context.Context, arg1 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteDeviceCodeSession", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// DeleteDeviceCodeSession indicates an expected call of DeleteDeviceCodeSession. -func (mr *MockCoreStorageMockRecorder) DeleteDeviceCodeSession(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteDeviceCodeSession", reflect.TypeOf((*MockCoreStorage)(nil).DeleteDeviceCodeSession), arg0, arg1) -} - // DeleteRefreshTokenSession mocks base method. func (m *MockCoreStorage) DeleteRefreshTokenSession(arg0 context.Context, arg1 string) error { m.ctrl.T.Helper() @@ -147,20 +133,6 @@ func (mr *MockCoreStorageMockRecorder) DeleteRefreshTokenSession(arg0, arg1 inte return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteRefreshTokenSession", reflect.TypeOf((*MockCoreStorage)(nil).DeleteRefreshTokenSession), arg0, arg1) } -// DeleteUserCodeSession mocks base method. -func (m *MockCoreStorage) DeleteUserCodeSession(arg0 context.Context, arg1 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteUserCodeSession", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// DeleteUserCodeSession indicates an expected call of DeleteUserCodeSession. -func (mr *MockCoreStorageMockRecorder) DeleteUserCodeSession(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteUserCodeSession", reflect.TypeOf((*MockCoreStorage)(nil).DeleteUserCodeSession), arg0, arg1) -} - // GetAccessTokenSession mocks base method. func (m *MockCoreStorage) GetAccessTokenSession(arg0 context.Context, arg1 string, arg2 fosite.Session) (fosite.Requester, error) { m.ctrl.T.Helper() @@ -249,3 +221,31 @@ func (mr *MockCoreStorageMockRecorder) InvalidateAuthorizeCodeSession(arg0, arg1 mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InvalidateAuthorizeCodeSession", reflect.TypeOf((*MockCoreStorage)(nil).InvalidateAuthorizeCodeSession), arg0, arg1) } + +// InvalidateDeviceCodeSession mocks base method. +func (m *MockCoreStorage) 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 *MockCoreStorageMockRecorder) InvalidateDeviceCodeSession(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InvalidateDeviceCodeSession", reflect.TypeOf((*MockCoreStorage)(nil).InvalidateDeviceCodeSession), arg0, arg1) +} + +// InvalidateUserCodeSession mocks base method. +func (m *MockCoreStorage) 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 *MockCoreStorageMockRecorder) InvalidateUserCodeSession(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InvalidateUserCodeSession", reflect.TypeOf((*MockCoreStorage)(nil).InvalidateUserCodeSession), arg0, arg1) +} diff --git a/internal/oauth2_strategy.go b/internal/oauth2_strategy.go index 3a6bc859c..4624cbcab 100644 --- a/internal/oauth2_strategy.go +++ b/internal/oauth2_strategy.go @@ -110,18 +110,19 @@ func (mr *MockCoreStrategyMockRecorder) GenerateAuthorizeCode(arg0, arg1 interfa } // GenerateDeviceCode mocks base method. -func (m *MockCoreStrategy) GenerateDeviceCode() (string, error) { +func (m *MockCoreStrategy) GenerateDeviceCode(arg0 context.Context) (string, string, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GenerateDeviceCode") + ret := m.ctrl.Call(m, "GenerateDeviceCode", arg0) ret0, _ := ret[0].(string) - ret1, _ := ret[1].(error) - return ret0, ret1 + ret1, _ := ret[1].(string) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 } // GenerateDeviceCode indicates an expected call of GenerateDeviceCode. -func (mr *MockCoreStrategyMockRecorder) GenerateDeviceCode() *gomock.Call { +func (mr *MockCoreStrategyMockRecorder) GenerateDeviceCode(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateDeviceCode", reflect.TypeOf((*MockCoreStrategy)(nil).GenerateDeviceCode)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateDeviceCode", reflect.TypeOf((*MockCoreStrategy)(nil).GenerateDeviceCode), arg0) } // GenerateRefreshToken mocks base method. @@ -141,18 +142,19 @@ func (mr *MockCoreStrategyMockRecorder) GenerateRefreshToken(arg0, arg1 interfac } // GenerateUserCode mocks base method. -func (m *MockCoreStrategy) GenerateUserCode() (string, error) { +func (m *MockCoreStrategy) GenerateUserCode(arg0 context.Context) (string, string, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GenerateUserCode") + ret := m.ctrl.Call(m, "GenerateUserCode", arg0) ret0, _ := ret[0].(string) - ret1, _ := ret[1].(error) - return ret0, ret1 + ret1, _ := ret[1].(string) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 } // GenerateUserCode indicates an expected call of GenerateUserCode. -func (mr *MockCoreStrategyMockRecorder) GenerateUserCode() *gomock.Call { +func (mr *MockCoreStrategyMockRecorder) GenerateUserCode(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateUserCode", reflect.TypeOf((*MockCoreStrategy)(nil).GenerateUserCode)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateUserCode", reflect.TypeOf((*MockCoreStrategy)(nil).GenerateUserCode), arg0) } // RefreshTokenSignature mocks base method. diff --git a/storage/memory.go b/storage/memory.go index 7557984ab..5b6e5e1f8 100644 --- a/storage/memory.go +++ b/storage/memory.go @@ -555,7 +555,7 @@ func (s *MemoryStore) GetDeviceCodeSession(_ context.Context, signature string, return rel, nil } -func (s *MemoryStore) DeleteDeviceCodeSession(_ context.Context, code string) error { +func (s *MemoryStore) InvalidateDeviceCodeSession(_ context.Context, code string) error { s.deviceCodesMutex.Lock() defer s.deviceCodesMutex.Unlock() @@ -587,7 +587,7 @@ func (s *MemoryStore) GetUserCodeSession(_ context.Context, signature string, _ return rel, nil } -func (s *MemoryStore) DeleteUserCodeSession(_ context.Context, code string) error { +func (s *MemoryStore) InvalidateUserCodeSession(_ context.Context, code string) error { s.userCodesMutex.Lock() defer s.userCodesMutex.Unlock() From 60e109bc5844608a237e2b0b36446e1430c378b7 Mon Sep 17 00:00:00 2001 From: Romain Caire Date: Wed, 7 Sep 2022 15:26:24 +0200 Subject: [PATCH 04/49] Fix HMACSHA Test --- token/hmac/hmacsha_test.go | 32 +------------------------------- 1 file changed, 1 insertion(+), 31 deletions(-) diff --git a/token/hmac/hmacsha_test.go b/token/hmac/hmacsha_test.go index b236ee290..c5a3d0e5f 100644 --- a/token/hmac/hmacsha_test.go +++ b/token/hmac/hmacsha_test.go @@ -78,36 +78,6 @@ func TestGenerate(t *testing.T) { } } -func TestGenerateFromString(t *testing.T) { - cg := HMACStrategy{ - 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(c.text) - assert.Equal(t, c.hash, hash) - } -} - func TestValidateSignatureRejects(t *testing.T) { var err error cg := HMACStrategy{ @@ -207,7 +177,7 @@ func TestGenerateFromString(t *testing.T) { hash: "zHYDOZGjzhVjx5r8RlBhpnJemX5JxEEBUjVT01n3IFM=", }, } { - hash := cg.GenerateHMACForString(c.text, context.Background()) + hash := cg.GenerateHMACForString(context.Background(), c.text) assert.Equal(t, c.hash, hash) } } From 7b5a9baddf73f83bb61428b0d2532882fce77a34 Mon Sep 17 00:00:00 2001 From: Romain Caire Date: Wed, 7 Sep 2022 16:10:36 +0200 Subject: [PATCH 05/49] Fix fosite tests --- handler/oauth2/flow_device_code_token.go | 7 +------ handler/oauth2/flow_device_code_token_test.go | 8 ++++---- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/handler/oauth2/flow_device_code_token.go b/handler/oauth2/flow_device_code_token.go index 5f0ab80f5..b5ed2191c 100644 --- a/handler/oauth2/flow_device_code_token.go +++ b/handler/oauth2/flow_device_code_token.go @@ -76,15 +76,10 @@ func (d *AuthorizeDeviceGrantTypeHandler) PopulateTokenEndpointResponse(ctx cont } signature := d.DeviceCodeStrategy.DeviceCodeSignature(ctx, code) - if err := d.DeviceCodeStrategy.ValidateDeviceCode(ctx, requester, code); err != nil { - // This needs to happen after store retrieval for the session to be hydrated properly - return err - } - // Get the device code session ready for exchange to auth / refresh / oidc sessions session, err := d.CoreStorage.GetDeviceCodeSession(ctx, signature, requester.GetSession()) if err != nil { - return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) + return errorsx.WithStack(fosite.ErrInvalidRequest.WithWrap(err).WithDebug(err.Error())) } else if err := d.DeviceCodeStrategy.ValidateDeviceCode(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())) diff --git a/handler/oauth2/flow_device_code_token_test.go b/handler/oauth2/flow_device_code_token_test.go index cb23f6243..25c988429 100644 --- a/handler/oauth2/flow_device_code_token_test.go +++ b/handler/oauth2/flow_device_code_token_test.go @@ -284,7 +284,7 @@ func TestAuthorizeCode_PopulateDeviceTokenEndpointResponse(t *testing.T) { areq: &fosite.AccessRequest{ GrantTypes: fosite.Arguments{"urn:ietf:params:oauth:grant-type:device_code"}, Request: fosite.Request{ - Client: &fosite.DefaultClient{ID: "foo", GrantTypes: []string{""}}, + Client: &fosite.DefaultClient{ID: "foo", GrantTypes: []string{"urn:ietf:params:oauth:grant-type:device_code"}}, Session: &fosite.DefaultSession{}, RequestedAt: time.Now().UTC(), Form: url.Values{"device_code": {"ABC1234"}}, @@ -299,7 +299,7 @@ func TestAuthorizeCode_PopulateDeviceTokenEndpointResponse(t *testing.T) { 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"}}, + Client: &fosite.DefaultClient{ID: "foo", GrantTypes: []string{"refresh_token", "urn:ietf:params:oauth:grant-type:device_code"}}, Session: &fosite.DefaultSession{}, RequestedAt: time.Now().UTC(), GrantedScope: fosite.Arguments{"openid", "offline"}, @@ -317,11 +317,11 @@ func TestAuthorizeCode_PopulateDeviceTokenEndpointResponse(t *testing.T) { if c.createDeviceSession { c.areq.SetID("ID1") deviceSig := hmacshaStrategy.DeviceCodeSignature(context.TODO(), c.areq.Form.Get("device_code")) - store.CreateDeviceCodeSession(nil, deviceSig, c.areq) + store.CreateDeviceCodeSession(context.TODO(), deviceSig, c.areq) } resp := fosite.NewAccessResponse() - err := c.handler.PopulateTokenEndpointResponse(nil, c.areq, resp) + err := c.handler.PopulateTokenEndpointResponse(context.TODO(), c.areq, resp) if c.expectErr != nil { require.EqualError(t, err, c.expectErr.Error()) } else { From 0ff26994d12a30fd82b8b20f3e8743808201f813 Mon Sep 17 00:00:00 2001 From: Romain Caire Date: Fri, 9 Sep 2022 01:40:02 +0200 Subject: [PATCH 06/49] Fix styling --- compose/compose.go | 30 ++++++++++++++--------------- compose/compose_openid.go | 3 +-- device_authorize_request_handler.go | 2 +- device_authorize_writer.go | 2 +- internal/access_request.go | 1 + internal/access_response.go | 1 + internal/access_token_storage.go | 1 + internal/access_token_strategy.go | 1 + internal/authorize_code_storage.go | 1 + internal/authorize_code_strategy.go | 1 + internal/authorize_handler.go | 1 + internal/authorize_request.go | 1 + internal/client.go | 28 +++++++++++++++++++++++++++ internal/id_token_strategy.go | 1 + internal/introspector.go | 1 + internal/oauth2_client_storage.go | 1 + internal/oauth2_owner_storage.go | 1 + internal/oauth2_revoke_storage.go | 1 + internal/oauth2_storage.go | 1 + internal/oauth2_strategy.go | 1 + internal/openid_id_token_storage.go | 1 + internal/pkce_storage_strategy.go | 1 + internal/refresh_token_strategy.go | 1 + internal/request.go | 1 + internal/revoke_handler.go | 1 + internal/storage.go | 1 + internal/token_handler.go | 1 + 27 files changed, 68 insertions(+), 19 deletions(-) diff --git a/compose/compose.go b/compose/compose.go index 714b20752..7bf11f429 100644 --- a/compose/compose.go +++ b/compose/compose.go @@ -32,24 +32,24 @@ type Factory func(config fosite.Configurator, storage interface{}, strategy inte // Compose takes a config, a storage, a strategy and handlers to instantiate an OAuth2Provider: // -// import "github.com/ory/fosite/compose" +// import "github.com/ory/fosite/compose" // -// // var storage = new(MyFositeStorage) -// var config = Config { -// AccessTokenLifespan: time.Minute * 30, -// // check Config for further configuration options -// } +// // var storage = new(MyFositeStorage) +// var config = Config { +// AccessTokenLifespan: time.Minute * 30, +// // check Config for further configuration options +// } // -// var strategy = NewOAuth2HMACStrategy(config) +// var strategy = NewOAuth2HMACStrategy(config) // -// var oauth2Provider = Compose( -// config, -// storage, -// strategy, -// NewOAuth2AuthorizeExplicitHandler, -// OAuth2ClientCredentialsGrantFactory, -// // for a complete list refer to the docs of this package -// ) +// var oauth2Provider = Compose( +// config, +// storage, +// strategy, +// NewOAuth2AuthorizeExplicitHandler, +// OAuth2ClientCredentialsGrantFactory, +// // for a complete list refer to the docs of this package +// ) // // Compose makes use of interface{} types in order to be able to handle a all types of stores, strategies and handlers. func Compose(config *fosite.Config, storage interface{}, strategy interface{}, factories ...Factory) fosite.OAuth2Provider { diff --git a/compose/compose_openid.go b/compose/compose_openid.go index ca2bfc1bb..e1b4b7e13 100644 --- a/compose/compose_openid.go +++ b/compose/compose_openid.go @@ -62,8 +62,7 @@ func OpenIDConnectImplicitFactory(config fosite.Configurator, storage interface{ AuthorizeImplicitGrantTypeHandler: &oauth2.AuthorizeImplicitGrantTypeHandler{ AccessTokenStrategy: strategy.(oauth2.AccessTokenStrategy), AccessTokenStorage: storage.(oauth2.AccessTokenStorage), - - Config: config, + Config: config, }, Config: config, IDTokenHandleHelper: &openid.IDTokenHandleHelper{ diff --git a/device_authorize_request_handler.go b/device_authorize_request_handler.go index ceeaf8297..9a851d685 100644 --- a/device_authorize_request_handler.go +++ b/device_authorize_request_handler.go @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. * - * @author Luke Stoward + * @author Aeneas Rekkas * @copyright 2015-2021 Aeneas Rekkas * @license Apache-2.0 * diff --git a/device_authorize_writer.go b/device_authorize_writer.go index ce3082ac4..2152b5dfe 100644 --- a/device_authorize_writer.go +++ b/device_authorize_writer.go @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. * - * @author Luke Stoward + * @author Aeneas Rekkas * @copyright 2015-2021 Aeneas Rekkas * @license Apache-2.0 * diff --git a/internal/access_request.go b/internal/access_request.go index 149b920aa..7f408e080 100644 --- a/internal/access_request.go +++ b/internal/access_request.go @@ -10,6 +10,7 @@ import ( time "time" gomock "github.com/golang/mock/gomock" + fosite "github.com/ory/fosite" ) diff --git a/internal/access_response.go b/internal/access_response.go index cdd100769..9c383cb79 100644 --- a/internal/access_response.go +++ b/internal/access_response.go @@ -9,6 +9,7 @@ import ( time "time" gomock "github.com/golang/mock/gomock" + fosite "github.com/ory/fosite" ) diff --git a/internal/access_token_storage.go b/internal/access_token_storage.go index 7d4d80549..a30404098 100644 --- a/internal/access_token_storage.go +++ b/internal/access_token_storage.go @@ -9,6 +9,7 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" + fosite "github.com/ory/fosite" ) diff --git a/internal/access_token_strategy.go b/internal/access_token_strategy.go index 965404f64..a811e51cf 100644 --- a/internal/access_token_strategy.go +++ b/internal/access_token_strategy.go @@ -9,6 +9,7 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" + fosite "github.com/ory/fosite" ) diff --git a/internal/authorize_code_storage.go b/internal/authorize_code_storage.go index 75e62d4ac..ef3ab3b3c 100644 --- a/internal/authorize_code_storage.go +++ b/internal/authorize_code_storage.go @@ -9,6 +9,7 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" + fosite "github.com/ory/fosite" ) diff --git a/internal/authorize_code_strategy.go b/internal/authorize_code_strategy.go index 8baf3a2c4..cb826a23d 100644 --- a/internal/authorize_code_strategy.go +++ b/internal/authorize_code_strategy.go @@ -9,6 +9,7 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" + fosite "github.com/ory/fosite" ) diff --git a/internal/authorize_handler.go b/internal/authorize_handler.go index a660d7ebb..964fb7e4a 100644 --- a/internal/authorize_handler.go +++ b/internal/authorize_handler.go @@ -9,6 +9,7 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" + fosite "github.com/ory/fosite" ) diff --git a/internal/authorize_request.go b/internal/authorize_request.go index e3d2f959b..a643e04e8 100644 --- a/internal/authorize_request.go +++ b/internal/authorize_request.go @@ -10,6 +10,7 @@ import ( time "time" gomock "github.com/golang/mock/gomock" + fosite "github.com/ory/fosite" ) diff --git a/internal/client.go b/internal/client.go index b2299dc02..771660924 100644 --- a/internal/client.go +++ b/internal/client.go @@ -6,8 +6,10 @@ package internal import ( reflect "reflect" + time "time" gomock "github.com/golang/mock/gomock" + fosite "github.com/ory/fosite" ) @@ -132,6 +134,20 @@ func (mr *MockClientMockRecorder) GetScopes() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetScopes", reflect.TypeOf((*MockClient)(nil).GetScopes)) } +// GetTokenLifespan mocks base method. +func (m *MockClient) GetTokenLifespan(arg0 fosite.GrantType, arg1 fosite.TokenType, arg2 time.Duration) time.Duration { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetTokenLifespan", arg0, arg1, arg2) + ret0, _ := ret[0].(time.Duration) + return ret0 +} + +// GetTokenLifespan indicates an expected call of GetTokenLifespan. +func (mr *MockClientMockRecorder) GetTokenLifespan(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTokenLifespan", reflect.TypeOf((*MockClient)(nil).GetTokenLifespan), arg0, arg1, arg2) +} + // IsPublic mocks base method. func (m *MockClient) IsPublic() bool { m.ctrl.T.Helper() @@ -145,3 +161,15 @@ func (mr *MockClientMockRecorder) IsPublic() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsPublic", reflect.TypeOf((*MockClient)(nil).IsPublic)) } + +// SetTokenLifespans mocks base method. +func (m *MockClient) SetTokenLifespans(arg0 map[fosite.TokenType]time.Duration) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetTokenLifespans", arg0) +} + +// SetTokenLifespans indicates an expected call of SetTokenLifespans. +func (mr *MockClientMockRecorder) SetTokenLifespans(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetTokenLifespans", reflect.TypeOf((*MockClient)(nil).SetTokenLifespans), arg0) +} diff --git a/internal/id_token_strategy.go b/internal/id_token_strategy.go index 9ec9b8ce6..2e36d1488 100644 --- a/internal/id_token_strategy.go +++ b/internal/id_token_strategy.go @@ -10,6 +10,7 @@ import ( time "time" gomock "github.com/golang/mock/gomock" + fosite "github.com/ory/fosite" ) diff --git a/internal/introspector.go b/internal/introspector.go index 6744ec5f7..c47cf76e6 100644 --- a/internal/introspector.go +++ b/internal/introspector.go @@ -9,6 +9,7 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" + fosite "github.com/ory/fosite" ) diff --git a/internal/oauth2_client_storage.go b/internal/oauth2_client_storage.go index bb15e6c53..54770c89d 100644 --- a/internal/oauth2_client_storage.go +++ b/internal/oauth2_client_storage.go @@ -9,6 +9,7 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" + fosite "github.com/ory/fosite" ) diff --git a/internal/oauth2_owner_storage.go b/internal/oauth2_owner_storage.go index 37ace932f..ce53de4c8 100644 --- a/internal/oauth2_owner_storage.go +++ b/internal/oauth2_owner_storage.go @@ -9,6 +9,7 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" + fosite "github.com/ory/fosite" ) diff --git a/internal/oauth2_revoke_storage.go b/internal/oauth2_revoke_storage.go index c4b28282a..71f8e10a5 100644 --- a/internal/oauth2_revoke_storage.go +++ b/internal/oauth2_revoke_storage.go @@ -9,6 +9,7 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" + fosite "github.com/ory/fosite" ) diff --git a/internal/oauth2_storage.go b/internal/oauth2_storage.go index 0b63462df..97633402a 100644 --- a/internal/oauth2_storage.go +++ b/internal/oauth2_storage.go @@ -9,6 +9,7 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" + fosite "github.com/ory/fosite" ) diff --git a/internal/oauth2_strategy.go b/internal/oauth2_strategy.go index 4624cbcab..01a781934 100644 --- a/internal/oauth2_strategy.go +++ b/internal/oauth2_strategy.go @@ -9,6 +9,7 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" + fosite "github.com/ory/fosite" ) diff --git a/internal/openid_id_token_storage.go b/internal/openid_id_token_storage.go index 11f63c68d..9d33b855f 100644 --- a/internal/openid_id_token_storage.go +++ b/internal/openid_id_token_storage.go @@ -9,6 +9,7 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" + fosite "github.com/ory/fosite" ) diff --git a/internal/pkce_storage_strategy.go b/internal/pkce_storage_strategy.go index 0f25f14f4..1ab44f9ad 100644 --- a/internal/pkce_storage_strategy.go +++ b/internal/pkce_storage_strategy.go @@ -9,6 +9,7 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" + fosite "github.com/ory/fosite" ) diff --git a/internal/refresh_token_strategy.go b/internal/refresh_token_strategy.go index 99951c794..d0e0422e5 100644 --- a/internal/refresh_token_strategy.go +++ b/internal/refresh_token_strategy.go @@ -9,6 +9,7 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" + fosite "github.com/ory/fosite" ) diff --git a/internal/request.go b/internal/request.go index b833be8ba..ce642cb0d 100644 --- a/internal/request.go +++ b/internal/request.go @@ -10,6 +10,7 @@ import ( time "time" gomock "github.com/golang/mock/gomock" + fosite "github.com/ory/fosite" ) diff --git a/internal/revoke_handler.go b/internal/revoke_handler.go index ed542c112..75418fc70 100644 --- a/internal/revoke_handler.go +++ b/internal/revoke_handler.go @@ -9,6 +9,7 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" + fosite "github.com/ory/fosite" ) diff --git a/internal/storage.go b/internal/storage.go index e7b934cc4..a17da5f7e 100644 --- a/internal/storage.go +++ b/internal/storage.go @@ -10,6 +10,7 @@ import ( time "time" gomock "github.com/golang/mock/gomock" + fosite "github.com/ory/fosite" ) diff --git a/internal/token_handler.go b/internal/token_handler.go index 5ad46ac60..5e31065d3 100644 --- a/internal/token_handler.go +++ b/internal/token_handler.go @@ -9,6 +9,7 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" + fosite "github.com/ory/fosite" ) From 3665fc68e615bf63bd27e57f01b92539656fa1bd Mon Sep 17 00:00:00 2001 From: Romain Caire Date: Fri, 9 Sep 2022 01:49:16 +0200 Subject: [PATCH 07/49] Fix merge errors --- .../oauth2/flow_authorize_code_auth_test.go | 2 +- .../oauth2/flow_authorize_code_token_test.go | 2 +- handler/oauth2/strategy_hmacsha.go | 38 +++++++++---------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/handler/oauth2/flow_authorize_code_auth_test.go b/handler/oauth2/flow_authorize_code_auth_test.go index c95fd5321..be0f1cc8d 100644 --- a/handler/oauth2/flow_authorize_code_auth_test.go +++ b/handler/oauth2/flow_authorize_code_auth_test.go @@ -40,7 +40,7 @@ func parseUrl(uu string) *url.URL { } func TestAuthorizeCode_HandleAuthorizeEndpointRequest(t *testing.T) { - for k, strategy := range map[string]AuthorizeCodeStrategy{ + for k, strategy := range map[string]CoreStrategy{ "hmac": &hmacshaStrategy, } { t.Run("strategy="+k, func(t *testing.T) { diff --git a/handler/oauth2/flow_authorize_code_token_test.go b/handler/oauth2/flow_authorize_code_token_test.go index 3bc754669..326d617c9 100644 --- a/handler/oauth2/flow_authorize_code_token_test.go +++ b/handler/oauth2/flow_authorize_code_token_test.go @@ -258,7 +258,7 @@ func TestAuthorizeCode_PopulateTokenEndpointResponse(t *testing.T) { } func TestAuthorizeCode_HandleTokenEndpointRequest(t *testing.T) { - for k, strategy := range map[string]AuthorizeCodeStrategy{ + for k, strategy := range map[string]CoreStrategy{ "hmac": &hmacshaStrategy, } { t.Run("strategy="+k, func(t *testing.T) { diff --git a/handler/oauth2/strategy_hmacsha.go b/handler/oauth2/strategy_hmacsha.go index a9c66e211..f2a387ee2 100644 --- a/handler/oauth2/strategy_hmacsha.go +++ b/handler/oauth2/strategy_hmacsha.go @@ -46,17 +46,17 @@ type HMACSHAStrategy struct { prefix *string } -func (h HMACSHAStrategy) AccessTokenSignature(ctx context.Context, token string) string { +func (h *HMACSHAStrategy) AccessTokenSignature(ctx context.Context, token string) string { return h.Enigma.Signature(token) } -func (h HMACSHAStrategy) RefreshTokenSignature(ctx context.Context, token string) string { +func (h *HMACSHAStrategy) RefreshTokenSignature(ctx context.Context, token string) string { return h.Enigma.Signature(token) } -func (h HMACSHAStrategy) AuthorizeCodeSignature(ctx context.Context, token string) string { +func (h *HMACSHAStrategy) AuthorizeCodeSignature(ctx context.Context, token string) string { return h.Enigma.Signature(token) } -func (h HMACSHAStrategy) getPrefix(part string) string { +func (h *HMACSHAStrategy) getPrefix(part string) string { if h.prefix == nil { prefix := "ory_%s_" h.prefix = &prefix @@ -67,15 +67,15 @@ func (h HMACSHAStrategy) getPrefix(part string) string { return fmt.Sprintf(*h.prefix, part) } -func (h HMACSHAStrategy) trimPrefix(token, part string) string { +func (h *HMACSHAStrategy) trimPrefix(token, part string) string { return strings.TrimPrefix(token, h.getPrefix(part)) } -func (h HMACSHAStrategy) setPrefix(token, part string) string { +func (h *HMACSHAStrategy) setPrefix(token, part string) string { return h.getPrefix(part) + token } -func (h HMACSHAStrategy) GenerateAccessToken(ctx context.Context, _ fosite.Requester) (token string, signature string, err error) { +func (h *HMACSHAStrategy) GenerateAccessToken(ctx context.Context, _ fosite.Requester) (token string, signature string, err error) { token, sig, err := h.Enigma.Generate(ctx) if err != nil { return "", "", err @@ -84,7 +84,7 @@ func (h HMACSHAStrategy) GenerateAccessToken(ctx context.Context, _ fosite.Reque return h.setPrefix(token, "at"), sig, nil } -func (h HMACSHAStrategy) ValidateAccessToken(ctx context.Context, r fosite.Requester, token string) (err error) { +func (h *HMACSHAStrategy) ValidateAccessToken(ctx context.Context, r fosite.Requester, token string) (err error) { var exp = r.GetSession().GetExpiresAt(fosite.AccessToken) if exp.IsZero() && r.GetRequestedAt().Add(h.Config.GetAccessTokenLifespan(ctx)).Before(time.Now().UTC()) { return errorsx.WithStack(fosite.ErrTokenExpired.WithHintf("Access token expired at '%s'.", r.GetRequestedAt().Add(h.Config.GetAccessTokenLifespan(ctx)))) @@ -97,7 +97,7 @@ func (h HMACSHAStrategy) ValidateAccessToken(ctx context.Context, r fosite.Reque return h.Enigma.Validate(ctx, h.trimPrefix(token, "at")) } -func (h HMACSHAStrategy) GenerateRefreshToken(ctx context.Context, _ fosite.Requester) (token string, signature string, err error) { +func (h *HMACSHAStrategy) GenerateRefreshToken(ctx context.Context, _ fosite.Requester) (token string, signature string, err error) { token, sig, err := h.Enigma.Generate(ctx) if err != nil { return "", "", err @@ -106,7 +106,7 @@ func (h HMACSHAStrategy) GenerateRefreshToken(ctx context.Context, _ fosite.Requ return h.setPrefix(token, "rt"), sig, nil } -func (h HMACSHAStrategy) ValidateRefreshToken(ctx context.Context, r fosite.Requester, token string) (err error) { +func (h *HMACSHAStrategy) ValidateRefreshToken(ctx context.Context, r fosite.Requester, token string) (err error) { var exp = r.GetSession().GetExpiresAt(fosite.RefreshToken) if exp.IsZero() { // Unlimited lifetime @@ -120,7 +120,7 @@ func (h HMACSHAStrategy) ValidateRefreshToken(ctx context.Context, r fosite.Requ return h.Enigma.Validate(ctx, h.trimPrefix(token, "rt")) } -func (h HMACSHAStrategy) GenerateAuthorizeCode(ctx context.Context, _ fosite.Requester) (token string, signature string, err error) { +func (h *HMACSHAStrategy) GenerateAuthorizeCode(ctx context.Context, _ fosite.Requester) (token string, signature string, err error) { token, sig, err := h.Enigma.Generate(ctx) if err != nil { return "", "", err @@ -129,7 +129,7 @@ func (h HMACSHAStrategy) GenerateAuthorizeCode(ctx context.Context, _ fosite.Req return h.setPrefix(token, "ac"), sig, nil } -func (h HMACSHAStrategy) ValidateAuthorizeCode(ctx context.Context, r fosite.Requester, token string) (err error) { +func (h *HMACSHAStrategy) ValidateAuthorizeCode(ctx context.Context, r fosite.Requester, token string) (err error) { var exp = r.GetSession().GetExpiresAt(fosite.AuthorizeCode) if exp.IsZero() && r.GetRequestedAt().Add(h.Config.GetAuthorizeCodeLifespan(ctx)).Before(time.Now().UTC()) { return errorsx.WithStack(fosite.ErrTokenExpired.WithHintf("Authorize code expired at '%s'.", r.GetRequestedAt().Add(h.Config.GetAuthorizeCodeLifespan(ctx)))) @@ -142,7 +142,7 @@ func (h HMACSHAStrategy) ValidateAuthorizeCode(ctx context.Context, r fosite.Req return h.Enigma.Validate(ctx, h.trimPrefix(token, "ac")) } -func (h HMACSHAStrategy) generateRandomString(length int) (token string, err error) { +func (h *HMACSHAStrategy) generateRandomString(length int) (token string, err error) { chars := [20]byte{'B', 'C', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'X', 'Z'} chars_length := int64(len(chars)) @@ -157,16 +157,16 @@ func (h HMACSHAStrategy) generateRandomString(length int) (token string, err err return string(code), nil } -func (h HMACSHAStrategy) GenerateUserCode(ctx context.Context) (token string, signature string, err error) { +func (h *HMACSHAStrategy) GenerateUserCode(ctx context.Context) (token string, signature string, err error) { userCode, err := h.generateRandomString(8) return userCode, h.UserCodeSignature(ctx, userCode), err } -func (h HMACSHAStrategy) UserCodeSignature(ctx context.Context, token string) string { +func (h *HMACSHAStrategy) UserCodeSignature(ctx context.Context, token string) string { return h.Enigma.GenerateHMACForString(ctx, token) } -func (h HMACSHAStrategy) ValidateUserCode(ctx context.Context, r fosite.Requester, code string) (err error) { +func (h *HMACSHAStrategy) 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.ErrTokenExpired.WithHintf("Access token expired at '%s'.", r.GetRequestedAt().Add(h.Config.GetDeviceAndUserCodeLifespan(ctx)))) @@ -177,16 +177,16 @@ func (h HMACSHAStrategy) ValidateUserCode(ctx context.Context, r fosite.Requeste return nil } -func (h HMACSHAStrategy) GenerateDeviceCode(ctx context.Context) (token string, signature string, err error) { +func (h *HMACSHAStrategy) GenerateDeviceCode(ctx context.Context) (token string, signature string, err error) { deviceCode, err := h.generateRandomString(100) return deviceCode, h.DeviceCodeSignature(ctx, deviceCode), err } -func (h HMACSHAStrategy) DeviceCodeSignature(ctx context.Context, token string) string { +func (h *HMACSHAStrategy) DeviceCodeSignature(ctx context.Context, token string) string { return h.Enigma.GenerateHMACForString(ctx, token) } -func (h HMACSHAStrategy) ValidateDeviceCode(ctx context.Context, r fosite.Requester, code string) (err error) { +func (h *HMACSHAStrategy) ValidateDeviceCode(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.ErrTokenExpired.WithHintf("1 Device code expired at '%s'.", r.GetRequestedAt().Add(h.Config.GetDeviceAndUserCodeLifespan(ctx)))) From d710a40f0ec02e8bfc35257a9bfad30a1562e0a5 Mon Sep 17 00:00:00 2001 From: Romain Caire Date: Fri, 9 Sep 2022 02:32:50 +0200 Subject: [PATCH 08/49] Remove debug logs --- device_authorize_response_writer.go | 2 -- handler/oauth2/device_authorization.go | 8 -------- handler/oauth2/flow_device_code_auth.go | 3 --- handler/oauth2/flow_device_code_token.go | 2 -- handler/openid/flow_device_token.go | 2 -- handler/openid/flow_explicit_token.go | 2 -- handler/pkce/handler_device.go | 9 +-------- 7 files changed, 1 insertion(+), 27 deletions(-) diff --git a/device_authorize_response_writer.go b/device_authorize_response_writer.go index 1b4deb97c..140989a57 100644 --- a/device_authorize_response_writer.go +++ b/device_authorize_response_writer.go @@ -23,14 +23,12 @@ package fosite import ( "context" - "fmt" ) func (f *Fosite) NewDeviceAuthorizeResponse(ctx context.Context, r Requester) (DeviceAuthorizeResponder, error) { var resp = NewDeviceAuthorizeResponse() for _, h := range f.Config.GetDeviceAuthorizeEndpointHandlers(ctx) { - fmt.Println("NewDeviceAuthorizeResponse +++") if err := h.HandleDeviceAuthorizeEndpointRequest(ctx, r, resp); err != nil { return nil, err } diff --git a/handler/oauth2/device_authorization.go b/handler/oauth2/device_authorization.go index 8c367a822..06c1b92a8 100644 --- a/handler/oauth2/device_authorization.go +++ b/handler/oauth2/device_authorization.go @@ -2,7 +2,6 @@ package oauth2 import ( "context" - "fmt" "time" "github.com/ory/fosite" @@ -20,7 +19,6 @@ type DeviceAuthorizationHandler struct { } func (d *DeviceAuthorizationHandler) HandleDeviceAuthorizeEndpointRequest(ctx context.Context, dar fosite.Requester, resp fosite.DeviceAuthorizeResponder) error { - fmt.Println("DeviceAuthorizationHandler :: HandleDeviceAuthorizeEndpointRequest ++") deviceCode, deviceCodeSignature, err := d.DeviceCodeStrategy.GenerateDeviceCode(ctx) if err != nil { return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) @@ -31,21 +29,15 @@ func (d *DeviceAuthorizationHandler) HandleDeviceAuthorizeEndpointRequest(ctx co return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) } - fmt.Println("DeviceAuthorizationHandler :: HandleDeviceAuthorizeEndpointRequest +++") - // Set User Code expiry time dar.GetSession().SetExpiresAt(fosite.UserCode, time.Now().UTC().Add(d.Config.GetDeviceAndUserCodeLifespan(ctx)).Round(time.Second)) dar.SetID(deviceCodeSignature) - fmt.Println("DeviceAuthorizationHandler :: HandleDeviceAuthorizeEndpointRequest ++++") - // 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 if err := d.UserCodeStorage.CreateUserCodeSession(ctx, userCodeSignature, dar); err != nil { return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) } - fmt.Println("DeviceAuthorizationHandler :: HandleDeviceAuthorizeEndpointRequest +++++") - // Populate the response fields resp.SetDeviceCode(deviceCode) resp.SetUserCode(userCode) diff --git a/handler/oauth2/flow_device_code_auth.go b/handler/oauth2/flow_device_code_auth.go index 6ebac1b19..b7cf28152 100644 --- a/handler/oauth2/flow_device_code_auth.go +++ b/handler/oauth2/flow_device_code_auth.go @@ -2,7 +2,6 @@ package oauth2 import ( "context" - "fmt" "github.com/ory/fosite" "github.com/ory/x/errorsx" @@ -38,8 +37,6 @@ func (c *AuthorizeDeviceGrantTypeHandler) HandleAuthorizeEndpointRequest(ctx con return err } - fmt.Println("SUBJECT : " + ar.GetSession().GetSubject()) - if session.GetClient().GetID() != ar.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.")) } diff --git a/handler/oauth2/flow_device_code_token.go b/handler/oauth2/flow_device_code_token.go index b5ed2191c..c5bd0fa08 100644 --- a/handler/oauth2/flow_device_code_token.go +++ b/handler/oauth2/flow_device_code_token.go @@ -2,7 +2,6 @@ package oauth2 import ( "context" - "fmt" "time" "github.com/ory/fosite" @@ -60,7 +59,6 @@ func (d *AuthorizeDeviceGrantTypeHandler) CanSkipClientAuth(ctx context.Context, func (d *AuthorizeDeviceGrantTypeHandler) CanHandleTokenEndpointRequest(ctx context.Context, requester fosite.AccessRequester) bool { // grant_type REQUIRED. // Value MUST be set to "urn:ietf:params:oauth:grant-type:device_code" - fmt.Println("CanHandleTokenEndpointRequest OAUTH") return requester.GetGrantTypes().ExactOne(deviceCodeGrantType) } diff --git a/handler/openid/flow_device_token.go b/handler/openid/flow_device_token.go index 6b5d4621a..a33bd1673 100644 --- a/handler/openid/flow_device_token.go +++ b/handler/openid/flow_device_token.go @@ -23,7 +23,6 @@ package openid import ( "context" - "fmt" "github.com/ory/x/errorsx" @@ -91,6 +90,5 @@ func (c *OpenIDConnectDeviceHandler) CanSkipClientAuth(ctx context.Context, requ } func (c *OpenIDConnectDeviceHandler) CanHandleTokenEndpointRequest(ctx context.Context, requester fosite.AccessRequester) bool { - fmt.Println("CanHandleTokenEndpointRequest OIDC") return requester.GetGrantTypes().ExactOne("urn:ietf:params:oauth:grant-type:device_code") } diff --git a/handler/openid/flow_explicit_token.go b/handler/openid/flow_explicit_token.go index 2e19e1f54..c7a16dcc5 100644 --- a/handler/openid/flow_explicit_token.go +++ b/handler/openid/flow_explicit_token.go @@ -23,7 +23,6 @@ package openid import ( "context" - "fmt" "github.com/ory/x/errorsx" @@ -84,6 +83,5 @@ func (c *OpenIDConnectExplicitHandler) CanSkipClientAuth(ctx context.Context, re } func (c *OpenIDConnectExplicitHandler) CanHandleTokenEndpointRequest(ctx context.Context, requester fosite.AccessRequester) bool { - fmt.Println("CanHandleTokenEndpointRequest EXPL TOKEN") return requester.GetGrantTypes().ExactOne("authorization_code") } diff --git a/handler/pkce/handler_device.go b/handler/pkce/handler_device.go index 25cddfcb1..8397fd639 100644 --- a/handler/pkce/handler_device.go +++ b/handler/pkce/handler_device.go @@ -45,8 +45,6 @@ type HandlerDevice struct { } func (c *HandlerDevice) HandleDeviceAuthorizeEndpointRequest(ctx context.Context, ar fosite.Requester, resp fosite.DeviceAuthorizeResponder) error { - fmt.Println("HandlerDevice :: HandleDeviceAuthorizeEndpointRequest ++") - if !ar.GetClient().GetGrantTypes().Has("urn:ietf:params:oauth:grant-type:device_code") { return nil } @@ -67,8 +65,6 @@ func (c *HandlerDevice) HandleDeviceAuthorizeEndpointRequest(ctx context.Context return err } - fmt.Println("PKCE ID : " + session.GetID()) - if err := c.Storage.CreatePKCERequestSession(ctx, session.GetID(), ar.Sanitize([]string{ "code_challenge", "code_challenge_method", @@ -146,8 +142,6 @@ func (c *HandlerDevice) HandleTokenEndpointRequest(ctx context.Context, request } codeSignature := c.DeviceCodeStrategy.DeviceCodeSignature(ctx, code) - fmt.Println("PKCE ID : " + codeSignature) - authorizeRequest, err := c.Storage.GetPKCERequestSession(ctx, codeSignature, request.GetSession()) if errors.Is(err, fosite.ErrNotFound) { return errorsx.WithStack(fosite.ErrInvalidGrant.WithHint("Unable to find initial PKCE data tied to this request").WithWrap(err).WithDebug(err.Error())) @@ -242,8 +236,7 @@ func (c *HandlerDevice) CanSkipClientAuth(ctx context.Context, requester fosite. } func (c *HandlerDevice) CanHandleTokenEndpointRequest(ctx context.Context, requester fosite.AccessRequester) bool { - fmt.Println("CanHandleTokenEndpointRequest PKCE") // grant_type REQUIRED. - // Value MUST be set to "authorization_code" + // Value MUST be set to "urn:ietf:params:oauth:grant-type:device_code" return requester.GetGrantTypes().ExactOne("urn:ietf:params:oauth:grant-type:device_code") } From eefdf61f9ea7351939bafb78bfb38d3d122665c6 Mon Sep 17 00:00:00 2001 From: Romain Caire Date: Fri, 9 Sep 2022 11:30:21 +0200 Subject: [PATCH 09/49] Fix test following fix merge error --- handler/oauth2/device_authorization_test.go | 4 ++-- handler/oauth2/flow_device_code_auth_test.go | 4 ++-- handler/oauth2/flow_device_code_token_test.go | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/handler/oauth2/device_authorization_test.go b/handler/oauth2/device_authorization_test.go index fe6395ee1..8e9f8bef5 100644 --- a/handler/oauth2/device_authorization_test.go +++ b/handler/oauth2/device_authorization_test.go @@ -19,8 +19,8 @@ func Test_HandleDeviceAuthorizeEndpointRequest(t *testing.T) { handler := DeviceAuthorizationHandler{ DeviceCodeStorage: deviceStore, UserCodeStorage: userStore, - DeviceCodeStrategy: hmacshaStrategy, - UserCodeStrategy: hmacshaStrategy, + DeviceCodeStrategy: &hmacshaStrategy, + UserCodeStrategy: &hmacshaStrategy, Config: &fosite.Config{ DeviceAndUserCodeLifespan: time.Minute * 10, DeviceAuthTokenPollingInterval: time.Second * 10, diff --git a/handler/oauth2/flow_device_code_auth_test.go b/handler/oauth2/flow_device_code_auth_test.go index 842b4c035..e74539662 100644 --- a/handler/oauth2/flow_device_code_auth_test.go +++ b/handler/oauth2/flow_device_code_auth_test.go @@ -20,8 +20,8 @@ func TestAuthorizeCode_HandleDeviceAuthorizeEndpointRequest(t *testing.T) { store := storage.NewMemoryStore() handler := AuthorizeDeviceGrantTypeHandler{ CoreStorage: store, - DeviceCodeStrategy: hmacshaStrategy, - UserCodeStrategy: hmacshaStrategy, + DeviceCodeStrategy: &hmacshaStrategy, + UserCodeStrategy: &hmacshaStrategy, AccessTokenStrategy: strategy, RefreshTokenStrategy: strategy, AuthorizeCodeStrategy: strategy, diff --git a/handler/oauth2/flow_device_code_token_test.go b/handler/oauth2/flow_device_code_token_test.go index 25c988429..889a7ff81 100644 --- a/handler/oauth2/flow_device_code_token_test.go +++ b/handler/oauth2/flow_device_code_token_test.go @@ -20,8 +20,8 @@ func TestAuthorizeCode_HandleDeviceTokenEndpointRequest(t *testing.T) { store := storage.NewMemoryStore() handler := AuthorizeDeviceGrantTypeHandler{ CoreStorage: store, - DeviceCodeStrategy: hmacshaStrategy, - UserCodeStrategy: hmacshaStrategy, + DeviceCodeStrategy: &hmacshaStrategy, + UserCodeStrategy: &hmacshaStrategy, AccessTokenStrategy: strategy, RefreshTokenStrategy: strategy, AuthorizeCodeStrategy: strategy, @@ -228,8 +228,8 @@ func TestAuthorizeCode_PopulateDeviceTokenEndpointResponse(t *testing.T) { store := storage.NewMemoryStore() handler := AuthorizeDeviceGrantTypeHandler{ CoreStorage: store, - DeviceCodeStrategy: hmacshaStrategy, - UserCodeStrategy: hmacshaStrategy, + DeviceCodeStrategy: &hmacshaStrategy, + UserCodeStrategy: &hmacshaStrategy, AccessTokenStrategy: strategy, RefreshTokenStrategy: strategy, AuthorizeCodeStrategy: strategy, From 57fb5ba983c8cd07a148ff89047694ed5ed8826c Mon Sep 17 00:00:00 2001 From: Romain Caire Date: Fri, 9 Sep 2022 18:57:08 +0200 Subject: [PATCH 10/49] Remove "device_code" auth flow --- compose/compose.go | 31 ++- compose/compose_oauth2.go | 23 +-- handler/oauth2/device_authorization.go | 13 +- handler/oauth2/flow_device_code_auth.go | 58 ------ handler/oauth2/flow_device_code_auth_test.go | 200 ------------------- handler/oauth2/flow_device_code_token.go | 12 +- 6 files changed, 33 insertions(+), 304 deletions(-) delete mode 100644 handler/oauth2/flow_device_code_auth.go delete mode 100644 handler/oauth2/flow_device_code_auth_test.go diff --git a/compose/compose.go b/compose/compose.go index 7bf11f429..f506f388f 100644 --- a/compose/compose.go +++ b/compose/compose.go @@ -32,24 +32,24 @@ type Factory func(config fosite.Configurator, storage interface{}, strategy inte // Compose takes a config, a storage, a strategy and handlers to instantiate an OAuth2Provider: // -// import "github.com/ory/fosite/compose" +// import "github.com/ory/fosite/compose" // -// // var storage = new(MyFositeStorage) -// var config = Config { -// AccessTokenLifespan: time.Minute * 30, -// // check Config for further configuration options -// } +// // var storage = new(MyFositeStorage) +// var config = Config { +// AccessTokenLifespan: time.Minute * 30, +// // check Config for further configuration options +// } // -// var strategy = NewOAuth2HMACStrategy(config) +// var strategy = NewOAuth2HMACStrategy(config) // -// var oauth2Provider = Compose( -// config, -// storage, -// strategy, -// NewOAuth2AuthorizeExplicitHandler, -// OAuth2ClientCredentialsGrantFactory, -// // for a complete list refer to the docs of this package -// ) +// var oauth2Provider = Compose( +// config, +// storage, +// strategy, +// NewOAuth2AuthorizeExplicitHandler, +// OAuth2ClientCredentialsGrantFactory, +// // for a complete list refer to the docs of this package +// ) // // Compose makes use of interface{} types in order to be able to handle a all types of stores, strategies and handlers. func Compose(config *fosite.Config, storage interface{}, strategy interface{}, factories ...Factory) fosite.OAuth2Provider { @@ -94,7 +94,6 @@ func ComposeAllEnabled(config *fosite.Config, storage interface{}, key interface }, OAuth2AuthorizeExplicitFactory, OAuth2AuthorizeImplicitFactory, - OAuth2AuthorizeDeviceFactory, OAuth2ClientCredentialsGrantFactory, OAuth2RefreshTokenGrantFactory, OAuth2DeviceAuthorizeFactory, diff --git a/compose/compose_oauth2.go b/compose/compose_oauth2.go index 174377c70..25442b074 100644 --- a/compose/compose_oauth2.go +++ b/compose/compose_oauth2.go @@ -126,24 +126,13 @@ func OAuth2StatelessJWTIntrospectionFactory(config fosite.Configurator, storage } } -func OAuth2AuthorizeDeviceFactory(config fosite.Configurator, storage interface{}, strategy interface{}) interface{} { - return &oauth2.AuthorizeDeviceGrantTypeHandler{ - DeviceCodeStrategy: strategy.(oauth2.DeviceCodeStrategy), - UserCodeStrategy: strategy.(oauth2.UserCodeStrategy), - AccessTokenStrategy: strategy.(oauth2.AccessTokenStrategy), - RefreshTokenStrategy: strategy.(oauth2.RefreshTokenStrategy), - AuthorizeCodeStrategy: strategy.(oauth2.AuthorizeCodeStrategy), - CoreStorage: storage.(oauth2.CoreStorage), - Config: config, - } -} - func OAuth2DeviceAuthorizeFactory(config fosite.Configurator, storage interface{}, strategy interface{}) interface{} { return &oauth2.DeviceAuthorizationHandler{ - DeviceCodeStorage: storage.(oauth2.DeviceCodeStorage), - UserCodeStorage: storage.(oauth2.UserCodeStorage), - DeviceCodeStrategy: strategy.(oauth2.DeviceCodeStrategy), - UserCodeStrategy: strategy.(oauth2.UserCodeStrategy), - Config: config, + DeviceCodeStrategy: strategy.(oauth2.DeviceCodeStrategy), + UserCodeStrategy: strategy.(oauth2.UserCodeStrategy), + AccessTokenStrategy: strategy.(oauth2.AccessTokenStrategy), + RefreshTokenStrategy: strategy.(oauth2.RefreshTokenStrategy), + CoreStorage: storage.(oauth2.CoreStorage), + Config: config, } } diff --git a/handler/oauth2/device_authorization.go b/handler/oauth2/device_authorization.go index 06c1b92a8..79ee84425 100644 --- a/handler/oauth2/device_authorization.go +++ b/handler/oauth2/device_authorization.go @@ -11,11 +11,12 @@ import ( // DeviceAuthorizationHandler is a response handler for the Device Authorisation Grant as // defined in https://tools.ietf.org/html/rfc8628#section-3.1 type DeviceAuthorizationHandler struct { - DeviceCodeStorage DeviceCodeStorage - UserCodeStorage UserCodeStorage - DeviceCodeStrategy DeviceCodeStrategy - UserCodeStrategy UserCodeStrategy - Config fosite.Configurator + CoreStorage CoreStorage + DeviceCodeStrategy DeviceCodeStrategy + UserCodeStrategy UserCodeStrategy + AccessTokenStrategy AccessTokenStrategy + RefreshTokenStrategy RefreshTokenStrategy + Config fosite.Configurator } func (d *DeviceAuthorizationHandler) HandleDeviceAuthorizeEndpointRequest(ctx context.Context, dar fosite.Requester, resp fosite.DeviceAuthorizeResponder) error { @@ -34,7 +35,7 @@ func (d *DeviceAuthorizationHandler) HandleDeviceAuthorizeEndpointRequest(ctx co dar.SetID(deviceCodeSignature) // 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 - if err := d.UserCodeStorage.CreateUserCodeSession(ctx, userCodeSignature, dar); err != nil { + if err := d.CoreStorage.CreateUserCodeSession(ctx, userCodeSignature, dar); err != nil { return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) } diff --git a/handler/oauth2/flow_device_code_auth.go b/handler/oauth2/flow_device_code_auth.go deleted file mode 100644 index b7cf28152..000000000 --- a/handler/oauth2/flow_device_code_auth.go +++ /dev/null @@ -1,58 +0,0 @@ -package oauth2 - -import ( - "context" - - "github.com/ory/fosite" - "github.com/ory/x/errorsx" -) - -type AuthorizeDeviceGrantTypeHandler struct { - CoreStorage CoreStorage - DeviceCodeStrategy DeviceCodeStrategy - UserCodeStrategy UserCodeStrategy - AccessTokenStrategy AccessTokenStrategy - RefreshTokenStrategy RefreshTokenStrategy - AuthorizeCodeStrategy AuthorizeCodeStrategy - Config fosite.Configurator -} - -func (c *AuthorizeDeviceGrantTypeHandler) HandleAuthorizeEndpointRequest(ctx context.Context, ar fosite.AuthorizeRequester, resp fosite.AuthorizeResponder) error { - - if !ar.GetResponseTypes().ExactOne("device_code") { - return nil - } - - if !ar.GetClient().GetGrantTypes().Has("urn:ietf:params:oauth:grant-type:device_code") { - return nil - } - - resp.AddParameter("state", ar.GetState()) - - userCode := ar.GetRequestForm().Get("user_code") - userCodeSignature := c.UserCodeStrategy.UserCodeSignature(ctx, userCode) - - session, err := c.CoreStorage.GetUserCodeSession(ctx, userCodeSignature, fosite.NewRequest().Session) - if err != nil { - return err - } - - if session.GetClient().GetID() != ar.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.")) - } - - /* - expires := session.GetSession().GetExpiresAt(fosite.UserCode) - if time.Now().UTC().After(expires) { - return errorsx.WithStack(fosite.ErrTokenExpired) - }*/ - - // session.GetID() is the HMAC signature of the device code generated in the inital request - err = c.CoreStorage.CreateDeviceCodeSession(ctx, session.GetID(), ar) - if err != nil { - return errorsx.WithStack(err) - } - - ar.SetResponseTypeHandled("device_code") - return nil -} diff --git a/handler/oauth2/flow_device_code_auth_test.go b/handler/oauth2/flow_device_code_auth_test.go deleted file mode 100644 index e74539662..000000000 --- a/handler/oauth2/flow_device_code_auth_test.go +++ /dev/null @@ -1,200 +0,0 @@ -package oauth2 - -import ( - "context" - "net/url" - "testing" - "time" - - "github.com/ory/fosite" - "github.com/ory/fosite/storage" - "github.com/stretchr/testify/require" -) - -func TestAuthorizeCode_HandleDeviceAuthorizeEndpointRequest(t *testing.T) { - - for k, strategy := range map[string]CoreStrategy{ - "hmac": &hmacshaStrategy, - } { - t.Run("strategy="+k, func(t *testing.T) { - store := storage.NewMemoryStore() - handler := AuthorizeDeviceGrantTypeHandler{ - CoreStorage: store, - DeviceCodeStrategy: &hmacshaStrategy, - UserCodeStrategy: &hmacshaStrategy, - AccessTokenStrategy: strategy, - RefreshTokenStrategy: strategy, - AuthorizeCodeStrategy: strategy, - Config: &fosite.Config{ - DeviceAndUserCodeLifespan: time.Minute * 10, - DeviceAuthTokenPollingInterval: time.Second * 10, - DeviceVerificationURL: "localhost", - AccessTokenLifespan: time.Hour, - RefreshTokenLifespan: time.Hour, - ScopeStrategy: fosite.HierarchicScopeStrategy, - AudienceMatchingStrategy: fosite.DefaultAudienceMatchingStrategy, - RefreshTokenScopes: []string{"offline"}, - }, - } - for _, c := range []struct { - handler AuthorizeDeviceGrantTypeHandler - areq *fosite.AuthorizeRequest - breq *fosite.AuthorizeRequest - expire time.Duration - description string - expectErr error - expect func(t *testing.T, areq *fosite.AuthorizeRequest, aresp *fosite.AuthorizeResponse) - }{ - { - handler: handler, - areq: &fosite.AuthorizeRequest{ - ResponseTypes: fosite.Arguments{""}, - Request: *fosite.NewRequest(), - }, - breq: &fosite.AuthorizeRequest{ - ResponseTypes: fosite.Arguments{""}, - Request: *fosite.NewRequest(), - }, - description: "should pass because not responsible for handling an empty response type", - expire: time.Minute * 10, - }, - { - handler: handler, - areq: &fosite.AuthorizeRequest{ - ResponseTypes: fosite.Arguments{"foo"}, - Request: *fosite.NewRequest(), - }, - breq: &fosite.AuthorizeRequest{ - ResponseTypes: fosite.Arguments{""}, - Request: *fosite.NewRequest(), - }, - description: "should pass because not responsible for handling an invalid response type", - expire: time.Minute * 10, - }, - { - handler: handler, - areq: &fosite.AuthorizeRequest{ - ResponseTypes: fosite.Arguments{"device_code"}, - Request: fosite.Request{ - Client: &fosite.DefaultClient{ - ID: "Default", - GrantTypes: fosite.Arguments{"code"}, - }, - }, - }, - breq: &fosite.AuthorizeRequest{ - ResponseTypes: fosite.Arguments{"device_code"}, - Request: fosite.Request{ - Client: &fosite.DefaultClient{ - ID: "Default", - GrantTypes: fosite.Arguments{"code"}, - }, - }, - }, - description: "should pass because not responsible for handling an invalid grant type", - expire: time.Minute * 10, - }, - { - handler: handler, - areq: &fosite.AuthorizeRequest{ - ResponseTypes: fosite.Arguments{"device_code"}, - Request: fosite.Request{ - Client: &fosite.DefaultClient{ - ID: "Default", - GrantTypes: fosite.Arguments{"urn:ietf:params:oauth:grant-type:device_code"}, - }, - Form: url.Values{"user_code": {"ABC123"}}, - }, - }, - breq: &fosite.AuthorizeRequest{ - ResponseTypes: fosite.Arguments{"device_code"}, - Request: fosite.Request{ - Client: &fosite.DefaultClient{ - ID: "Default", - GrantTypes: fosite.Arguments{"urn:ietf:params:oauth:grant-type:device_code"}, - }, - Form: url.Values{"user_code": {"ABC123"}}, - }, - }, - description: "should pass as session and request have matching client id", - expire: time.Minute * 10, - }, - { - handler: handler, - areq: &fosite.AuthorizeRequest{ - ResponseTypes: fosite.Arguments{"device_code"}, - Request: fosite.Request{ - Client: &fosite.DefaultClient{ - ID: "Default", - GrantTypes: fosite.Arguments{"urn:ietf:params:oauth:grant-type:device_code"}, - }, - Form: url.Values{"user_code": {"ABC123"}}, - }, - }, - breq: &fosite.AuthorizeRequest{ - ResponseTypes: fosite.Arguments{"device_code"}, - Request: fosite.Request{ - Client: &fosite.DefaultClient{ - ID: "Broken", - GrantTypes: fosite.Arguments{"urn:ietf:params:oauth:grant-type:device_code"}, - }, - Form: url.Values{"user_code": {"ABC123"}}, - }, - }, - description: "should fail due to a missmatch in session and request ClientID", - expire: time.Minute * 10, - expectErr: fosite.ErrInvalidGrant, - }, - { - handler: handler, - areq: &fosite.AuthorizeRequest{ - ResponseTypes: fosite.Arguments{"device_code"}, - Request: fosite.Request{ - Client: &fosite.DefaultClient{ - ID: "Default", - GrantTypes: fosite.Arguments{"urn:ietf:params:oauth:grant-type:device_code"}, - }, - Form: url.Values{"user_code": {"ABC123"}}, - }, - }, - breq: &fosite.AuthorizeRequest{ - ResponseTypes: fosite.Arguments{"device_code"}, - Request: fosite.Request{ - Client: &fosite.DefaultClient{ - ID: "Default", - GrantTypes: fosite.Arguments{"urn:ietf:params:oauth:grant-type:device_code"}, - }, - Form: url.Values{"user_code": {"ABC123"}}, - }, - }, - description: "should fail due to expired user session", - expire: -(time.Minute * 10), - //expectErr: fosite.ErrTokenExpired, - }, - } { - t.Run("case="+c.description, func(t *testing.T) { - - c.areq.SetID("ID1") - c.areq.Session = &fosite.DefaultSession{Subject: "A"} - c.breq.Session = &fosite.DefaultSession{Subject: "A"} - expireAt := time.Now().UTC().Add(c.expire) - c.areq.Session.SetExpiresAt(fosite.UserCode, expireAt) - userCodeSig := hmacshaStrategy.UserCodeSignature(context.Background(), c.areq.Form.Get("user_code")) - store.CreateUserCodeSession(nil, userCodeSig, c.areq) - - aresp := fosite.NewAuthorizeResponse() - err := c.handler.HandleAuthorizeEndpointRequest(nil, c.breq, aresp) - if c.expectErr != nil { - require.EqualError(t, err, c.expectErr.Error()) - } else { - require.NoError(t, err) - } - - if c.expect != nil { - c.expect(t, c.areq, aresp) - } - }) - } - }) - } -} diff --git a/handler/oauth2/flow_device_code_token.go b/handler/oauth2/flow_device_code_token.go index c5bd0fa08..53f8c70fb 100644 --- a/handler/oauth2/flow_device_code_token.go +++ b/handler/oauth2/flow_device_code_token.go @@ -11,8 +11,7 @@ import ( const deviceCodeGrantType = "urn:ietf:params:oauth:grant-type:device_code" -func (d *AuthorizeDeviceGrantTypeHandler) HandleTokenEndpointRequest(ctx context.Context, requester fosite.AccessRequester) error { - +func (d *DeviceAuthorizationHandler) HandleTokenEndpointRequest(ctx context.Context, requester fosite.AccessRequester) error { if !d.CanHandleTokenEndpointRequest(ctx, requester) { return errorsx.WithStack(errorsx.WithStack(fosite.ErrUnknownRequest)) } @@ -52,18 +51,17 @@ func (d *AuthorizeDeviceGrantTypeHandler) HandleTokenEndpointRequest(ctx context return nil } -func (d *AuthorizeDeviceGrantTypeHandler) CanSkipClientAuth(ctx context.Context, requester fosite.AccessRequester) bool { +func (d *DeviceAuthorizationHandler) CanSkipClientAuth(ctx context.Context, requester fosite.AccessRequester) bool { return true } -func (d *AuthorizeDeviceGrantTypeHandler) CanHandleTokenEndpointRequest(ctx context.Context, requester fosite.AccessRequester) bool { +func (d *DeviceAuthorizationHandler) CanHandleTokenEndpointRequest(ctx context.Context, requester fosite.AccessRequester) bool { // grant_type REQUIRED. // Value MUST be set to "urn:ietf:params:oauth:grant-type:device_code" return requester.GetGrantTypes().ExactOne(deviceCodeGrantType) } -func (d *AuthorizeDeviceGrantTypeHandler) PopulateTokenEndpointResponse(ctx context.Context, requester fosite.AccessRequester, responder fosite.AccessResponder) error { - +func (d *DeviceAuthorizationHandler) PopulateTokenEndpointResponse(ctx context.Context, requester fosite.AccessRequester, responder fosite.AccessResponder) error { if !d.CanHandleTokenEndpointRequest(ctx, requester) { return errorsx.WithStack(fosite.ErrUnknownRequest) } @@ -142,7 +140,7 @@ func (d *AuthorizeDeviceGrantTypeHandler) PopulateTokenEndpointResponse(ctx cont return nil } -func (c *AuthorizeDeviceGrantTypeHandler) canIssueRefreshToken(ctx context.Context, config *AuthorizeDeviceGrantTypeHandler, request fosite.Requester) bool { +func (c *DeviceAuthorizationHandler) canIssueRefreshToken(ctx context.Context, config *DeviceAuthorizationHandler, request fosite.Requester) bool { scope := config.Config.GetRefreshTokenScopes(ctx) // Require one of the refresh token scopes, if set. if len(scope) > 0 && !request.GetGrantedScopes().HasOneOf(scope...) { From 74ef995da749fa917d1a328ac12adfb9e17b1bc5 Mon Sep 17 00:00:00 2001 From: Romain Caire Date: Fri, 9 Sep 2022 18:59:41 +0200 Subject: [PATCH 11/49] Rename to follow Ory's file naming pattern --- .../oauth2/{device_authorization.go => flow_device_grant_auth.go} | 0 ...evice_authorization_test.go => flow_device_grant_auth_test.go} | 0 .../{flow_device_code_token.go => flow_device_grant_token.go} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename handler/oauth2/{device_authorization.go => flow_device_grant_auth.go} (100%) rename handler/oauth2/{device_authorization_test.go => flow_device_grant_auth_test.go} (100%) rename handler/oauth2/{flow_device_code_token.go => flow_device_grant_token.go} (100%) diff --git a/handler/oauth2/device_authorization.go b/handler/oauth2/flow_device_grant_auth.go similarity index 100% rename from handler/oauth2/device_authorization.go rename to handler/oauth2/flow_device_grant_auth.go diff --git a/handler/oauth2/device_authorization_test.go b/handler/oauth2/flow_device_grant_auth_test.go similarity index 100% rename from handler/oauth2/device_authorization_test.go rename to handler/oauth2/flow_device_grant_auth_test.go diff --git a/handler/oauth2/flow_device_code_token.go b/handler/oauth2/flow_device_grant_token.go similarity index 100% rename from handler/oauth2/flow_device_code_token.go rename to handler/oauth2/flow_device_grant_token.go From 873f022379415eb3e6726d3cceb5d4516a028e63 Mon Sep 17 00:00:00 2001 From: Romain Caire Date: Mon, 12 Sep 2022 02:10:48 +0200 Subject: [PATCH 12/49] Add some tests & documentations --- authorize_request_handler_test.go | 4 +- compose/compose_oauth2.go | 2 + config_default.go | 1 + device_authorize_request_handler.go | 16 +- device_authorize_request_handler_test.go | 245 +++++++ device_authorize_request_test.go | 78 +++ device_authorize_response.go | 8 - device_authorize_response_writer.go | 2 +- device_authorize_response_writer_test.go | 94 +++ device_authorize_write_test.go | 80 +++ device_authorize_writer.go | 2 + errors.go | 6 + generate-mocks.sh | 3 + generate.go | 3 + handler/oauth2/flow_device_code_token_test.go | 350 ---------- handler/oauth2/flow_device_grant_auth_test.go | 8 +- handler/oauth2/flow_device_grant_token.go | 3 - .../oauth2/flow_device_grant_token_test.go | 650 ++++++++++++++++++ internal/device_authorize_handler.go | 50 ++ internal/device_authorize_request.go | 299 ++++++++ internal/device_authorize_response.go | 190 +++++ 21 files changed, 1719 insertions(+), 375 deletions(-) create mode 100644 device_authorize_request_handler_test.go create mode 100644 device_authorize_request_test.go create mode 100644 device_authorize_response_writer_test.go create mode 100644 device_authorize_write_test.go delete mode 100644 handler/oauth2/flow_device_code_token_test.go create mode 100644 handler/oauth2/flow_device_grant_token_test.go create mode 100644 internal/device_authorize_handler.go create mode 100644 internal/device_authorize_request.go create mode 100644 internal/device_authorize_response.go diff --git a/authorize_request_handler_test.go b/authorize_request_handler_test.go index 3e76184bf..ebd73970a 100644 --- a/authorize_request_handler_test.go +++ b/authorize_request_handler_test.go @@ -170,9 +170,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_oauth2.go b/compose/compose_oauth2.go index 25442b074..78ae247f7 100644 --- a/compose/compose_oauth2.go +++ b/compose/compose_oauth2.go @@ -126,6 +126,8 @@ func OAuth2StatelessJWTIntrospectionFactory(config fosite.Configurator, storage } } +// OAuth2AuthorizeExplicitFactory creates an OAuth2 device code grant ("Device Authorization Grant") handler and registers +// an access token, refresh token and device code validator. func OAuth2DeviceAuthorizeFactory(config fosite.Configurator, storage interface{}, strategy interface{}) interface{} { return &oauth2.DeviceAuthorizationHandler{ DeviceCodeStrategy: strategy.(oauth2.DeviceCodeStrategy), diff --git a/config_default.go b/config_default.go index 1da029d7a..b88a1fed9 100644 --- a/config_default.go +++ b/config_default.go @@ -222,6 +222,7 @@ type Config struct { // PushedAuthorizeEndpointHandlers is a list of handlers that are called before the PAR endpoint is served. PushedAuthorizeEndpointHandlers PushedAuthorizeEndpointHandlers + // DeviceAuthorizeEndpointHandlers is a list of handlers that are called before the device authorization endpoint is served. DeviceAuthorizeEndpointHandlers DeviceAuthorizeEndpointHandlers // GlobalSecret is the global secret used to sign and verify signatures. diff --git a/device_authorize_request_handler.go b/device_authorize_request_handler.go index 9a851d685..cd3d52852 100644 --- a/device_authorize_request_handler.go +++ b/device_authorize_request_handler.go @@ -35,19 +35,19 @@ func (f *Fosite) NewDeviceAuthorizeGetRequest(ctx context.Context, r *http.Reque request.Lang = i18n.GetLangFromRequest(f.Config.GetMessageCatalog(ctx), r) if err := r.ParseMultipartForm(1 << 20); err != nil && err != http.ErrNotMultipart { - return request, errorsx.WithStack(ErrInvalidRequest.WithHint("Unable to parse HTTP body, make sure to send a properly formatted form request body.").WithWrap(err).WithDebug(err.Error())) + 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 = r.Form if request.GetRequestForm().Has("device_verifier") { client, err := f.Store.GetClient(ctx, request.GetRequestForm().Get("client_id")) if err != nil { - return request, errorsx.WithStack(ErrInvalidClient.WithHint("The requested OAuth 2.0 Client does not exist.").WithWrap(err).WithDebug(err.Error())) + 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("urn:ietf:params:oauth:grant-type:device_code") { - return request, errorsx.WithStack(ErrInvalidClient.WithHint("The requested OAuth 2.0 Client does not have the 'urn:ietf:params:oauth:grant-type:device_code' grant.").WithWrap(err).WithDebug(err.Error())) + 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.")) } } @@ -59,18 +59,22 @@ func (f *Fosite) NewDeviceAuthorizePostRequest(ctx context.Context, req *http.Re request.Lang = i18n.GetLangFromRequest(f.Config.GetMessageCatalog(ctx), req) if err := req.ParseMultipartForm(1 << 20); err != nil && err != http.ErrNotMultipart { - return request, errorsx.WithStack(ErrInvalidRequest.WithHint("Unable to parse HTTP body, make sure to send a properly formatted form request body.").WithWrap(err).WithDebug(err.Error())) + 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 request, errorsx.WithStack(ErrInvalidClient.WithHint("The requested OAuth 2.0 Client does not exist.").WithWrap(err).WithDebug(err.Error())) + 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("urn:ietf:params:oauth:grant-type:device_code") { + 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.validateDeviceAuthorizeScope(ctx, req, request); err != nil { - return request, err + return nil, err } return request, nil diff --git a/device_authorize_request_handler_test.go b/device_authorize_request_handler_test.go new file mode 100644 index 000000000..67eba526d --- /dev/null +++ b/device_authorize_request_handler_test.go @@ -0,0 +1,245 @@ +/* + * Copyright © 2015-2018 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-2018 Aeneas Rekkas + * @license 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 TestNewDeviceAuthorizeGetRequest(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 *DeviceAuthorizeRequest + }{ + /* invalid client */ + { + desc: "invalid client fails", + conf: &Fosite{Store: store, Config: &Config{ScopeStrategy: ExactScopeStrategy, AudienceMatchingStrategy: DefaultAudienceMatchingStrategy}}, + query: url.Values{"device_verifier": []string{"AAAA"}}, + 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: &DeviceAuthorizeRequest{ + 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: &DeviceAuthorizeRequest{ + 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()} + } + } + + c.conf.Store = store + ar, err := c.conf.NewDeviceAuthorizeGetRequest(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()) + } + }) + } +} + +func TestNewDeviceAuthorizePostRequest(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 *DeviceAuthorizeRequest + }{ + /* 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: &DeviceAuthorizeRequest{ + 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.NewDeviceAuthorizePostRequest(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_authorize_request_test.go b/device_authorize_request_test.go new file mode 100644 index 000000000..701c85255 --- /dev/null +++ b/device_authorize_request_test.go @@ -0,0 +1,78 @@ +/* + * Copyright © 2015-2018 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-2018 Aeneas Rekkas + * @license Apache-2.0 + * + */ + +package fosite + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestDeviceAuthorizeRequest(t *testing.T) { + for k, c := range []struct { + ar *DeviceAuthorizeRequest + }{ + { + ar: NewDeviceAuthorizeRequest(), + }, + { + ar: &DeviceAuthorizeRequest{}, + }, + { + ar: &DeviceAuthorizeRequest{ + Request: Request{ + Client: &DefaultClient{RedirectURIs: []string{""}}, + }, + }, + }, + { + ar: &DeviceAuthorizeRequest{ + DeviceCodeSignature: "AAAA", + Request: Request{ + Client: &DefaultClient{RedirectURIs: []string{""}}, + }, + }, + }, + { + ar: &DeviceAuthorizeRequest{ + 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.DeviceCodeSignature, 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_authorize_response.go b/device_authorize_response.go index 648e14a42..26776dbbb 100644 --- a/device_authorize_response.go +++ b/device_authorize_response.go @@ -21,10 +21,7 @@ package fosite -import "context" - type DeviceAuthorizeResponse struct { - context context.Context deviceCode string userCode string verificationURI string @@ -33,11 +30,6 @@ type DeviceAuthorizeResponse struct { expiresIn int64 } -// GetDeviceCode returns the response's device code -func NewDeviceAuthorizeResponse() *DeviceAuthorizeResponse { - return &DeviceAuthorizeResponse{} -} - func (d *DeviceAuthorizeResponse) GetDeviceCode() string { return d.deviceCode } diff --git a/device_authorize_response_writer.go b/device_authorize_response_writer.go index 140989a57..be041bc12 100644 --- a/device_authorize_response_writer.go +++ b/device_authorize_response_writer.go @@ -26,7 +26,7 @@ import ( ) func (f *Fosite) NewDeviceAuthorizeResponse(ctx context.Context, r Requester) (DeviceAuthorizeResponder, error) { - var resp = NewDeviceAuthorizeResponse() + var resp = &DeviceAuthorizeResponse{} for _, h := range f.Config.GetDeviceAuthorizeEndpointHandlers(ctx) { if err := h.HandleDeviceAuthorizeEndpointRequest(ctx, r, resp); err != nil { diff --git a/device_authorize_response_writer_test.go b/device_authorize_response_writer_test.go new file mode 100644 index 000000000..51447034f --- /dev/null +++ b/device_authorize_response_writer_test.go @@ -0,0 +1,94 @@ +/* + * Copyright © 2015-2018 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-2018 Aeneas Rekkas + * @license 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 TestNewDeviceAuthorizeResponse(t *testing.T) { + ctrl := gomock.NewController(t) + handlers := []*MockDeviceAuthorizeEndpointHandler{NewMockDeviceAuthorizeEndpointHandler(ctrl)} + dar := NewMockDeviceAuthorizeRequester(ctrl) + defer ctrl.Finish() + + ctx := context.Background() + oauth2 := &Fosite{Config: &Config{DeviceAuthorizeEndpointHandlers: DeviceAuthorizeEndpointHandlers{handlers[0]}}} + duo := &Fosite{Config: &Config{DeviceAuthorizeEndpointHandlers: DeviceAuthorizeEndpointHandlers{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().HandleDeviceAuthorizeEndpointRequest(gomock.Any(), gomock.Any(), gomock.Any()).Return(fooErr) + }, + isErr: true, + expectErr: fooErr, + }, + { + mock: func() { + handlers[0].EXPECT().HandleDeviceAuthorizeEndpointRequest(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + }, + isErr: false, + }, + { + mock: func() { + oauth2 = duo + handlers[0].EXPECT().HandleDeviceAuthorizeEndpointRequest(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + handlers[0].EXPECT().HandleDeviceAuthorizeEndpointRequest(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + }, + isErr: false, + }, + { + mock: func() { + oauth2 = duo + handlers[0].EXPECT().HandleDeviceAuthorizeEndpointRequest(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + handlers[0].EXPECT().HandleDeviceAuthorizeEndpointRequest(gomock.Any(), gomock.Any(), gomock.Any()).Return(fooErr) + }, + isErr: true, + expectErr: fooErr, + }, + } { + c.mock() + responder, err := oauth2.NewDeviceAuthorizeResponse(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_authorize_write_test.go b/device_authorize_write_test.go new file mode 100644 index 000000000..1d684d673 --- /dev/null +++ b/device_authorize_write_test.go @@ -0,0 +1,80 @@ +/* + * Copyright © 2015-2018 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-2018 Aeneas Rekkas + * @license Apache-2.0 + * + */ + +package fosite_test + +import ( + "context" + "encoding/json" + "net/http/httptest" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + . "github.com/ory/fosite" +) + +func TestWriteDeviceAuthorizeResponse(t *testing.T) { + oauth2 := &Fosite{Config: &Config{ + DeviceAndUserCodeLifespan: time.Minute, + DeviceAuthTokenPollingInterval: time.Minute, + DeviceVerificationURL: "http://ory.sh", + }} + + rw := httptest.NewRecorder() + ar := &DeviceAuthorizeRequest{} + resp := &DeviceAuthorizeResponse{} + 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.WriteDeviceAuthorizeResponse(context.Background(), rw, ar, resp) + var params struct { + 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"` + } + + assert.Equal(t, 200, rw.Code) + err := json.NewDecoder(rw.Body).Decode(¶ms) + require.NoError(t, err) + + assert.Equal(t, resp.GetUserCode(), params.UserCode) + assert.Equal(t, resp.GetDeviceCode(), params.DeviceCode) + assert.Equal(t, resp.GetVerificationURI(), params.VerificationURI) + assert.Equal(t, resp.GetVerificationURIComplete(), params.VerificationURIComplete) + assert.Equal(t, resp.GetInterval(), params.Interval) + assert.Equal(t, resp.GetExpiresIn(), params.ExpiresIn) +} diff --git a/device_authorize_writer.go b/device_authorize_writer.go index 2152b5dfe..544e29943 100644 --- a/device_authorize_writer.go +++ b/device_authorize_writer.go @@ -27,6 +27,8 @@ import ( "net/http" ) +// TODO: Do documentation + func (f *Fosite) WriteDeviceAuthorizeResponse(ctx context.Context, rw http.ResponseWriter, r Requester, resp DeviceAuthorizeResponder) { rw.Header().Set("Content-Type", "application/json;charset=UTF-8") rw.Header().Set("Cache-Control", "no-store") diff --git a/errors.go b/errors.go index 85f9a6426..4b145358e 100644 --- a/errors.go +++ b/errors.go @@ -230,6 +230,11 @@ var ( ErrorField: errAuthorizationPending, CodeField: http.StatusForbidden, } + ErrExpiredToken = &RFC6749Error{ + DescriptionField: "The device_code has expired, and the device authorization session has concluded.", + ErrorField: errExpiredToken, + CodeField: http.StatusForbidden, + } ) const ( @@ -268,6 +273,7 @@ const ( errRegistrationNotSupportedName = "registration_not_supported" errJTIKnownName = "jti_known" errAuthorizationPending = "authorization_pending" + errExpiredToken = "expired_token" ) type ( diff --git a/generate-mocks.sh b/generate-mocks.sh index d4dded4ea..1fe0229e4 100755 --- a/generate-mocks.sh +++ b/generate-mocks.sh @@ -19,6 +19,7 @@ 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_authorize_handler.go github.com/ory/fosite DeviceAuthorizeEndpointHandler 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 +29,7 @@ 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_authorize_request.go github.com/ory/fosite DeviceAuthorizeRequester +mockgen -package internal -destination internal/device_authorize_response.go github.com/ory/fosite DeviceAuthorizeResponder goimports -w internal/ \ No newline at end of file diff --git a/generate.go b/generate.go index 067eed9cc..5dc910d96 100644 --- a/generate.go +++ b/generate.go @@ -19,6 +19,7 @@ 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_authorize_handler.go github.com/ory/fosite DeviceAuthorizeEndpointHandler //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 @@ -28,3 +29,5 @@ 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_authorize_request.go github.com/ory/fosite DeviceAuthorizeRequester +//go:generate go run github.com/golang/mock/mockgen -package internal -destination internal/device_authorize_response.go github.com/ory/fosite DeviceAuthorizeResponder diff --git a/handler/oauth2/flow_device_code_token_test.go b/handler/oauth2/flow_device_code_token_test.go deleted file mode 100644 index 889a7ff81..000000000 --- a/handler/oauth2/flow_device_code_token_test.go +++ /dev/null @@ -1,350 +0,0 @@ -package oauth2 - -import ( - "context" - "net/url" - "testing" - "time" - - "github.com/ory/fosite" - "github.com/ory/fosite/storage" - "github.com/stretchr/testify/require" -) - -func TestAuthorizeCode_HandleDeviceTokenEndpointRequest(t *testing.T) { - - for k, strategy := range map[string]CoreStrategy{ - "hmac": &hmacshaStrategy, - } { - t.Run("strategy="+k, func(t *testing.T) { - store := storage.NewMemoryStore() - handler := AuthorizeDeviceGrantTypeHandler{ - CoreStorage: store, - DeviceCodeStrategy: &hmacshaStrategy, - UserCodeStrategy: &hmacshaStrategy, - AccessTokenStrategy: strategy, - RefreshTokenStrategy: strategy, - AuthorizeCodeStrategy: strategy, - Config: &fosite.Config{ - DeviceAndUserCodeLifespan: time.Minute * 10, - DeviceAuthTokenPollingInterval: time.Second * 10, - DeviceVerificationURL: "localhost", - AccessTokenLifespan: time.Hour, - RefreshTokenLifespan: time.Hour, - ScopeStrategy: fosite.HierarchicScopeStrategy, - AudienceMatchingStrategy: fosite.DefaultAudienceMatchingStrategy, - RefreshTokenScopes: []string{"offline"}, - }, - } - for _, c := range []struct { - handler AuthorizeDeviceGrantTypeHandler - areq *fosite.AccessRequest - breq *fosite.AccessRequest - createDeviceSession bool - expire time.Duration - description string - expectErr error - expect func(t *testing.T, areq *fosite.AccessRequest) - }{ - { - handler: handler, - areq: &fosite.AccessRequest{ - GrantTypes: fosite.Arguments{"authorization_code"}, - Request: fosite.Request{ - Client: &fosite.DefaultClient{ID: "foo", GrantTypes: []string{""}}, - Session: &fosite.DefaultSession{Subject: "A"}, - RequestedAt: time.Now().UTC(), - }, - }, - breq: &fosite.AccessRequest{ - GrantTypes: fosite.Arguments{"authorization_code"}, - Request: fosite.Request{ - Client: &fosite.DefaultClient{ID: "foo", GrantTypes: []string{""}}, - Session: &fosite.DefaultSession{Subject: "A"}, - RequestedAt: time.Now().UTC(), - }, - }, - description: "Should fail due to wrong grant type", - expectErr: fosite.ErrUnknownRequest, - createDeviceSession: false, - expire: time.Minute * 10, - }, - { - handler: handler, - 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{Subject: "A"}, - RequestedAt: time.Now().UTC(), - }, - }, - breq: &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{Subject: "A"}, - RequestedAt: time.Now().UTC(), - }, - }, - description: "Should fail due to no device_code supplied", - expectErr: fosite.ErrUnauthorizedClient, - createDeviceSession: false, - expire: time.Minute * 10, - }, - { - handler: handler, - 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{Subject: "A"}, - RequestedAt: time.Now().UTC(), - Form: url.Values{"device_code": {"ABC1234"}}, - }, - }, - breq: &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{Subject: "A"}, - RequestedAt: time.Now().UTC(), - Form: url.Values{"device_code": {"ABC1234"}}, - }, - }, - description: "Should fail due to no user_code session available", - expectErr: fosite.ErrUnauthorizedClient, - createDeviceSession: false, - expire: time.Minute * 10, - }, - { - handler: handler, - 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{Subject: "A"}, - RequestedAt: time.Now().UTC(), - Form: url.Values{"device_code": {"ABC1234"}}, - }, - }, - breq: &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{Subject: "A"}, - RequestedAt: time.Now().UTC(), - Form: url.Values{"device_code": {"ABC1234"}}, - }, - }, - description: "Should pass as device_code form data and session are available", - createDeviceSession: true, - expire: time.Minute * 10, - }, - { - handler: handler, - 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{Subject: "A"}, - RequestedAt: time.Now().UTC(), - Form: url.Values{"device_code": {"ABC1234"}}, - }, - }, - breq: &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{Subject: "A"}, - RequestedAt: time.Now().UTC(), - Form: url.Values{"device_code": {"ABC1234"}}, - }, - }, - description: "Should fail as session expired", - createDeviceSession: true, - expire: -(time.Minute * 10), - expectErr: fosite.ErrUnauthorizedClient, - }, - { - handler: handler, - 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{Subject: "A"}, - RequestedAt: time.Now().UTC(), - Form: url.Values{"device_code": {"ABC1234"}}, - }, - }, - breq: &fosite.AccessRequest{ - GrantTypes: fosite.Arguments{"urn:ietf:params:oauth:grant-type:device_code"}, - Request: fosite.Request{ - Client: &fosite.DefaultClient{ID: "bar", GrantTypes: []string{""}}, - Session: &fosite.DefaultSession{Subject: "A"}, - RequestedAt: time.Now().UTC(), - Form: url.Values{"device_code": {"ABC1234"}}, - }, - }, - description: "Should fail as session and request clients do not match", - createDeviceSession: true, - expire: time.Minute * 10, - expectErr: fosite.ErrUnauthorizedClient, - }, - } { - t.Run("case="+c.description, func(t *testing.T) { - - if c.createDeviceSession { - c.areq.SetID("ID1") - c.areq.Session = &fosite.DefaultSession{} - expireAt := time.Now().UTC().Add(c.expire) - c.areq.Session.SetExpiresAt(fosite.UserCode, expireAt) - deviceSignature := hmacshaStrategy.DeviceCodeSignature(context.Background(), c.areq.Form.Get("device_code")) - store.CreateDeviceCodeSession(nil, deviceSignature, c.areq) - } - - err := c.handler.HandleTokenEndpointRequest(nil, c.breq) - if c.expectErr != nil { - require.EqualError(t, err, c.expectErr.Error()) - } else { - require.NoError(t, err) - } - - if c.expect != nil { - c.expect(t, c.areq) - } - }) - } - }) - } -} - -func TestAuthorizeCode_PopulateDeviceTokenEndpointResponse(t *testing.T) { - - for k, strategy := range map[string]CoreStrategy{ - "hmac": &hmacshaStrategy, - } { - t.Run("strategy="+k, func(t *testing.T) { - store := storage.NewMemoryStore() - handler := AuthorizeDeviceGrantTypeHandler{ - CoreStorage: store, - DeviceCodeStrategy: &hmacshaStrategy, - UserCodeStrategy: &hmacshaStrategy, - AccessTokenStrategy: strategy, - RefreshTokenStrategy: strategy, - AuthorizeCodeStrategy: strategy, - Config: &fosite.Config{ - DeviceAndUserCodeLifespan: time.Minute * 10, - DeviceAuthTokenPollingInterval: time.Second * 10, - DeviceVerificationURL: "localhost", - AccessTokenLifespan: time.Hour, - RefreshTokenLifespan: time.Hour, - ScopeStrategy: fosite.HierarchicScopeStrategy, - AudienceMatchingStrategy: fosite.DefaultAudienceMatchingStrategy, - RefreshTokenScopes: []string{"offline"}, - }, - } - for _, c := range []struct { - handler AuthorizeDeviceGrantTypeHandler - areq *fosite.AccessRequest - createDeviceSession bool - description string - expectErr error - expect func(t *testing.T, areq *fosite.AccessRequest) - }{ - { - handler: handler, - areq: &fosite.AccessRequest{ - GrantTypes: fosite.Arguments{"authorization_code"}, - Request: fosite.Request{ - Client: &fosite.DefaultClient{ID: "foo", GrantTypes: []string{""}}, - Session: &fosite.DefaultSession{}, - RequestedAt: time.Now().UTC(), - }, - }, - description: "Should fail due to wrong grant type", - expectErr: fosite.ErrUnknownRequest, - createDeviceSession: false, - }, { - handler: handler, - 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 due to no device_code supplied", - expectErr: fosite.ErrUnknownRequest, - createDeviceSession: false, - }, - { - handler: handler, - 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(), - Form: url.Values{"device_code": {"ABC1234"}}, - }, - }, - description: "Should fail due to no user_code session available", - expectErr: fosite.ErrInvalidRequest, - createDeviceSession: false, - }, - { - handler: handler, - areq: &fosite.AccessRequest{ - GrantTypes: fosite.Arguments{"urn:ietf:params:oauth:grant-type:device_code"}, - Request: fosite.Request{ - Client: &fosite.DefaultClient{ID: "foo", GrantTypes: []string{"refresh_token", "urn:ietf:params:oauth:grant-type:device_code"}}, - Session: &fosite.DefaultSession{}, - RequestedAt: time.Now().UTC(), - GrantedScope: fosite.Arguments{"openid", "offline"}, - GrantedAudience: fosite.Arguments{"www.websitesite.com"}, - Form: url.Values{"device_code": {"ABC1234"}}, - }, - }, - description: "Should pass as device_code form data and session are available", - createDeviceSession: true, - }, - } { - t.Run("case="+c.description, func(t *testing.T) { - - c.areq.GetSession().SetExpiresAt(fosite.UserCode, time.Now().Add(time.Minute*5)) - if c.createDeviceSession { - c.areq.SetID("ID1") - deviceSig := hmacshaStrategy.DeviceCodeSignature(context.TODO(), c.areq.Form.Get("device_code")) - store.CreateDeviceCodeSession(context.TODO(), deviceSig, c.areq) - } - - resp := fosite.NewAccessResponse() - err := c.handler.PopulateTokenEndpointResponse(context.TODO(), c.areq, resp) - if c.expectErr != nil { - require.EqualError(t, err, c.expectErr.Error()) - } else { - require.NoError(t, err) - } - - accessToken := resp.GetAccessToken() - refreshToken := resp.GetExtra("refresh_token") - - // Make sure we only create tokens if we have a device session available - if c.createDeviceSession { - require.NotEmpty(t, accessToken) - require.NotEmpty(t, refreshToken) - } else { - require.Empty(t, accessToken) - require.Empty(t, refreshToken) - } - - if c.expect != nil { - c.expect(t, c.areq) - } - }) - } - }) - } -} diff --git a/handler/oauth2/flow_device_grant_auth_test.go b/handler/oauth2/flow_device_grant_auth_test.go index 8e9f8bef5..8ffaccf5c 100644 --- a/handler/oauth2/flow_device_grant_auth_test.go +++ b/handler/oauth2/flow_device_grant_auth_test.go @@ -14,11 +14,9 @@ func Test_HandleDeviceAuthorizeEndpointRequest(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - deviceStore := storage.NewMemoryStore() - userStore := storage.NewMemoryStore() + store := storage.NewMemoryStore() handler := DeviceAuthorizationHandler{ - DeviceCodeStorage: deviceStore, - UserCodeStorage: userStore, + CoreStorage: store, DeviceCodeStrategy: &hmacshaStrategy, UserCodeStrategy: &hmacshaStrategy, Config: &fosite.Config{ @@ -39,7 +37,7 @@ func Test_HandleDeviceAuthorizeEndpointRequest(t *testing.T) { Session: &fosite.DefaultSession{}, }, } - resp := fosite.NewDeviceAuthorizeResponse() + resp := &fosite.DeviceAuthorizeResponse{} handler.HandleDeviceAuthorizeEndpointRequest(nil, req, resp) diff --git a/handler/oauth2/flow_device_grant_token.go b/handler/oauth2/flow_device_grant_token.go index 53f8c70fb..c8d2f5ae0 100644 --- a/handler/oauth2/flow_device_grant_token.go +++ b/handler/oauth2/flow_device_grant_token.go @@ -76,9 +76,6 @@ func (d *DeviceAuthorizationHandler) PopulateTokenEndpointResponse(ctx context.C session, err := d.CoreStorage.GetDeviceCodeSession(ctx, signature, requester.GetSession()) if err != nil { return errorsx.WithStack(fosite.ErrInvalidRequest.WithWrap(err).WithDebug(err.Error())) - } else if err := d.DeviceCodeStrategy.ValidateDeviceCode(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 session.GetGrantedScopes() { diff --git a/handler/oauth2/flow_device_grant_token_test.go b/handler/oauth2/flow_device_grant_token_test.go new file mode 100644 index 000000000..f798d389d --- /dev/null +++ b/handler/oauth2/flow_device_grant_token_test.go @@ -0,0 +1,650 @@ +/* + * Copyright © 2015-2018 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-2018 Aeneas Rekkas + * @license Apache-2.0 + * + */ + +package oauth2 + +import ( + "context" + "fmt" + "net/url" + "testing" //"time" + + "github.com/golang/mock/gomock" + + "github.com/ory/fosite/internal" + + //"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" +) + +func TestDeviceAuthorizeCode_PopulateTokenEndpointResponse(t *testing.T) { + for k, strategy := range map[string]CoreStrategy{ + "hmac": &hmacshaStrategy, + } { + t.Run("strategy="+k, func(t *testing.T) { + store := storage.NewMemoryStore() + + var h DeviceAuthorizationHandler + 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.ErrInvalidRequest, + }, + { + 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 = DeviceAuthorizationHandler{ + CoreStorage: store, + DeviceCodeStrategy: strategy, + UserCodeStrategy: strategy, + AccessTokenStrategy: strategy, + RefreshTokenStrategy: strategy, + Config: config, + } + + 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 TestDeviceAuthorizeCode_HandleTokenEndpointRequest(t *testing.T) { + for k, strategy := range map[string]CoreStrategy{ + "hmac": &hmacshaStrategy, + } { + t.Run("strategy="+k, func(t *testing.T) { + store := storage.NewMemoryStore() + + h := DeviceAuthorizationHandler{ + CoreStorage: store, + DeviceCodeStrategy: &hmacshaStrategy, + UserCodeStrategy: &hmacshaStrategy, + Config: &fosite.Config{ + ScopeStrategy: fosite.HierarchicScopeStrategy, + AudienceMatchingStrategy: fosite.DefaultAudienceMatchingStrategy, + AuthorizeCodeLifespan: time.Minute, + }, + } + for i, c := range []struct { + areq *fosite.AccessRequest + authreq *fosite.DeviceAuthorizeRequest + description string + setup func(t *testing.T, areq *fosite.AccessRequest, authreq *fosite.DeviceAuthorizeRequest) + check func(t *testing.T, areq *fosite.AccessRequest, authreq *fosite.DeviceAuthorizeRequest) + 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 (1)", + setup: func(t *testing.T, areq *fosite.AccessRequest, authreq *fosite.DeviceAuthorizeRequest) { + deviceCode, _, err := strategy.GenerateDeviceCode(context.TODO()) + require.NoError(t, err) + areq.Form = url.Values{"device_code": {deviceCode}} + }, + expectErr: fosite.ErrAuthorizationPending, + }, + // { + // 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.DeviceAuthorizeRequest{ + 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.DeviceAuthorizeRequest) { + 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.DeviceAuthorizeRequest{ + 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.DeviceAuthorizeRequest) { + 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.DeviceAuthorizeRequest) { + // 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.DeviceAuthorizeRequest) { + // 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)) + // require.NoError(t, store.InvalidateDeviceCodeSession(context.TODO(), sig)) + // }, + // description: "should fail because device code has expired", + // expectErr: fosite.ErrExpiredToken, + // }, + } { + 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 TestDeviceAuthorizeCodeTransactional_HandleTokenEndpointRequest(t *testing.T) { + var mockTransactional *internal.MockTransactional + var mockCoreStore *internal.MockCoreStorage + strategy := hmacshaStrategy + 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 := strategy.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 transactionalStore struct { + storage.Transactional + CoreStorage + } + + for _, testCase := range []struct { + description string + setup func() + expectError error + }{ + { + description: "transaction should be committed successfully if no errors occur", + setup: func() { + mockCoreStore. + EXPECT(). + GetDeviceCodeSession(gomock.Any(), gomock.Any(), gomock.Any()). + Return(request, nil). + Times(1) + mockTransactional. + EXPECT(). + BeginTX(propagatedContext). + Return(propagatedContext, nil) + mockCoreStore. + 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() { + mockCoreStore. + EXPECT(). + GetDeviceCodeSession(gomock.Any(), gomock.Any(), gomock.Any()). + Return(request, nil). + Times(1) + mockTransactional. + EXPECT(). + BeginTX(propagatedContext). + Return(propagatedContext, nil) + mockCoreStore. + 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() { + mockCoreStore. + EXPECT(). + GetDeviceCodeSession(gomock.Any(), gomock.Any(), gomock.Any()). + Return(request, nil). + Times(1) + mockTransactional. + EXPECT(). + BeginTX(propagatedContext). + Return(propagatedContext, nil) + mockCoreStore. + 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() { + mockCoreStore. + 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() { + mockCoreStore. + EXPECT(). + GetDeviceCodeSession(gomock.Any(), gomock.Any(), gomock.Any()). + Return(request, nil). + Times(1) + mockTransactional. + EXPECT(). + BeginTX(propagatedContext). + Return(propagatedContext, nil) + mockCoreStore. + 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() { + mockCoreStore. + EXPECT(). + GetDeviceCodeSession(gomock.Any(), gomock.Any(), gomock.Any()). + Return(request, nil). + Times(1) + mockTransactional. + EXPECT(). + BeginTX(propagatedContext). + Return(propagatedContext, nil) + mockCoreStore. + 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) + testCase.setup() + + handler := DeviceAuthorizationHandler{ + CoreStorage: transactionalStore{ + mockTransactional, + mockCoreStore, + }, + AccessTokenStrategy: &strategy, + RefreshTokenStrategy: &strategy, + DeviceCodeStrategy: &strategy, + UserCodeStrategy: &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/internal/device_authorize_handler.go b/internal/device_authorize_handler.go new file mode 100644 index 000000000..fa5666b55 --- /dev/null +++ b/internal/device_authorize_handler.go @@ -0,0 +1,50 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/ory/fosite (interfaces: DeviceAuthorizeEndpointHandler) + +// 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" +) + +// MockDeviceAuthorizeEndpointHandler is a mock of DeviceAuthorizeEndpointHandler interface. +type MockDeviceAuthorizeEndpointHandler struct { + ctrl *gomock.Controller + recorder *MockDeviceAuthorizeEndpointHandlerMockRecorder +} + +// MockDeviceAuthorizeEndpointHandlerMockRecorder is the mock recorder for MockDeviceAuthorizeEndpointHandler. +type MockDeviceAuthorizeEndpointHandlerMockRecorder struct { + mock *MockDeviceAuthorizeEndpointHandler +} + +// NewMockDeviceAuthorizeEndpointHandler creates a new mock instance. +func NewMockDeviceAuthorizeEndpointHandler(ctrl *gomock.Controller) *MockDeviceAuthorizeEndpointHandler { + mock := &MockDeviceAuthorizeEndpointHandler{ctrl: ctrl} + mock.recorder = &MockDeviceAuthorizeEndpointHandlerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDeviceAuthorizeEndpointHandler) EXPECT() *MockDeviceAuthorizeEndpointHandlerMockRecorder { + return m.recorder +} + +// HandleDeviceAuthorizeEndpointRequest mocks base method. +func (m *MockDeviceAuthorizeEndpointHandler) HandleDeviceAuthorizeEndpointRequest(arg0 context.Context, arg1 fosite.Requester, arg2 fosite.DeviceAuthorizeResponder) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HandleDeviceAuthorizeEndpointRequest", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// HandleDeviceAuthorizeEndpointRequest indicates an expected call of HandleDeviceAuthorizeEndpointRequest. +func (mr *MockDeviceAuthorizeEndpointHandlerMockRecorder) HandleDeviceAuthorizeEndpointRequest(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandleDeviceAuthorizeEndpointRequest", reflect.TypeOf((*MockDeviceAuthorizeEndpointHandler)(nil).HandleDeviceAuthorizeEndpointRequest), arg0, arg1, arg2) +} diff --git a/internal/device_authorize_request.go b/internal/device_authorize_request.go new file mode 100644 index 000000000..caba5a89e --- /dev/null +++ b/internal/device_authorize_request.go @@ -0,0 +1,299 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/ory/fosite (interfaces: DeviceAuthorizeRequester) + +// 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" +) + +// MockDeviceAuthorizeRequester is a mock of DeviceAuthorizeRequester interface. +type MockDeviceAuthorizeRequester struct { + ctrl *gomock.Controller + recorder *MockDeviceAuthorizeRequesterMockRecorder +} + +// MockDeviceAuthorizeRequesterMockRecorder is the mock recorder for MockDeviceAuthorizeRequester. +type MockDeviceAuthorizeRequesterMockRecorder struct { + mock *MockDeviceAuthorizeRequester +} + +// NewMockDeviceAuthorizeRequester creates a new mock instance. +func NewMockDeviceAuthorizeRequester(ctrl *gomock.Controller) *MockDeviceAuthorizeRequester { + mock := &MockDeviceAuthorizeRequester{ctrl: ctrl} + mock.recorder = &MockDeviceAuthorizeRequesterMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDeviceAuthorizeRequester) EXPECT() *MockDeviceAuthorizeRequesterMockRecorder { + return m.recorder +} + +// AppendRequestedScope mocks base method. +func (m *MockDeviceAuthorizeRequester) AppendRequestedScope(arg0 string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "AppendRequestedScope", arg0) +} + +// AppendRequestedScope indicates an expected call of AppendRequestedScope. +func (mr *MockDeviceAuthorizeRequesterMockRecorder) AppendRequestedScope(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AppendRequestedScope", reflect.TypeOf((*MockDeviceAuthorizeRequester)(nil).AppendRequestedScope), arg0) +} + +// GetClient mocks base method. +func (m *MockDeviceAuthorizeRequester) 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 *MockDeviceAuthorizeRequesterMockRecorder) GetClient() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClient", reflect.TypeOf((*MockDeviceAuthorizeRequester)(nil).GetClient)) +} + +// GetDeviceCodeSignature mocks base method. +func (m *MockDeviceAuthorizeRequester) 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 *MockDeviceAuthorizeRequesterMockRecorder) GetDeviceCodeSignature() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDeviceCodeSignature", reflect.TypeOf((*MockDeviceAuthorizeRequester)(nil).GetDeviceCodeSignature)) +} + +// GetGrantedAudience mocks base method. +func (m *MockDeviceAuthorizeRequester) 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 *MockDeviceAuthorizeRequesterMockRecorder) GetGrantedAudience() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGrantedAudience", reflect.TypeOf((*MockDeviceAuthorizeRequester)(nil).GetGrantedAudience)) +} + +// GetGrantedScopes mocks base method. +func (m *MockDeviceAuthorizeRequester) 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 *MockDeviceAuthorizeRequesterMockRecorder) GetGrantedScopes() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGrantedScopes", reflect.TypeOf((*MockDeviceAuthorizeRequester)(nil).GetGrantedScopes)) +} + +// GetID mocks base method. +func (m *MockDeviceAuthorizeRequester) 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 *MockDeviceAuthorizeRequesterMockRecorder) GetID() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetID", reflect.TypeOf((*MockDeviceAuthorizeRequester)(nil).GetID)) +} + +// GetRequestForm mocks base method. +func (m *MockDeviceAuthorizeRequester) 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 *MockDeviceAuthorizeRequesterMockRecorder) GetRequestForm() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRequestForm", reflect.TypeOf((*MockDeviceAuthorizeRequester)(nil).GetRequestForm)) +} + +// GetRequestedAt mocks base method. +func (m *MockDeviceAuthorizeRequester) 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 *MockDeviceAuthorizeRequesterMockRecorder) GetRequestedAt() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRequestedAt", reflect.TypeOf((*MockDeviceAuthorizeRequester)(nil).GetRequestedAt)) +} + +// GetRequestedAudience mocks base method. +func (m *MockDeviceAuthorizeRequester) 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 *MockDeviceAuthorizeRequesterMockRecorder) GetRequestedAudience() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRequestedAudience", reflect.TypeOf((*MockDeviceAuthorizeRequester)(nil).GetRequestedAudience)) +} + +// GetRequestedScopes mocks base method. +func (m *MockDeviceAuthorizeRequester) 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 *MockDeviceAuthorizeRequesterMockRecorder) GetRequestedScopes() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRequestedScopes", reflect.TypeOf((*MockDeviceAuthorizeRequester)(nil).GetRequestedScopes)) +} + +// GetSession mocks base method. +func (m *MockDeviceAuthorizeRequester) 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 *MockDeviceAuthorizeRequesterMockRecorder) GetSession() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSession", reflect.TypeOf((*MockDeviceAuthorizeRequester)(nil).GetSession)) +} + +// GrantAudience mocks base method. +func (m *MockDeviceAuthorizeRequester) GrantAudience(arg0 string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "GrantAudience", arg0) +} + +// GrantAudience indicates an expected call of GrantAudience. +func (mr *MockDeviceAuthorizeRequesterMockRecorder) GrantAudience(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GrantAudience", reflect.TypeOf((*MockDeviceAuthorizeRequester)(nil).GrantAudience), arg0) +} + +// GrantScope mocks base method. +func (m *MockDeviceAuthorizeRequester) GrantScope(arg0 string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "GrantScope", arg0) +} + +// GrantScope indicates an expected call of GrantScope. +func (mr *MockDeviceAuthorizeRequesterMockRecorder) GrantScope(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GrantScope", reflect.TypeOf((*MockDeviceAuthorizeRequester)(nil).GrantScope), arg0) +} + +// Merge mocks base method. +func (m *MockDeviceAuthorizeRequester) Merge(arg0 fosite.Requester) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Merge", arg0) +} + +// Merge indicates an expected call of Merge. +func (mr *MockDeviceAuthorizeRequesterMockRecorder) Merge(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Merge", reflect.TypeOf((*MockDeviceAuthorizeRequester)(nil).Merge), arg0) +} + +// Sanitize mocks base method. +func (m *MockDeviceAuthorizeRequester) 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 *MockDeviceAuthorizeRequesterMockRecorder) Sanitize(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Sanitize", reflect.TypeOf((*MockDeviceAuthorizeRequester)(nil).Sanitize), arg0) +} + +// SetDeviceCodeSignature mocks base method. +func (m *MockDeviceAuthorizeRequester) SetDeviceCodeSignature(arg0 string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetDeviceCodeSignature", arg0) +} + +// SetDeviceCodeSignature indicates an expected call of SetDeviceCodeSignature. +func (mr *MockDeviceAuthorizeRequesterMockRecorder) SetDeviceCodeSignature(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDeviceCodeSignature", reflect.TypeOf((*MockDeviceAuthorizeRequester)(nil).SetDeviceCodeSignature), arg0) +} + +// SetID mocks base method. +func (m *MockDeviceAuthorizeRequester) SetID(arg0 string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetID", arg0) +} + +// SetID indicates an expected call of SetID. +func (mr *MockDeviceAuthorizeRequesterMockRecorder) SetID(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetID", reflect.TypeOf((*MockDeviceAuthorizeRequester)(nil).SetID), arg0) +} + +// SetRequestedAudience mocks base method. +func (m *MockDeviceAuthorizeRequester) SetRequestedAudience(arg0 fosite.Arguments) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetRequestedAudience", arg0) +} + +// SetRequestedAudience indicates an expected call of SetRequestedAudience. +func (mr *MockDeviceAuthorizeRequesterMockRecorder) SetRequestedAudience(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetRequestedAudience", reflect.TypeOf((*MockDeviceAuthorizeRequester)(nil).SetRequestedAudience), arg0) +} + +// SetRequestedScopes mocks base method. +func (m *MockDeviceAuthorizeRequester) SetRequestedScopes(arg0 fosite.Arguments) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetRequestedScopes", arg0) +} + +// SetRequestedScopes indicates an expected call of SetRequestedScopes. +func (mr *MockDeviceAuthorizeRequesterMockRecorder) SetRequestedScopes(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetRequestedScopes", reflect.TypeOf((*MockDeviceAuthorizeRequester)(nil).SetRequestedScopes), arg0) +} + +// SetSession mocks base method. +func (m *MockDeviceAuthorizeRequester) SetSession(arg0 fosite.Session) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetSession", arg0) +} + +// SetSession indicates an expected call of SetSession. +func (mr *MockDeviceAuthorizeRequesterMockRecorder) SetSession(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetSession", reflect.TypeOf((*MockDeviceAuthorizeRequester)(nil).SetSession), arg0) +} diff --git a/internal/device_authorize_response.go b/internal/device_authorize_response.go new file mode 100644 index 000000000..e23e6606b --- /dev/null +++ b/internal/device_authorize_response.go @@ -0,0 +1,190 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/ory/fosite (interfaces: DeviceAuthorizeResponder) + +// Package internal is a generated GoMock package. +package internal + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" +) + +// MockDeviceAuthorizeResponder is a mock of DeviceAuthorizeResponder interface. +type MockDeviceAuthorizeResponder struct { + ctrl *gomock.Controller + recorder *MockDeviceAuthorizeResponderMockRecorder +} + +// MockDeviceAuthorizeResponderMockRecorder is the mock recorder for MockDeviceAuthorizeResponder. +type MockDeviceAuthorizeResponderMockRecorder struct { + mock *MockDeviceAuthorizeResponder +} + +// NewMockDeviceAuthorizeResponder creates a new mock instance. +func NewMockDeviceAuthorizeResponder(ctrl *gomock.Controller) *MockDeviceAuthorizeResponder { + mock := &MockDeviceAuthorizeResponder{ctrl: ctrl} + mock.recorder = &MockDeviceAuthorizeResponderMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDeviceAuthorizeResponder) EXPECT() *MockDeviceAuthorizeResponderMockRecorder { + return m.recorder +} + +// GetDeviceCode mocks base method. +func (m *MockDeviceAuthorizeResponder) 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 *MockDeviceAuthorizeResponderMockRecorder) GetDeviceCode() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDeviceCode", reflect.TypeOf((*MockDeviceAuthorizeResponder)(nil).GetDeviceCode)) +} + +// GetExpiresIn mocks base method. +func (m *MockDeviceAuthorizeResponder) 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 *MockDeviceAuthorizeResponderMockRecorder) GetExpiresIn() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetExpiresIn", reflect.TypeOf((*MockDeviceAuthorizeResponder)(nil).GetExpiresIn)) +} + +// GetInterval mocks base method. +func (m *MockDeviceAuthorizeResponder) 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 *MockDeviceAuthorizeResponderMockRecorder) GetInterval() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInterval", reflect.TypeOf((*MockDeviceAuthorizeResponder)(nil).GetInterval)) +} + +// GetUserCode mocks base method. +func (m *MockDeviceAuthorizeResponder) 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 *MockDeviceAuthorizeResponderMockRecorder) GetUserCode() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserCode", reflect.TypeOf((*MockDeviceAuthorizeResponder)(nil).GetUserCode)) +} + +// GetVerificationURI mocks base method. +func (m *MockDeviceAuthorizeResponder) 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 *MockDeviceAuthorizeResponderMockRecorder) GetVerificationURI() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetVerificationURI", reflect.TypeOf((*MockDeviceAuthorizeResponder)(nil).GetVerificationURI)) +} + +// GetVerificationURIComplete mocks base method. +func (m *MockDeviceAuthorizeResponder) 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 *MockDeviceAuthorizeResponderMockRecorder) GetVerificationURIComplete() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetVerificationURIComplete", reflect.TypeOf((*MockDeviceAuthorizeResponder)(nil).GetVerificationURIComplete)) +} + +// SetDeviceCode mocks base method. +func (m *MockDeviceAuthorizeResponder) SetDeviceCode(arg0 string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetDeviceCode", arg0) +} + +// SetDeviceCode indicates an expected call of SetDeviceCode. +func (mr *MockDeviceAuthorizeResponderMockRecorder) SetDeviceCode(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDeviceCode", reflect.TypeOf((*MockDeviceAuthorizeResponder)(nil).SetDeviceCode), arg0) +} + +// SetExpiresIn mocks base method. +func (m *MockDeviceAuthorizeResponder) SetExpiresIn(arg0 int64) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetExpiresIn", arg0) +} + +// SetExpiresIn indicates an expected call of SetExpiresIn. +func (mr *MockDeviceAuthorizeResponderMockRecorder) SetExpiresIn(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetExpiresIn", reflect.TypeOf((*MockDeviceAuthorizeResponder)(nil).SetExpiresIn), arg0) +} + +// SetInterval mocks base method. +func (m *MockDeviceAuthorizeResponder) SetInterval(arg0 int) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetInterval", arg0) +} + +// SetInterval indicates an expected call of SetInterval. +func (mr *MockDeviceAuthorizeResponderMockRecorder) SetInterval(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetInterval", reflect.TypeOf((*MockDeviceAuthorizeResponder)(nil).SetInterval), arg0) +} + +// SetUserCode mocks base method. +func (m *MockDeviceAuthorizeResponder) SetUserCode(arg0 string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetUserCode", arg0) +} + +// SetUserCode indicates an expected call of SetUserCode. +func (mr *MockDeviceAuthorizeResponderMockRecorder) SetUserCode(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetUserCode", reflect.TypeOf((*MockDeviceAuthorizeResponder)(nil).SetUserCode), arg0) +} + +// SetVerificationURI mocks base method. +func (m *MockDeviceAuthorizeResponder) SetVerificationURI(arg0 string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetVerificationURI", arg0) +} + +// SetVerificationURI indicates an expected call of SetVerificationURI. +func (mr *MockDeviceAuthorizeResponderMockRecorder) SetVerificationURI(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetVerificationURI", reflect.TypeOf((*MockDeviceAuthorizeResponder)(nil).SetVerificationURI), arg0) +} + +// SetVerificationURIComplete mocks base method. +func (m *MockDeviceAuthorizeResponder) SetVerificationURIComplete(arg0 string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetVerificationURIComplete", arg0) +} + +// SetVerificationURIComplete indicates an expected call of SetVerificationURIComplete. +func (mr *MockDeviceAuthorizeResponderMockRecorder) SetVerificationURIComplete(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetVerificationURIComplete", reflect.TypeOf((*MockDeviceAuthorizeResponder)(nil).SetVerificationURIComplete), arg0) +} From 17b8665f8bc47e06d42a6a9699c4bf72b6e64374 Mon Sep 17 00:00:00 2001 From: Romain Caire Date: Mon, 12 Sep 2022 12:48:47 +0200 Subject: [PATCH 13/49] Remove unused files & add some more tests --- compose/compose.go | 1 - compose/compose_openid.go | 17 -- device_authorize_response.go | 4 + handler/oauth2/strategy_hmacsha_test.go | 68 +++++++ handler/openid/flow_device_auth.go | 82 -------- handler/openid/flow_device_token.go | 94 --------- handler/openid/validator_test.go | 246 ++++++++++++++++++++++++ 7 files changed, 318 insertions(+), 194 deletions(-) delete mode 100644 handler/openid/flow_device_auth.go delete mode 100644 handler/openid/flow_device_token.go diff --git a/compose/compose.go b/compose/compose.go index f506f388f..be56804e6 100644 --- a/compose/compose.go +++ b/compose/compose.go @@ -104,7 +104,6 @@ func ComposeAllEnabled(config *fosite.Config, storage interface{}, key interface OpenIDConnectImplicitFactory, OpenIDConnectHybridFactory, OpenIDConnectRefreshFactory, - OpenIDConnectDeviceFactory, OAuth2TokenIntrospectionFactory, OAuth2TokenRevocationFactory, diff --git a/compose/compose_openid.go b/compose/compose_openid.go index e1b4b7e13..4ced36a55 100644 --- a/compose/compose_openid.go +++ b/compose/compose_openid.go @@ -97,20 +97,3 @@ 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{ - CoreStorage: storage.(oauth2.CoreStorage), - DeviceCodeStrategy: strategy.(oauth2.DeviceCodeStrategy), - UserCodeStrategy: strategy.(oauth2.UserCodeStrategy), - OpenIDConnectRequestStorage: storage.(openid.OpenIDConnectRequestStorage), - IDTokenHandleHelper: &openid.IDTokenHandleHelper{ - IDTokenStrategy: strategy.(openid.OpenIDConnectTokenStrategy), - }, - OpenIDConnectRequestValidator: openid.NewOpenIDConnectRequestValidator(strategy.(jwt.Signer), config), - Config: config, - } -} diff --git a/device_authorize_response.go b/device_authorize_response.go index 26776dbbb..54dbab92d 100644 --- a/device_authorize_response.go +++ b/device_authorize_response.go @@ -30,6 +30,10 @@ type DeviceAuthorizeResponse struct { expiresIn int64 } +func NewDeviceAuthorizeResponse() *DeviceAuthorizeResponse { + return &DeviceAuthorizeResponse{} +} + func (d *DeviceAuthorizeResponse) GetDeviceCode() string { return d.deviceCode } diff --git a/handler/oauth2/strategy_hmacsha_test.go b/handler/oauth2/strategy_hmacsha_test.go index 3a5c1653d..ada94becb 100644 --- a/handler/oauth2/strategy_hmacsha_test.go +++ b/handler/oauth2/strategy_hmacsha_test.go @@ -23,6 +23,7 @@ package oauth2 import ( "fmt" + "regexp" "strings" "testing" "time" @@ -51,6 +52,8 @@ var hmacExpiredCase = fosite.Request{ fosite.AccessToken: time.Now().UTC().Add(-time.Hour), fosite.AuthorizeCode: time.Now().UTC().Add(-time.Hour), fosite.RefreshToken: time.Now().UTC().Add(-time.Hour), + fosite.UserCode: time.Now().UTC().Add(-time.Hour), + fosite.DeviceCode: time.Now().UTC().Add(-time.Hour), }, }, } @@ -64,6 +67,8 @@ var hmacValidCase = fosite.Request{ fosite.AccessToken: time.Now().UTC().Add(time.Hour), fosite.AuthorizeCode: time.Now().UTC().Add(time.Hour), fosite.RefreshToken: time.Now().UTC().Add(time.Hour), + fosite.UserCode: time.Now().UTC().Add(time.Hour), + fosite.DeviceCode: time.Now().UTC().Add(time.Hour), }, }, } @@ -197,3 +202,66 @@ func TestHMACAuthorizeCode(t *testing.T) { }) } } + +func TestHMACUserCode(t *testing.T) { + for k, c := range []struct { + r fosite.Request + pass bool + }{ + { + r: hmacValidCase, + pass: true, + }, + { + r: hmacExpiredCase, + pass: false, + }, + } { + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + userCode, signature, err := hmacshaStrategy.GenerateUserCode(nil) + assert.NoError(t, err) + regex := regexp.MustCompile("[BCDFGHJKLMNPQRSTVWXZ]{8}") + assert.Equal(t, len(regex.FindString(userCode)), len(userCode)) + + err = hmacshaStrategy.ValidateUserCode(nil, &c.r, userCode) + if c.pass { + assert.NoError(t, err) + validate := hmacshaStrategy.Enigma.GenerateHMACForString(nil, userCode) + assert.Equal(t, signature, validate) + } 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, + }, + } { + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + token, signature, err := hmacshaStrategy.GenerateDeviceCode(nil) + assert.NoError(t, err) + assert.Equal(t, 100, len(token)) + + err = hmacshaStrategy.ValidateDeviceCode(nil, &c.r, token) + if c.pass { + assert.NoError(t, err) + validate := hmacshaStrategy.Enigma.GenerateHMACForString(nil, token) + assert.Equal(t, signature, validate) + } else { + assert.Error(t, err) + } + }) + } +} diff --git a/handler/openid/flow_device_auth.go b/handler/openid/flow_device_auth.go deleted file mode 100644 index 291faa633..000000000 --- a/handler/openid/flow_device_auth.go +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright © 2015-2018 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-2018 Aeneas Rekkas - * @license Apache-2.0 - * - */ - -package openid - -import ( - "context" - - "github.com/ory/fosite/handler/oauth2" - "github.com/ory/x/errorsx" - - "github.com/ory/fosite" -) - -type OpenIDConnectDeviceHandler struct { - CoreStorage oauth2.CoreStorage - DeviceCodeStrategy oauth2.DeviceCodeStrategy - UserCodeStrategy oauth2.UserCodeStrategy - - OpenIDConnectRequestStorage OpenIDConnectRequestStorage - OpenIDConnectRequestValidator *OpenIDConnectRequestValidator - - Config fosite.Configurator - - *IDTokenHandleHelper -} - -func (c *OpenIDConnectDeviceHandler) HandleAuthorizeEndpointRequest(ctx context.Context, ar fosite.AuthorizeRequester, resp fosite.AuthorizeResponder) error { - if !(ar.GetGrantedScopes().Has("openid") && ar.GetResponseTypes().ExactOne("device_code")) { - return nil - } - - if !ar.GetClient().GetGrantTypes().Has("urn:ietf:params:oauth:grant-type:device_code") { - return nil - } - - userCode := ar.GetRequestForm().Get("user_code") - userCodeSignature := c.UserCodeStrategy.UserCodeSignature(ctx, userCode) - - userSession, err := c.CoreStorage.GetUserCodeSession(ctx, userCodeSignature, fosite.NewRequest().Session) - if err != nil { - return errorsx.WithStack(fosite.ErrNotFound.WithDebug("User session not found.")) - } - - deviceSession, err := c.CoreStorage.GetDeviceCodeSession(ctx, userSession.GetID(), fosite.NewRequest().Session) - if err != nil { - return errorsx.WithStack(fosite.ErrNotFound.WithDebug("The devicve code has not been issued yet.")) - } - - if len(deviceSession.GetID()) == 0 { - return errorsx.WithStack(fosite.ErrMisconfiguration.WithDebug("The devicve code has not been issued yet, indicating a broken code configuration.")) - } - - if err := c.OpenIDConnectRequestValidator.ValidatePrompt(ctx, ar); err != nil { - return err - } - - // The device code is stored in the ID field of the requester, use this to build the OpenID session as the token endpoint will not know about the user_code - if err := c.OpenIDConnectRequestStorage.CreateOpenIDConnectSession(ctx, userSession.GetID(), ar.Sanitize(oidcParameters)); err != nil { - return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) - } - - return nil -} diff --git a/handler/openid/flow_device_token.go b/handler/openid/flow_device_token.go deleted file mode 100644 index a33bd1673..000000000 --- a/handler/openid/flow_device_token.go +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright © 2015-2018 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-2018 Aeneas Rekkas - * @license Apache-2.0 - * - */ - -package openid - -import ( - "context" - - "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 (c *OpenIDConnectDeviceHandler) PopulateTokenEndpointResponse(ctx context.Context, requester fosite.AccessRequester, responder fosite.AccessResponder) error { - - if !c.CanHandleTokenEndpointRequest(ctx, requester) { - return errorsx.WithStack(fosite.ErrUnknownRequest) - } - - code := requester.GetRequestForm().Get("device_code") - if code == "" { - return errorsx.WithStack(errorsx.WithStack(fosite.ErrUnknownRequest.WithHint("device_code missing form body"))) - } - codeSignature := c.DeviceCodeStrategy.DeviceCodeSignature(ctx, code) - - authorize, err := c.OpenIDConnectRequestStorage.GetOpenIDConnectSession(ctx, codeSignature, 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("urn:ietf:params:oauth:grant-type:device_code") { - 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 := requester.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("urn:ietf:params:oauth:grant-type:device_code") -} diff --git a/handler/openid/validator_test.go b/handler/openid/validator_test.go index 9c3ff8775..4a47a45a4 100644 --- a/handler/openid/validator_test.go +++ b/handler/openid/validator_test.go @@ -286,6 +286,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.DeviceAuthorizeRequest{ + 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 From 375d89e37e01f2b5267fbbdd66f9bc414bf23962 Mon Sep 17 00:00:00 2001 From: Romain Caire Date: Mon, 12 Sep 2022 13:04:46 +0200 Subject: [PATCH 14/49] Use same method do generate authorization code & device code --- handler/oauth2/flow_device_grant_auth_test.go | 3 ++- handler/oauth2/strategy_hmacsha.go | 20 ++++++++++------ handler/oauth2/strategy_hmacsha_test.go | 24 ++++++++++++------- 3 files changed, 31 insertions(+), 16 deletions(-) diff --git a/handler/oauth2/flow_device_grant_auth_test.go b/handler/oauth2/flow_device_grant_auth_test.go index 8ffaccf5c..1a4e1e8ca 100644 --- a/handler/oauth2/flow_device_grant_auth_test.go +++ b/handler/oauth2/flow_device_grant_auth_test.go @@ -44,7 +44,8 @@ func Test_HandleDeviceAuthorizeEndpointRequest(t *testing.T) { assert.NotEmpty(t, resp.GetDeviceCode()) assert.NotEmpty(t, resp.GetUserCode()) assert.Equal(t, len(resp.GetUserCode()), 8) - assert.Equal(t, len(resp.GetDeviceCode()), 100) + assert.Contains(t, resp.GetDeviceCode(), "ory_dc_") + assert.Contains(t, resp.GetDeviceCode(), ".") assert.Equal(t, resp.GetVerificationURI(), "www.test.com") } diff --git a/handler/oauth2/strategy_hmacsha.go b/handler/oauth2/strategy_hmacsha.go index f2a387ee2..641ff769f 100644 --- a/handler/oauth2/strategy_hmacsha.go +++ b/handler/oauth2/strategy_hmacsha.go @@ -178,21 +178,27 @@ func (h *HMACSHAStrategy) ValidateUserCode(ctx context.Context, r fosite.Request } func (h *HMACSHAStrategy) GenerateDeviceCode(ctx context.Context) (token string, signature string, err error) { - deviceCode, err := h.generateRandomString(100) - return deviceCode, h.DeviceCodeSignature(ctx, deviceCode), err + token, sig, err := h.Enigma.Generate(ctx) + if err != nil { + return "", "", err + } + + return h.setPrefix(token, "dc"), sig, nil } func (h *HMACSHAStrategy) DeviceCodeSignature(ctx context.Context, token string) string { - return h.Enigma.GenerateHMACForString(ctx, token) + return h.Enigma.Signature(token) } func (h *HMACSHAStrategy) ValidateDeviceCode(ctx context.Context, r fosite.Requester, code string) (err error) { - var exp = r.GetSession().GetExpiresAt(fosite.UserCode) + 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.ErrTokenExpired.WithHintf("1 Device code expired at '%s'.", r.GetRequestedAt().Add(h.Config.GetDeviceAndUserCodeLifespan(ctx)))) + return errorsx.WithStack(fosite.ErrTokenExpired.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.ErrTokenExpired.WithHintf("2 Device code expired at '%s'.", exp)) + return errorsx.WithStack(fosite.ErrTokenExpired.WithHintf("Device code expired at '%s'.", exp)) } - return nil + + return h.Enigma.Validate(ctx, h.trimPrefix(code, "dc")) } diff --git a/handler/oauth2/strategy_hmacsha_test.go b/handler/oauth2/strategy_hmacsha_test.go index ada94becb..623584e0a 100644 --- a/handler/oauth2/strategy_hmacsha_test.go +++ b/handler/oauth2/strategy_hmacsha_test.go @@ -252,15 +252,23 @@ func TestHMACDeviceCode(t *testing.T) { t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { token, signature, err := hmacshaStrategy.GenerateDeviceCode(nil) assert.NoError(t, err) - assert.Equal(t, 100, len(token)) + assert.Equal(t, strings.Split(token, ".")[1], signature) + assert.Contains(t, token, "ory_dc_") - err = hmacshaStrategy.ValidateDeviceCode(nil, &c.r, token) - if c.pass { - assert.NoError(t, err) - validate := hmacshaStrategy.Enigma.GenerateHMACForString(nil, token) - assert.Equal(t, signature, validate) - } else { - assert.Error(t, err) + 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(nil, &c.r, token) + if c.pass { + assert.NoError(t, err) + validate := hmacshaStrategy.Enigma.Signature(token) + assert.Equal(t, signature, validate) + } else { + assert.Error(t, err) + } + }) } }) } From dbd7860a632ab38594a9053e7bef67c526358956 Mon Sep 17 00:00:00 2001 From: Romain Caire Date: Mon, 12 Sep 2022 13:44:10 +0200 Subject: [PATCH 15/49] Fix comment formating --- compose/compose.go | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/compose/compose.go b/compose/compose.go index be56804e6..47894ba33 100644 --- a/compose/compose.go +++ b/compose/compose.go @@ -32,24 +32,24 @@ type Factory func(config fosite.Configurator, storage interface{}, strategy inte // Compose takes a config, a storage, a strategy and handlers to instantiate an OAuth2Provider: // -// import "github.com/ory/fosite/compose" +// import "github.com/ory/fosite/compose" // -// // var storage = new(MyFositeStorage) -// var config = Config { -// AccessTokenLifespan: time.Minute * 30, -// // check Config for further configuration options -// } +// // var storage = new(MyFositeStorage) +// var config = Config { +// AccessTokenLifespan: time.Minute * 30, +// // check Config for further configuration options +// } // -// var strategy = NewOAuth2HMACStrategy(config) +// var strategy = NewOAuth2HMACStrategy(config) // -// var oauth2Provider = Compose( -// config, -// storage, -// strategy, -// NewOAuth2AuthorizeExplicitHandler, -// OAuth2ClientCredentialsGrantFactory, -// // for a complete list refer to the docs of this package -// ) +// var oauth2Provider = Compose( +// config, +// storage, +// strategy, +// NewOAuth2AuthorizeExplicitHandler, +// OAuth2ClientCredentialsGrantFactory, +// // for a complete list refer to the docs of this package +// ) // // Compose makes use of interface{} types in order to be able to handle a all types of stores, strategies and handlers. func Compose(config *fosite.Config, storage interface{}, strategy interface{}, factories ...Factory) fosite.OAuth2Provider { From 8585c750e8945467cf8ebc5935923d91d25aaf71 Mon Sep 17 00:00:00 2001 From: Romain Caire Date: Mon, 12 Sep 2022 14:20:05 +0200 Subject: [PATCH 16/49] Add test to device pkce --- compose/compose_pkce.go | 10 +- handler/pkce/handler_device.go | 52 ++-- handler/pkce/handler_device_test.go | 388 ++++++++++++++++++++++++++++ 3 files changed, 418 insertions(+), 32 deletions(-) create mode 100644 handler/pkce/handler_device_test.go diff --git a/compose/compose_pkce.go b/compose/compose_pkce.go index 4dfc003d0..405a8d488 100644 --- a/compose/compose_pkce.go +++ b/compose/compose_pkce.go @@ -38,11 +38,9 @@ func OAuth2PKCEFactory(config fosite.Configurator, storage interface{}, strategy func OAuth2DevicePKCEFactory(config fosite.Configurator, storage interface{}, strategy interface{}) interface{} { return &pkce.HandlerDevice{ - CoreStorage: storage.(oauth2.CoreStorage), - DeviceCodeStrategy: strategy.(oauth2.DeviceCodeStrategy), - UserCodeStrategy: strategy.(oauth2.UserCodeStrategy), - AuthorizeCodeStrategy: strategy.(oauth2.AuthorizeCodeStrategy), - Storage: storage.(pkce.PKCERequestStorage), - Config: config, + DeviceCodeStrategy: strategy.(oauth2.DeviceCodeStrategy), + UserCodeStrategy: strategy.(oauth2.UserCodeStrategy), + Storage: storage.(pkce.PKCERequestStorage), + Config: config, } } diff --git a/handler/pkce/handler_device.go b/handler/pkce/handler_device.go index 8397fd639..1595f4694 100644 --- a/handler/pkce/handler_device.go +++ b/handler/pkce/handler_device.go @@ -25,7 +25,7 @@ import ( "context" "crypto/sha256" "encoding/base64" - "fmt" + "regexp" "github.com/ory/x/errorsx" @@ -35,15 +35,24 @@ import ( "github.com/ory/fosite/handler/oauth2" ) +var _ fosite.TokenEndpointHandler = (*HandlerDevice)(nil) + type HandlerDevice struct { - CoreStorage oauth2.CoreStorage - DeviceCodeStrategy oauth2.DeviceCodeStrategy - UserCodeStrategy oauth2.UserCodeStrategy - AuthorizeCodeStrategy oauth2.AuthorizeCodeStrategy - Storage PKCERequestStorage - Config fosite.Configurator + DeviceCodeStrategy oauth2.DeviceCodeStrategy + UserCodeStrategy oauth2.UserCodeStrategy + Storage PKCERequestStorage + Config interface { + fosite.GlobalSecretProvider + fosite.EnforcePKCEProvider + fosite.EnforcePKCEForPublicClientsProvider + fosite.EnablePKCEPlainChallengeMethodProvider + } } +var _ fosite.TokenEndpointHandler = (*HandlerDevice)(nil) + +var deviceVerifierWrongFormat = regexp.MustCompile(`[^\w\.\-~]`) + func (c *HandlerDevice) HandleDeviceAuthorizeEndpointRequest(ctx context.Context, ar fosite.Requester, resp fosite.DeviceAuthorizeResponder) error { if !ar.GetClient().GetGrantTypes().Has("urn:ietf:params:oauth:grant-type:device_code") { return nil @@ -52,28 +61,24 @@ func (c *HandlerDevice) HandleDeviceAuthorizeEndpointRequest(ctx context.Context challenge := ar.GetRequestForm().Get("code_challenge") method := ar.GetRequestForm().Get("code_challenge_method") client := ar.GetClient() - userCode := resp.GetUserCode() - - userCodeSignature := c.UserCodeStrategy.UserCodeSignature(ctx, userCode) - session, err := c.CoreStorage.GetUserCodeSession(ctx, userCodeSignature, fosite.NewRequest().Session) - if err != nil { + if err := c.validate(ctx, challenge, method, client); err != nil { return err } - if err := c.validate(ctx, challenge, method, client); err != nil { - return err + code := resp.GetDeviceCode() + if len(code) == 0 { + return errorsx.WithStack(fosite.ErrServerError.WithDebug("The PKCE handler must be loaded after the device code handler.")) } - if err := c.Storage.CreatePKCERequestSession(ctx, session.GetID(), ar.Sanitize([]string{ + signature := c.DeviceCodeStrategy.DeviceCodeSignature(ctx, code) + if err := c.Storage.CreatePKCERequestSession(ctx, signature, ar.Sanitize([]string{ "code_challenge", "code_challenge_method", })); err != nil { return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) } - fmt.Println(resp) - return nil } @@ -85,7 +90,6 @@ func (c *HandlerDevice) validate(ctx context.Context, challenge string, method s // error response with the "error" value set to "invalid_request". The // "error_description" or the response of "error_uri" SHOULD explain the // nature of error, e.g., code challenge required. - if c.Config.GetEnforcePKCE(ctx) { return errorsx.WithStack(fosite.ErrInvalidRequest. WithHint("Clients must include a code_challenge when performing the authorize code flow, but it is missing."). @@ -137,19 +141,15 @@ func (c *HandlerDevice) HandleTokenEndpointRequest(ctx context.Context, request verifier := request.GetRequestForm().Get("code_verifier") code := request.GetRequestForm().Get("device_code") - if code == "" { - return errorsx.WithStack(errorsx.WithStack(fosite.ErrUnknownRequest.WithHint("device_code missing form body"))) - } - codeSignature := c.DeviceCodeStrategy.DeviceCodeSignature(ctx, code) - - authorizeRequest, err := c.Storage.GetPKCERequestSession(ctx, codeSignature, request.GetSession()) + signature := c.DeviceCodeStrategy.DeviceCodeSignature(ctx, code) + authorizeRequest, err := c.Storage.GetPKCERequestSession(ctx, signature, request.GetSession()) if errors.Is(err, fosite.ErrNotFound) { return errorsx.WithStack(fosite.ErrInvalidGrant.WithHint("Unable to find initial PKCE data tied to this request").WithWrap(err).WithDebug(err.Error())) } else if err != nil { return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) } - if err := c.Storage.DeletePKCERequestSession(ctx, codeSignature); err != nil { + if err := c.Storage.DeletePKCERequestSession(ctx, signature); err != nil { return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) } @@ -177,7 +177,7 @@ func (c *HandlerDevice) HandleTokenEndpointRequest(ctx context.Context, request } else if len(verifier) > 128 { return errorsx.WithStack(fosite.ErrInvalidGrant. WithHint("The PKCE code verifier can not be longer than 128 characters.")) - } else if verifierWrongFormat.MatchString(verifier) { + } else if deviceVerifierWrongFormat.MatchString(verifier) { return errorsx.WithStack(fosite.ErrInvalidGrant. WithHint("The PKCE code verifier must only contain [a-Z], [0-9], '-', '.', '_', '~'.")) } diff --git a/handler/pkce/handler_device_test.go b/handler/pkce/handler_device_test.go new file mode 100644 index 000000000..f1ae2105e --- /dev/null +++ b/handler/pkce/handler_device_test.go @@ -0,0 +1,388 @@ +/* + * Copyright © 2015-2018 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-2018 Aeneas Rekkas + * @license Apache-2.0 + * + */ + +package pkce + +import ( + "context" + "crypto/sha256" + "encoding/base64" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/fosite" + "github.com/ory/fosite/handler/oauth2" + "github.com/ory/fosite/storage" +) + +type mockDeviceCodeStrategy struct { + signature string +} + +func (m *mockDeviceCodeStrategy) DeviceCodeSignature(ctx context.Context, token string) string { + return m.signature +} + +func (m *mockDeviceCodeStrategy) GenerateDeviceCode(ctx context.Context) (token string, signature string, err error) { + return "", "", nil +} + +func (m *mockDeviceCodeStrategy) ValidateDeviceCode(ctx context.Context, requester fosite.Requester, token string) (err error) { + return nil +} + +type mockUserCodeStrategy struct { + signature string +} + +func (m *mockUserCodeStrategy) UserCodeSignature(ctx context.Context, token string) string { + return m.signature +} + +func (m *mockUserCodeStrategy) GenerateUserCode(ctx context.Context) (token string, signature string, err error) { + return "", "", nil +} + +func (m *mockUserCodeStrategy) ValidateUserCode(ctx context.Context, requester fosite.Requester, token string) (err error) { + return nil +} + +func TestPKCEHandlerDevice_HandleDeviceAuthorizeEndpointRequest(t *testing.T) { + var config fosite.Config + h := &HandlerDevice{ + Storage: storage.NewMemoryStore(), + DeviceCodeStrategy: new(oauth2.HMACSHAStrategy), + UserCodeStrategy: new(oauth2.HMACSHAStrategy), + Config: &config, + } + w := fosite.NewDeviceAuthorizeResponse() + r := fosite.NewDeviceAuthorizeRequest() + config.GlobalSecret = []byte("thisissecret") + + w.SetDeviceCode("foo") + + r.Form.Add("code_challenge", "challenge") + r.Form.Add("code_challenge_method", "plain") + + c := &fosite.DefaultClient{} + r.Client = c + require.NoError(t, h.HandleDeviceAuthorizeEndpointRequest(context.Background(), r, w)) + + c = &fosite.DefaultClient{ + GrantTypes: []string{"urn:ietf:params:oauth:grant-type:device_code"}, + } + r.Client = c + require.Error(t, h.HandleDeviceAuthorizeEndpointRequest(context.Background(), r, w)) + + c.Public = true + config.EnablePKCEPlainChallengeMethod = true + require.NoError(t, h.HandleDeviceAuthorizeEndpointRequest(context.Background(), r, w)) + + c.Public = false + config.EnablePKCEPlainChallengeMethod = true + require.NoError(t, h.HandleDeviceAuthorizeEndpointRequest(context.Background(), r, w)) + + config.EnablePKCEPlainChallengeMethod = false + require.Error(t, h.HandleDeviceAuthorizeEndpointRequest(context.Background(), r, w)) + + r.Form.Set("code_challenge_method", "S256") + r.Form.Set("code_challenge", "") + config.EnforcePKCE = true + require.Error(t, h.HandleDeviceAuthorizeEndpointRequest(context.Background(), r, w)) + + r.Form.Set("code_challenge", "challenge") + require.NoError(t, h.HandleDeviceAuthorizeEndpointRequest(context.Background(), r, w)) +} + +func TestPKCEHandlerDevice_HandlerDeviceValidate(t *testing.T) { + s := storage.NewMemoryStore() + ds := &mockDeviceCodeStrategy{} + us := &mockUserCodeStrategy{} + config := &fosite.Config{} + h := &HandlerDevice{ + Storage: s, + UserCodeStrategy: us, + DeviceCodeStrategy: ds, + Config: config, + } + pc := &fosite.DefaultClient{Public: true} + + s256verifier := "KGCt4m8AmjUvIR5ArTByrmehjtbxn1A49YpTZhsH8N7fhDr7LQayn9xx6mck" + hash := sha256.New() + hash.Write([]byte(s256verifier)) + s256challenge := base64.RawURLEncoding.EncodeToString(hash.Sum([]byte{})) + + for k, tc := range []struct { + d string + grant string + force bool + enablePlain bool + challenge string + method string + verifier string + code string + expectErr error + client *fosite.DefaultClient + }{ + { + d: "fails because not auth code flow", + grant: "not_urn:ietf:params:oauth:grant-type:device_code", + expectErr: fosite.ErrUnknownRequest, + }, + { + d: "passes with private client", + grant: "urn:ietf:params:oauth:grant-type:device_code", + challenge: "foofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoo", + verifier: "foofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoo", + method: "plain", + client: &fosite.DefaultClient{Public: false}, + enablePlain: true, + force: true, + code: "valid-code-1", + }, + { + d: "fails because invalid code", + grant: "urn:ietf:params:oauth:grant-type:device_code", + expectErr: fosite.ErrInvalidGrant, + client: pc, + code: "invalid-code-2", + }, + { + d: "passes because auth code flow but pkce is not forced and no challenge given", + grant: "urn:ietf:params:oauth:grant-type:device_code", + client: pc, + code: "valid-code-3", + }, + { + d: "fails because auth code flow and pkce challenge given but plain is disabled", + grant: "urn:ietf:params:oauth:grant-type:device_code", + challenge: "foo", + client: pc, + expectErr: fosite.ErrInvalidRequest, + code: "valid-code-4", + }, + { + d: "passes", + grant: "urn:ietf:params:oauth:grant-type:device_code", + challenge: "foofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoo", + verifier: "foofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoo", + client: pc, + enablePlain: true, + force: true, + code: "valid-code-5", + }, + { + d: "passes", + grant: "urn:ietf:params:oauth:grant-type:device_code", + challenge: "foofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoo", + verifier: "foofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoo", + method: "plain", + client: pc, + enablePlain: true, + force: true, + code: "valid-code-6", + }, + { + d: "fails because challenge and verifier do not match", + grant: "urn:ietf:params:oauth:grant-type:device_code", + challenge: "not-foo", + verifier: "foofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoo", + method: "plain", + client: pc, + enablePlain: true, + code: "valid-code-7", + expectErr: fosite.ErrInvalidGrant, + }, + { + d: "fails because challenge and verifier do not match", + grant: "urn:ietf:params:oauth:grant-type:device_code", + challenge: "not-foonot-foonot-foonot-foonot-foonot-foonot-foonot-foo", + verifier: "foofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoo", + client: pc, + enablePlain: true, + code: "valid-code-8", + expectErr: fosite.ErrInvalidGrant, + }, + { + d: "fails because verifier is too short", + grant: "urn:ietf:params:oauth:grant-type:device_code", + challenge: "foo", + verifier: "foo", + method: "S256", + client: pc, + force: true, + code: "valid-code-9a", + expectErr: fosite.ErrInvalidGrant, + }, + { + d: "fails because verifier is too long", + grant: "urn:ietf:params:oauth:grant-type:device_code", + challenge: "foo", + verifier: "foofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoo", + method: "S256", + client: pc, + force: true, + code: "valid-code-10", + expectErr: fosite.ErrInvalidGrant, + }, + { + d: "fails because verifier is malformed", + grant: "urn:ietf:params:oauth:grant-type:device_code", + challenge: "foo", + verifier: `(!"/$%Z&$T()/)OUZI>$"&=/T(PUOI>"%/)TUOI&/(O/()RGTE>=/(%"/()="$/)(=()=/R/()=))`, + method: "S256", + client: pc, + force: true, + code: "valid-code-11", + expectErr: fosite.ErrInvalidGrant, + }, + { + d: "fails because challenge and verifier do not match", + grant: "urn:ietf:params:oauth:grant-type:device_code", + challenge: "Zm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9v", + verifier: "Zm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9v", + method: "S256", + client: pc, + force: true, + code: "valid-code-12", + expectErr: fosite.ErrInvalidGrant, + }, + { + d: "passes because challenge and verifier match", + grant: "urn:ietf:params:oauth:grant-type:device_code", + challenge: s256challenge, + verifier: s256verifier, + method: "S256", + client: pc, + force: true, + code: "valid-code-13", + }, + } { + t.Run(fmt.Sprintf("case=%d/description=%s", k, tc.d), func(t *testing.T) { + config.EnablePKCEPlainChallengeMethod = tc.enablePlain + config.EnforcePKCE = tc.force + ds.signature = tc.code + ar := fosite.NewAuthorizeRequest() + ar.Form.Add("code_challenge", tc.challenge) + ar.Form.Add("code_challenge_method", tc.method) + require.NoError(t, s.CreatePKCERequestSession(nil, fmt.Sprintf("valid-code-%d", k), ar)) + + r := fosite.NewAccessRequest(nil) + r.Client = tc.client + r.GrantTypes = fosite.Arguments{tc.grant} + r.Form.Add("code_verifier", tc.verifier) + if tc.expectErr == nil { + require.NoError(t, h.HandleTokenEndpointRequest(context.Background(), r)) + } else { + err := h.HandleTokenEndpointRequest(context.Background(), r) + require.EqualError(t, err, tc.expectErr.Error(), "%+v", err) + } + }) + } +} + +func TestPKCEHandlerDevice_HandleTokenEndpointRequest(t *testing.T) { + for k, tc := range []struct { + d string + force bool + forcePublic bool + enablePlain bool + challenge string + method string + expectErr bool + client *fosite.DefaultClient + }{ + { + d: "should pass because pkce is not enforced", + }, + { + d: "should fail because plain is not enabled and method is empty which defaults to plain", + expectErr: true, + force: true, + }, + { + d: "should fail because force is enabled and no challenge was given", + force: true, + enablePlain: true, + expectErr: true, + method: "S256", + }, + { + d: "should fail because forcePublic is enabled, the client is public, and no challenge was given", + forcePublic: true, + client: &fosite.DefaultClient{Public: true}, + expectErr: true, + method: "S256", + }, + { + d: "should fail because although force is enabled and a challenge was given, plain is disabled", + force: true, + expectErr: true, + method: "plain", + challenge: "challenge", + }, + { + d: "should fail because although force is enabled and a challenge was given, plain is disabled and method is empty", + force: true, + expectErr: true, + challenge: "challenge", + }, + { + d: "should fail because invalid challenge method", + force: true, + expectErr: true, + method: "invalid", + challenge: "challenge", + }, + { + d: "should pass because force is enabled with challenge given and method is S256", + force: true, + method: "S256", + challenge: "challenge", + }, + { + d: "should pass because forcePublic is enabled with challenge given and method is S256", + forcePublic: true, + client: &fosite.DefaultClient{Public: true}, + method: "S256", + challenge: "challenge", + }, + } { + t.Run(fmt.Sprintf("case=%d/description=%s", k, tc.d), func(t *testing.T) { + h := &HandlerDevice{ + Config: &fosite.Config{ + EnforcePKCE: tc.force, + EnforcePKCEForPublicClients: tc.forcePublic, + EnablePKCEPlainChallengeMethod: tc.enablePlain, + }, + } + + if tc.expectErr { + assert.Error(t, h.validate(context.Background(), tc.challenge, tc.method, tc.client)) + } else { + assert.NoError(t, h.validate(context.Background(), tc.challenge, tc.method, tc.client)) + } + }) + } +} From 4f7eb2b79ab9b3c7b79e13ff5295f12d8ca18884 Mon Sep 17 00:00:00 2001 From: Romain Caire Date: Tue, 13 Sep 2022 14:31:26 +0200 Subject: [PATCH 17/49] rcaire/create-device-code-at-start --- device_authorize_request.go | 10 +- device_authorize_request_test.go | 4 +- errors.go | 6 +- handler/oauth2/flow_authorize_code_token.go | 4 +- handler/oauth2/flow_device_grant_auth.go | 32 +++- handler/oauth2/flow_device_grant_token.go | 174 ++++++++++++------ .../oauth2/flow_device_grant_token_test.go | 113 +++++++----- handler/oauth2/storage.go | 5 +- handler/oauth2/strategy_hmacsha.go | 8 +- internal/device_authorize_request.go | 24 +-- oauth2.go | 8 +- storage/memory.go | 14 ++ 12 files changed, 259 insertions(+), 143 deletions(-) diff --git a/device_authorize_request.go b/device_authorize_request.go index 75cd9d9d1..fa2477117 100644 --- a/device_authorize_request.go +++ b/device_authorize_request.go @@ -23,16 +23,16 @@ package fosite // DeviceAuthorizeRequest is an implementation of DeviceAuthorizeRequester type DeviceAuthorizeRequest struct { - DeviceCodeSignature string + DeviceRequestId string Request } -func (d *DeviceAuthorizeRequest) GetDeviceCodeSignature() string { - return d.DeviceCodeSignature +func (d *DeviceAuthorizeRequest) GetDeviceRequestId() string { + return d.DeviceRequestId } -func (d *DeviceAuthorizeRequest) SetDeviceCodeSignature(signature string) { - d.DeviceCodeSignature = signature +func (d *DeviceAuthorizeRequest) SetDeviceRequestId(id string) { + d.DeviceRequestId = id } func NewDeviceAuthorizeRequest() *DeviceAuthorizeRequest { diff --git a/device_authorize_request_test.go b/device_authorize_request_test.go index 701c85255..e39af6ee2 100644 --- a/device_authorize_request_test.go +++ b/device_authorize_request_test.go @@ -47,7 +47,7 @@ func TestDeviceAuthorizeRequest(t *testing.T) { }, { ar: &DeviceAuthorizeRequest{ - DeviceCodeSignature: "AAAA", + DeviceRequestId: "a-uuid-xxx", Request: Request{ Client: &DefaultClient{RedirectURIs: []string{""}}, }, @@ -64,7 +64,7 @@ func TestDeviceAuthorizeRequest(t *testing.T) { }, } { assert.Equal(t, c.ar.Client, c.ar.GetClient(), "%d", k) - assert.Equal(t, c.ar.DeviceCodeSignature, c.ar.GetDeviceCodeSignature(), "%d", k) + assert.Equal(t, c.ar.DeviceRequestId, c.ar.GetDeviceRequestId(), "%d", k) assert.Equal(t, c.ar.RequestedAt, c.ar.GetRequestedAt(), "%d", k) assert.Equal(t, c.ar.RequestedScope, c.ar.GetRequestedScopes(), "%d", k) diff --git a/errors.go b/errors.go index 4b145358e..ffafb5d62 100644 --- a/errors.go +++ b/errors.go @@ -230,9 +230,9 @@ var ( ErrorField: errAuthorizationPending, CodeField: http.StatusForbidden, } - ErrExpiredToken = &RFC6749Error{ + ErrDeviceExpiredToken = &RFC6749Error{ DescriptionField: "The device_code has expired, and the device authorization session has concluded.", - ErrorField: errExpiredToken, + ErrorField: errDeviceExpiredToken, CodeField: http.StatusForbidden, } ) @@ -273,7 +273,7 @@ const ( errRegistrationNotSupportedName = "registration_not_supported" errJTIKnownName = "jti_known" errAuthorizationPending = "authorization_pending" - errExpiredToken = "expired_token" + errDeviceExpiredToken = "expired_token" ) type ( diff --git a/handler/oauth2/flow_authorize_code_token.go b/handler/oauth2/flow_authorize_code_token.go index 462713994..88a0d89b4 100644 --- a/handler/oauth2/flow_authorize_code_token.go +++ b/handler/oauth2/flow_authorize_code_token.go @@ -121,7 +121,7 @@ func (c *AuthorizeExplicitGrantHandler) HandleTokenEndpointRequest(ctx context.C return nil } -func canIssueRefreshToken(ctx context.Context, c *AuthorizeExplicitGrantHandler, request fosite.Requester) bool { +func (*AuthorizeExplicitGrantHandler) 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...) { @@ -163,7 +163,7 @@ func (c *AuthorizeExplicitGrantHandler) PopulateTokenEndpointResponse(ctx contex } var refresh, refreshSignature string - if canIssueRefreshToken(ctx, c, authorizeRequest) { + if c.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())) diff --git a/handler/oauth2/flow_device_grant_auth.go b/handler/oauth2/flow_device_grant_auth.go index 79ee84425..f72cd28a0 100644 --- a/handler/oauth2/flow_device_grant_auth.go +++ b/handler/oauth2/flow_device_grant_auth.go @@ -11,12 +11,22 @@ import ( // DeviceAuthorizationHandler is a response handler for the Device Authorisation Grant as // defined in https://tools.ietf.org/html/rfc8628#section-3.1 type DeviceAuthorizationHandler struct { - CoreStorage CoreStorage - DeviceCodeStrategy DeviceCodeStrategy - UserCodeStrategy UserCodeStrategy - AccessTokenStrategy AccessTokenStrategy - RefreshTokenStrategy RefreshTokenStrategy - Config fosite.Configurator + AccessTokenStrategy AccessTokenStrategy + RefreshTokenStrategy RefreshTokenStrategy + DeviceCodeStrategy DeviceCodeStrategy + UserCodeStrategy UserCodeStrategy + CoreStorage CoreStorage + TokenRevocationStorage TokenRevocationStorage + Config interface { + fosite.DeviceUriProvider + fosite.DeviceAndUserCodeLifespanProvider + fosite.AccessTokenLifespanProvider + fosite.RefreshTokenLifespanProvider + fosite.ScopeStrategyProvider + fosite.AudienceStrategyProvider + fosite.RefreshTokenScopesProvider + fosite.SanitationAllowedProvider + } } func (d *DeviceAuthorizationHandler) HandleDeviceAuthorizeEndpointRequest(ctx context.Context, dar fosite.Requester, resp fosite.DeviceAuthorizeResponder) error { @@ -30,11 +40,13 @@ func (d *DeviceAuthorizationHandler) HandleDeviceAuthorizeEndpointRequest(ctx co return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) } - // Set User Code expiry time - dar.GetSession().SetExpiresAt(fosite.UserCode, time.Now().UTC().Add(d.Config.GetDeviceAndUserCodeLifespan(ctx)).Round(time.Second)) - dar.SetID(deviceCodeSignature) - // 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.CoreStorage.CreateDeviceCodeSession(ctx, deviceCodeSignature, dar); err != nil { + return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) + } + + dar.GetSession().SetExpiresAt(fosite.UserCode, time.Now().UTC().Add(d.Config.GetDeviceAndUserCodeLifespan(ctx)).Round(time.Second)) if err := d.CoreStorage.CreateUserCodeSession(ctx, userCodeSignature, dar); err != nil { return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) } diff --git a/handler/oauth2/flow_device_grant_token.go b/handler/oauth2/flow_device_grant_token.go index c8d2f5ae0..8cbf43e4d 100644 --- a/handler/oauth2/flow_device_grant_token.go +++ b/handler/oauth2/flow_device_grant_token.go @@ -1,3 +1,24 @@ +/* + * Copyright © 2015-2018 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-2018 Aeneas Rekkas + * @license Apache-2.0 + * + */ + package oauth2 import ( @@ -7,145 +28,184 @@ import ( "github.com/ory/fosite" "github.com/ory/fosite/storage" "github.com/ory/x/errorsx" + "github.com/pkg/errors" ) const deviceCodeGrantType = "urn:ietf:params:oauth:grant-type:device_code" -func (d *DeviceAuthorizationHandler) HandleTokenEndpointRequest(ctx context.Context, requester fosite.AccessRequester) error { - if !d.CanHandleTokenEndpointRequest(ctx, requester) { +// HandleTokenEndpointRequest implements +// * https://tools.ietf.org/html/rfc8628#section-3.4 (everything) +func (c *DeviceAuthorizationHandler) HandleTokenEndpointRequest(ctx context.Context, request fosite.AccessRequester) error { + if !c.CanHandleTokenEndpointRequest(ctx, request) { return errorsx.WithStack(errorsx.WithStack(fosite.ErrUnknownRequest)) } - if !requester.GetClient().GetGrantTypes().Has(deviceCodeGrantType) { + if !request.GetClient().GetGrantTypes().Has(deviceCodeGrantType) { return errorsx.WithStack(fosite.ErrUnauthorizedClient.WithHint("The OAuth 2.0 Client is not allowed to use authorization grant \"" + deviceCodeGrantType + "\".")) } - code := requester.GetRequestForm().Get("device_code") - if code == "" { - return errorsx.WithStack(errorsx.WithStack(fosite.ErrUnknownRequest.WithHint("device_code missing form body"))) - } - codeSignature := d.DeviceCodeStrategy.DeviceCodeSignature(ctx, code) + code := request.GetRequestForm().Get("device_code") + signature := c.DeviceCodeStrategy.DeviceCodeSignature(ctx, code) + authorizeRequest, err := c.CoreStorage.GetDeviceCodeSession(ctx, signature, request.GetSession()) + if 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("GetAuthorizeCodeSession must return a value for \"fosite.Requester\" when returning \"ErrInvalidatedAuthorizeCode\".") + } - // Get the device code session to validate based on HMAC of the device code supplied - session, err := d.CoreStorage.GetDeviceCodeSession(ctx, codeSignature, requester.GetSession()) + // 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) { + 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())) + } - if err != nil { - return errorsx.WithStack(fosite.ErrAuthorizationPending) + // 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.DeviceCodeStrategy.ValidateDeviceCode(ctx, request, code); err != nil { + return errorsx.WithStack(err) } - requester.SetRequestedScopes(session.GetRequestedScopes()) - requester.SetRequestedAudience(session.GetRequestedAudience()) + // Override scopes + request.SetRequestedScopes(authorizeRequest.GetRequestedScopes()) + + // Override audiences + request.SetRequestedAudience(authorizeRequest.GetRequestedAudience()) - if requester.GetClient().GetID() != session.GetClient().GetID() { + // 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.")) } - atLifespan := fosite.GetEffectiveLifespan(requester.GetClient(), fosite.GrantTypeAuthorizationCode, fosite.AccessToken, d.Config.GetAccessTokenLifespan(ctx)) - requester.GetSession().SetExpiresAt(fosite.AccessToken, time.Now().UTC().Add(atLifespan).Round(time.Second)) + // 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()) - rtLifespan := fosite.GetEffectiveLifespan(requester.GetClient(), fosite.GrantTypeAuthorizationCode, fosite.RefreshToken, d.Config.GetRefreshTokenLifespan(ctx)) + 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 { - requester.GetSession().SetExpiresAt(fosite.RefreshToken, time.Now().UTC().Add(rtLifespan).Round(time.Second)) + request.GetSession().SetExpiresAt(fosite.RefreshToken, time.Now().UTC().Add(rtLifespan).Round(time.Second)) } return nil } -func (d *DeviceAuthorizationHandler) CanSkipClientAuth(ctx context.Context, requester fosite.AccessRequester) bool { +func (*DeviceAuthorizationHandler) canIssueRefreshToken(ctx context.Context, c *DeviceAuthorizationHandler, 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 (d *DeviceAuthorizationHandler) CanHandleTokenEndpointRequest(ctx context.Context, requester fosite.AccessRequester) bool { - // grant_type REQUIRED. - // Value MUST be set to "urn:ietf:params:oauth:grant-type:device_code" - return requester.GetGrantTypes().ExactOne(deviceCodeGrantType) -} - -func (d *DeviceAuthorizationHandler) PopulateTokenEndpointResponse(ctx context.Context, requester fosite.AccessRequester, responder fosite.AccessResponder) error { - if !d.CanHandleTokenEndpointRequest(ctx, requester) { +func (c *DeviceAuthorizationHandler) PopulateTokenEndpointResponse(ctx context.Context, requester fosite.AccessRequester, responder fosite.AccessResponder) error { + if !c.CanHandleTokenEndpointRequest(ctx, requester) { return errorsx.WithStack(fosite.ErrUnknownRequest) } code := requester.GetRequestForm().Get("device_code") - if code == "" { - return errorsx.WithStack(errorsx.WithStack(fosite.ErrUnknownRequest.WithHint("device_code missing form body"))) - } - signature := d.DeviceCodeStrategy.DeviceCodeSignature(ctx, code) - - // Get the device code session ready for exchange to auth / refresh / oidc sessions - session, err := d.CoreStorage.GetDeviceCodeSession(ctx, signature, requester.GetSession()) + signature := c.DeviceCodeStrategy.DeviceCodeSignature(ctx, code) + authorizeRequest, err := c.CoreStorage.GetDeviceCodeSession(ctx, signature, requester.GetSession()) if err != nil { + return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) + } else if err := c.DeviceCodeStrategy.ValidateDeviceCode(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 session.GetGrantedScopes() { + for _, scope := range authorizeRequest.GetGrantedScopes() { requester.GrantScope(scope) } - for _, audience := range session.GetGrantedAudience() { + for _, audience := range authorizeRequest.GetGrantedAudience() { requester.GrantAudience(audience) } - access, accessSignature, err := d.AccessTokenStrategy.GenerateAccessToken(ctx, requester) + 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 d.canIssueRefreshToken(ctx, d, session) { - refresh, refreshSignature, err = d.RefreshTokenStrategy.GenerateRefreshToken(ctx, requester) + if c.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, d.CoreStorage) + 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, d.CoreStorage); rollBackTxnErr != 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 = d.CoreStorage.InvalidateDeviceCodeSession(ctx, signature); err != nil { + if err = c.CoreStorage.InvalidateDeviceCodeSession(ctx, signature); err != nil { return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) - } else if err = d.CoreStorage.CreateAccessTokenSession(ctx, accessSignature, requester.Sanitize([]string{})); err != nil { + } 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 = d.CoreStorage.CreateRefreshTokenSession(ctx, refreshSignature, requester.Sanitize([]string{})); err != nil { + 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, d.Config.GetAccessTokenLifespan(ctx)) + 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, d.CoreStorage); err != nil { + if err = storage.MaybeCommitTx(ctx, c.CoreStorage); err != nil { return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) } return nil } -func (c *DeviceAuthorizationHandler) canIssueRefreshToken(ctx context.Context, config *DeviceAuthorizationHandler, request fosite.Requester) bool { - scope := config.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 - } +func (c *DeviceAuthorizationHandler) CanSkipClientAuth(ctx context.Context, requester fosite.AccessRequester) bool { return true } + +func (c *DeviceAuthorizationHandler) CanHandleTokenEndpointRequest(ctx context.Context, requester fosite.AccessRequester) bool { + // grant_type REQUIRED. + // Value MUST be set to "urn:ietf:params:oauth:grant-type:device_code" + return requester.GetGrantTypes().ExactOne(deviceCodeGrantType) +} diff --git a/handler/oauth2/flow_device_grant_token_test.go b/handler/oauth2/flow_device_grant_token_test.go index f798d389d..0409a97d1 100644 --- a/handler/oauth2/flow_device_grant_token_test.go +++ b/handler/oauth2/flow_device_grant_token_test.go @@ -81,7 +81,7 @@ func TestDeviceAuthorizeCode_PopulateTokenEndpointResponse(t *testing.T) { require.NoError(t, err) areq.Form.Set("device_code", code) }, - expectErr: fosite.ErrInvalidRequest, + expectErr: fosite.ErrServerError, }, { areq: &fosite.AccessRequest{ @@ -293,27 +293,27 @@ func TestDeviceAuthorizeCode_HandleTokenEndpointRequest(t *testing.T) { RequestedAt: time.Now().UTC(), }, }, - description: "should fail because device code could not be retrieved (1)", + description: "should fail because device code could not be retrieved", setup: func(t *testing.T, areq *fosite.AccessRequest, authreq *fosite.DeviceAuthorizeRequest) { deviceCode, _, err := strategy.GenerateDeviceCode(context.TODO()) require.NoError(t, err) areq.Form = url.Values{"device_code": {deviceCode}} }, - expectErr: fosite.ErrAuthorizationPending, + 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{ - // 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"}, @@ -365,34 +365,61 @@ func TestDeviceAuthorizeCode_HandleTokenEndpointRequest(t *testing.T) { 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.DeviceAuthorizeRequest) { - // 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.DeviceAuthorizeRequest) { - // code, sig, err := strategy.GenerateDeviceCode(context.TODO()) - // require.NoError(t, err) - // areq.Form.Add("device_code", code) + { + 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.DeviceAuthorizeRequest) { + 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.DeviceAuthorizeRequest) { + code, sig, err := strategy.GenerateAuthorizeCode(nil, nil) + require.NoError(t, err) + areq.Form.Add("code", code) - // require.NoError(t, store.CreateDeviceCodeSession(context.TODO(), sig, areq)) - // require.NoError(t, store.InvalidateDeviceCodeSession(context.TODO(), sig)) - // }, - // description: "should fail because device code has expired", - // expectErr: fosite.ErrExpiredToken, - // }, + require.NoError(t, store.CreateAuthorizeCodeSession(nil, sig, areq)) + require.NoError(t, store.InvalidateAuthorizeCodeSession(nil, sig)) + }, + description: "should fail because code has been used already", + expectErr: fosite.ErrInvalidGrant, + }, + { + 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.DeviceAuthorizeRequest) { + 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.DeviceAuthorizeRequest) { + 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 { diff --git a/handler/oauth2/storage.go b/handler/oauth2/storage.go index 963d5ac42..450828768 100644 --- a/handler/oauth2/storage.go +++ b/handler/oauth2/storage.go @@ -73,8 +73,11 @@ type DeviceCodeStorage interface { // CreateDeviceCodeSession stores the device request for a given device code. CreateDeviceCodeSession(ctx context.Context, signature string, request fosite.Requester) (err error) + // UpdateDeviceCodeSessionByRequestId udpate in store the device request for a given device code. + UpdateDeviceCodeSessionByRequestId(ctx context.Context, id string, request fosite.Requester) (err error) + // GetDeviceCodeSession hydrates the session based on the given device code and returns the device request. - // If the user code has been invalidated with `InvalidateDeviceCodeSession`, this + // 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! diff --git a/handler/oauth2/strategy_hmacsha.go b/handler/oauth2/strategy_hmacsha.go index 641ff769f..a585e3956 100644 --- a/handler/oauth2/strategy_hmacsha.go +++ b/handler/oauth2/strategy_hmacsha.go @@ -169,10 +169,10 @@ func (h *HMACSHAStrategy) UserCodeSignature(ctx context.Context, token string) s func (h *HMACSHAStrategy) 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.ErrTokenExpired.WithHintf("Access token expired at '%s'.", r.GetRequestedAt().Add(h.Config.GetDeviceAndUserCodeLifespan(ctx)))) + 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.ErrTokenExpired.WithHintf("Access token expired at '%s'.", exp)) + return errorsx.WithStack(fosite.ErrDeviceExpiredToken.WithHintf("User code expired at '%s'.", exp)) } return nil } @@ -193,11 +193,11 @@ func (h *HMACSHAStrategy) DeviceCodeSignature(ctx context.Context, token string) func (h *HMACSHAStrategy) 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.ErrTokenExpired.WithHintf("Device code expired at '%s'.", r.GetRequestedAt().Add(h.Config.GetDeviceAndUserCodeLifespan(ctx)))) + 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.ErrTokenExpired.WithHintf("Device code expired at '%s'.", exp)) + return errorsx.WithStack(fosite.ErrDeviceExpiredToken.WithHintf("Device code expired at '%s'.", exp)) } return h.Enigma.Validate(ctx, h.trimPrefix(code, "dc")) diff --git a/internal/device_authorize_request.go b/internal/device_authorize_request.go index caba5a89e..13457ac2a 100644 --- a/internal/device_authorize_request.go +++ b/internal/device_authorize_request.go @@ -62,18 +62,18 @@ func (mr *MockDeviceAuthorizeRequesterMockRecorder) GetClient() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClient", reflect.TypeOf((*MockDeviceAuthorizeRequester)(nil).GetClient)) } -// GetDeviceCodeSignature mocks base method. -func (m *MockDeviceAuthorizeRequester) GetDeviceCodeSignature() string { +// GetDeviceRequestId mocks base method. +func (m *MockDeviceAuthorizeRequester) GetDeviceRequestId() string { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetDeviceCodeSignature") + ret := m.ctrl.Call(m, "GetDeviceRequestId") ret0, _ := ret[0].(string) return ret0 } -// GetDeviceCodeSignature indicates an expected call of GetDeviceCodeSignature. -func (mr *MockDeviceAuthorizeRequesterMockRecorder) GetDeviceCodeSignature() *gomock.Call { +// GetDeviceRequestId indicates an expected call of GetDeviceRequestId. +func (mr *MockDeviceAuthorizeRequesterMockRecorder) GetDeviceRequestId() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDeviceCodeSignature", reflect.TypeOf((*MockDeviceAuthorizeRequester)(nil).GetDeviceCodeSignature)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDeviceRequestId", reflect.TypeOf((*MockDeviceAuthorizeRequester)(nil).GetDeviceRequestId)) } // GetGrantedAudience mocks base method. @@ -238,16 +238,16 @@ func (mr *MockDeviceAuthorizeRequesterMockRecorder) Sanitize(arg0 interface{}) * return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Sanitize", reflect.TypeOf((*MockDeviceAuthorizeRequester)(nil).Sanitize), arg0) } -// SetDeviceCodeSignature mocks base method. -func (m *MockDeviceAuthorizeRequester) SetDeviceCodeSignature(arg0 string) { +// SetDeviceRequestId mocks base method. +func (m *MockDeviceAuthorizeRequester) SetDeviceRequestId(arg0 string) { m.ctrl.T.Helper() - m.ctrl.Call(m, "SetDeviceCodeSignature", arg0) + m.ctrl.Call(m, "SetDeviceRequestId", arg0) } -// SetDeviceCodeSignature indicates an expected call of SetDeviceCodeSignature. -func (mr *MockDeviceAuthorizeRequesterMockRecorder) SetDeviceCodeSignature(arg0 interface{}) *gomock.Call { +// SetDeviceRequestId indicates an expected call of SetDeviceRequestId. +func (mr *MockDeviceAuthorizeRequesterMockRecorder) SetDeviceRequestId(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDeviceCodeSignature", reflect.TypeOf((*MockDeviceAuthorizeRequester)(nil).SetDeviceCodeSignature), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDeviceRequestId", reflect.TypeOf((*MockDeviceAuthorizeRequester)(nil).SetDeviceRequestId), arg0) } // SetID mocks base method. diff --git a/oauth2.go b/oauth2.go index 9a70d84e4..9662a60d2 100644 --- a/oauth2.go +++ b/oauth2.go @@ -281,11 +281,11 @@ type AccessRequester interface { // DeviceAuthorizeRequester is an device authorize endpoint's request context. type DeviceAuthorizeRequester interface { - // SetDeviceCodeSignature set the device code signature - SetDeviceCodeSignature(signature string) + // SetDeviceRequestId set the device request id + SetDeviceRequestId(signature string) - // GetDeviceCodeSignature returns the device code signature - GetDeviceCodeSignature() string + // GetDeviceRequestId returns the device code signature + GetDeviceRequestId() string Requester } diff --git a/storage/memory.go b/storage/memory.go index 5b6e5e1f8..3fac804ab 100644 --- a/storage/memory.go +++ b/storage/memory.go @@ -544,6 +544,20 @@ func (s *MemoryStore) CreateDeviceCodeSession(_ context.Context, signature strin return nil } +func (s *MemoryStore) UpdateDeviceCodeSessionByRequestId(_ context.Context, id string, req fosite.Requester) error { + s.deviceCodesRequestIDsMutex.Lock() + defer s.deviceCodesRequestIDsMutex.Unlock() + + if signature, exists := s.DeviceCodesRequestIDs[id]; exists { + _, ok := s.DeviceCodes[signature] + if !ok { + return fosite.ErrNotFound + } + s.DeviceCodes[signature] = req + } + return nil +} + func (s *MemoryStore) GetDeviceCodeSession(_ context.Context, signature string, _ fosite.Session) (fosite.Requester, error) { s.deviceCodesMutex.RLock() defer s.deviceCodesMutex.RUnlock() From eea03e0c3411344e120ab6e0583ceb4302eb0298 Mon Sep 17 00:00:00 2001 From: Romain Caire Date: Wed, 14 Sep 2022 12:09:12 +0200 Subject: [PATCH 18/49] Split & Move files - Split request handling between the Device (Non-Interactive Side) & the Device Authorize (Interactive Side) --- compose/compose.go | 37 +-- compose/compose_oauth2.go | 8 +- compose/compose_openid.go | 14 + config.go | 14 +- config_default.go | 18 +- device_authorize_request.go | 10 +- device_authorize_request_handler.go | 40 +-- device_authorize_request_handler_test.go | 122 +------- device_authorize_request_test.go | 4 +- device_authorize_response.go | 59 ---- device_authorize_response_writer.go | 5 +- device_authorize_writer.go | 22 +- device_request.go | 33 +++ device_request_handler.go | 68 +++++ device_request_handler_test.go | 156 ++++++++++ device_response.go | 88 ++++++ device_response_writer.go | 38 +++ ..._test.go => device_response_writer_test.go | 22 +- ...rize_write_test.go => device_write_test.go | 6 +- device_writer.go | 52 ++++ fosite.go | 20 +- generate-mocks.sh | 5 + generate.go | 5 + handler.go | 12 +- ...vice_grant_auth.go => flow_device_auth.go} | 16 +- ..._auth_test.go => flow_device_auth_test.go} | 13 +- ...ce_grant_token.go => flow_device_token.go} | 27 +- ...oken_test.go => flow_device_token_test.go} | 8 +- handler/oauth2/storage.go | 4 +- handler/openid/flow_device_auth.go | 67 +++++ handler/openid/flow_device_token.go | 99 +++++++ handler/pkce/handler_device.go | 3 +- handler/pkce/handler_device_test.go | 18 +- internal/device_authorize_handler.go | 2 +- internal/device_authorize_request.go | 24 +- internal/device_authorize_response.go | 158 ---------- internal/device_code_storage.go | 93 ++++++ internal/device_handler.go | 50 ++++ internal/device_request.go | 273 ++++++++++++++++++ internal/device_response.go | 190 ++++++++++++ internal/oauth2_storage.go | 14 + internal/user_code_storage.go | 79 +++++ oauth2.go | 49 +++- storage/memory.go | 12 +- 44 files changed, 1539 insertions(+), 518 deletions(-) create mode 100644 device_request.go create mode 100644 device_request_handler.go create mode 100644 device_request_handler_test.go create mode 100644 device_response.go create mode 100644 device_response_writer.go rename device_authorize_response_writer_test.go => device_response_writer_test.go (61%) rename device_authorize_write_test.go => device_write_test.go (94%) create mode 100644 device_writer.go rename handler/oauth2/{flow_device_grant_auth.go => flow_device_auth.go} (83%) rename handler/oauth2/{flow_device_grant_auth_test.go => flow_device_auth_test.go} (80%) rename handler/oauth2/{flow_device_grant_token.go => flow_device_token.go} (88%) rename handler/oauth2/{flow_device_grant_token_test.go => flow_device_token_test.go} (99%) create mode 100644 handler/openid/flow_device_auth.go create mode 100644 handler/openid/flow_device_token.go create mode 100644 internal/device_code_storage.go create mode 100644 internal/device_handler.go create mode 100644 internal/device_request.go create mode 100644 internal/device_response.go create mode 100644 internal/user_code_storage.go diff --git a/compose/compose.go b/compose/compose.go index 47894ba33..1c7105068 100644 --- a/compose/compose.go +++ b/compose/compose.go @@ -32,24 +32,24 @@ type Factory func(config fosite.Configurator, storage interface{}, strategy inte // Compose takes a config, a storage, a strategy and handlers to instantiate an OAuth2Provider: // -// import "github.com/ory/fosite/compose" +// import "github.com/ory/fosite/compose" // -// // var storage = new(MyFositeStorage) -// var config = Config { -// AccessTokenLifespan: time.Minute * 30, -// // check Config for further configuration options -// } +// // var storage = new(MyFositeStorage) +// var config = Config { +// AccessTokenLifespan: time.Minute * 30, +// // check Config for further configuration options +// } // -// var strategy = NewOAuth2HMACStrategy(config) +// var strategy = NewOAuth2HMACStrategy(config) // -// var oauth2Provider = Compose( -// config, -// storage, -// strategy, -// NewOAuth2AuthorizeExplicitHandler, -// OAuth2ClientCredentialsGrantFactory, -// // for a complete list refer to the docs of this package -// ) +// var oauth2Provider = Compose( +// config, +// storage, +// strategy, +// NewOAuth2AuthorizeExplicitHandler, +// OAuth2ClientCredentialsGrantFactory, +// // for a complete list refer to the docs of this package +// ) // // Compose makes use of interface{} types in order to be able to handle a all types of stores, strategies and handlers. func Compose(config *fosite.Config, storage interface{}, strategy interface{}, factories ...Factory) fosite.OAuth2Provider { @@ -71,8 +71,8 @@ func Compose(config *fosite.Config, storage interface{}, strategy interface{}, f if ph, ok := res.(fosite.PushedAuthorizeEndpointHandler); ok { config.PushedAuthorizeEndpointHandlers.Append(ph) } - if dah, ok := res.(fosite.DeviceAuthorizeEndpointHandler); ok { - config.DeviceAuthorizeEndpointHandlers.Append(dah) + if dah, ok := res.(fosite.DeviceEndpointHandler); ok { + config.DeviceEndpointHandlers.Append(dah) } } @@ -96,7 +96,7 @@ func ComposeAllEnabled(config *fosite.Config, storage interface{}, key interface OAuth2AuthorizeImplicitFactory, OAuth2ClientCredentialsGrantFactory, OAuth2RefreshTokenGrantFactory, - OAuth2DeviceAuthorizeFactory, + OAuth2DeviceFactory, OAuth2ResourceOwnerPasswordCredentialsFactory, RFC7523AssertionGrantFactory, @@ -104,6 +104,7 @@ func ComposeAllEnabled(config *fosite.Config, storage interface{}, key interface OpenIDConnectImplicitFactory, OpenIDConnectHybridFactory, OpenIDConnectRefreshFactory, + OpenIDConnectDeviceFactory, OAuth2TokenIntrospectionFactory, OAuth2TokenRevocationFactory, diff --git a/compose/compose_oauth2.go b/compose/compose_oauth2.go index 78ae247f7..e76f03444 100644 --- a/compose/compose_oauth2.go +++ b/compose/compose_oauth2.go @@ -126,10 +126,10 @@ func OAuth2StatelessJWTIntrospectionFactory(config fosite.Configurator, storage } } -// OAuth2AuthorizeExplicitFactory creates an OAuth2 device code grant ("Device Authorization Grant") handler and registers -// an access token, refresh token and device code validator. -func OAuth2DeviceAuthorizeFactory(config fosite.Configurator, storage interface{}, strategy interface{}) interface{} { - return &oauth2.DeviceAuthorizationHandler{ +// OAuth2DeviceFactory 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 OAuth2DeviceFactory(config fosite.Configurator, storage interface{}, strategy interface{}) interface{} { + return &oauth2.DeviceHandler{ DeviceCodeStrategy: strategy.(oauth2.DeviceCodeStrategy), UserCodeStrategy: strategy.(oauth2.UserCodeStrategy), AccessTokenStrategy: strategy.(oauth2.AccessTokenStrategy), diff --git a/compose/compose_openid.go b/compose/compose_openid.go index 4ced36a55..89f65caff 100644 --- a/compose/compose_openid.go +++ b/compose/compose_openid.go @@ -97,3 +97,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/config.go b/config.go index 5d4d31e4c..6ce5e4e7f 100644 --- a/config.go +++ b/config.go @@ -23,7 +23,11 @@ type DeviceAndUserCodeLifespanProvider interface { GetDeviceAndUserCodeLifespan(ctx context.Context) time.Duration } -type DeviceUriProvider interface { +type DeviceAuthorizeProvider interface { + GetDeviceDone(ctx context.Context) string +} + +type DeviceProvider interface { GetDeviceVerificationURL(ctx context.Context) string GetDeviceAuthTokenPollingInterval(ctx context.Context) time.Duration } @@ -281,7 +285,15 @@ 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 +} + +// DeviceAuthorizeEndpointHandlersProvider returns the provider for setting up the Device Authorize handlers. type DeviceAuthorizeEndpointHandlersProvider interface { + // GetDeviceAuthorizeEndpointHandlers returns the handlers. GetDeviceAuthorizeEndpointHandlers(ctx context.Context) DeviceAuthorizeEndpointHandlers } diff --git a/config_default.go b/config_default.go index b88a1fed9..ce571a867 100644 --- a/config_default.go +++ b/config_default.go @@ -80,7 +80,7 @@ var ( _ RevocationHandlersProvider = (*Config)(nil) _ PushedAuthorizeRequestHandlersProvider = (*Config)(nil) _ PushedAuthorizeRequestConfigProvider = (*Config)(nil) - _ DeviceAuthorizeEndpointHandlersProvider = (*Config)(nil) + _ DeviceEndpointHandlersProvider = (*Config)(nil) ) type Config struct { @@ -103,6 +103,9 @@ type Config struct { // 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 @@ -222,7 +225,10 @@ type Config struct { // PushedAuthorizeEndpointHandlers is a list of handlers that are called before the PAR endpoint is served. PushedAuthorizeEndpointHandlers PushedAuthorizeEndpointHandlers - // DeviceAuthorizeEndpointHandlers is a list of handlers that are called before the device authorization endpoint is served. + // DeviceEndpointHandlers is a list of handlers that are called before the device endpoint is served. + DeviceEndpointHandlers DeviceEndpointHandlers + + // DeviceAuthorizeEndpointHandlers is a list of handlers that are called before the device authorize endpoint is served. DeviceAuthorizeEndpointHandlers DeviceAuthorizeEndpointHandlers // GlobalSecret is the global secret used to sign and verify signatures. @@ -273,6 +279,10 @@ 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) GetDeviceAuthorizeEndpointHandlers(ctx context.Context) DeviceAuthorizeEndpointHandlers { return c.DeviceAuthorizeEndpointHandlers } @@ -531,6 +541,10 @@ 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 } diff --git a/device_authorize_request.go b/device_authorize_request.go index fa2477117..5c71375ae 100644 --- a/device_authorize_request.go +++ b/device_authorize_request.go @@ -23,16 +23,16 @@ package fosite // DeviceAuthorizeRequest is an implementation of DeviceAuthorizeRequester type DeviceAuthorizeRequest struct { - DeviceRequestId string + deviceCodeSignature string Request } -func (d *DeviceAuthorizeRequest) GetDeviceRequestId() string { - return d.DeviceRequestId +func (d *DeviceAuthorizeRequest) GetDeviceCodeSignature() string { + return d.deviceCodeSignature } -func (d *DeviceAuthorizeRequest) SetDeviceRequestId(id string) { - d.DeviceRequestId = id +func (d *DeviceAuthorizeRequest) SetDeviceCodeSignature(signature string) { + d.deviceCodeSignature = signature } func NewDeviceAuthorizeRequest() *DeviceAuthorizeRequest { diff --git a/device_authorize_request_handler.go b/device_authorize_request_handler.go index cd3d52852..1dcf715d5 100644 --- a/device_authorize_request_handler.go +++ b/device_authorize_request_handler.go @@ -24,13 +24,12 @@ package fosite import ( "context" "net/http" - "strings" "github.com/ory/fosite/i18n" "github.com/ory/x/errorsx" ) -func (f *Fosite) NewDeviceAuthorizeGetRequest(ctx context.Context, r *http.Request) (DeviceAuthorizeRequester, error) { +func (f *Fosite) NewDeviceAuthorizeRequest(ctx context.Context, r *http.Request) (DeviceAuthorizeRequester, error) { request := NewDeviceAuthorizeRequest() request.Lang = i18n.GetLangFromRequest(f.Config.GetMessageCatalog(ctx), r) @@ -53,40 +52,3 @@ func (f *Fosite) NewDeviceAuthorizeGetRequest(ctx context.Context, r *http.Reque return request, nil } - -func (f *Fosite) NewDeviceAuthorizePostRequest(ctx context.Context, req *http.Request) (DeviceAuthorizeRequester, error) { - request := NewDeviceAuthorizeRequest() - request.Lang = i18n.GetLangFromRequest(f.Config.GetMessageCatalog(ctx), req) - - if err := req.ParseMultipartForm(1 << 20); err != nil && err != http.ErrNotMultipart { - 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("urn:ietf:params:oauth:grant-type:device_code") { - 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.validateDeviceAuthorizeScope(ctx, req, request); err != nil { - return nil, err - } - - return request, nil -} - -func (f *Fosite) validateDeviceAuthorizeScope(ctx context.Context, req *http.Request, request *DeviceAuthorizeRequest) 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_authorize_request_handler_test.go b/device_authorize_request_handler_test.go index 67eba526d..469b43542 100644 --- a/device_authorize_request_handler_test.go +++ b/device_authorize_request_handler_test.go @@ -37,7 +37,7 @@ import ( . "github.com/ory/fosite/internal" ) -func TestNewDeviceAuthorizeGetRequest(t *testing.T) { +func TestNewDeviceAuthorizeRequest(t *testing.T) { var store *MockStorage for k, c := range []struct { desc string @@ -115,125 +115,7 @@ func TestNewDeviceAuthorizeGetRequest(t *testing.T) { } c.conf.Store = store - ar, err := c.conf.NewDeviceAuthorizeGetRequest(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()) - } - }) - } -} - -func TestNewDeviceAuthorizePostRequest(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 *DeviceAuthorizeRequest - }{ - /* 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: &DeviceAuthorizeRequest{ - 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.NewDeviceAuthorizePostRequest(context.Background(), c.r) + ar, err := c.conf.NewDeviceAuthorizeRequest(context.Background(), c.r) if c.expectedError != nil { assert.EqualError(t, err, c.expectedError.Error()) } else { diff --git a/device_authorize_request_test.go b/device_authorize_request_test.go index e39af6ee2..78e1502cc 100644 --- a/device_authorize_request_test.go +++ b/device_authorize_request_test.go @@ -47,7 +47,7 @@ func TestDeviceAuthorizeRequest(t *testing.T) { }, { ar: &DeviceAuthorizeRequest{ - DeviceRequestId: "a-uuid-xxx", + deviceCodeSignature: "AAAA", Request: Request{ Client: &DefaultClient{RedirectURIs: []string{""}}, }, @@ -64,7 +64,7 @@ func TestDeviceAuthorizeRequest(t *testing.T) { }, } { assert.Equal(t, c.ar.Client, c.ar.GetClient(), "%d", k) - assert.Equal(t, c.ar.DeviceRequestId, c.ar.GetDeviceRequestId(), "%d", k) + assert.Equal(t, c.ar.deviceCodeSignature, 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) diff --git a/device_authorize_response.go b/device_authorize_response.go index 54dbab92d..399645591 100644 --- a/device_authorize_response.go +++ b/device_authorize_response.go @@ -22,67 +22,8 @@ package fosite type DeviceAuthorizeResponse struct { - deviceCode string - userCode string - verificationURI string - verificationURIComplete string - interval int - expiresIn int64 } func NewDeviceAuthorizeResponse() *DeviceAuthorizeResponse { return &DeviceAuthorizeResponse{} } - -func (d *DeviceAuthorizeResponse) GetDeviceCode() string { - return d.deviceCode -} - -// GetUserCode returns the response's user code -func (d *DeviceAuthorizeResponse) SetDeviceCode(code string) { - d.deviceCode = code -} - -func (d *DeviceAuthorizeResponse) GetUserCode() string { - return d.userCode -} - -func (d *DeviceAuthorizeResponse) SetUserCode(code string) { - d.userCode = code -} - -// GetVerificationURI returns the response's verification uri -func (d *DeviceAuthorizeResponse) GetVerificationURI() string { - return d.verificationURI -} - -func (d *DeviceAuthorizeResponse) SetVerificationURI(uri string) { - d.verificationURI = uri -} - -// GetVerificationURIComplete returns the response's complete verification uri if set -func (d *DeviceAuthorizeResponse) GetVerificationURIComplete() string { - return d.verificationURIComplete -} - -func (d *DeviceAuthorizeResponse) SetVerificationURIComplete(uri string) { - d.verificationURIComplete = uri -} - -// GetExpiresIn returns the response's device code and user code lifetime in seconds if set -func (d *DeviceAuthorizeResponse) GetExpiresIn() int64 { - return d.expiresIn -} - -func (d *DeviceAuthorizeResponse) SetExpiresIn(seconds int64) { - d.expiresIn = seconds -} - -// GetInterval returns the response's polling interval if set -func (d *DeviceAuthorizeResponse) GetInterval() int { - return d.interval -} - -func (d *DeviceAuthorizeResponse) SetInterval(seconds int) { - d.interval = seconds -} diff --git a/device_authorize_response_writer.go b/device_authorize_response_writer.go index be041bc12..3866a4a2c 100644 --- a/device_authorize_response_writer.go +++ b/device_authorize_response_writer.go @@ -25,11 +25,12 @@ import ( "context" ) -func (f *Fosite) NewDeviceAuthorizeResponse(ctx context.Context, r Requester) (DeviceAuthorizeResponder, error) { +func (f *Fosite) NewDeviceAuthorizeResponse(ctx context.Context, dar DeviceAuthorizeRequester, session Session) (DeviceAuthorizeResponder, error) { var resp = &DeviceAuthorizeResponse{} + dar.SetSession(session) for _, h := range f.Config.GetDeviceAuthorizeEndpointHandlers(ctx) { - if err := h.HandleDeviceAuthorizeEndpointRequest(ctx, r, resp); err != nil { + if err := h.HandleDeviceAuthorizeEndpointRequest(ctx, dar, resp); err != nil { return nil, err } } diff --git a/device_authorize_writer.go b/device_authorize_writer.go index 544e29943..c6e07f2c2 100644 --- a/device_authorize_writer.go +++ b/device_authorize_writer.go @@ -23,30 +23,14 @@ package fosite import ( "context" - "encoding/json" "net/http" ) // TODO: Do documentation -func (f *Fosite) WriteDeviceAuthorizeResponse(ctx context.Context, rw http.ResponseWriter, r Requester, resp DeviceAuthorizeResponder) { - rw.Header().Set("Content-Type", "application/json;charset=UTF-8") +func (f *Fosite) WriteDeviceAuthorizeResponse(ctx context.Context, rw http.ResponseWriter, requester DeviceAuthorizeRequester, responder DeviceAuthorizeResponder) { rw.Header().Set("Cache-Control", "no-store") rw.Header().Set("Pragma", "no-cache") - - _ = json.NewEncoder(rw).Encode(struct { - 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"` - }{ - DeviceCode: resp.GetDeviceCode(), - UserCode: resp.GetUserCode(), - VerificationURI: resp.GetVerificationURI(), - VerificationURIComplete: resp.GetVerificationURIComplete(), - ExpiresIn: resp.GetExpiresIn(), - Interval: resp.GetInterval(), - }) + rw.Header().Set("Location", f.Config.GetDeviceDone(ctx)) + rw.WriteHeader(http.StatusSeeOther) } diff --git a/device_request.go b/device_request.go new file mode 100644 index 000000000..643239431 --- /dev/null +++ b/device_request.go @@ -0,0 +1,33 @@ +/* + * Copyright © 2015-2018 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-2018 Aeneas Rekkas + * @license 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..71723ce02 --- /dev/null +++ b/device_request_handler.go @@ -0,0 +1,68 @@ +/* + * 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.ParseMultipartForm(1 << 20); err != nil && err != http.ErrNotMultipart { + 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("urn:ietf:params:oauth:grant-type:device_code") { + 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..8474a23cf --- /dev/null +++ b/device_request_handler_test.go @@ -0,0 +1,156 @@ +/* + * Copyright © 2015-2018 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-2018 Aeneas Rekkas + * @license 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 *DeviceAuthorizeRequest + }{ + /* 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: &DeviceAuthorizeRequest{ + 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..cfd6885ea --- /dev/null +++ b/device_response.go @@ -0,0 +1,88 @@ +/* + * Copyright © 2015-2018 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-2018 Aeneas Rekkas + * @license Apache-2.0 + * + */ + +package fosite + +type DeviceResponse struct { + deviceCode string + userCode string + verificationURI string + verificationURIComplete string + interval int + expiresIn int64 +} + +func NewDeviceResponse() *DeviceResponse { + return &DeviceResponse{} +} + +func (d *DeviceResponse) GetDeviceCode() string { + return d.deviceCode +} + +// GetUserCode returns the response's user code +func (d *DeviceResponse) SetDeviceCode(code string) { + d.deviceCode = code +} + +func (d *DeviceResponse) GetUserCode() string { + return d.userCode +} + +func (d *DeviceResponse) SetUserCode(code string) { + d.userCode = code +} + +// GetVerificationURI returns the response's verification uri +func (d *DeviceResponse) GetVerificationURI() string { + return d.verificationURI +} + +func (d *DeviceResponse) SetVerificationURI(uri string) { + d.verificationURI = uri +} + +// GetVerificationURIComplete returns the response's complete verification uri if set +func (d *DeviceResponse) GetVerificationURIComplete() string { + return d.verificationURIComplete +} + +func (d *DeviceResponse) SetVerificationURIComplete(uri string) { + d.verificationURIComplete = uri +} + +// GetExpiresIn returns the response's device code and user code lifetime in seconds if set +func (d *DeviceResponse) GetExpiresIn() int64 { + return d.expiresIn +} + +func (d *DeviceResponse) SetExpiresIn(seconds int64) { + d.expiresIn = seconds +} + +// GetInterval returns the response's polling interval if set +func (d *DeviceResponse) GetInterval() int { + return d.interval +} + +func (d *DeviceResponse) SetInterval(seconds int) { + d.interval = seconds +} diff --git a/device_response_writer.go b/device_response_writer.go new file mode 100644 index 000000000..90ae18ef9 --- /dev/null +++ b/device_response_writer.go @@ -0,0 +1,38 @@ +/* + * Copyright © 2015-2018 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-2018 Aeneas Rekkas + * @license 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_authorize_response_writer_test.go b/device_response_writer_test.go similarity index 61% rename from device_authorize_response_writer_test.go rename to device_response_writer_test.go index 51447034f..a35cc243d 100644 --- a/device_authorize_response_writer_test.go +++ b/device_response_writer_test.go @@ -33,15 +33,15 @@ import ( . "github.com/ory/fosite/internal" ) -func TestNewDeviceAuthorizeResponse(t *testing.T) { +func TestNewDeviceResponse(t *testing.T) { ctrl := gomock.NewController(t) - handlers := []*MockDeviceAuthorizeEndpointHandler{NewMockDeviceAuthorizeEndpointHandler(ctrl)} + handlers := []*MockDeviceEndpointHandler{NewMockDeviceEndpointHandler(ctrl)} dar := NewMockDeviceAuthorizeRequester(ctrl) defer ctrl.Finish() ctx := context.Background() - oauth2 := &Fosite{Config: &Config{DeviceAuthorizeEndpointHandlers: DeviceAuthorizeEndpointHandlers{handlers[0]}}} - duo := &Fosite{Config: &Config{DeviceAuthorizeEndpointHandlers: DeviceAuthorizeEndpointHandlers{handlers[0], handlers[0]}}} + 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 { @@ -51,37 +51,37 @@ func TestNewDeviceAuthorizeResponse(t *testing.T) { }{ { mock: func() { - handlers[0].EXPECT().HandleDeviceAuthorizeEndpointRequest(gomock.Any(), gomock.Any(), gomock.Any()).Return(fooErr) + handlers[0].EXPECT().HandleDeviceEndpointRequest(gomock.Any(), gomock.Any(), gomock.Any()).Return(fooErr) }, isErr: true, expectErr: fooErr, }, { mock: func() { - handlers[0].EXPECT().HandleDeviceAuthorizeEndpointRequest(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().HandleDeviceAuthorizeEndpointRequest(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) - handlers[0].EXPECT().HandleDeviceAuthorizeEndpointRequest(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + 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().HandleDeviceAuthorizeEndpointRequest(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) - handlers[0].EXPECT().HandleDeviceAuthorizeEndpointRequest(gomock.Any(), gomock.Any(), gomock.Any()).Return(fooErr) + 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.NewDeviceAuthorizeResponse(ctx, dar) + 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) diff --git a/device_authorize_write_test.go b/device_write_test.go similarity index 94% rename from device_authorize_write_test.go rename to device_write_test.go index 1d684d673..cf3cc0316 100644 --- a/device_authorize_write_test.go +++ b/device_write_test.go @@ -42,8 +42,8 @@ func TestWriteDeviceAuthorizeResponse(t *testing.T) { }} rw := httptest.NewRecorder() - ar := &DeviceAuthorizeRequest{} - resp := &DeviceAuthorizeResponse{} + ar := &Request{} + resp := &DeviceResponse{} resp.SetUserCode("AAAA") resp.SetDeviceCode("BBBB") resp.SetInterval(int( @@ -57,7 +57,7 @@ func TestWriteDeviceAuthorizeResponse(t *testing.T) { oauth2.Config.GetDeviceVerificationURL(context.TODO()) + "?user_code=" + resp.GetUserCode(), ) - oauth2.WriteDeviceAuthorizeResponse(context.Background(), rw, ar, resp) + oauth2.WriteDeviceResponse(context.Background(), rw, ar, resp) var params struct { DeviceCode string `json:"device_code"` UserCode string `json:"user_code"` diff --git a/device_writer.go b/device_writer.go new file mode 100644 index 000000000..5b57ef04d --- /dev/null +++ b/device_writer.go @@ -0,0 +1,52 @@ +/* + * 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" + "encoding/json" + "net/http" +) + +// TODO: Do documentation + +func (f *Fosite) WriteDeviceResponse(ctx context.Context, rw http.ResponseWriter, requester DeviceRequester, responder DeviceResponder) { + rw.Header().Set("Content-Type", "application/json;charset=UTF-8") + rw.Header().Set("Cache-Control", "no-store") + rw.Header().Set("Pragma", "no-cache") + + _ = json.NewEncoder(rw).Encode(struct { + 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"` + }{ + DeviceCode: responder.GetDeviceCode(), + UserCode: responder.GetUserCode(), + VerificationURI: responder.GetVerificationURI(), + VerificationURIComplete: responder.GetVerificationURIComplete(), + ExpiresIn: responder.GetExpiresIn(), + Interval: responder.GetInterval(), + }) +} diff --git a/fosite.go b/fosite.go index db1f512aa..050c06dcf 100644 --- a/fosite.go +++ b/fosite.go @@ -100,7 +100,21 @@ func (a *PushedAuthorizeEndpointHandlers) Append(h PushedAuthorizeEndpointHandle *a = append(*a, h) } -// DeviceAuthorizeEndpointHandler is a list of DeviceAuthorizeEndpointHandler +// DeviceEndpointHandlers is a list of DeviceEndpointHandler +type DeviceEndpointHandlers []DeviceEndpointHandler + +// Append adds an AuthorizeEndpointHandler 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) +} + +// DeviceEndpointHandlers is a list of DeviceEndpointHandler type DeviceAuthorizeEndpointHandlers []DeviceAuthorizeEndpointHandler // Append adds an AuthorizeEndpointHandler to this list. Ignores duplicates based on reflect.TypeOf. @@ -164,8 +178,10 @@ type Configurator interface { TokenIntrospectionHandlersProvider RevocationHandlersProvider UseLegacyErrorFormatProvider + DeviceEndpointHandlersProvider DeviceAuthorizeEndpointHandlersProvider - DeviceUriProvider + DeviceProvider + DeviceAuthorizeProvider } func NewOAuth2Provider(s Storage, c Configurator) *Fosite { diff --git a/generate-mocks.sh b/generate-mocks.sh index 1fe0229e4..091e5f4c8 100755 --- a/generate-mocks.sh +++ b/generate-mocks.sh @@ -6,6 +6,8 @@ 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/oauth2 DeviceCodeStorage +mockgen -package internal -destination internal/user_code_storage.go github.com/ory/fosite/handler/oauth2 UserCodeStorage mockgen -package internal -destination internal/oauth2_auth_jwt_storage.go github.com/ory/fosite/handler/rfc7523 RFC7523KeyStorage 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 @@ -19,6 +21,7 @@ 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_authorize_handler.go github.com/ory/fosite DeviceAuthorizeEndpointHandler 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 @@ -31,5 +34,7 @@ mockgen -package internal -destination internal/authorize_request.go github.com/ mockgen -package internal -destination internal/authorize_response.go github.com/ory/fosite AuthorizeResponder mockgen -package internal -destination internal/device_authorize_request.go github.com/ory/fosite DeviceAuthorizeRequester mockgen -package internal -destination internal/device_authorize_response.go github.com/ory/fosite DeviceAuthorizeResponder +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 5dc910d96..68165cf87 100644 --- a/generate.go +++ b/generate.go @@ -6,6 +6,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 @@ -19,6 +21,7 @@ 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_authorize_handler.go github.com/ory/fosite DeviceAuthorizeEndpointHandler //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 @@ -31,3 +34,5 @@ package fosite //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_authorize_request.go github.com/ory/fosite DeviceAuthorizeRequester //go:generate go run github.com/golang/mock/mockgen -package internal -destination internal/device_authorize_response.go github.com/ory/fosite DeviceAuthorizeResponder +//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/handler.go b/handler.go index 0432e6036..55df80254 100644 --- a/handler.go +++ b/handler.go @@ -85,12 +85,18 @@ type PushedAuthorizeEndpointHandler interface { HandlePushedAuthorizeEndpointRequest(ctx context.Context, requester AuthorizeRequester, responder PushedAuthorizeResponder) error } -type DeviceAuthorizeEndpointHandler interface { - // HandleDeviceAuthorizeRequest handles a device authorize endpoint request. To extend the handler's capabilities, the http request +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 HandleDeviceAuthorizeRequest should do. // * https://tools.ietf.org/html/rfc8628#section-3.2 - HandleDeviceAuthorizeEndpointRequest(ctx context.Context, requester Requester, responder DeviceAuthorizeResponder) error + HandleDeviceEndpointRequest(ctx context.Context, requester Requester, responder DeviceResponder) error +} + +type DeviceAuthorizeEndpointHandler interface { + // HandleDeviceAuthorizeEndpointRequest handles a device authorize endpoint request. + // TODO doc + HandleDeviceAuthorizeEndpointRequest(ctx context.Context, requester DeviceAuthorizeRequester, responder DeviceAuthorizeResponder) error } diff --git a/handler/oauth2/flow_device_grant_auth.go b/handler/oauth2/flow_device_auth.go similarity index 83% rename from handler/oauth2/flow_device_grant_auth.go rename to handler/oauth2/flow_device_auth.go index f72cd28a0..1b9c8bbb5 100644 --- a/handler/oauth2/flow_device_grant_auth.go +++ b/handler/oauth2/flow_device_auth.go @@ -10,7 +10,7 @@ import ( // DeviceAuthorizationHandler is a response handler for the Device Authorisation Grant as // defined in https://tools.ietf.org/html/rfc8628#section-3.1 -type DeviceAuthorizationHandler struct { +type DeviceHandler struct { AccessTokenStrategy AccessTokenStrategy RefreshTokenStrategy RefreshTokenStrategy DeviceCodeStrategy DeviceCodeStrategy @@ -18,7 +18,7 @@ type DeviceAuthorizationHandler struct { CoreStorage CoreStorage TokenRevocationStorage TokenRevocationStorage Config interface { - fosite.DeviceUriProvider + fosite.DeviceProvider fosite.DeviceAndUserCodeLifespanProvider fosite.AccessTokenLifespanProvider fosite.RefreshTokenLifespanProvider @@ -29,7 +29,7 @@ type DeviceAuthorizationHandler struct { } } -func (d *DeviceAuthorizationHandler) HandleDeviceAuthorizeEndpointRequest(ctx context.Context, dar fosite.Requester, resp fosite.DeviceAuthorizeResponder) error { +func (d *DeviceHandler) HandleDeviceEndpointRequest(ctx context.Context, dar fosite.Requester, resp fosite.DeviceResponder) error { deviceCode, deviceCodeSignature, err := d.DeviceCodeStrategy.GenerateDeviceCode(ctx) if err != nil { return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) @@ -40,16 +40,22 @@ func (d *DeviceAuthorizationHandler) HandleDeviceAuthorizeEndpointRequest(ctx co 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.CoreStorage.CreateDeviceCodeSession(ctx, deviceCodeSignature, dar); err != nil { + if err := d.CoreStorage.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.CoreStorage.CreateUserCodeSession(ctx, userCodeSignature, dar); err != nil { + if err := d.CoreStorage.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) diff --git a/handler/oauth2/flow_device_grant_auth_test.go b/handler/oauth2/flow_device_auth_test.go similarity index 80% rename from handler/oauth2/flow_device_grant_auth_test.go rename to handler/oauth2/flow_device_auth_test.go index 1a4e1e8ca..0bed5f8bd 100644 --- a/handler/oauth2/flow_device_grant_auth_test.go +++ b/handler/oauth2/flow_device_auth_test.go @@ -15,7 +15,7 @@ func Test_HandleDeviceAuthorizeEndpointRequest(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() store := storage.NewMemoryStore() - handler := DeviceAuthorizationHandler{ + handler := DeviceHandler{ CoreStorage: store, DeviceCodeStrategy: &hmacshaStrategy, UserCodeStrategy: &hmacshaStrategy, @@ -31,15 +31,12 @@ func Test_HandleDeviceAuthorizeEndpointRequest(t *testing.T) { }, } - req := &fosite.AuthorizeRequest{ - ResponseTypes: fosite.Arguments{"code"}, - Request: fosite.Request{ - Session: &fosite.DefaultSession{}, - }, + req := &fosite.Request{ + Session: &fosite.DefaultSession{}, } - resp := &fosite.DeviceAuthorizeResponse{} + resp := &fosite.DeviceResponse{} - handler.HandleDeviceAuthorizeEndpointRequest(nil, req, resp) + handler.HandleDeviceEndpointRequest(nil, req, resp) assert.NotEmpty(t, resp.GetDeviceCode()) assert.NotEmpty(t, resp.GetUserCode()) diff --git a/handler/oauth2/flow_device_grant_token.go b/handler/oauth2/flow_device_token.go similarity index 88% rename from handler/oauth2/flow_device_grant_token.go rename to handler/oauth2/flow_device_token.go index 8cbf43e4d..a271c34e4 100644 --- a/handler/oauth2/flow_device_grant_token.go +++ b/handler/oauth2/flow_device_token.go @@ -25,23 +25,24 @@ import ( "context" "time" - "github.com/ory/fosite" - "github.com/ory/fosite/storage" "github.com/ory/x/errorsx" + + "github.com/ory/fosite/storage" + "github.com/pkg/errors" -) -const deviceCodeGrantType = "urn:ietf:params:oauth:grant-type:device_code" + "github.com/ory/fosite" +) // HandleTokenEndpointRequest implements // * https://tools.ietf.org/html/rfc8628#section-3.4 (everything) -func (c *DeviceAuthorizationHandler) HandleTokenEndpointRequest(ctx context.Context, request fosite.AccessRequester) error { +func (c *DeviceHandler) 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(deviceCodeGrantType) { - return errorsx.WithStack(fosite.ErrUnauthorizedClient.WithHint("The OAuth 2.0 Client is not allowed to use authorization grant \"" + deviceCodeGrantType + "\".")) + if !request.GetClient().GetGrantTypes().Has("urn:ietf:params:oauth:grant-type:device_code") { + 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\".")) } code := request.GetRequestForm().Get("device_code") @@ -51,7 +52,7 @@ func (c *DeviceAuthorizationHandler) HandleTokenEndpointRequest(ctx context.Cont 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\".") + WithDebug("GetDeviceCodeSession must return a value for \"fosite.Requester\" when returning \"ErrInvalidatedDeviceCode\".") } // If an authorize code is used twice, we revoke all refresh and access tokens associated with this request. @@ -113,7 +114,7 @@ func (c *DeviceAuthorizationHandler) HandleTokenEndpointRequest(ctx context.Cont return nil } -func (*DeviceAuthorizationHandler) canIssueRefreshToken(ctx context.Context, c *DeviceAuthorizationHandler, request fosite.Requester) bool { +func (*DeviceHandler) canIssueRefreshToken(ctx context.Context, c *DeviceHandler, 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...) { @@ -126,7 +127,7 @@ func (*DeviceAuthorizationHandler) canIssueRefreshToken(ctx context.Context, c * return true } -func (c *DeviceAuthorizationHandler) PopulateTokenEndpointResponse(ctx context.Context, requester fosite.AccessRequester, responder fosite.AccessResponder) error { +func (c *DeviceHandler) PopulateTokenEndpointResponse(ctx context.Context, requester fosite.AccessRequester, responder fosite.AccessResponder) error { if !c.CanHandleTokenEndpointRequest(ctx, requester) { return errorsx.WithStack(fosite.ErrUnknownRequest) } @@ -200,12 +201,12 @@ func (c *DeviceAuthorizationHandler) PopulateTokenEndpointResponse(ctx context.C return nil } -func (c *DeviceAuthorizationHandler) CanSkipClientAuth(ctx context.Context, requester fosite.AccessRequester) bool { +func (c *DeviceHandler) CanSkipClientAuth(ctx context.Context, requester fosite.AccessRequester) bool { return true } -func (c *DeviceAuthorizationHandler) CanHandleTokenEndpointRequest(ctx context.Context, requester fosite.AccessRequester) bool { +func (c *DeviceHandler) CanHandleTokenEndpointRequest(ctx context.Context, requester fosite.AccessRequester) bool { // grant_type REQUIRED. // Value MUST be set to "urn:ietf:params:oauth:grant-type:device_code" - return requester.GetGrantTypes().ExactOne(deviceCodeGrantType) + return requester.GetGrantTypes().ExactOne("urn:ietf:params:oauth:grant-type:device_code") } diff --git a/handler/oauth2/flow_device_grant_token_test.go b/handler/oauth2/flow_device_token_test.go similarity index 99% rename from handler/oauth2/flow_device_grant_token_test.go rename to handler/oauth2/flow_device_token_test.go index 0409a97d1..030ae2c4f 100644 --- a/handler/oauth2/flow_device_grant_token_test.go +++ b/handler/oauth2/flow_device_token_test.go @@ -48,7 +48,7 @@ func TestDeviceAuthorizeCode_PopulateTokenEndpointResponse(t *testing.T) { t.Run("strategy="+k, func(t *testing.T) { store := storage.NewMemoryStore() - var h DeviceAuthorizationHandler + var h DeviceHandler for _, c := range []struct { areq *fosite.AccessRequest description string @@ -209,7 +209,7 @@ func TestDeviceAuthorizeCode_PopulateTokenEndpointResponse(t *testing.T) { AccessTokenLifespan: time.Minute, RefreshTokenScopes: []string{"offline"}, } - h = DeviceAuthorizationHandler{ + h = DeviceHandler{ CoreStorage: store, DeviceCodeStrategy: strategy, UserCodeStrategy: strategy, @@ -247,7 +247,7 @@ func TestDeviceAuthorizeCode_HandleTokenEndpointRequest(t *testing.T) { t.Run("strategy="+k, func(t *testing.T) { store := storage.NewMemoryStore() - h := DeviceAuthorizationHandler{ + h := DeviceHandler{ CoreStorage: store, DeviceCodeStrategy: &hmacshaStrategy, UserCodeStrategy: &hmacshaStrategy, @@ -653,7 +653,7 @@ func TestDeviceAuthorizeCodeTransactional_HandleTokenEndpointRequest(t *testing. mockCoreStore = internal.NewMockCoreStorage(ctrl) testCase.setup() - handler := DeviceAuthorizationHandler{ + handler := DeviceHandler{ CoreStorage: transactionalStore{ mockTransactional, mockCoreStore, diff --git a/handler/oauth2/storage.go b/handler/oauth2/storage.go index 450828768..038a2dd17 100644 --- a/handler/oauth2/storage.go +++ b/handler/oauth2/storage.go @@ -73,8 +73,8 @@ type DeviceCodeStorage interface { // CreateDeviceCodeSession stores the device request for a given device code. CreateDeviceCodeSession(ctx context.Context, signature string, request fosite.Requester) (err error) - // UpdateDeviceCodeSessionByRequestId udpate in store the device request for a given device code. - UpdateDeviceCodeSessionByRequestId(ctx context.Context, id 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 diff --git a/handler/openid/flow_device_auth.go b/handler/openid/flow_device_auth.go new file mode 100644 index 000000000..2d91c8820 --- /dev/null +++ b/handler/openid/flow_device_auth.go @@ -0,0 +1,67 @@ +/* + * Copyright © 2015-2018 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-2018 Aeneas Rekkas + * @license 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) HandleDeviceAuthorizeEndpointRequest(ctx context.Context, ar fosite.DeviceAuthorizeRequester, resp fosite.DeviceAuthorizeResponder) error { + if !(ar.GetGrantedScopes().Has("openid")) { + return nil + } + + if !ar.GetClient().GetGrantTypes().Has("urn:ietf:params:oauth:grant-type:device_code") { + 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..1ab27957f --- /dev/null +++ b/handler/openid/flow_device_token.go @@ -0,0 +1,99 @@ +/* + * Copyright © 2015-2018 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-2018 Aeneas Rekkas + * @license 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("urn:ietf:params:oauth:grant-type:device_code") { + 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("urn:ietf:params:oauth:grant-type:device_code") +} diff --git a/handler/pkce/handler_device.go b/handler/pkce/handler_device.go index 1595f4694..1ec14805e 100644 --- a/handler/pkce/handler_device.go +++ b/handler/pkce/handler_device.go @@ -42,7 +42,6 @@ type HandlerDevice struct { UserCodeStrategy oauth2.UserCodeStrategy Storage PKCERequestStorage Config interface { - fosite.GlobalSecretProvider fosite.EnforcePKCEProvider fosite.EnforcePKCEForPublicClientsProvider fosite.EnablePKCEPlainChallengeMethodProvider @@ -53,7 +52,7 @@ var _ fosite.TokenEndpointHandler = (*HandlerDevice)(nil) var deviceVerifierWrongFormat = regexp.MustCompile(`[^\w\.\-~]`) -func (c *HandlerDevice) HandleDeviceAuthorizeEndpointRequest(ctx context.Context, ar fosite.Requester, resp fosite.DeviceAuthorizeResponder) error { +func (c *HandlerDevice) HandleDeviceEndpointRequest(ctx context.Context, ar fosite.Requester, resp fosite.DeviceResponder) error { if !ar.GetClient().GetGrantTypes().Has("urn:ietf:params:oauth:grant-type:device_code") { return nil } diff --git a/handler/pkce/handler_device_test.go b/handler/pkce/handler_device_test.go index f1ae2105e..22e45ff24 100644 --- a/handler/pkce/handler_device_test.go +++ b/handler/pkce/handler_device_test.go @@ -76,8 +76,8 @@ func TestPKCEHandlerDevice_HandleDeviceAuthorizeEndpointRequest(t *testing.T) { UserCodeStrategy: new(oauth2.HMACSHAStrategy), Config: &config, } - w := fosite.NewDeviceAuthorizeResponse() - r := fosite.NewDeviceAuthorizeRequest() + w := fosite.NewDeviceResponse() + r := fosite.NewDeviceRequest() config.GlobalSecret = []byte("thisissecret") w.SetDeviceCode("foo") @@ -87,32 +87,32 @@ func TestPKCEHandlerDevice_HandleDeviceAuthorizeEndpointRequest(t *testing.T) { c := &fosite.DefaultClient{} r.Client = c - require.NoError(t, h.HandleDeviceAuthorizeEndpointRequest(context.Background(), r, w)) + require.NoError(t, h.HandleDeviceEndpointRequest(context.Background(), r, w)) c = &fosite.DefaultClient{ GrantTypes: []string{"urn:ietf:params:oauth:grant-type:device_code"}, } r.Client = c - require.Error(t, h.HandleDeviceAuthorizeEndpointRequest(context.Background(), r, w)) + require.Error(t, h.HandleDeviceEndpointRequest(context.Background(), r, w)) c.Public = true config.EnablePKCEPlainChallengeMethod = true - require.NoError(t, h.HandleDeviceAuthorizeEndpointRequest(context.Background(), r, w)) + require.NoError(t, h.HandleDeviceEndpointRequest(context.Background(), r, w)) c.Public = false config.EnablePKCEPlainChallengeMethod = true - require.NoError(t, h.HandleDeviceAuthorizeEndpointRequest(context.Background(), r, w)) + require.NoError(t, h.HandleDeviceEndpointRequest(context.Background(), r, w)) config.EnablePKCEPlainChallengeMethod = false - require.Error(t, h.HandleDeviceAuthorizeEndpointRequest(context.Background(), r, w)) + require.Error(t, h.HandleDeviceEndpointRequest(context.Background(), r, w)) r.Form.Set("code_challenge_method", "S256") r.Form.Set("code_challenge", "") config.EnforcePKCE = true - require.Error(t, h.HandleDeviceAuthorizeEndpointRequest(context.Background(), r, w)) + require.Error(t, h.HandleDeviceEndpointRequest(context.Background(), r, w)) r.Form.Set("code_challenge", "challenge") - require.NoError(t, h.HandleDeviceAuthorizeEndpointRequest(context.Background(), r, w)) + require.NoError(t, h.HandleDeviceEndpointRequest(context.Background(), r, w)) } func TestPKCEHandlerDevice_HandlerDeviceValidate(t *testing.T) { diff --git a/internal/device_authorize_handler.go b/internal/device_authorize_handler.go index fa5666b55..ee934ac84 100644 --- a/internal/device_authorize_handler.go +++ b/internal/device_authorize_handler.go @@ -36,7 +36,7 @@ func (m *MockDeviceAuthorizeEndpointHandler) EXPECT() *MockDeviceAuthorizeEndpoi } // HandleDeviceAuthorizeEndpointRequest mocks base method. -func (m *MockDeviceAuthorizeEndpointHandler) HandleDeviceAuthorizeEndpointRequest(arg0 context.Context, arg1 fosite.Requester, arg2 fosite.DeviceAuthorizeResponder) error { +func (m *MockDeviceAuthorizeEndpointHandler) HandleDeviceAuthorizeEndpointRequest(arg0 context.Context, arg1 fosite.DeviceAuthorizeRequester, arg2 fosite.DeviceAuthorizeResponder) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "HandleDeviceAuthorizeEndpointRequest", arg0, arg1, arg2) ret0, _ := ret[0].(error) diff --git a/internal/device_authorize_request.go b/internal/device_authorize_request.go index 13457ac2a..caba5a89e 100644 --- a/internal/device_authorize_request.go +++ b/internal/device_authorize_request.go @@ -62,18 +62,18 @@ func (mr *MockDeviceAuthorizeRequesterMockRecorder) GetClient() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClient", reflect.TypeOf((*MockDeviceAuthorizeRequester)(nil).GetClient)) } -// GetDeviceRequestId mocks base method. -func (m *MockDeviceAuthorizeRequester) GetDeviceRequestId() string { +// GetDeviceCodeSignature mocks base method. +func (m *MockDeviceAuthorizeRequester) GetDeviceCodeSignature() string { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetDeviceRequestId") + ret := m.ctrl.Call(m, "GetDeviceCodeSignature") ret0, _ := ret[0].(string) return ret0 } -// GetDeviceRequestId indicates an expected call of GetDeviceRequestId. -func (mr *MockDeviceAuthorizeRequesterMockRecorder) GetDeviceRequestId() *gomock.Call { +// GetDeviceCodeSignature indicates an expected call of GetDeviceCodeSignature. +func (mr *MockDeviceAuthorizeRequesterMockRecorder) GetDeviceCodeSignature() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDeviceRequestId", reflect.TypeOf((*MockDeviceAuthorizeRequester)(nil).GetDeviceRequestId)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDeviceCodeSignature", reflect.TypeOf((*MockDeviceAuthorizeRequester)(nil).GetDeviceCodeSignature)) } // GetGrantedAudience mocks base method. @@ -238,16 +238,16 @@ func (mr *MockDeviceAuthorizeRequesterMockRecorder) Sanitize(arg0 interface{}) * return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Sanitize", reflect.TypeOf((*MockDeviceAuthorizeRequester)(nil).Sanitize), arg0) } -// SetDeviceRequestId mocks base method. -func (m *MockDeviceAuthorizeRequester) SetDeviceRequestId(arg0 string) { +// SetDeviceCodeSignature mocks base method. +func (m *MockDeviceAuthorizeRequester) SetDeviceCodeSignature(arg0 string) { m.ctrl.T.Helper() - m.ctrl.Call(m, "SetDeviceRequestId", arg0) + m.ctrl.Call(m, "SetDeviceCodeSignature", arg0) } -// SetDeviceRequestId indicates an expected call of SetDeviceRequestId. -func (mr *MockDeviceAuthorizeRequesterMockRecorder) SetDeviceRequestId(arg0 interface{}) *gomock.Call { +// SetDeviceCodeSignature indicates an expected call of SetDeviceCodeSignature. +func (mr *MockDeviceAuthorizeRequesterMockRecorder) SetDeviceCodeSignature(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDeviceRequestId", reflect.TypeOf((*MockDeviceAuthorizeRequester)(nil).SetDeviceRequestId), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDeviceCodeSignature", reflect.TypeOf((*MockDeviceAuthorizeRequester)(nil).SetDeviceCodeSignature), arg0) } // SetID mocks base method. diff --git a/internal/device_authorize_response.go b/internal/device_authorize_response.go index e23e6606b..e23d9e7ca 100644 --- a/internal/device_authorize_response.go +++ b/internal/device_authorize_response.go @@ -5,8 +5,6 @@ package internal import ( - reflect "reflect" - gomock "github.com/golang/mock/gomock" ) @@ -32,159 +30,3 @@ func NewMockDeviceAuthorizeResponder(ctrl *gomock.Controller) *MockDeviceAuthori func (m *MockDeviceAuthorizeResponder) EXPECT() *MockDeviceAuthorizeResponderMockRecorder { return m.recorder } - -// GetDeviceCode mocks base method. -func (m *MockDeviceAuthorizeResponder) 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 *MockDeviceAuthorizeResponderMockRecorder) GetDeviceCode() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDeviceCode", reflect.TypeOf((*MockDeviceAuthorizeResponder)(nil).GetDeviceCode)) -} - -// GetExpiresIn mocks base method. -func (m *MockDeviceAuthorizeResponder) 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 *MockDeviceAuthorizeResponderMockRecorder) GetExpiresIn() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetExpiresIn", reflect.TypeOf((*MockDeviceAuthorizeResponder)(nil).GetExpiresIn)) -} - -// GetInterval mocks base method. -func (m *MockDeviceAuthorizeResponder) 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 *MockDeviceAuthorizeResponderMockRecorder) GetInterval() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInterval", reflect.TypeOf((*MockDeviceAuthorizeResponder)(nil).GetInterval)) -} - -// GetUserCode mocks base method. -func (m *MockDeviceAuthorizeResponder) 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 *MockDeviceAuthorizeResponderMockRecorder) GetUserCode() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserCode", reflect.TypeOf((*MockDeviceAuthorizeResponder)(nil).GetUserCode)) -} - -// GetVerificationURI mocks base method. -func (m *MockDeviceAuthorizeResponder) 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 *MockDeviceAuthorizeResponderMockRecorder) GetVerificationURI() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetVerificationURI", reflect.TypeOf((*MockDeviceAuthorizeResponder)(nil).GetVerificationURI)) -} - -// GetVerificationURIComplete mocks base method. -func (m *MockDeviceAuthorizeResponder) 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 *MockDeviceAuthorizeResponderMockRecorder) GetVerificationURIComplete() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetVerificationURIComplete", reflect.TypeOf((*MockDeviceAuthorizeResponder)(nil).GetVerificationURIComplete)) -} - -// SetDeviceCode mocks base method. -func (m *MockDeviceAuthorizeResponder) SetDeviceCode(arg0 string) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "SetDeviceCode", arg0) -} - -// SetDeviceCode indicates an expected call of SetDeviceCode. -func (mr *MockDeviceAuthorizeResponderMockRecorder) SetDeviceCode(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDeviceCode", reflect.TypeOf((*MockDeviceAuthorizeResponder)(nil).SetDeviceCode), arg0) -} - -// SetExpiresIn mocks base method. -func (m *MockDeviceAuthorizeResponder) SetExpiresIn(arg0 int64) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "SetExpiresIn", arg0) -} - -// SetExpiresIn indicates an expected call of SetExpiresIn. -func (mr *MockDeviceAuthorizeResponderMockRecorder) SetExpiresIn(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetExpiresIn", reflect.TypeOf((*MockDeviceAuthorizeResponder)(nil).SetExpiresIn), arg0) -} - -// SetInterval mocks base method. -func (m *MockDeviceAuthorizeResponder) SetInterval(arg0 int) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "SetInterval", arg0) -} - -// SetInterval indicates an expected call of SetInterval. -func (mr *MockDeviceAuthorizeResponderMockRecorder) SetInterval(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetInterval", reflect.TypeOf((*MockDeviceAuthorizeResponder)(nil).SetInterval), arg0) -} - -// SetUserCode mocks base method. -func (m *MockDeviceAuthorizeResponder) SetUserCode(arg0 string) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "SetUserCode", arg0) -} - -// SetUserCode indicates an expected call of SetUserCode. -func (mr *MockDeviceAuthorizeResponderMockRecorder) SetUserCode(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetUserCode", reflect.TypeOf((*MockDeviceAuthorizeResponder)(nil).SetUserCode), arg0) -} - -// SetVerificationURI mocks base method. -func (m *MockDeviceAuthorizeResponder) SetVerificationURI(arg0 string) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "SetVerificationURI", arg0) -} - -// SetVerificationURI indicates an expected call of SetVerificationURI. -func (mr *MockDeviceAuthorizeResponderMockRecorder) SetVerificationURI(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetVerificationURI", reflect.TypeOf((*MockDeviceAuthorizeResponder)(nil).SetVerificationURI), arg0) -} - -// SetVerificationURIComplete mocks base method. -func (m *MockDeviceAuthorizeResponder) SetVerificationURIComplete(arg0 string) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "SetVerificationURIComplete", arg0) -} - -// SetVerificationURIComplete indicates an expected call of SetVerificationURIComplete. -func (mr *MockDeviceAuthorizeResponderMockRecorder) SetVerificationURIComplete(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetVerificationURIComplete", reflect.TypeOf((*MockDeviceAuthorizeResponder)(nil).SetVerificationURIComplete), arg0) -} diff --git a/internal/device_code_storage.go b/internal/device_code_storage.go new file mode 100644 index 000000000..0b0c609a2 --- /dev/null +++ b/internal/device_code_storage.go @@ -0,0 +1,93 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/ory/fosite/handler/oauth2 (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..ea35edbfd --- /dev/null +++ b/internal/device_handler.go @@ -0,0 +1,50 @@ +// 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.Requester, 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..ad6530bfa --- /dev/null +++ b/internal/device_request.go @@ -0,0 +1,273 @@ +// 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..c57a4dd9e --- /dev/null +++ b/internal/device_response.go @@ -0,0 +1,190 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/ory/fosite (interfaces: DeviceResponder) + +// Package internal is a generated GoMock package. +package internal + +import ( + 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 +} + +// 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)) +} + +// 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_storage.go b/internal/oauth2_storage.go index 97633402a..3a88c7ad3 100644 --- a/internal/oauth2_storage.go +++ b/internal/oauth2_storage.go @@ -250,3 +250,17 @@ func (mr *MockCoreStorageMockRecorder) InvalidateUserCodeSession(arg0, arg1 inte mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InvalidateUserCodeSession", reflect.TypeOf((*MockCoreStorage)(nil).InvalidateUserCodeSession), arg0, arg1) } + +// UpdateDeviceCodeSession mocks base method. +func (m *MockCoreStorage) 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 *MockCoreStorageMockRecorder) UpdateDeviceCodeSession(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateDeviceCodeSession", reflect.TypeOf((*MockCoreStorage)(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..4914cca68 --- /dev/null +++ b/internal/user_code_storage.go @@ -0,0 +1,79 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/ory/fosite/handler/oauth2 (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 9662a60d2..8901947ec 100644 --- a/oauth2.go +++ b/oauth2.go @@ -77,12 +77,6 @@ type OAuth2Provider interface { // * https://tools.ietf.org/html/rfc6749#section-3.1.2.2 (everything MUST be implemented) NewAuthorizeRequest(ctx context.Context, req *http.Request) (AuthorizeRequester, error) - // ToDo add ietf docs - NewDeviceAuthorizeGetRequest(ctx context.Context, req *http.Request) (DeviceAuthorizeRequester, error) - NewDeviceAuthorizePostRequest(ctx context.Context, req *http.Request) (DeviceAuthorizeRequester, error) - NewDeviceAuthorizeResponse(ctx context.Context, requester Requester) (DeviceAuthorizeResponder, error) - WriteDeviceAuthorizeResponse(ctx context.Context, rw http.ResponseWriter, requester Requester, responder DeviceAuthorizeResponder) - // NewAuthorizeResponse iterates through all response type handlers and returns their result or // ErrUnsupportedResponseType if none of the handler's were able to handle it. // @@ -126,6 +120,37 @@ 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) + // NewDeviceAuthorizeRequest returns an DeviceAuthorizeRequest. + // TODO Add documentation + NewDeviceAuthorizeRequest(ctx context.Context, req *http.Request) (DeviceAuthorizeRequester, error) + + // NewDeviceAuthorizeResponse + // TODO Add documentation + NewDeviceAuthorizeResponse(ctx context.Context, requester DeviceAuthorizeRequester, session Session) (DeviceAuthorizeResponder, error) + + // WriteDeviceAuthorizeResponse + // TODO Add documentation + WriteDeviceAuthorizeResponse(ctx context.Context, rw http.ResponseWriter, requester DeviceAuthorizeRequester, responder DeviceAuthorizeResponder) + + // 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) + 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) + NewDeviceResponse(ctx context.Context, requester DeviceRequester) (DeviceResponder, error) + + // WriteDeviceAuthorizeResponse 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) + WriteDeviceResponse(ctx context.Context, rw http.ResponseWriter, requester DeviceRequester, responder DeviceResponder) + // NewAccessRequest creates a new access request object and validates // various parameters. // @@ -279,13 +304,18 @@ type AccessRequester interface { Requester } +// DeviceRequester is an device endpoint's request context. +type DeviceRequester interface { + Requester +} + // DeviceAuthorizeRequester is an device authorize endpoint's request context. type DeviceAuthorizeRequester interface { // SetDeviceRequestId set the device request id - SetDeviceRequestId(signature string) + SetDeviceCodeSignature(signature string) // GetDeviceRequestId returns the device code signature - GetDeviceRequestId() string + GetDeviceCodeSignature() string Requester } @@ -404,6 +434,9 @@ type G11NContext interface { } type DeviceAuthorizeResponder interface { +} + +type DeviceResponder interface { GetDeviceCode() string SetDeviceCode(code string) diff --git a/storage/memory.go b/storage/memory.go index 3fac804ab..9980b536c 100644 --- a/storage/memory.go +++ b/storage/memory.go @@ -544,16 +544,16 @@ func (s *MemoryStore) CreateDeviceCodeSession(_ context.Context, signature strin return nil } -func (s *MemoryStore) UpdateDeviceCodeSessionByRequestId(_ context.Context, id string, req fosite.Requester) error { +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() - if signature, exists := s.DeviceCodesRequestIDs[id]; exists { - _, ok := s.DeviceCodes[signature] - if !ok { - return fosite.ErrNotFound - } + // Only update if exist + if _, exists := s.DeviceCodes[signature]; exists { s.DeviceCodes[signature] = req + s.DeviceCodesRequestIDs[req.GetID()] = signature } return nil } From 341d4112286e10419d1f8c3abd73fa31af7806c2 Mon Sep 17 00:00:00 2001 From: Romain Caire Date: Wed, 14 Sep 2022 14:09:40 +0200 Subject: [PATCH 19/49] fix: edit comments --- fosite.go | 6 +++--- oauth2.go | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/fosite.go b/fosite.go index 050c06dcf..403e45114 100644 --- a/fosite.go +++ b/fosite.go @@ -103,7 +103,7 @@ func (a *PushedAuthorizeEndpointHandlers) Append(h PushedAuthorizeEndpointHandle // DeviceEndpointHandlers is a list of DeviceEndpointHandler type DeviceEndpointHandlers []DeviceEndpointHandler -// Append adds an AuthorizeEndpointHandler to this list. Ignores duplicates based on reflect.TypeOf. +// 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) { @@ -114,10 +114,10 @@ func (a *DeviceEndpointHandlers) Append(h DeviceEndpointHandler) { *a = append(*a, h) } -// DeviceEndpointHandlers is a list of DeviceEndpointHandler +// DeviceAuthorizeEndpointHandlers is a list of DeviceAuthorizeEndpointHandler type DeviceAuthorizeEndpointHandlers []DeviceAuthorizeEndpointHandler -// Append adds an AuthorizeEndpointHandler to this list. Ignores duplicates based on reflect.TypeOf. +// Append adds an DeviceAuthorizeEndpointHandlers to this list. Ignores duplicates based on reflect.TypeOf. func (a *DeviceAuthorizeEndpointHandlers) Append(h DeviceAuthorizeEndpointHandler) { for _, this := range *a { if reflect.TypeOf(this) == reflect.TypeOf(h) { diff --git a/oauth2.go b/oauth2.go index 8901947ec..5672081ca 100644 --- a/oauth2.go +++ b/oauth2.go @@ -311,10 +311,10 @@ type DeviceRequester interface { // DeviceAuthorizeRequester is an device authorize endpoint's request context. type DeviceAuthorizeRequester interface { - // SetDeviceRequestId set the device request id + // SetDeviceCodeSignature set the device code signature SetDeviceCodeSignature(signature string) - // GetDeviceRequestId returns the device code signature + // GetDeviceCodeSignature returns the device code signature GetDeviceCodeSignature() string Requester From fef42ec82c440e6bc72afec75432647eddce09e9 Mon Sep 17 00:00:00 2001 From: Romain Caire Date: Fri, 16 Sep 2022 12:38:13 +0200 Subject: [PATCH 20/49] Add missing handler in composer --- compose/compose.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/compose/compose.go b/compose/compose.go index 1c7105068..0ac7781cb 100644 --- a/compose/compose.go +++ b/compose/compose.go @@ -71,8 +71,11 @@ func Compose(config *fosite.Config, storage interface{}, strategy interface{}, f if ph, ok := res.(fosite.PushedAuthorizeEndpointHandler); ok { config.PushedAuthorizeEndpointHandlers.Append(ph) } - if dah, ok := res.(fosite.DeviceEndpointHandler); ok { - config.DeviceEndpointHandlers.Append(dah) + if dh, ok := res.(fosite.DeviceEndpointHandler); ok { + config.DeviceEndpointHandlers.Append(dh) + } + if dah, ok := res.(fosite.DeviceAuthorizeEndpointHandler); ok { + config.DeviceAuthorizeEndpointHandlers.Append(dah) } } From 9a30db2b2cecb8ad2025924f8de3eedc043c0384 Mon Sep 17 00:00:00 2001 From: Romain Caire Date: Thu, 29 Sep 2022 12:07:25 +0200 Subject: [PATCH 21/49] Retry CI --- device_authorize_request_handler_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/device_authorize_request_handler_test.go b/device_authorize_request_handler_test.go index 67eba526d..b2bd4158f 100644 --- a/device_authorize_request_handler_test.go +++ b/device_authorize_request_handler_test.go @@ -52,7 +52,7 @@ func TestNewDeviceAuthorizeGetRequest(t *testing.T) { { desc: "invalid client fails", conf: &Fosite{Store: store, Config: &Config{ScopeStrategy: ExactScopeStrategy, AudienceMatchingStrategy: DefaultAudienceMatchingStrategy}}, - query: url.Values{"device_verifier": []string{"AAAA"}}, + query: url.Values{"device_verifier": []string{"BBBB"}}, expectedError: ErrInvalidClient, mock: func() { store.EXPECT().GetClient(gomock.Any(), gomock.Any()).Return(nil, errors.New("foo")) From 7bbeb0234f6ef562b5ce3a679ea6f9f14e75b389 Mon Sep 17 00:00:00 2001 From: Romain Caire Date: Thu, 29 Sep 2022 12:27:41 +0200 Subject: [PATCH 22/49] Don't use `url.Values.Has()` as it doesn't exist in Golang 1.16... --- device_authorize_request_handler.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/device_authorize_request_handler.go b/device_authorize_request_handler.go index cd3d52852..2f3407963 100644 --- a/device_authorize_request_handler.go +++ b/device_authorize_request_handler.go @@ -39,7 +39,8 @@ func (f *Fosite) NewDeviceAuthorizeGetRequest(ctx context.Context, r *http.Reque } request.Form = r.Form - if request.GetRequestForm().Has("device_verifier") { + verifier := request.GetRequestForm().Get("device_verifier") + if verifier != "" { 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())) From 142b7ffd8c2909023faf998d422a517a366dd6d7 Mon Sep 17 00:00:00 2001 From: Romain Caire Date: Wed, 5 Oct 2022 12:12:05 +0200 Subject: [PATCH 23/49] Change from Forbiden to BadRequest for authorization_pending to better adehere RFC --- errors.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/errors.go b/errors.go index ffafb5d62..2c83b70f3 100644 --- a/errors.go +++ b/errors.go @@ -228,12 +228,12 @@ var ( 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.StatusForbidden, + CodeField: http.StatusBadRequest, } ErrDeviceExpiredToken = &RFC6749Error{ DescriptionField: "The device_code has expired, and the device authorization session has concluded.", ErrorField: errDeviceExpiredToken, - CodeField: http.StatusForbidden, + CodeField: http.StatusBadRequest, } ) From 5722afacf2050583c12d1f2d314d56d3293ba415 Mon Sep 17 00:00:00 2001 From: Romain Caire Date: Thu, 20 Oct 2022 21:48:32 +0200 Subject: [PATCH 24/49] Update go/x/text module to 0.4.0 --- go.mod | 11 +++++------ go.sum | 19 ++++++++++++++----- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/go.mod b/go.mod index f6af041ca..2169b14eb 100644 --- a/go.mod +++ b/go.mod @@ -36,9 +36,9 @@ require ( github.com/stretchr/testify v1.7.0 github.com/tidwall/gjson v1.7.1 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 - golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f + golang.org/x/net v0.0.0-20220722155237-a158d28d115b golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d - golang.org/x/text v0.3.7 + golang.org/x/text v0.4.0 gopkg.in/square/go-jose.v2 v2.5.2-0.20210529014059-a5c7eec3c614 ) @@ -69,10 +69,9 @@ require ( github.com/subosito/gotenv v1.2.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect - golang.org/x/mod v0.4.2 // indirect - golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect - golang.org/x/tools v0.1.1 // indirect - golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect + golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect + golang.org/x/tools v0.1.12 // indirect google.golang.org/appengine v1.6.5 // indirect google.golang.org/protobuf v1.25.0 // indirect gopkg.in/ini.v1 v1.57.0 // indirect diff --git a/go.sum b/go.sum index 302b10978..5df212333 100644 --- a/go.sum +++ b/go.sum @@ -707,6 +707,7 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= go.elastic.co/apm v1.8.0/go.mod h1:tCw6CkOJgkWnzEthFN9HUP1uL3Gjc/Ur6m7gRPLaoH0= @@ -778,8 +779,9 @@ golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 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-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -814,8 +816,9 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f h1:OfiFi4JbukWwe3lzw+xunroH1mnC1e2Gy5cxNJApiSY= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181003184128-c57b0facaced/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -833,6 +836,7 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180831094639-fa5fdf94c789/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -877,16 +881,20 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 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/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -930,8 +938,9 @@ golang.org/x/tools v0.0.0-20200522201501-cb1345f3a375/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200717024301-6ddee64345a6/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.1 h1:wGiQel/hW0NnEkJUk8lbzkX2gFJU6PFxf1v5OlCfuOs= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 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= From 3483d6e9515920e656ac4c8a91c5ecff1a741b4e Mon Sep 17 00:00:00 2001 From: Romain Caire Date: Mon, 12 Dec 2022 16:17:45 +0100 Subject: [PATCH 25/49] Update following merge --- token/hmac/hmacsha.go | 15 ++++++++++++--- token/hmac/hmacsha_test.go | 2 +- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/token/hmac/hmacsha.go b/token/hmac/hmacsha.go index 134a11688..35d300259 100644 --- a/token/hmac/hmacsha.go +++ b/token/hmac/hmacsha.go @@ -170,15 +170,24 @@ func (c *HMACStrategy) Signature(token string) string { return split[1] } -func (c *HMACStrategy) GenerateHMACForString(ctx context.Context, text string) string { +func (c *HMACStrategy) GenerateHMACForString(ctx context.Context, text string) (string, error) { var signingKey [32]byte - copy(signingKey[:], c.Config.GetGlobalSecret(ctx)) + + 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.URLEncoding.EncodeToString(hashBytes) - return b64 + return b64, nil } func (c *HMACStrategy) generateHMAC(ctx context.Context, data []byte, key *[32]byte) []byte { diff --git a/token/hmac/hmacsha_test.go b/token/hmac/hmacsha_test.go index 2a3500b53..19c6b11ac 100644 --- a/token/hmac/hmacsha_test.go +++ b/token/hmac/hmacsha_test.go @@ -159,7 +159,7 @@ func TestGenerateFromString(t *testing.T) { hash: "zHYDOZGjzhVjx5r8RlBhpnJemX5JxEEBUjVT01n3IFM=", }, } { - hash := cg.GenerateHMACForString(context.Background(), c.text) + hash, _ := cg.GenerateHMACForString(context.Background(), c.text) assert.Equal(t, c.hash, hash) } } From 86c4f9508d528afa5fc52d80736c38d18cb06e05 Mon Sep 17 00:00:00 2001 From: Romain Caire Date: Tue, 13 Dec 2022 19:16:48 +0100 Subject: [PATCH 26/49] Initial refactoring following @hackerman recommandations Please don't use at this point as it's not tested with hydra yet (only unit test) --- compose/compose.go | 4 +- compose/compose_oauth2.go | 13 - compose/compose_pkce.go | 9 - compose/compose_rfc8628.go | 19 + compose/compose_strategy.go | 9 + device_authorize_request.go | 28 +- device_authorize_request_handler.go | 24 +- device_authorize_request_handler_test.go | 50 +- device_authorize_request_test.go | 26 +- device_authorize_response.go | 33 +- device_authorize_response_writer.go | 22 +- device_authorize_writer.go | 8 +- device_request.go | 22 +- device_request_handler.go | 5 +- device_request_handler_test.go | 22 +- device_response.go | 33 +- device_response_writer.go | 22 +- device_response_writer_test.go | 22 +- device_writer.go => device_write.go | 29 +- device_write_test.go | 22 +- generate-mocks.sh | 5 +- handler.go | 4 +- handler/.DS_Store | Bin 0 -> 6148 bytes handler/oauth2/flow_authorize_code_auth.go | 3 + handler/oauth2/flow_authorize_code_token.go | 99 ++- .../oauth2/flow_authorize_code_token_test.go | 630 ++++++++++++++++ handler/oauth2/flow_device_token.go | 212 ------ handler/oauth2/flow_device_token_test.go | 677 ------------------ handler/oauth2/storage.go | 39 - handler/oauth2/strategy.go | 14 - handler/oauth2/strategy_hmacsha.go | 64 -- handler/oauth2/strategy_hmacsha_test.go | 87 +-- handler/oauth2/strategy_jwt.go | 24 - handler/openid/flow_device_auth.go | 24 +- handler/openid/flow_device_token.go | 26 +- handler/pkce/handler.go | 75 +- handler/pkce/handler_device.go | 241 ------- handler/pkce/handler_device_test.go | 63 +- handler/pkce/handler_test.go | 3 +- .../auth_handler.go} | 37 +- .../auth_handler_test.go} | 17 +- handler/rfc8628/storage.go | 52 ++ handler/rfc8628/strategy.go | 27 + handler/rfc8628/strategy_hmacsha.go | 92 +++ handler/rfc8628/strategy_hmacsha_test.go | 150 ++++ internal/device_authorize_handler.go | 3 + internal/device_authorize_request.go | 3 + internal/device_authorize_response.go | 32 + internal/device_code_storage.go | 5 +- internal/device_handler.go | 3 + internal/device_request.go | 3 + internal/device_response.go | 30 + internal/oauth2_auth_device_storage.go | 139 ++++ internal/oauth2_storage.go | 100 --- internal/oauth2_strategy.go | 88 --- internal/user_code_storage.go | 5 +- oauth2.go | 47 +- 57 files changed, 1534 insertions(+), 2011 deletions(-) create mode 100644 compose/compose_rfc8628.go rename device_writer.go => device_write.go (59%) create mode 100644 handler/.DS_Store delete mode 100644 handler/oauth2/flow_device_token.go delete mode 100644 handler/oauth2/flow_device_token_test.go delete mode 100644 handler/pkce/handler_device.go rename handler/{oauth2/flow_device_auth.go => rfc8628/auth_handler.go} (63%) rename handler/{oauth2/flow_device_auth_test.go => rfc8628/auth_handler_test.go} (81%) create mode 100644 handler/rfc8628/storage.go create mode 100644 handler/rfc8628/strategy.go create mode 100644 handler/rfc8628/strategy_hmacsha.go create mode 100644 handler/rfc8628/strategy_hmacsha_test.go create mode 100644 internal/oauth2_auth_device_storage.go diff --git a/compose/compose.go b/compose/compose.go index 100eac1ea..a5179bb7b 100644 --- a/compose/compose.go +++ b/compose/compose.go @@ -74,6 +74,7 @@ func ComposeAllEnabled(config *fosite.Config, storage interface{}, key interface storage, &CommonStrategy{ CoreStrategy: NewOAuth2HMACStrategy(config), + RFC8628CodeStrategy: NewRFC8628CodeStrategy(config), OpenIDConnectTokenStrategy: NewOpenIDConnectStrategy(keyGetter, config), Signer: &jwt.DefaultSigner{GetPrivateKey: keyGetter}, }, @@ -81,9 +82,9 @@ func ComposeAllEnabled(config *fosite.Config, storage interface{}, key interface OAuth2AuthorizeImplicitFactory, OAuth2ClientCredentialsGrantFactory, OAuth2RefreshTokenGrantFactory, - OAuth2DeviceFactory, OAuth2ResourceOwnerPasswordCredentialsFactory, RFC7523AssertionGrantFactory, + RFC8628DeviceFactory, OpenIDConnectExplicitFactory, OpenIDConnectImplicitFactory, @@ -96,6 +97,5 @@ func ComposeAllEnabled(config *fosite.Config, storage interface{}, key interface OAuth2PKCEFactory, PushedAuthorizeHandlerFactory, - OAuth2DevicePKCEFactory, ) } diff --git a/compose/compose_oauth2.go b/compose/compose_oauth2.go index 0ed17a955..accc7d4eb 100644 --- a/compose/compose_oauth2.go +++ b/compose/compose_oauth2.go @@ -107,16 +107,3 @@ func OAuth2StatelessJWTIntrospectionFactory(config fosite.Configurator, storage Config: config, } } - -// OAuth2DeviceFactory 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 OAuth2DeviceFactory(config fosite.Configurator, storage interface{}, strategy interface{}) interface{} { - return &oauth2.DeviceHandler{ - DeviceCodeStrategy: strategy.(oauth2.DeviceCodeStrategy), - UserCodeStrategy: strategy.(oauth2.UserCodeStrategy), - AccessTokenStrategy: strategy.(oauth2.AccessTokenStrategy), - RefreshTokenStrategy: strategy.(oauth2.RefreshTokenStrategy), - CoreStorage: storage.(oauth2.CoreStorage), - Config: config, - } -} diff --git a/compose/compose_pkce.go b/compose/compose_pkce.go index 8ab75c5db..286f4ae93 100644 --- a/compose/compose_pkce.go +++ b/compose/compose_pkce.go @@ -17,12 +17,3 @@ func OAuth2PKCEFactory(config fosite.Configurator, storage interface{}, strategy Config: config, } } - -func OAuth2DevicePKCEFactory(config fosite.Configurator, storage interface{}, strategy interface{}) interface{} { - return &pkce.HandlerDevice{ - DeviceCodeStrategy: strategy.(oauth2.DeviceCodeStrategy), - UserCodeStrategy: strategy.(oauth2.UserCodeStrategy), - Storage: storage.(pkce.PKCERequestStorage), - Config: config, - } -} diff --git a/compose/compose_rfc8628.go b/compose/compose_rfc8628.go new file mode 100644 index 000000000..4bceeed5e --- /dev/null +++ b/compose/compose_rfc8628.go @@ -0,0 +1,19 @@ +// Copyright © 2022 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package compose + +import ( + "github.com/ory/fosite" + "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, + } +} diff --git a/compose/compose_strategy.go b/compose/compose_strategy.go index a0c948daa..aecb5c225 100644 --- a/compose/compose_strategy.go +++ b/compose/compose_strategy.go @@ -9,12 +9,14 @@ 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" ) type CommonStrategy struct { oauth2.CoreStrategy + rfc8628.RFC8628CodeStrategy openid.OpenIDConnectTokenStrategy jwt.Signer } @@ -51,3 +53,10 @@ func NewOpenIDConnectStrategy(keyGetter func(context.Context) (interface{}, erro Config: config, } } + +func NewRFC8628CodeStrategy(config fosite.Configurator) *rfc8628.DefaultDeviceStrategy { + return &rfc8628.DefaultDeviceStrategy{ + Enigma: &hmac.HMACStrategy{Config: config}, + Config: config, + } +} diff --git a/device_authorize_request.go b/device_authorize_request.go index 5c71375ae..e2f0d7140 100644 --- a/device_authorize_request.go +++ b/device_authorize_request.go @@ -1,38 +1,20 @@ -/* - * Copyright © 2015-2018 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-2018 Aeneas Rekkas - * @license Apache-2.0 - * - */ +// Copyright © 2022 Ory Corp +// SPDX-License-Identifier: Apache-2.0 package fosite // DeviceAuthorizeRequest is an implementation of DeviceAuthorizeRequester type DeviceAuthorizeRequest struct { - deviceCodeSignature string + signature string Request } func (d *DeviceAuthorizeRequest) GetDeviceCodeSignature() string { - return d.deviceCodeSignature + return d.signature } func (d *DeviceAuthorizeRequest) SetDeviceCodeSignature(signature string) { - d.deviceCodeSignature = signature + d.signature = signature } func NewDeviceAuthorizeRequest() *DeviceAuthorizeRequest { diff --git a/device_authorize_request_handler.go b/device_authorize_request_handler.go index f2c358334..d11bab4f4 100644 --- a/device_authorize_request_handler.go +++ b/device_authorize_request_handler.go @@ -1,23 +1,5 @@ -/* - * 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 - * - */ +// Copyright © 2022 Ory Corp +// SPDX-License-Identifier: Apache-2.0 package fosite @@ -33,7 +15,7 @@ func (f *Fosite) NewDeviceAuthorizeRequest(ctx context.Context, r *http.Request) request := NewDeviceAuthorizeRequest() request.Lang = i18n.GetLangFromRequest(f.Config.GetMessageCatalog(ctx), r) - if err := r.ParseMultipartForm(1 << 20); err != nil && err != http.ErrNotMultipart { + if err := r.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 = r.Form diff --git a/device_authorize_request_handler_test.go b/device_authorize_request_handler_test.go index 9033c6b54..6ec74e9e0 100644 --- a/device_authorize_request_handler_test.go +++ b/device_authorize_request_handler_test.go @@ -1,31 +1,15 @@ -/* - * Copyright © 2015-2018 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-2018 Aeneas Rekkas - * @license Apache-2.0 - * - */ +// Copyright © 2022 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" @@ -44,6 +28,7 @@ func TestNewDeviceAuthorizeRequest(t *testing.T) { conf *Fosite r *http.Request query url.Values + form url.Values expectedError error mock func() expect *DeviceAuthorizeRequest @@ -88,6 +73,26 @@ func TestNewDeviceAuthorizeRequest(t *testing.T) { }, }, }, + { + 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: &DeviceAuthorizeRequest{ + 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}}, @@ -112,6 +117,11 @@ func TestNewDeviceAuthorizeRequest(t *testing.T) { 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 diff --git a/device_authorize_request_test.go b/device_authorize_request_test.go index 78e1502cc..e0710cc60 100644 --- a/device_authorize_request_test.go +++ b/device_authorize_request_test.go @@ -1,23 +1,5 @@ -/* - * Copyright © 2015-2018 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-2018 Aeneas Rekkas - * @license Apache-2.0 - * - */ +// Copyright © 2022 Ory Corp +// SPDX-License-Identifier: Apache-2.0 package fosite @@ -47,7 +29,7 @@ func TestDeviceAuthorizeRequest(t *testing.T) { }, { ar: &DeviceAuthorizeRequest{ - deviceCodeSignature: "AAAA", + signature: "AAAA", Request: Request{ Client: &DefaultClient{RedirectURIs: []string{""}}, }, @@ -64,7 +46,7 @@ func TestDeviceAuthorizeRequest(t *testing.T) { }, } { assert.Equal(t, c.ar.Client, c.ar.GetClient(), "%d", k) - assert.Equal(t, c.ar.deviceCodeSignature, c.ar.GetDeviceCodeSignature(), "%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) diff --git a/device_authorize_response.go b/device_authorize_response.go index 399645591..274d28039 100644 --- a/device_authorize_response.go +++ b/device_authorize_response.go @@ -1,29 +1,22 @@ -/* - * Copyright © 2015-2018 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-2018 Aeneas Rekkas - * @license Apache-2.0 - * - */ +// Copyright © 2022 Ory Corp +// SPDX-License-Identifier: Apache-2.0 package fosite +import "net/http" + type DeviceAuthorizeResponse struct { + Header http.Header } func NewDeviceAuthorizeResponse() *DeviceAuthorizeResponse { return &DeviceAuthorizeResponse{} } + +func (a *DeviceAuthorizeResponse) GetHeader() http.Header { + return a.Header +} + +func (a *DeviceAuthorizeResponse) AddHeader(key, value string) { + a.Header.Add(key, value) +} diff --git a/device_authorize_response_writer.go b/device_authorize_response_writer.go index 3866a4a2c..493dc5555 100644 --- a/device_authorize_response_writer.go +++ b/device_authorize_response_writer.go @@ -1,23 +1,5 @@ -/* - * Copyright © 2015-2018 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-2018 Aeneas Rekkas - * @license Apache-2.0 - * - */ +// Copyright © 2022 Ory Corp +// SPDX-License-Identifier: Apache-2.0 package fosite diff --git a/device_authorize_writer.go b/device_authorize_writer.go index c6e07f2c2..f12c6c121 100644 --- a/device_authorize_writer.go +++ b/device_authorize_writer.go @@ -1,3 +1,6 @@ +// Copyright © 2022 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + /* * Copyright © 2015-2021 Aeneas Rekkas * @@ -26,8 +29,9 @@ import ( "net/http" ) -// TODO: Do documentation - +// Once the user has approved the grant he will be redirected on his loggin machine +// to a webpage (usally hosted in hydra-ui) to understand that he was connected successfully +// and that he can close this tab and return to his non-interactive device; func (f *Fosite) WriteDeviceAuthorizeResponse(ctx context.Context, rw http.ResponseWriter, requester DeviceAuthorizeRequester, responder DeviceAuthorizeResponder) { rw.Header().Set("Cache-Control", "no-store") rw.Header().Set("Pragma", "no-cache") diff --git a/device_request.go b/device_request.go index 643239431..3c521a113 100644 --- a/device_request.go +++ b/device_request.go @@ -1,23 +1,5 @@ -/* - * Copyright © 2015-2018 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-2018 Aeneas Rekkas - * @license Apache-2.0 - * - */ +// Copyright © 2022 Ory Corp +// SPDX-License-Identifier: Apache-2.0 package fosite diff --git a/device_request_handler.go b/device_request_handler.go index 71723ce02..0df47bf6c 100644 --- a/device_request_handler.go +++ b/device_request_handler.go @@ -1,3 +1,6 @@ +// Copyright © 2022 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + /* * Copyright © 2015-2021 Aeneas Rekkas * @@ -45,7 +48,7 @@ func (f *Fosite) NewDeviceRequest(ctx context.Context, req *http.Request) (Devic } request.Client = client - if !client.GetGrantTypes().Has("urn:ietf:params:oauth:grant-type:device_code") { + 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.")) } diff --git a/device_request_handler_test.go b/device_request_handler_test.go index 8474a23cf..13ef52cb8 100644 --- a/device_request_handler_test.go +++ b/device_request_handler_test.go @@ -1,23 +1,5 @@ -/* - * Copyright © 2015-2018 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-2018 Aeneas Rekkas - * @license Apache-2.0 - * - */ +// Copyright © 2022 Ory Corp +// SPDX-License-Identifier: Apache-2.0 package fosite_test diff --git a/device_response.go b/device_response.go index cfd6885ea..ca6eec774 100644 --- a/device_response.go +++ b/device_response.go @@ -1,27 +1,12 @@ -/* - * Copyright © 2015-2018 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-2018 Aeneas Rekkas - * @license Apache-2.0 - * - */ +// Copyright © 2022 Ory Corp +// SPDX-License-Identifier: Apache-2.0 package fosite +import "net/http" + type DeviceResponse struct { + Header http.Header deviceCode string userCode string verificationURI string @@ -86,3 +71,11 @@ func (d *DeviceResponse) GetInterval() int { func (d *DeviceResponse) SetInterval(seconds int) { d.interval = seconds } + +func (a *DeviceResponse) GetHeader() http.Header { + return a.Header +} + +func (a *DeviceResponse) AddHeader(key, value string) { + a.Header.Add(key, value) +} diff --git a/device_response_writer.go b/device_response_writer.go index 90ae18ef9..5080f7635 100644 --- a/device_response_writer.go +++ b/device_response_writer.go @@ -1,23 +1,5 @@ -/* - * Copyright © 2015-2018 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-2018 Aeneas Rekkas - * @license Apache-2.0 - * - */ +// Copyright © 2022 Ory Corp +// SPDX-License-Identifier: Apache-2.0 package fosite diff --git a/device_response_writer_test.go b/device_response_writer_test.go index a35cc243d..b8eca533f 100644 --- a/device_response_writer_test.go +++ b/device_response_writer_test.go @@ -1,23 +1,5 @@ -/* - * Copyright © 2015-2018 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-2018 Aeneas Rekkas - * @license Apache-2.0 - * - */ +// Copyright © 2022 Ory Corp +// SPDX-License-Identifier: Apache-2.0 package fosite_test diff --git a/device_writer.go b/device_write.go similarity index 59% rename from device_writer.go rename to device_write.go index 5b57ef04d..a9abb57f8 100644 --- a/device_writer.go +++ b/device_write.go @@ -1,23 +1,5 @@ -/* - * 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 - * - */ +// Copyright © 2022 Ory Corp +// SPDX-License-Identifier: Apache-2.0 package fosite @@ -30,6 +12,13 @@ import ( // 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") diff --git a/device_write_test.go b/device_write_test.go index cf3cc0316..05b5c6a70 100644 --- a/device_write_test.go +++ b/device_write_test.go @@ -1,23 +1,5 @@ -/* - * Copyright © 2015-2018 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-2018 Aeneas Rekkas - * @license Apache-2.0 - * - */ +// Copyright © 2022 Ory Corp +// SPDX-License-Identifier: Apache-2.0 package fosite_test diff --git a/generate-mocks.sh b/generate-mocks.sh index 091e5f4c8..c0ca047d5 100755 --- a/generate-mocks.sh +++ b/generate-mocks.sh @@ -6,9 +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/oauth2 DeviceCodeStorage -mockgen -package internal -destination internal/user_code_storage.go github.com/ory/fosite/handler/oauth2 UserCodeStorage +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 diff --git a/handler.go b/handler.go index 255d14729..2f54ffb05 100644 --- a/handler.go +++ b/handler.go @@ -79,6 +79,8 @@ type DeviceEndpointHandler interface { type DeviceAuthorizeEndpointHandler interface { // HandleDeviceAuthorizeEndpointRequest handles a device authorize endpoint request. - // TODO doc + // 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. HandleDeviceAuthorizeEndpointRequest(ctx context.Context, requester DeviceAuthorizeRequester, responder DeviceAuthorizeResponder) error } diff --git a/handler/.DS_Store b/handler/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..bded983cf73288f70939dfee390fe9479f3f9c53 GIT binary patch literal 6148 zcmeHKyJ`bL3>+nf7~HsYxxbJU!R2D?FYpfpY@8-IU`Xn#{9XC9j2?uTbDBV!Fal|I zwVFL{iqi>zt={(cz#PDo?ufI8vH7|C$ZjfQL^|(y!~>S^+tcyobyR&l;oLi%aKH}B z@BHI)-g=#;tQ3#}Qa}nw0V(iH1-$ptrn5vvDIf);z?TC4eQ0#YUN|Jir-LCz0OEw{ zFs@^kAU01Bd*P7C49${COsdt0VM%AcRb4L}5|a+A;lt|5RuhWF(|Lc3a#&APlmb%V zQi0pt&b|L{=s(Q=mn7|^fE4&w3fN@5U9b2`)muj|=e@SkpXgrmL3iUiC=Ah#iP4U^ g@pin3qO5Da=6Nq15`)fs(24pPa9w0l;I9?<1lOn(CIA2c literal 0 HcmV?d00001 diff --git a/handler/oauth2/flow_authorize_code_auth.go b/handler/oauth2/flow_authorize_code_auth.go index f038647d6..d4b6325b8 100644 --- a/handler/oauth2/flow_authorize_code_auth.go +++ b/handler/oauth2/flow_authorize_code_auth.go @@ -9,6 +9,7 @@ import ( "strings" "time" + "github.com/ory/fosite/handler/rfc8628" "github.com/ory/x/errorsx" "github.com/ory/fosite" @@ -25,6 +26,8 @@ type AuthorizeExplicitGrantHandler struct { AuthorizeCodeStrategy AuthorizeCodeStrategy CoreStorage CoreStorage TokenRevocationStorage TokenRevocationStorage + DeviceStrategy rfc8628.RFC8628CodeStrategy + DeviceStorage rfc8628.RFC8628CodeStorage Config interface { fosite.AuthorizeCodeLifespanProvider fosite.AccessTokenLifespanProvider diff --git a/handler/oauth2/flow_authorize_code_token.go b/handler/oauth2/flow_authorize_code_token.go index 5265b6a54..d07f11202 100644 --- a/handler/oauth2/flow_authorize_code_token.go +++ b/handler/oauth2/flow_authorize_code_token.go @@ -23,18 +23,22 @@ func (c *AuthorizeExplicitGrantHandler) HandleTokenEndpointRequest(ctx context.C 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\".")) + if isAuthorizationCode(request) { + 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\".")) + } + } else if isDeviceCode(request) { + if !request.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\".")) + } } - 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) { + 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("GetAuthorizeCodeSession must return a value for \"fosite.Requester\" when returning \"ErrInvalidatedAuthorizeCode\".") + 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. @@ -56,10 +60,18 @@ func (c *AuthorizeExplicitGrantHandler) HandleTokenEndpointRequest(ctx context.C 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())) + if isAuthorizationCode(request) { + // 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())) + } + } else if isDeviceCode(request) { + // The authorization server MUST verify that the device code is valid + // This needs to happen after store retrieval for the session to be hydrated properly + if err := c.DeviceStrategy.ValidateDeviceCode(ctx, request, code); err != nil { + return errorsx.WithStack(err) + } } // Override scopes @@ -121,14 +133,19 @@ func (c *AuthorizeExplicitGrantHandler) PopulateTokenEndpointResponse(ctx contex return errorsx.WithStack(fosite.ErrUnknownRequest) } - code := requester.GetRequestForm().Get("code") - signature := c.AuthorizeCodeStrategy.AuthorizeCodeSignature(ctx, code) - authorizeRequest, err := c.CoreStorage.GetAuthorizeCodeSession(ctx, signature, requester.GetSession()) + 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.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())) + } else if isAuthorizationCode(requester) { + 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())) + } + } else if isDeviceCode(requester) { + if err := c.DeviceStrategy.ValidateDeviceCode(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() { @@ -164,9 +181,17 @@ func (c *AuthorizeExplicitGrantHandler) PopulateTokenEndpointResponse(ctx contex } }() - 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 { + if isAuthorizationCode(requester) { + if err = c.CoreStorage.InvalidateAuthorizeCodeSession(ctx, signature); err != nil { + return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) + } + } else if isDeviceCode(requester) { + if err = c.DeviceStorage.InvalidateDeviceCodeSession(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 { @@ -191,11 +216,41 @@ func (c *AuthorizeExplicitGrantHandler) PopulateTokenEndpointResponse(ctx contex } func (c *AuthorizeExplicitGrantHandler) CanSkipClientAuth(ctx context.Context, requester fosite.AccessRequester) bool { - return false + return isDeviceCode(requester) } func (c *AuthorizeExplicitGrantHandler) CanHandleTokenEndpointRequest(ctx context.Context, requester fosite.AccessRequester) bool { + return isDeviceCode(requester) || isAuthorizationCode(requester) +} + +func (c *AuthorizeExplicitGrantHandler) getCodeAndSession(ctx context.Context, requester fosite.AccessRequester) (code string, signature string, request fosite.Requester, err error) { + if isAuthorizationCode(requester) { + code := requester.GetRequestForm().Get("code") + signature := c.AuthorizeCodeStrategy.AuthorizeCodeSignature(ctx, code) + req, err := c.CoreStorage.GetAuthorizeCodeSession(ctx, signature, requester.GetSession()) + return code, signature, req, err + } else if isDeviceCode(requester) { + code := requester.GetRequestForm().Get("device_code") + 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 + } + + // We should never fall here + return "", "", nil, errorsx.WithStack(fosite.ErrAccessDenied.WithHint("This OAuth 2.0 request could not be identified")) +} + +func isDeviceCode(requester fosite.AccessRequester) bool { + // grant_type REQUIRED. + // Value MUST be set to "urn:ietf:params:oauth:grant-type:device_code" + return requester.GetGrantTypes().ExactOne(string(fosite.GrantTypeDeviceCode)) // && len(requester.GetRequestForm().Get("device_code")) > 0 +} + +func isAuthorizationCode(requester fosite.AccessRequester) bool { // grant_type REQUIRED. // Value MUST be set to "authorization_code" - return requester.GetGrantTypes().ExactOne("authorization_code") + return requester.GetGrantTypes().ExactOne("authorization_code") // && len(requester.GetRequestForm().Get("code")) > 0 } diff --git a/handler/oauth2/flow_authorize_code_token_test.go b/handler/oauth2/flow_authorize_code_token_test.go index 419409938..1bc0ceadd 100644 --- a/handler/oauth2/flow_authorize_code_token_test.go +++ b/handler/oauth2/flow_authorize_code_token_test.go @@ -11,6 +11,7 @@ import ( "github.com/golang/mock/gomock" + "github.com/ory/fosite/handler/rfc8628" "github.com/ory/fosite/internal" //"github.com/golang/mock/gomock" @@ -673,3 +674,632 @@ func TestAuthorizeCodeTransactional_HandleTokenEndpointRequest(t *testing.T) { }) } } + +func TestDeviceAuthorizeCode_PopulateTokenEndpointResponse(t *testing.T) { + for k, strategy := range map[string]struct { + CoreStrategy + rfc8628.RFC8628CodeStrategy + }{ + "hmac": {&hmacshaStrategy, &RFC8628HMACSHAStrategy}, + } { + t.Run("strategy="+k, func(t *testing.T) { + store := storage.NewMemoryStore() + + var h AuthorizeExplicitGrantHandler + 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 = AuthorizeExplicitGrantHandler{ + CoreStorage: store, + DeviceStorage: store, + AuthorizeCodeStrategy: strategy.CoreStrategy, + AccessTokenStrategy: strategy.CoreStrategy, + RefreshTokenStrategy: strategy.CoreStrategy, + DeviceStrategy: strategy.RFC8628CodeStrategy, + Config: config, + } + + 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 TestDeviceAuthorizeCode_HandleTokenEndpointRequest(t *testing.T) { + for k, strategy := range map[string]struct { + CoreStrategy + rfc8628.RFC8628CodeStrategy + }{ + "hmac": {&hmacshaStrategy, &RFC8628HMACSHAStrategy}, + } { + t.Run("strategy="+k, func(t *testing.T) { + store := storage.NewMemoryStore() + + h := AuthorizeExplicitGrantHandler{ + CoreStorage: store, + DeviceStorage: store, + AuthorizeCodeStrategy: strategy.CoreStrategy, + AccessTokenStrategy: strategy.CoreStrategy, + RefreshTokenStrategy: strategy.CoreStrategy, + DeviceStrategy: strategy.RFC8628CodeStrategy, + Config: &fosite.Config{ + ScopeStrategy: fosite.HierarchicScopeStrategy, + AudienceMatchingStrategy: fosite.DefaultAudienceMatchingStrategy, + AuthorizeCodeLifespan: time.Minute, + }, + } + for i, c := range []struct { + areq *fosite.AccessRequest + authreq *fosite.DeviceAuthorizeRequest + description string + setup func(t *testing.T, areq *fosite.AccessRequest, authreq *fosite.DeviceAuthorizeRequest) + check func(t *testing.T, areq *fosite.AccessRequest, authreq *fosite.DeviceAuthorizeRequest) + 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.DeviceAuthorizeRequest) { + 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.DeviceAuthorizeRequest{ + 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.DeviceAuthorizeRequest) { + 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.DeviceAuthorizeRequest{ + 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.DeviceAuthorizeRequest) { + 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.DeviceAuthorizeRequest) { + 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.DeviceAuthorizeRequest) { + 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 TestDeviceAuthorizeCodeTransactional_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 + CoreStorage + } + + type deviceTransactionalStore struct { + storage.Transactional + rfc8628.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 := AuthorizeExplicitGrantHandler{ + CoreStorage: coreTransactionalStore{ + mockTransactional, + mockCoreStore, + }, + DeviceStorage: deviceTransactionalStore{ + mockTransactional, + mockDeviceStore, + }, + AuthorizeCodeStrategy: &strategy, + AccessTokenStrategy: &strategy, + RefreshTokenStrategy: &strategy, + DeviceStrategy: &deviceStrategy, + 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/handler/oauth2/flow_device_token.go b/handler/oauth2/flow_device_token.go deleted file mode 100644 index a271c34e4..000000000 --- a/handler/oauth2/flow_device_token.go +++ /dev/null @@ -1,212 +0,0 @@ -/* - * Copyright © 2015-2018 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-2018 Aeneas Rekkas - * @license 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" -) - -// HandleTokenEndpointRequest implements -// * https://tools.ietf.org/html/rfc8628#section-3.4 (everything) -func (c *DeviceHandler) 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("urn:ietf:params:oauth:grant-type:device_code") { - 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\".")) - } - - code := request.GetRequestForm().Get("device_code") - signature := c.DeviceCodeStrategy.DeviceCodeSignature(ctx, code) - authorizeRequest, err := c.CoreStorage.GetDeviceCodeSession(ctx, signature, request.GetSession()) - if 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("GetDeviceCodeSession must return a value for \"fosite.Requester\" when returning \"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) { - 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())) - } - - // 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.DeviceCodeStrategy.ValidateDeviceCode(ctx, request, code); 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.")) - } - - // 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 (*DeviceHandler) canIssueRefreshToken(ctx context.Context, c *DeviceHandler, 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 *DeviceHandler) PopulateTokenEndpointResponse(ctx context.Context, requester fosite.AccessRequester, responder fosite.AccessResponder) error { - if !c.CanHandleTokenEndpointRequest(ctx, requester) { - return errorsx.WithStack(fosite.ErrUnknownRequest) - } - - code := requester.GetRequestForm().Get("device_code") - signature := c.DeviceCodeStrategy.DeviceCodeSignature(ctx, code) - authorizeRequest, err := c.CoreStorage.GetDeviceCodeSession(ctx, signature, requester.GetSession()) - if err != nil { - return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) - } else if err := c.DeviceCodeStrategy.ValidateDeviceCode(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, 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.InvalidateDeviceCodeSession(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())) - } - - return nil -} - -func (c *DeviceHandler) CanSkipClientAuth(ctx context.Context, requester fosite.AccessRequester) bool { - return true -} - -func (c *DeviceHandler) CanHandleTokenEndpointRequest(ctx context.Context, requester fosite.AccessRequester) bool { - // grant_type REQUIRED. - // Value MUST be set to "urn:ietf:params:oauth:grant-type:device_code" - return requester.GetGrantTypes().ExactOne("urn:ietf:params:oauth:grant-type:device_code") -} diff --git a/handler/oauth2/flow_device_token_test.go b/handler/oauth2/flow_device_token_test.go deleted file mode 100644 index 030ae2c4f..000000000 --- a/handler/oauth2/flow_device_token_test.go +++ /dev/null @@ -1,677 +0,0 @@ -/* - * Copyright © 2015-2018 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-2018 Aeneas Rekkas - * @license Apache-2.0 - * - */ - -package oauth2 - -import ( - "context" - "fmt" - "net/url" - "testing" //"time" - - "github.com/golang/mock/gomock" - - "github.com/ory/fosite/internal" - - //"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" -) - -func TestDeviceAuthorizeCode_PopulateTokenEndpointResponse(t *testing.T) { - for k, strategy := range map[string]CoreStrategy{ - "hmac": &hmacshaStrategy, - } { - t.Run("strategy="+k, func(t *testing.T) { - store := storage.NewMemoryStore() - - var h DeviceHandler - 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 = DeviceHandler{ - CoreStorage: store, - DeviceCodeStrategy: strategy, - UserCodeStrategy: strategy, - AccessTokenStrategy: strategy, - RefreshTokenStrategy: strategy, - Config: config, - } - - 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 TestDeviceAuthorizeCode_HandleTokenEndpointRequest(t *testing.T) { - for k, strategy := range map[string]CoreStrategy{ - "hmac": &hmacshaStrategy, - } { - t.Run("strategy="+k, func(t *testing.T) { - store := storage.NewMemoryStore() - - h := DeviceHandler{ - CoreStorage: store, - DeviceCodeStrategy: &hmacshaStrategy, - UserCodeStrategy: &hmacshaStrategy, - Config: &fosite.Config{ - ScopeStrategy: fosite.HierarchicScopeStrategy, - AudienceMatchingStrategy: fosite.DefaultAudienceMatchingStrategy, - AuthorizeCodeLifespan: time.Minute, - }, - } - for i, c := range []struct { - areq *fosite.AccessRequest - authreq *fosite.DeviceAuthorizeRequest - description string - setup func(t *testing.T, areq *fosite.AccessRequest, authreq *fosite.DeviceAuthorizeRequest) - check func(t *testing.T, areq *fosite.AccessRequest, authreq *fosite.DeviceAuthorizeRequest) - 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.DeviceAuthorizeRequest) { - 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.DeviceAuthorizeRequest{ - 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.DeviceAuthorizeRequest) { - 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.DeviceAuthorizeRequest{ - 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.DeviceAuthorizeRequest) { - 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.DeviceAuthorizeRequest) { - 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.DeviceAuthorizeRequest) { - code, sig, err := strategy.GenerateAuthorizeCode(nil, nil) - require.NoError(t, err) - areq.Form.Add("code", code) - - require.NoError(t, store.CreateAuthorizeCodeSession(nil, sig, areq)) - require.NoError(t, store.InvalidateAuthorizeCodeSession(nil, sig)) - }, - description: "should fail because code has been used already", - expectErr: fosite.ErrInvalidGrant, - }, - { - 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.DeviceAuthorizeRequest) { - 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.DeviceAuthorizeRequest) { - 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 TestDeviceAuthorizeCodeTransactional_HandleTokenEndpointRequest(t *testing.T) { - var mockTransactional *internal.MockTransactional - var mockCoreStore *internal.MockCoreStorage - strategy := hmacshaStrategy - 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 := strategy.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 transactionalStore struct { - storage.Transactional - CoreStorage - } - - for _, testCase := range []struct { - description string - setup func() - expectError error - }{ - { - description: "transaction should be committed successfully if no errors occur", - setup: func() { - mockCoreStore. - EXPECT(). - GetDeviceCodeSession(gomock.Any(), gomock.Any(), gomock.Any()). - Return(request, nil). - Times(1) - mockTransactional. - EXPECT(). - BeginTX(propagatedContext). - Return(propagatedContext, nil) - mockCoreStore. - 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() { - mockCoreStore. - EXPECT(). - GetDeviceCodeSession(gomock.Any(), gomock.Any(), gomock.Any()). - Return(request, nil). - Times(1) - mockTransactional. - EXPECT(). - BeginTX(propagatedContext). - Return(propagatedContext, nil) - mockCoreStore. - 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() { - mockCoreStore. - EXPECT(). - GetDeviceCodeSession(gomock.Any(), gomock.Any(), gomock.Any()). - Return(request, nil). - Times(1) - mockTransactional. - EXPECT(). - BeginTX(propagatedContext). - Return(propagatedContext, nil) - mockCoreStore. - 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() { - mockCoreStore. - 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() { - mockCoreStore. - EXPECT(). - GetDeviceCodeSession(gomock.Any(), gomock.Any(), gomock.Any()). - Return(request, nil). - Times(1) - mockTransactional. - EXPECT(). - BeginTX(propagatedContext). - Return(propagatedContext, nil) - mockCoreStore. - 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() { - mockCoreStore. - EXPECT(). - GetDeviceCodeSession(gomock.Any(), gomock.Any(), gomock.Any()). - Return(request, nil). - Times(1) - mockTransactional. - EXPECT(). - BeginTX(propagatedContext). - Return(propagatedContext, nil) - mockCoreStore. - 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) - testCase.setup() - - handler := DeviceHandler{ - CoreStorage: transactionalStore{ - mockTransactional, - mockCoreStore, - }, - AccessTokenStrategy: &strategy, - RefreshTokenStrategy: &strategy, - DeviceCodeStrategy: &strategy, - UserCodeStrategy: &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/handler/oauth2/storage.go b/handler/oauth2/storage.go index fbcfda6b3..1a2860635 100644 --- a/handler/oauth2/storage.go +++ b/handler/oauth2/storage.go @@ -13,8 +13,6 @@ type CoreStorage interface { AuthorizeCodeStorage AccessTokenStorage RefreshTokenStorage - DeviceCodeStorage - UserCodeStorage } // AuthorizeCodeStorage handles storage requests related to authorization codes. @@ -50,40 +48,3 @@ type RefreshTokenStorage interface { DeleteRefreshTokenSession(ctx context.Context, signature string) (err error) } - -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/oauth2/strategy.go b/handler/oauth2/strategy.go index 034c1086d..f1e618ccb 100644 --- a/handler/oauth2/strategy.go +++ b/handler/oauth2/strategy.go @@ -13,8 +13,6 @@ type CoreStrategy interface { AccessTokenStrategy RefreshTokenStrategy AuthorizeCodeStrategy - DeviceCodeStrategy - UserCodeStrategy } type AccessTokenStrategy interface { @@ -34,15 +32,3 @@ type AuthorizeCodeStrategy interface { GenerateAuthorizeCode(ctx context.Context, requester fosite.Requester) (token string, signature string, err error) ValidateAuthorizeCode(ctx context.Context, requester fosite.Requester, token string) (err error) } - -type DeviceCodeStrategy interface { - DeviceCodeSignature(ctx context.Context, code string) string - 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) string - 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/oauth2/strategy_hmacsha.go b/handler/oauth2/strategy_hmacsha.go index eb561b3be..fef520792 100644 --- a/handler/oauth2/strategy_hmacsha.go +++ b/handler/oauth2/strategy_hmacsha.go @@ -5,9 +5,7 @@ package oauth2 import ( "context" - "crypto/rand" "fmt" - "math/big" "strings" "time" @@ -23,7 +21,6 @@ type HMACSHAStrategy struct { fosite.AccessTokenLifespanProvider fosite.RefreshTokenLifespanProvider fosite.AuthorizeCodeLifespanProvider - fosite.DeviceAndUserCodeLifespanProvider } prefix *string } @@ -123,64 +120,3 @@ func (h *HMACSHAStrategy) ValidateAuthorizeCode(ctx context.Context, r fosite.Re return h.Enigma.Validate(ctx, h.trimPrefix(token, "ac")) } - -func (h *HMACSHAStrategy) generateRandomString(length int) (token string, err error) { - chars := [20]byte{'B', 'C', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'X', 'Z'} - chars_length := int64(len(chars)) - - code := make([]byte, length) - for i := 0; i < length; i++ { - num, err := rand.Int(rand.Reader, big.NewInt(chars_length)) - if err != nil { - return "", err - } - code[i] = chars[num.Int64()] - } - return string(code), nil -} - -func (h *HMACSHAStrategy) GenerateUserCode(ctx context.Context) (token string, signature string, err error) { - userCode, err := h.generateRandomString(8) - return userCode, h.UserCodeSignature(ctx, userCode), err -} - -func (h *HMACSHAStrategy) UserCodeSignature(ctx context.Context, token string) string { - return h.Enigma.GenerateHMACForString(ctx, token) -} - -func (h *HMACSHAStrategy) 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 *HMACSHAStrategy) GenerateDeviceCode(ctx context.Context) (token string, signature string, err error) { - token, sig, err := h.Enigma.Generate(ctx) - if err != nil { - return "", "", err - } - - return h.setPrefix(token, "dc"), sig, nil -} - -func (h *HMACSHAStrategy) DeviceCodeSignature(ctx context.Context, token string) string { - return h.Enigma.Signature(token) -} - -func (h *HMACSHAStrategy) 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, h.trimPrefix(code, "dc")) -} diff --git a/handler/oauth2/strategy_hmacsha_test.go b/handler/oauth2/strategy_hmacsha_test.go index 3b4d1b440..04008fefe 100644 --- a/handler/oauth2/strategy_hmacsha_test.go +++ b/handler/oauth2/strategy_hmacsha_test.go @@ -5,7 +5,6 @@ package oauth2 import ( "fmt" - "regexp" "strings" "testing" "time" @@ -13,14 +12,21 @@ import ( "github.com/stretchr/testify/assert" "github.com/ory/fosite" + "github.com/ory/fosite/handler/rfc8628" "github.com/ory/fosite/token/hmac" ) var hmacshaStrategy = HMACSHAStrategy{ Enigma: &hmac.HMACStrategy{Config: &fosite.Config{GlobalSecret: []byte("foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobar")}}, Config: &fosite.Config{ - AccessTokenLifespan: time.Hour * 24, - AuthorizeCodeLifespan: time.Hour * 24, + AccessTokenLifespan: time.Hour * 24, + AuthorizeCodeLifespan: time.Hour * 24, + }, +} + +var RFC8628HMACSHAStrategy = rfc8628.DefaultDeviceStrategy{ + Enigma: &hmac.HMACStrategy{Config: &fosite.Config{GlobalSecret: []byte("foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobar")}}, + Config: &fosite.Config{ DeviceAndUserCodeLifespan: time.Hour * 24, }, } @@ -34,8 +40,6 @@ var hmacExpiredCase = fosite.Request{ fosite.AccessToken: time.Now().UTC().Add(-time.Hour), fosite.AuthorizeCode: time.Now().UTC().Add(-time.Hour), fosite.RefreshToken: time.Now().UTC().Add(-time.Hour), - fosite.UserCode: time.Now().UTC().Add(-time.Hour), - fosite.DeviceCode: time.Now().UTC().Add(-time.Hour), }, }, } @@ -49,8 +53,6 @@ var hmacValidCase = fosite.Request{ fosite.AccessToken: time.Now().UTC().Add(time.Hour), fosite.AuthorizeCode: time.Now().UTC().Add(time.Hour), fosite.RefreshToken: time.Now().UTC().Add(time.Hour), - fosite.UserCode: time.Now().UTC().Add(time.Hour), - fosite.DeviceCode: time.Now().UTC().Add(time.Hour), }, }, } @@ -184,74 +186,3 @@ func TestHMACAuthorizeCode(t *testing.T) { }) } } - -func TestHMACUserCode(t *testing.T) { - for k, c := range []struct { - r fosite.Request - pass bool - }{ - { - r: hmacValidCase, - pass: true, - }, - { - r: hmacExpiredCase, - pass: false, - }, - } { - t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { - userCode, signature, err := hmacshaStrategy.GenerateUserCode(nil) - assert.NoError(t, err) - regex := regexp.MustCompile("[BCDFGHJKLMNPQRSTVWXZ]{8}") - assert.Equal(t, len(regex.FindString(userCode)), len(userCode)) - - err = hmacshaStrategy.ValidateUserCode(nil, &c.r, userCode) - if c.pass { - assert.NoError(t, err) - validate := hmacshaStrategy.Enigma.GenerateHMACForString(nil, userCode) - assert.Equal(t, signature, validate) - } 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, - }, - } { - t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { - token, signature, err := hmacshaStrategy.GenerateDeviceCode(nil) - 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(nil, &c.r, token) - if c.pass { - assert.NoError(t, err) - validate := hmacshaStrategy.Enigma.Signature(token) - assert.Equal(t, signature, validate) - } else { - assert.Error(t, err) - } - }) - } - }) - } -} diff --git a/handler/oauth2/strategy_jwt.go b/handler/oauth2/strategy_jwt.go index 685c7a0dd..7287dea26 100644 --- a/handler/oauth2/strategy_jwt.go +++ b/handler/oauth2/strategy_jwt.go @@ -131,27 +131,3 @@ func (h *DefaultJWTStrategy) generate(ctx context.Context, tokenType fosite.Toke return h.Signer.Generate(ctx, claims.ToMapClaims(), jwtSession.GetJWTHeader()) } } - -func (h DefaultJWTStrategy) DeviceCodeSignature(ctx context.Context, token string) string { - return h.HMACSHAStrategy.DeviceCodeSignature(ctx, token) -} - -func (h *DefaultJWTStrategy) GenerateDeviceCode(ctx context.Context) (token string, signature string, err error) { - return h.HMACSHAStrategy.GenerateDeviceCode(ctx) -} - -func (h *DefaultJWTStrategy) ValidateDeviceCode(ctx context.Context, r fosite.Requester, code string) (err error) { - return h.HMACSHAStrategy.ValidateDeviceCode(ctx, r, code) -} - -func (h DefaultJWTStrategy) UserCodeSignature(ctx context.Context, token string) string { - return h.HMACSHAStrategy.UserCodeSignature(ctx, token) -} - -func (h *DefaultJWTStrategy) GenerateUserCode(ctx context.Context) (token string, signature string, err error) { - return h.HMACSHAStrategy.GenerateUserCode(ctx) -} - -func (h *DefaultJWTStrategy) ValidateUserCode(ctx context.Context, r fosite.Requester, code string) (err error) { - return h.HMACSHAStrategy.ValidateUserCode(ctx, r, code) -} diff --git a/handler/openid/flow_device_auth.go b/handler/openid/flow_device_auth.go index 2d91c8820..a3b3ec201 100644 --- a/handler/openid/flow_device_auth.go +++ b/handler/openid/flow_device_auth.go @@ -1,23 +1,5 @@ -/* - * Copyright © 2015-2018 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-2018 Aeneas Rekkas - * @license Apache-2.0 - * - */ +// Copyright © 2022 Ory Corp +// SPDX-License-Identifier: Apache-2.0 package openid @@ -45,7 +27,7 @@ func (c *OpenIDConnectDeviceHandler) HandleDeviceAuthorizeEndpointRequest(ctx co return nil } - if !ar.GetClient().GetGrantTypes().Has("urn:ietf:params:oauth:grant-type:device_code") { + if !ar.GetClient().GetGrantTypes().Has(string(fosite.GrantTypeDeviceCode)) { return nil } diff --git a/handler/openid/flow_device_token.go b/handler/openid/flow_device_token.go index 1ab27957f..ee1a69e7f 100644 --- a/handler/openid/flow_device_token.go +++ b/handler/openid/flow_device_token.go @@ -1,23 +1,5 @@ -/* - * Copyright © 2015-2018 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-2018 Aeneas Rekkas - * @license Apache-2.0 - * - */ +// Copyright © 2022 Ory Corp +// SPDX-License-Identifier: Apache-2.0 package openid @@ -63,7 +45,7 @@ func (c *OpenIDConnectDeviceHandler) PopulateTokenEndpointResponse(ctx context.C 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("urn:ietf:params:oauth:grant-type:device_code") { + 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\".")) } @@ -95,5 +77,5 @@ func (c *OpenIDConnectDeviceHandler) CanSkipClientAuth(ctx context.Context, requ } func (c *OpenIDConnectDeviceHandler) CanHandleTokenEndpointRequest(ctx context.Context, requester fosite.AccessRequester) bool { - return requester.GetGrantTypes().ExactOne("urn:ietf:params:oauth:grant-type:device_code") + return requester.GetGrantTypes().ExactOne(string(fosite.GrantTypeDeviceCode)) } diff --git a/handler/pkce/handler.go b/handler/pkce/handler.go index 3231693d1..9b8fd0f95 100644 --- a/handler/pkce/handler.go +++ b/handler/pkce/handler.go @@ -15,12 +15,14 @@ import ( "github.com/ory/fosite" "github.com/ory/fosite/handler/oauth2" + "github.com/ory/fosite/handler/rfc8628" ) var _ fosite.TokenEndpointHandler = (*Handler)(nil) type Handler struct { AuthorizeCodeStrategy oauth2.AuthorizeCodeStrategy + DeviceCodeStrategy rfc8628.DeviceCodeStrategy Storage PKCERequestStorage Config interface { fosite.EnforcePKCEProvider @@ -33,27 +35,51 @@ var _ fosite.TokenEndpointHandler = (*Handler)(nil) var verifierWrongFormat = regexp.MustCompile("[^\\w\\.\\-~]") +func (c *Handler) HandleDeviceEndpointRequest(ctx context.Context, dr fosite.DeviceRequester, resp fosite.DeviceResponder) error { + return c.handlePkceEndpointRequest(ctx, dr, resp) +} + func (c *Handler) HandleAuthorizeEndpointRequest(ctx context.Context, ar fosite.AuthorizeRequester, resp fosite.AuthorizeResponder) error { + return c.handlePkceEndpointRequest(ctx, ar, resp) +} + +func (c *Handler) handlePkceEndpointRequest(ctx context.Context, r fosite.Requester, resp fosite.Responder) error { // This let's us define multiple response types, for example open id connect's id_token - if !ar.GetResponseTypes().Has("code") { + if !(isAuthorizationCode(r) || isDeviceCode(r)) { return nil } - challenge := ar.GetRequestForm().Get("code_challenge") - method := ar.GetRequestForm().Get("code_challenge_method") - client := ar.GetClient() + challenge := r.GetRequestForm().Get("code_challenge") + method := r.GetRequestForm().Get("code_challenge_method") + client := r.GetClient() if err := c.validate(ctx, challenge, method, client); err != nil { return err } - code := resp.GetCode() - if len(code) == 0 { - return errorsx.WithStack(fosite.ErrServerError.WithDebug("The PKCE handler must be loaded after the authorize code handler.")) + var signature string + if authorizeResp, ok := resp.(fosite.AuthorizeResponder); ok { + code := authorizeResp.GetCode() + if len(code) == 0 { + return errorsx.WithStack(fosite.ErrServerError.WithDebug("The PKCE handler must be loaded after the authorize/device code handler.")) + } + signature = c.AuthorizeCodeStrategy.AuthorizeCodeSignature(ctx, code) + } else if deviceResp, ok := resp.(fosite.DeviceResponder); ok { + code := deviceResp.GetDeviceCode() + if len(code) == 0 { + return errorsx.WithStack(fosite.ErrServerError.WithDebug("The PKCE handler must be loaded after the device code handler.")) + } + + var err error + signature, err = c.DeviceCodeStrategy.DeviceCodeSignature(ctx, code) + if err != nil { + return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) + } + } else { + return errorsx.WithStack(fosite.ErrServerError.WithDebug("This PKCE handle could not find the proper response type")) } - signature := c.AuthorizeCodeStrategy.AuthorizeCodeSignature(ctx, code) - if err := c.Storage.CreatePKCERequestSession(ctx, signature, ar.Sanitize([]string{ + if err := c.Storage.CreatePKCERequestSession(ctx, signature, r.Sanitize([]string{ "code_challenge", "code_challenge_method", })); err != nil { @@ -63,7 +89,7 @@ func (c *Handler) HandleAuthorizeEndpointRequest(ctx context.Context, ar fosite. return nil } -func (c *Handler) validate(ctx context.Context, challenge, method string, client fosite.Client) error { +func (c *Handler) validate(ctx context.Context, challenge string, method string, client fosite.Client) error { if challenge == "" { // If the server requires Proof Key for Code Exchange (PKCE) by OAuth // clients and the client does not send the "code_challenge" in @@ -121,8 +147,19 @@ func (c *Handler) HandleTokenEndpointRequest(ctx context.Context, request fosite // endpoint MUST use to verify the "code_verifier". verifier := request.GetRequestForm().Get("code_verifier") - code := request.GetRequestForm().Get("code") - signature := c.AuthorizeCodeStrategy.AuthorizeCodeSignature(ctx, code) + var signature string + if request.GetGrantTypes().ExactOne("authorization_code") { + code := request.GetRequestForm().Get("code") + signature = c.AuthorizeCodeStrategy.AuthorizeCodeSignature(ctx, code) + } else if request.GetGrantTypes().ExactOne(string(fosite.GrantTypeDeviceCode)) { + var err error + code := request.GetRequestForm().Get("device_code") + signature, err = c.DeviceCodeStrategy.DeviceCodeSignature(ctx, code) + if err != nil { + return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) + } + } + authorizeRequest, err := c.Storage.GetPKCERequestSession(ctx, signature, request.GetSession()) if errors.Is(err, fosite.ErrNotFound) { return errorsx.WithStack(fosite.ErrInvalidGrant.WithHint("Unable to find initial PKCE data tied to this request").WithWrap(err).WithDebug(err.Error())) @@ -217,7 +254,15 @@ func (c *Handler) CanSkipClientAuth(ctx context.Context, requester fosite.Access } func (c *Handler) CanHandleTokenEndpointRequest(ctx context.Context, requester fosite.AccessRequester) bool { - // grant_type REQUIRED. - // Value MUST be set to "authorization_code" - return requester.GetGrantTypes().ExactOne("authorization_code") + return requester.GetGrantTypes().ExactOne("authorization_code") || + requester.GetGrantTypes().ExactOne(string(fosite.GrantTypeDeviceCode)) +} + +func isDeviceCode(r fosite.Requester) bool { + return r.GetClient().GetGrantTypes().Has(string(fosite.GrantTypeDeviceCode)) +} + +func isAuthorizationCode(r fosite.Requester) bool { + ar, ok := r.(*fosite.AuthorizeRequest) + return ok && ar.GetResponseTypes().Has("code") } diff --git a/handler/pkce/handler_device.go b/handler/pkce/handler_device.go deleted file mode 100644 index 1ec14805e..000000000 --- a/handler/pkce/handler_device.go +++ /dev/null @@ -1,241 +0,0 @@ -/* - * Copyright © 2015-2018 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-2018 Aeneas Rekkas - * @license Apache-2.0 - * - */ - -package pkce - -import ( - "context" - "crypto/sha256" - "encoding/base64" - "regexp" - - "github.com/ory/x/errorsx" - - "github.com/pkg/errors" - - "github.com/ory/fosite" - "github.com/ory/fosite/handler/oauth2" -) - -var _ fosite.TokenEndpointHandler = (*HandlerDevice)(nil) - -type HandlerDevice struct { - DeviceCodeStrategy oauth2.DeviceCodeStrategy - UserCodeStrategy oauth2.UserCodeStrategy - Storage PKCERequestStorage - Config interface { - fosite.EnforcePKCEProvider - fosite.EnforcePKCEForPublicClientsProvider - fosite.EnablePKCEPlainChallengeMethodProvider - } -} - -var _ fosite.TokenEndpointHandler = (*HandlerDevice)(nil) - -var deviceVerifierWrongFormat = regexp.MustCompile(`[^\w\.\-~]`) - -func (c *HandlerDevice) HandleDeviceEndpointRequest(ctx context.Context, ar fosite.Requester, resp fosite.DeviceResponder) error { - if !ar.GetClient().GetGrantTypes().Has("urn:ietf:params:oauth:grant-type:device_code") { - return nil - } - - challenge := ar.GetRequestForm().Get("code_challenge") - method := ar.GetRequestForm().Get("code_challenge_method") - client := ar.GetClient() - - if err := c.validate(ctx, challenge, method, client); err != nil { - return err - } - - code := resp.GetDeviceCode() - if len(code) == 0 { - return errorsx.WithStack(fosite.ErrServerError.WithDebug("The PKCE handler must be loaded after the device code handler.")) - } - - signature := c.DeviceCodeStrategy.DeviceCodeSignature(ctx, code) - if err := c.Storage.CreatePKCERequestSession(ctx, signature, ar.Sanitize([]string{ - "code_challenge", - "code_challenge_method", - })); err != nil { - return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) - } - - return nil -} - -func (c *HandlerDevice) validate(ctx context.Context, challenge string, method string, client fosite.Client) error { - if challenge == "" { - // If the server requires Proof Key for Code Exchange (PKCE) by OAuth - // clients and the client does not send the "code_challenge" in - // the request, the authorization endpoint MUST return the authorization - // error response with the "error" value set to "invalid_request". The - // "error_description" or the response of "error_uri" SHOULD explain the - // nature of error, e.g., code challenge required. - if c.Config.GetEnforcePKCE(ctx) { - return errorsx.WithStack(fosite.ErrInvalidRequest. - WithHint("Clients must include a code_challenge when performing the authorize code flow, but it is missing."). - WithDebug("The server is configured in a way that enforces PKCE for clients.")) - } - if c.Config.GetEnforcePKCEForPublicClients(ctx) && client.IsPublic() { - return errorsx.WithStack(fosite.ErrInvalidRequest. - WithHint("This client must include a code_challenge when performing the authorize code flow, but it is missing."). - WithDebug("The server is configured in a way that enforces PKCE for this client.")) - } - return nil - } - - // If the server supporting PKCE does not support the requested - // transformation, the authorization endpoint MUST return the - // authorization error response with "error" value set to - // "invalid_request". The "error_description" or the response of - // "error_uri" SHOULD explain the nature of error, e.g., transform - // algorithm not supported. - switch method { - case "S256": - break - case "plain": - fallthrough - case "": - if !c.Config.GetEnablePKCEPlainChallengeMethod(ctx) { - return errorsx.WithStack(fosite.ErrInvalidRequest. - WithHint("Clients must use code_challenge_method=S256, plain is not allowed."). - WithDebug("The server is configured in a way that enforces PKCE S256 as challenge method for clients.")) - } - default: - return errorsx.WithStack(fosite.ErrInvalidRequest. - WithHint("The code_challenge_method is not supported, use S256 instead.")) - } - return nil -} - -func (c *HandlerDevice) HandleTokenEndpointRequest(ctx context.Context, request fosite.AccessRequester) error { - if !c.CanHandleTokenEndpointRequest(ctx, request) { - return errorsx.WithStack(fosite.ErrUnknownRequest) - } - - // code_verifier - // REQUIRED. Code verifier - // - // The "code_challenge_method" is bound to the Authorization Code when - // the Authorization Code is issued. That is the method that the token - // endpoint MUST use to verify the "code_verifier". - verifier := request.GetRequestForm().Get("code_verifier") - - code := request.GetRequestForm().Get("device_code") - signature := c.DeviceCodeStrategy.DeviceCodeSignature(ctx, code) - authorizeRequest, err := c.Storage.GetPKCERequestSession(ctx, signature, request.GetSession()) - if errors.Is(err, fosite.ErrNotFound) { - return errorsx.WithStack(fosite.ErrInvalidGrant.WithHint("Unable to find initial PKCE data tied to this request").WithWrap(err).WithDebug(err.Error())) - } else if err != nil { - return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) - } - - if err := c.Storage.DeletePKCERequestSession(ctx, signature); err != nil { - return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) - } - - challenge := authorizeRequest.GetRequestForm().Get("code_challenge") - method := authorizeRequest.GetRequestForm().Get("code_challenge_method") - client := authorizeRequest.GetClient() - if err := c.validate(ctx, challenge, method, client); err != nil { - return err - } - - if !c.Config.GetEnforcePKCE(ctx) && challenge == "" && verifier == "" { - return nil - } - - // NOTE: The code verifier SHOULD have enough entropy to make it - // impractical to guess the value. It is RECOMMENDED that the output of - // a suitable random number generator be used to create a 32-octet - // sequence. The octet sequence is then base64url-encoded to produce a - // 43-octet URL safe string to use as the code verifier. - - // Validation - if len(verifier) < 43 { - return errorsx.WithStack(fosite.ErrInvalidGrant. - WithHint("The PKCE code verifier must be at least 43 characters.")) - } else if len(verifier) > 128 { - return errorsx.WithStack(fosite.ErrInvalidGrant. - WithHint("The PKCE code verifier can not be longer than 128 characters.")) - } else if deviceVerifierWrongFormat.MatchString(verifier) { - return errorsx.WithStack(fosite.ErrInvalidGrant. - WithHint("The PKCE code verifier must only contain [a-Z], [0-9], '-', '.', '_', '~'.")) - } - - // Upon receipt of the request at the token endpoint, the server - // verifies it by calculating the code challenge from the received - // "code_verifier" and comparing it with the previously associated - // "code_challenge", after first transforming it according to the - // "code_challenge_method" method specified by the client. - // - // If the "code_challenge_method" from Section 4.3 was "S256", the - // received "code_verifier" is hashed by SHA-256, base64url-encoded, and - // then compared to the "code_challenge", i.e.: - // - // BASE64URL-ENCODE(SHA256(ASCII(code_verifier))) == code_challenge - // - // If the "code_challenge_method" from Section 4.3 was "plain", they are - // compared directly, i.e.: - // - // code_verifier == code_challenge. - // - // If the values are equal, the token endpoint MUST continue processing - // as normal (as defined by OAuth 2.0 [RFC6749]). If the values are not - // equal, an error response indicating "invalid_grant" as described in - // Section 5.2 of [RFC6749] MUST be returned. - switch method { - case "S256": - hash := sha256.New() - if _, err := hash.Write([]byte(verifier)); err != nil { - return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) - } - - if base64.RawURLEncoding.EncodeToString(hash.Sum([]byte{})) != challenge { - return errorsx.WithStack(fosite.ErrInvalidGrant. - WithHint("The PKCE code challenge did not match the code verifier.")) - } - break - case "plain": - fallthrough - default: - if verifier != challenge { - return errorsx.WithStack(fosite.ErrInvalidGrant. - WithHint("The PKCE code challenge did not match the code verifier.")) - } - } - - return nil -} - -func (c *HandlerDevice) PopulateTokenEndpointResponse(ctx context.Context, requester fosite.AccessRequester, responder fosite.AccessResponder) error { - return nil -} - -func (c *HandlerDevice) CanSkipClientAuth(ctx context.Context, requester fosite.AccessRequester) bool { - return false -} - -func (c *HandlerDevice) CanHandleTokenEndpointRequest(ctx context.Context, requester fosite.AccessRequester) bool { - // grant_type REQUIRED. - // Value MUST be set to "urn:ietf:params:oauth:grant-type:device_code" - return requester.GetGrantTypes().ExactOne("urn:ietf:params:oauth:grant-type:device_code") -} diff --git a/handler/pkce/handler_device_test.go b/handler/pkce/handler_device_test.go index 22e45ff24..a3108ebfc 100644 --- a/handler/pkce/handler_device_test.go +++ b/handler/pkce/handler_device_test.go @@ -1,23 +1,5 @@ -/* - * Copyright © 2015-2018 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-2018 Aeneas Rekkas - * @license Apache-2.0 - * - */ +// Copyright © 2022 Ory Corp +// SPDX-License-Identifier: Apache-2.0 package pkce @@ -32,7 +14,7 @@ import ( "github.com/stretchr/testify/require" "github.com/ory/fosite" - "github.com/ory/fosite/handler/oauth2" + "github.com/ory/fosite/handler/rfc8628" "github.com/ory/fosite/storage" ) @@ -40,8 +22,8 @@ type mockDeviceCodeStrategy struct { signature string } -func (m *mockDeviceCodeStrategy) DeviceCodeSignature(ctx context.Context, token string) string { - return m.signature +func (m *mockDeviceCodeStrategy) DeviceCodeSignature(ctx context.Context, token string) (signature string, err error) { + return m.signature, nil } func (m *mockDeviceCodeStrategy) GenerateDeviceCode(ctx context.Context) (token string, signature string, err error) { @@ -52,28 +34,11 @@ func (m *mockDeviceCodeStrategy) ValidateDeviceCode(ctx context.Context, request return nil } -type mockUserCodeStrategy struct { - signature string -} - -func (m *mockUserCodeStrategy) UserCodeSignature(ctx context.Context, token string) string { - return m.signature -} - -func (m *mockUserCodeStrategy) GenerateUserCode(ctx context.Context) (token string, signature string, err error) { - return "", "", nil -} - -func (m *mockUserCodeStrategy) ValidateUserCode(ctx context.Context, requester fosite.Requester, token string) (err error) { - return nil -} - -func TestPKCEHandlerDevice_HandleDeviceAuthorizeEndpointRequest(t *testing.T) { +func TestPKCEHandlerDevice_HandleAuthorizeEndpointRequest(t *testing.T) { var config fosite.Config - h := &HandlerDevice{ + h := &Handler{ Storage: storage.NewMemoryStore(), - DeviceCodeStrategy: new(oauth2.HMACSHAStrategy), - UserCodeStrategy: new(oauth2.HMACSHAStrategy), + DeviceCodeStrategy: new(rfc8628.DefaultDeviceStrategy), Config: &config, } w := fosite.NewDeviceResponse() @@ -115,14 +80,12 @@ func TestPKCEHandlerDevice_HandleDeviceAuthorizeEndpointRequest(t *testing.T) { require.NoError(t, h.HandleDeviceEndpointRequest(context.Background(), r, w)) } -func TestPKCEHandlerDevice_HandlerDeviceValidate(t *testing.T) { +func TestPKCEHandlerDevice_HandlerValidate(t *testing.T) { s := storage.NewMemoryStore() ds := &mockDeviceCodeStrategy{} - us := &mockUserCodeStrategy{} config := &fosite.Config{} - h := &HandlerDevice{ + h := &Handler{ Storage: s, - UserCodeStrategy: us, DeviceCodeStrategy: ds, Config: config, } @@ -149,6 +112,7 @@ func TestPKCEHandlerDevice_HandlerDeviceValidate(t *testing.T) { d: "fails because not auth code flow", grant: "not_urn:ietf:params:oauth:grant-type:device_code", expectErr: fosite.ErrUnknownRequest, + client: &fosite.DefaultClient{Public: false}, }, { d: "passes with private client", @@ -286,12 +250,13 @@ func TestPKCEHandlerDevice_HandlerDeviceValidate(t *testing.T) { ar := fosite.NewAuthorizeRequest() ar.Form.Add("code_challenge", tc.challenge) ar.Form.Add("code_challenge_method", tc.method) - require.NoError(t, s.CreatePKCERequestSession(nil, fmt.Sprintf("valid-code-%d", k), ar)) + require.NoError(t, s.CreatePKCERequestSession(context.TODO(), fmt.Sprintf("valid-code-%d", k), ar)) r := fosite.NewAccessRequest(nil) r.Client = tc.client r.GrantTypes = fosite.Arguments{tc.grant} r.Form.Add("code_verifier", tc.verifier) + r.Form.Add("device_code", tc.code) if tc.expectErr == nil { require.NoError(t, h.HandleTokenEndpointRequest(context.Background(), r)) } else { @@ -370,7 +335,7 @@ func TestPKCEHandlerDevice_HandleTokenEndpointRequest(t *testing.T) { }, } { t.Run(fmt.Sprintf("case=%d/description=%s", k, tc.d), func(t *testing.T) { - h := &HandlerDevice{ + h := &Handler{ Config: &fosite.Config{ EnforcePKCE: tc.force, EnforcePKCEForPublicClients: tc.forcePublic, diff --git a/handler/pkce/handler_test.go b/handler/pkce/handler_test.go index 15ea5b96e..bed8f1d4b 100644 --- a/handler/pkce/handler_test.go +++ b/handler/pkce/handler_test.go @@ -245,12 +245,13 @@ func TestPKCEHandlerValidate(t *testing.T) { ar := fosite.NewAuthorizeRequest() ar.Form.Add("code_challenge", tc.challenge) ar.Form.Add("code_challenge_method", tc.method) - require.NoError(t, s.CreatePKCERequestSession(nil, fmt.Sprintf("valid-code-%d", k), ar)) + require.NoError(t, s.CreatePKCERequestSession(context.TODO(), fmt.Sprintf("valid-code-%d", k), ar)) r := fosite.NewAccessRequest(nil) r.Client = tc.client r.GrantTypes = fosite.Arguments{tc.grant} r.Form.Add("code_verifier", tc.verifier) + r.Form.Add("code", tc.code) if tc.expectErr == nil { require.NoError(t, h.HandleTokenEndpointRequest(context.Background(), r)) } else { diff --git a/handler/oauth2/flow_device_auth.go b/handler/rfc8628/auth_handler.go similarity index 63% rename from handler/oauth2/flow_device_auth.go rename to handler/rfc8628/auth_handler.go index 1b9c8bbb5..10e6f9ee8 100644 --- a/handler/oauth2/flow_device_auth.go +++ b/handler/rfc8628/auth_handler.go @@ -1,4 +1,7 @@ -package oauth2 +// Copyright © 2022 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package rfc8628 import ( "context" @@ -8,34 +11,24 @@ import ( "github.com/ory/x/errorsx" ) -// DeviceAuthorizationHandler is a response handler for the Device Authorisation Grant as -// defined in https://tools.ietf.org/html/rfc8628#section-3.1 -type DeviceHandler struct { - AccessTokenStrategy AccessTokenStrategy - RefreshTokenStrategy RefreshTokenStrategy - DeviceCodeStrategy DeviceCodeStrategy - UserCodeStrategy UserCodeStrategy - CoreStorage CoreStorage - TokenRevocationStorage TokenRevocationStorage - Config interface { +type DeviceAuthHandler struct { + Storage RFC8628CodeStorage + Strategy RFC8628CodeStrategy + Config interface { fosite.DeviceProvider fosite.DeviceAndUserCodeLifespanProvider - fosite.AccessTokenLifespanProvider - fosite.RefreshTokenLifespanProvider - fosite.ScopeStrategyProvider - fosite.AudienceStrategyProvider - fosite.RefreshTokenScopesProvider - fosite.SanitationAllowedProvider } } -func (d *DeviceHandler) HandleDeviceEndpointRequest(ctx context.Context, dar fosite.Requester, resp fosite.DeviceResponder) error { - deviceCode, deviceCodeSignature, err := d.DeviceCodeStrategy.GenerateDeviceCode(ctx) +// 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.Requester, 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.UserCodeStrategy.GenerateUserCode(ctx) + userCode, userCodeSignature, err := d.Strategy.GenerateUserCode(ctx) if err != nil { return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) } @@ -45,14 +38,14 @@ func (d *DeviceHandler) HandleDeviceEndpointRequest(ctx context.Context, dar fos // 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.CoreStorage.CreateDeviceCodeSession(ctx, deviceCodeSignature, dar.Sanitize(nil)); err != nil { + 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.CoreStorage.CreateUserCodeSession(ctx, userCodeSignature, dar.Sanitize(nil)); err != nil { + 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) diff --git a/handler/oauth2/flow_device_auth_test.go b/handler/rfc8628/auth_handler_test.go similarity index 81% rename from handler/oauth2/flow_device_auth_test.go rename to handler/rfc8628/auth_handler_test.go index 0bed5f8bd..505a3fb0b 100644 --- a/handler/oauth2/flow_device_auth_test.go +++ b/handler/rfc8628/auth_handler_test.go @@ -1,4 +1,7 @@ -package oauth2 +// Copyright © 2022 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package rfc8628_test import ( "testing" @@ -6,19 +9,18 @@ import ( "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_HandleDeviceAuthorizeEndpointRequest(t *testing.T) { - +func Test_HandleDeviceEndpointRequest(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() store := storage.NewMemoryStore() - handler := DeviceHandler{ - CoreStorage: store, - DeviceCodeStrategy: &hmacshaStrategy, - UserCodeStrategy: &hmacshaStrategy, + handler := DeviceAuthHandler{ + Storage: store, + Strategy: &hmacshaStrategy, Config: &fosite.Config{ DeviceAndUserCodeLifespan: time.Minute * 10, DeviceAuthTokenPollingInterval: time.Second * 10, @@ -44,5 +46,4 @@ func Test_HandleDeviceAuthorizeEndpointRequest(t *testing.T) { 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..b520dab8a --- /dev/null +++ b/handler/rfc8628/storage.go @@ -0,0 +1,52 @@ +// Copyright © 2022 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..7c525be2b --- /dev/null +++ b/handler/rfc8628/strategy.go @@ -0,0 +1,27 @@ +// Copyright © 2022 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package rfc8628 + +import ( + "context" + + "github.com/ory/fosite" +) + +type RFC8628CodeStrategy interface { + DeviceCodeStrategy + UserCodeStrategy +} + +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..bf78961b0 --- /dev/null +++ b/handler/rfc8628/strategy_hmacsha.go @@ -0,0 +1,92 @@ +// Copyright © 2022 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package rfc8628 + +import ( + "context" + "crypto/rand" + "math/big" + "strings" + "time" + + "github.com/ory/x/errorsx" + + "github.com/ory/fosite" + enigma "github.com/ory/fosite/token/hmac" +) + +type DefaultDeviceStrategy struct { + Enigma *enigma.HMACStrategy + Config interface { + fosite.DeviceAndUserCodeLifespanProvider + } +} + +func (h *DefaultDeviceStrategy) generateRandomString(length int) (token string, err error) { + chars := [20]byte{'B', 'C', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'X', 'Z'} + chars_length := int64(len(chars)) + + code := make([]byte, length) + for i := 0; i < length; i++ { + num, err := rand.Int(rand.Reader, big.NewInt(chars_length)) + if err != nil { + return "", err + } + code[i] = chars[num.Int64()] + } + return string(code), nil +} + +func (h *DefaultDeviceStrategy) GenerateUserCode(ctx context.Context) (token string, signature string, err error) { + userCode, err := h.generateRandomString(8) + if err != nil { + return "", "", err + } + 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_")) +} diff --git a/handler/rfc8628/strategy_hmacsha_test.go b/handler/rfc8628/strategy_hmacsha_test.go new file mode 100644 index 000000000..8d604b43c --- /dev/null +++ b/handler/rfc8628/strategy_hmacsha_test.go @@ -0,0 +1,150 @@ +// Copyright © 2022 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package rfc8628_test + +import ( + "context" + "fmt" + "regexp" + "strings" + "testing" + "time" + + "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")}}, + Config: &fosite.Config{ + AccessTokenLifespan: time.Minute * 24, + AuthorizeCodeLifespan: time.Minute * 24, + DeviceAndUserCodeLifespan: time.Minute * 24, + }, +} + +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) + } + }) + } + }) + } +} diff --git a/internal/device_authorize_handler.go b/internal/device_authorize_handler.go index ee934ac84..354070dcc 100644 --- a/internal/device_authorize_handler.go +++ b/internal/device_authorize_handler.go @@ -1,3 +1,6 @@ +// Copyright © 2022 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + // Code generated by MockGen. DO NOT EDIT. // Source: github.com/ory/fosite (interfaces: DeviceAuthorizeEndpointHandler) diff --git a/internal/device_authorize_request.go b/internal/device_authorize_request.go index caba5a89e..759fbeb80 100644 --- a/internal/device_authorize_request.go +++ b/internal/device_authorize_request.go @@ -1,3 +1,6 @@ +// Copyright © 2022 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + // Code generated by MockGen. DO NOT EDIT. // Source: github.com/ory/fosite (interfaces: DeviceAuthorizeRequester) diff --git a/internal/device_authorize_response.go b/internal/device_authorize_response.go index e23d9e7ca..25d40310c 100644 --- a/internal/device_authorize_response.go +++ b/internal/device_authorize_response.go @@ -1,3 +1,6 @@ +// Copyright © 2022 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + // Code generated by MockGen. DO NOT EDIT. // Source: github.com/ory/fosite (interfaces: DeviceAuthorizeResponder) @@ -5,6 +8,9 @@ package internal import ( + http "net/http" + reflect "reflect" + gomock "github.com/golang/mock/gomock" ) @@ -30,3 +36,29 @@ func NewMockDeviceAuthorizeResponder(ctrl *gomock.Controller) *MockDeviceAuthori func (m *MockDeviceAuthorizeResponder) EXPECT() *MockDeviceAuthorizeResponderMockRecorder { return m.recorder } + +// AddHeader mocks base method. +func (m *MockDeviceAuthorizeResponder) AddHeader(arg0, arg1 string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "AddHeader", arg0, arg1) +} + +// AddHeader indicates an expected call of AddHeader. +func (mr *MockDeviceAuthorizeResponderMockRecorder) AddHeader(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddHeader", reflect.TypeOf((*MockDeviceAuthorizeResponder)(nil).AddHeader), arg0, arg1) +} + +// GetHeader mocks base method. +func (m *MockDeviceAuthorizeResponder) 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 *MockDeviceAuthorizeResponderMockRecorder) GetHeader() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetHeader", reflect.TypeOf((*MockDeviceAuthorizeResponder)(nil).GetHeader)) +} diff --git a/internal/device_code_storage.go b/internal/device_code_storage.go index 0b0c609a2..a5c60ba3f 100644 --- a/internal/device_code_storage.go +++ b/internal/device_code_storage.go @@ -1,5 +1,8 @@ +// Copyright © 2022 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/ory/fosite/handler/oauth2 (interfaces: DeviceCodeStorage) +// Source: github.com/ory/fosite/handler/rfc8628 (interfaces: DeviceCodeStorage) // Package internal is a generated GoMock package. package internal diff --git a/internal/device_handler.go b/internal/device_handler.go index ea35edbfd..1b390c805 100644 --- a/internal/device_handler.go +++ b/internal/device_handler.go @@ -1,3 +1,6 @@ +// Copyright © 2022 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + // Code generated by MockGen. DO NOT EDIT. // Source: github.com/ory/fosite (interfaces: DeviceEndpointHandler) diff --git a/internal/device_request.go b/internal/device_request.go index ad6530bfa..4a79697d4 100644 --- a/internal/device_request.go +++ b/internal/device_request.go @@ -1,3 +1,6 @@ +// Copyright © 2022 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + // Code generated by MockGen. DO NOT EDIT. // Source: github.com/ory/fosite (interfaces: DeviceRequester) diff --git a/internal/device_response.go b/internal/device_response.go index c57a4dd9e..641785354 100644 --- a/internal/device_response.go +++ b/internal/device_response.go @@ -1,3 +1,6 @@ +// Copyright © 2022 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + // Code generated by MockGen. DO NOT EDIT. // Source: github.com/ory/fosite (interfaces: DeviceResponder) @@ -5,6 +8,7 @@ package internal import ( + http "net/http" reflect "reflect" gomock "github.com/golang/mock/gomock" @@ -33,6 +37,18 @@ 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() @@ -61,6 +77,20 @@ func (mr *MockDeviceResponderMockRecorder) GetExpiresIn() *gomock.Call { 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() diff --git a/internal/oauth2_auth_device_storage.go b/internal/oauth2_auth_device_storage.go new file mode 100644 index 000000000..59fb51a75 --- /dev/null +++ b/internal/oauth2_auth_device_storage.go @@ -0,0 +1,139 @@ +// Copyright © 2022 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/oauth2_storage.go b/internal/oauth2_storage.go index 79118d9a5..c53afe752 100644 --- a/internal/oauth2_storage.go +++ b/internal/oauth2_storage.go @@ -67,20 +67,6 @@ func (mr *MockCoreStorageMockRecorder) CreateAuthorizeCodeSession(arg0, arg1, ar return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateAuthorizeCodeSession", reflect.TypeOf((*MockCoreStorage)(nil).CreateAuthorizeCodeSession), arg0, arg1, arg2) } -// CreateDeviceCodeSession mocks base method. -func (m *MockCoreStorage) 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 *MockCoreStorageMockRecorder) CreateDeviceCodeSession(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateDeviceCodeSession", reflect.TypeOf((*MockCoreStorage)(nil).CreateDeviceCodeSession), arg0, arg1, arg2) -} - // CreateRefreshTokenSession mocks base method. func (m *MockCoreStorage) CreateRefreshTokenSession(arg0 context.Context, arg1 string, arg2 fosite.Requester) error { m.ctrl.T.Helper() @@ -95,20 +81,6 @@ func (mr *MockCoreStorageMockRecorder) CreateRefreshTokenSession(arg0, arg1, arg return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateRefreshTokenSession", reflect.TypeOf((*MockCoreStorage)(nil).CreateRefreshTokenSession), arg0, arg1, arg2) } -// CreateUserCodeSession mocks base method. -func (m *MockCoreStorage) 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 *MockCoreStorageMockRecorder) CreateUserCodeSession(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUserCodeSession", reflect.TypeOf((*MockCoreStorage)(nil).CreateUserCodeSession), arg0, arg1, arg2) -} - // DeleteAccessTokenSession mocks base method. func (m *MockCoreStorage) DeleteAccessTokenSession(arg0 context.Context, arg1 string) error { m.ctrl.T.Helper() @@ -167,21 +139,6 @@ func (mr *MockCoreStorageMockRecorder) GetAuthorizeCodeSession(arg0, arg1, arg2 return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuthorizeCodeSession", reflect.TypeOf((*MockCoreStorage)(nil).GetAuthorizeCodeSession), arg0, arg1, arg2) } -// GetDeviceCodeSession mocks base method. -func (m *MockCoreStorage) 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 *MockCoreStorageMockRecorder) GetDeviceCodeSession(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDeviceCodeSession", reflect.TypeOf((*MockCoreStorage)(nil).GetDeviceCodeSession), arg0, arg1, arg2) -} - // GetRefreshTokenSession mocks base method. func (m *MockCoreStorage) GetRefreshTokenSession(arg0 context.Context, arg1 string, arg2 fosite.Session) (fosite.Requester, error) { m.ctrl.T.Helper() @@ -197,21 +154,6 @@ func (mr *MockCoreStorageMockRecorder) GetRefreshTokenSession(arg0, arg1, arg2 i return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRefreshTokenSession", reflect.TypeOf((*MockCoreStorage)(nil).GetRefreshTokenSession), arg0, arg1, arg2) } -// GetUserCodeSession mocks base method. -func (m *MockCoreStorage) 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 *MockCoreStorageMockRecorder) GetUserCodeSession(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserCodeSession", reflect.TypeOf((*MockCoreStorage)(nil).GetUserCodeSession), arg0, arg1, arg2) -} - // InvalidateAuthorizeCodeSession mocks base method. func (m *MockCoreStorage) InvalidateAuthorizeCodeSession(arg0 context.Context, arg1 string) error { m.ctrl.T.Helper() @@ -225,45 +167,3 @@ func (mr *MockCoreStorageMockRecorder) InvalidateAuthorizeCodeSession(arg0, arg1 mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InvalidateAuthorizeCodeSession", reflect.TypeOf((*MockCoreStorage)(nil).InvalidateAuthorizeCodeSession), arg0, arg1) } - -// InvalidateDeviceCodeSession mocks base method. -func (m *MockCoreStorage) 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 *MockCoreStorageMockRecorder) InvalidateDeviceCodeSession(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InvalidateDeviceCodeSession", reflect.TypeOf((*MockCoreStorage)(nil).InvalidateDeviceCodeSession), arg0, arg1) -} - -// InvalidateUserCodeSession mocks base method. -func (m *MockCoreStorage) 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 *MockCoreStorageMockRecorder) InvalidateUserCodeSession(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InvalidateUserCodeSession", reflect.TypeOf((*MockCoreStorage)(nil).InvalidateUserCodeSession), arg0, arg1) -} - -// UpdateDeviceCodeSession mocks base method. -func (m *MockCoreStorage) 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 *MockCoreStorageMockRecorder) UpdateDeviceCodeSession(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateDeviceCodeSession", reflect.TypeOf((*MockCoreStorage)(nil).UpdateDeviceCodeSession), arg0, arg1, arg2) -} diff --git a/internal/oauth2_strategy.go b/internal/oauth2_strategy.go index 7cf8232bc..e6c7663cb 100644 --- a/internal/oauth2_strategy.go +++ b/internal/oauth2_strategy.go @@ -67,20 +67,6 @@ func (mr *MockCoreStrategyMockRecorder) AuthorizeCodeSignature(arg0, arg1 interf return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthorizeCodeSignature", reflect.TypeOf((*MockCoreStrategy)(nil).AuthorizeCodeSignature), arg0, arg1) } -// DeviceCodeSignature mocks base method. -func (m *MockCoreStrategy) DeviceCodeSignature(arg0 context.Context, arg1 string) string { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeviceCodeSignature", arg0, arg1) - ret0, _ := ret[0].(string) - return ret0 -} - -// DeviceCodeSignature indicates an expected call of DeviceCodeSignature. -func (mr *MockCoreStrategyMockRecorder) DeviceCodeSignature(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeviceCodeSignature", reflect.TypeOf((*MockCoreStrategy)(nil).DeviceCodeSignature), arg0, arg1) -} - // GenerateAccessToken mocks base method. func (m *MockCoreStrategy) GenerateAccessToken(arg0 context.Context, arg1 fosite.Requester) (string, string, error) { m.ctrl.T.Helper() @@ -113,22 +99,6 @@ func (mr *MockCoreStrategyMockRecorder) GenerateAuthorizeCode(arg0, arg1 interfa return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateAuthorizeCode", reflect.TypeOf((*MockCoreStrategy)(nil).GenerateAuthorizeCode), arg0, arg1) } -// GenerateDeviceCode mocks base method. -func (m *MockCoreStrategy) GenerateDeviceCode(arg0 context.Context) (string, string, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GenerateDeviceCode", arg0) - ret0, _ := ret[0].(string) - ret1, _ := ret[1].(string) - ret2, _ := ret[2].(error) - return ret0, ret1, ret2 -} - -// GenerateDeviceCode indicates an expected call of GenerateDeviceCode. -func (mr *MockCoreStrategyMockRecorder) GenerateDeviceCode(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateDeviceCode", reflect.TypeOf((*MockCoreStrategy)(nil).GenerateDeviceCode), arg0) -} - // GenerateRefreshToken mocks base method. func (m *MockCoreStrategy) GenerateRefreshToken(arg0 context.Context, arg1 fosite.Requester) (string, string, error) { m.ctrl.T.Helper() @@ -145,22 +115,6 @@ func (mr *MockCoreStrategyMockRecorder) GenerateRefreshToken(arg0, arg1 interfac return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateRefreshToken", reflect.TypeOf((*MockCoreStrategy)(nil).GenerateRefreshToken), arg0, arg1) } -// GenerateUserCode mocks base method. -func (m *MockCoreStrategy) GenerateUserCode(arg0 context.Context) (string, string, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GenerateUserCode", arg0) - ret0, _ := ret[0].(string) - ret1, _ := ret[1].(string) - ret2, _ := ret[2].(error) - return ret0, ret1, ret2 -} - -// GenerateUserCode indicates an expected call of GenerateUserCode. -func (mr *MockCoreStrategyMockRecorder) GenerateUserCode(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateUserCode", reflect.TypeOf((*MockCoreStrategy)(nil).GenerateUserCode), arg0) -} - // RefreshTokenSignature mocks base method. func (m *MockCoreStrategy) RefreshTokenSignature(arg0 context.Context, arg1 string) string { m.ctrl.T.Helper() @@ -175,20 +129,6 @@ func (mr *MockCoreStrategyMockRecorder) RefreshTokenSignature(arg0, arg1 interfa return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RefreshTokenSignature", reflect.TypeOf((*MockCoreStrategy)(nil).RefreshTokenSignature), arg0, arg1) } -// UserCodeSignature mocks base method. -func (m *MockCoreStrategy) UserCodeSignature(arg0 context.Context, arg1 string) string { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UserCodeSignature", arg0, arg1) - ret0, _ := ret[0].(string) - return ret0 -} - -// UserCodeSignature indicates an expected call of UserCodeSignature. -func (mr *MockCoreStrategyMockRecorder) UserCodeSignature(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserCodeSignature", reflect.TypeOf((*MockCoreStrategy)(nil).UserCodeSignature), arg0, arg1) -} - // ValidateAccessToken mocks base method. func (m *MockCoreStrategy) ValidateAccessToken(arg0 context.Context, arg1 fosite.Requester, arg2 string) error { m.ctrl.T.Helper() @@ -217,20 +157,6 @@ func (mr *MockCoreStrategyMockRecorder) ValidateAuthorizeCode(arg0, arg1, arg2 i return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateAuthorizeCode", reflect.TypeOf((*MockCoreStrategy)(nil).ValidateAuthorizeCode), arg0, arg1, arg2) } -// ValidateDeviceCode mocks base method. -func (m *MockCoreStrategy) ValidateDeviceCode(arg0 context.Context, arg1 fosite.Requester, arg2 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ValidateDeviceCode", arg0, arg1, arg2) - ret0, _ := ret[0].(error) - return ret0 -} - -// ValidateDeviceCode indicates an expected call of ValidateDeviceCode. -func (mr *MockCoreStrategyMockRecorder) ValidateDeviceCode(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateDeviceCode", reflect.TypeOf((*MockCoreStrategy)(nil).ValidateDeviceCode), arg0, arg1, arg2) -} - // ValidateRefreshToken mocks base method. func (m *MockCoreStrategy) ValidateRefreshToken(arg0 context.Context, arg1 fosite.Requester, arg2 string) error { m.ctrl.T.Helper() @@ -244,17 +170,3 @@ func (mr *MockCoreStrategyMockRecorder) ValidateRefreshToken(arg0, arg1, arg2 in mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateRefreshToken", reflect.TypeOf((*MockCoreStrategy)(nil).ValidateRefreshToken), arg0, arg1, arg2) } - -// ValidateUserCode mocks base method. -func (m *MockCoreStrategy) ValidateUserCode(arg0 context.Context, arg1 fosite.Requester, arg2 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ValidateUserCode", arg0, arg1, arg2) - ret0, _ := ret[0].(error) - return ret0 -} - -// ValidateUserCode indicates an expected call of ValidateUserCode. -func (mr *MockCoreStrategyMockRecorder) ValidateUserCode(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateUserCode", reflect.TypeOf((*MockCoreStrategy)(nil).ValidateUserCode), arg0, arg1, arg2) -} diff --git a/internal/user_code_storage.go b/internal/user_code_storage.go index 4914cca68..2928a8db1 100644 --- a/internal/user_code_storage.go +++ b/internal/user_code_storage.go @@ -1,5 +1,8 @@ +// Copyright © 2022 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/ory/fosite/handler/oauth2 (interfaces: UserCodeStorage) +// Source: github.com/ory/fosite/handler/rfc8628 (interfaces: UserCodeStorage) // Package internal is a generated GoMock package. package internal diff --git a/oauth2.go b/oauth2.go index 9dc6db5b7..e1136871e 100644 --- a/oauth2.go +++ b/oauth2.go @@ -33,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" ) @@ -103,27 +104,36 @@ type OAuth2Provider interface { WriteAuthorizeResponse(ctx context.Context, rw http.ResponseWriter, requester AuthorizeRequester, responder AuthorizeResponder) // NewDeviceAuthorizeRequest returns an DeviceAuthorizeRequest. - // TODO Add documentation + // 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 NewDeviceAuthorizeRequest(ctx context.Context, req *http.Request) (DeviceAuthorizeRequester, error) // NewDeviceAuthorizeResponse - // TODO Add documentation + // 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 NewDeviceAuthorizeResponse(ctx context.Context, requester DeviceAuthorizeRequester, session Session) (DeviceAuthorizeResponder, error) // WriteDeviceAuthorizeResponse - // TODO Add documentation + // Once the user is authorized, it is being redirect to the login page; WriteDeviceAuthorizeResponse(ctx context.Context, rw http.ResponseWriter, requester DeviceAuthorizeRequester, responder DeviceAuthorizeResponder) // 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) // WriteDeviceAuthorizeResponse return to the user both codes and @@ -131,6 +141,8 @@ type OAuth2Provider interface { // // 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 @@ -336,6 +348,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. @@ -369,17 +389,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 @@ -393,12 +409,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{}) @@ -407,6 +417,8 @@ type PushedAuthorizeResponder interface { // ToMap converts the response to a map. ToMap() map[string]interface{} + + Responder } // G11NContext is the globalization context @@ -416,6 +428,7 @@ type G11NContext interface { } type DeviceAuthorizeResponder interface { + Responder } type DeviceResponder interface { @@ -436,4 +449,6 @@ type DeviceResponder interface { GetInterval() int SetInterval(seconds int) + + Responder } From e23a26ae400c02f0d68c0971558f08f6e854e9aa Mon Sep 17 00:00:00 2001 From: Romain Caire Date: Wed, 14 Dec 2022 16:49:40 +0100 Subject: [PATCH 27/49] Update following integration test with Hydra --- compose/compose.go | 2 +- compose/compose_oauth2.go | 3 +++ compose/compose_pkce.go | 2 ++ compose/compose_strategy.go | 2 +- device_request_handler.go | 2 +- handler.go | 2 +- handler/oauth2/flow_authorize_code_token.go | 2 ++ handler/rfc8628/auth_handler.go | 2 +- handler/rfc8628/strategy_hmacsha.go | 2 ++ 9 files changed, 14 insertions(+), 5 deletions(-) diff --git a/compose/compose.go b/compose/compose.go index a5179bb7b..0548aba5a 100644 --- a/compose/compose.go +++ b/compose/compose.go @@ -74,7 +74,7 @@ func ComposeAllEnabled(config *fosite.Config, storage interface{}, key interface storage, &CommonStrategy{ CoreStrategy: NewOAuth2HMACStrategy(config), - RFC8628CodeStrategy: NewRFC8628CodeStrategy(config), + RFC8628CodeStrategy: NewDeviceStrategy(config), OpenIDConnectTokenStrategy: NewOpenIDConnectStrategy(keyGetter, config), Signer: &jwt.DefaultSigner{GetPrivateKey: keyGetter}, }, diff --git a/compose/compose_oauth2.go b/compose/compose_oauth2.go index accc7d4eb..2ec77b5d3 100644 --- a/compose/compose_oauth2.go +++ b/compose/compose_oauth2.go @@ -6,6 +6,7 @@ package compose import ( "github.com/ory/fosite" "github.com/ory/fosite/handler/oauth2" + "github.com/ory/fosite/handler/rfc8628" "github.com/ory/fosite/token/jwt" ) @@ -16,8 +17,10 @@ func OAuth2AuthorizeExplicitFactory(config fosite.Configurator, storage interfac AccessTokenStrategy: strategy.(oauth2.AccessTokenStrategy), RefreshTokenStrategy: strategy.(oauth2.RefreshTokenStrategy), AuthorizeCodeStrategy: strategy.(oauth2.AuthorizeCodeStrategy), + DeviceStrategy: strategy.(rfc8628.RFC8628CodeStrategy), CoreStorage: storage.(oauth2.CoreStorage), TokenRevocationStorage: storage.(oauth2.TokenRevocationStorage), + DeviceStorage: storage.(rfc8628.RFC8628CodeStorage), Config: config, } } diff --git a/compose/compose_pkce.go b/compose/compose_pkce.go index 286f4ae93..1d27e4da3 100644 --- a/compose/compose_pkce.go +++ b/compose/compose_pkce.go @@ -7,12 +7,14 @@ import ( "github.com/ory/fosite" "github.com/ory/fosite/handler/oauth2" "github.com/ory/fosite/handler/pkce" + "github.com/ory/fosite/handler/rfc8628" ) // OAuth2PKCEFactory creates a PKCE handler. func OAuth2PKCEFactory(config fosite.Configurator, storage interface{}, strategy interface{}) interface{} { return &pkce.Handler{ AuthorizeCodeStrategy: strategy.(oauth2.AuthorizeCodeStrategy), + DeviceCodeStrategy: strategy.(rfc8628.DeviceCodeStrategy), Storage: storage.(pkce.PKCERequestStorage), Config: config, } diff --git a/compose/compose_strategy.go b/compose/compose_strategy.go index aecb5c225..fdba0813f 100644 --- a/compose/compose_strategy.go +++ b/compose/compose_strategy.go @@ -54,7 +54,7 @@ func NewOpenIDConnectStrategy(keyGetter func(context.Context) (interface{}, erro } } -func NewRFC8628CodeStrategy(config fosite.Configurator) *rfc8628.DefaultDeviceStrategy { +func NewDeviceStrategy(config fosite.Configurator) *rfc8628.DefaultDeviceStrategy { return &rfc8628.DefaultDeviceStrategy{ Enigma: &hmac.HMACStrategy{Config: config}, Config: config, diff --git a/device_request_handler.go b/device_request_handler.go index 0df47bf6c..389b64813 100644 --- a/device_request_handler.go +++ b/device_request_handler.go @@ -37,7 +37,7 @@ func (f *Fosite) NewDeviceRequest(ctx context.Context, req *http.Request) (Devic request := NewDeviceRequest() request.Lang = i18n.GetLangFromRequest(f.Config.GetMessageCatalog(ctx), req) - if err := req.ParseMultipartForm(1 << 20); err != nil && err != http.ErrNotMultipart { + 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 diff --git a/handler.go b/handler.go index 2f54ffb05..123f25832 100644 --- a/handler.go +++ b/handler.go @@ -74,7 +74,7 @@ type DeviceEndpointHandler interface { // // The following spec is a good example of what HandleDeviceAuthorizeRequest should do. // * https://tools.ietf.org/html/rfc8628#section-3.2 - HandleDeviceEndpointRequest(ctx context.Context, requester Requester, responder DeviceResponder) error + HandleDeviceEndpointRequest(ctx context.Context, requester DeviceRequester, responder DeviceResponder) error } type DeviceAuthorizeEndpointHandler interface { diff --git a/handler/oauth2/flow_authorize_code_token.go b/handler/oauth2/flow_authorize_code_token.go index d07f11202..6695f9424 100644 --- a/handler/oauth2/flow_authorize_code_token.go +++ b/handler/oauth2/flow_authorize_code_token.go @@ -54,6 +54,8 @@ func (c *AuthorizeExplicitGrantHandler) HandleTokenEndpointRequest(ctx context.C 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) { + 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 { diff --git a/handler/rfc8628/auth_handler.go b/handler/rfc8628/auth_handler.go index 10e6f9ee8..3b84544cd 100644 --- a/handler/rfc8628/auth_handler.go +++ b/handler/rfc8628/auth_handler.go @@ -22,7 +22,7 @@ type DeviceAuthHandler struct { // 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.Requester, resp fosite.DeviceResponder) error { +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())) diff --git a/handler/rfc8628/strategy_hmacsha.go b/handler/rfc8628/strategy_hmacsha.go index bf78961b0..2f184aa76 100644 --- a/handler/rfc8628/strategy_hmacsha.go +++ b/handler/rfc8628/strategy_hmacsha.go @@ -17,6 +17,8 @@ import ( ) type DefaultDeviceStrategy struct { + RFC8628CodeStrategy + Enigma *enigma.HMACStrategy Config interface { fosite.DeviceAndUserCodeLifespanProvider From a99ed892e3ad2ce1ab0dc038a46d16bc5b780829 Mon Sep 17 00:00:00 2001 From: Romain Caire Date: Wed, 14 Dec 2022 17:59:51 +0100 Subject: [PATCH 28/49] Fix build error --- internal/device_handler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/device_handler.go b/internal/device_handler.go index 1b390c805..bd3709f78 100644 --- a/internal/device_handler.go +++ b/internal/device_handler.go @@ -39,7 +39,7 @@ func (m *MockDeviceEndpointHandler) EXPECT() *MockDeviceEndpointHandlerMockRecor } // HandleDeviceEndpointRequest mocks base method. -func (m *MockDeviceEndpointHandler) HandleDeviceEndpointRequest(arg0 context.Context, arg1 fosite.Requester, arg2 fosite.DeviceResponder) error { +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) From 88abd8da3b7434b74876161d8e67640eaf995eec Mon Sep 17 00:00:00 2001 From: Romain Caire Date: Wed, 14 Dec 2022 18:13:17 +0100 Subject: [PATCH 29/49] Fix test --- integration/helper_setup_test.go | 46 ++++++++++++++++++++++++++------ 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/integration/helper_setup_test.go b/integration/helper_setup_test.go index 176af0af3..afed1535d 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 { From 3313b71a11a71b2d75f2c18a9b31e41bd8a4d5d3 Mon Sep 17 00:00:00 2001 From: Romain Caire Date: Mon, 9 Jan 2023 11:14:38 +0100 Subject: [PATCH 30/49] Use HTTP redirect instead of raw http header redirection --- device_authorize_writer.go | 7 ++----- oauth2.go | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/device_authorize_writer.go b/device_authorize_writer.go index f12c6c121..e1ef62d55 100644 --- a/device_authorize_writer.go +++ b/device_authorize_writer.go @@ -32,9 +32,6 @@ import ( // Once the user has approved the grant he will be redirected on his loggin machine // to a webpage (usally hosted in hydra-ui) to understand that he was connected successfully // and that he can close this tab and return to his non-interactive device; -func (f *Fosite) WriteDeviceAuthorizeResponse(ctx context.Context, rw http.ResponseWriter, requester DeviceAuthorizeRequester, responder DeviceAuthorizeResponder) { - rw.Header().Set("Cache-Control", "no-store") - rw.Header().Set("Pragma", "no-cache") - rw.Header().Set("Location", f.Config.GetDeviceDone(ctx)) - rw.WriteHeader(http.StatusSeeOther) +func (f *Fosite) WriteDeviceAuthorizeResponse(ctx context.Context, r *http.Request, rw http.ResponseWriter, requester DeviceAuthorizeRequester, responder DeviceAuthorizeResponder) { + http.Redirect(rw, r, f.Config.GetDeviceDone(ctx), http.StatusSeeOther) } diff --git a/oauth2.go b/oauth2.go index e1136871e..ca2b90fb4 100644 --- a/oauth2.go +++ b/oauth2.go @@ -115,7 +115,7 @@ type OAuth2Provider interface { // WriteDeviceAuthorizeResponse // Once the user is authorized, it is being redirect to the login page; - WriteDeviceAuthorizeResponse(ctx context.Context, rw http.ResponseWriter, requester DeviceAuthorizeRequester, responder DeviceAuthorizeResponder) + WriteDeviceAuthorizeResponse(ctx context.Context, r *http.Request, rw http.ResponseWriter, requester DeviceAuthorizeRequester, responder DeviceAuthorizeResponder) // NewDeviceRequest validate the OAuth 2.0 Device Authorization Flow Request // From e99e2bc859b4dd34a7eabb0008fb5b97d42991ac Mon Sep 17 00:00:00 2001 From: Romain Caire Date: Mon, 9 Jan 2023 11:16:39 +0100 Subject: [PATCH 31/49] Refactor the DeviceResponse struct to only have one definition of it. --- device_response.go | 60 ++++++++++++++++++++++++++++---------------- device_write.go | 28 +++++++++------------ device_write_test.go | 25 +++++++----------- 3 files changed, 59 insertions(+), 54 deletions(-) diff --git a/device_response.go b/device_response.go index ca6eec774..a2844feca 100644 --- a/device_response.go +++ b/device_response.go @@ -3,16 +3,24 @@ package fosite -import "net/http" +import ( + "encoding/json" + "io" + "net/http" +) -type DeviceResponse struct { +type deviceResponse struct { Header http.Header - deviceCode string - userCode string - verificationURI string - verificationURIComplete string - interval int - expiresIn int64 + 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 { @@ -20,62 +28,70 @@ func NewDeviceResponse() *DeviceResponse { } func (d *DeviceResponse) GetDeviceCode() string { - return d.deviceCode + return d.deviceResponse.DeviceCode } // GetUserCode returns the response's user code func (d *DeviceResponse) SetDeviceCode(code string) { - d.deviceCode = code + d.deviceResponse.DeviceCode = code } func (d *DeviceResponse) GetUserCode() string { - return d.userCode + return d.deviceResponse.UserCode } func (d *DeviceResponse) SetUserCode(code string) { - d.userCode = code + d.deviceResponse.UserCode = code } // GetVerificationURI returns the response's verification uri func (d *DeviceResponse) GetVerificationURI() string { - return d.verificationURI + return d.deviceResponse.VerificationURI } func (d *DeviceResponse) SetVerificationURI(uri string) { - d.verificationURI = uri + d.deviceResponse.VerificationURI = uri } // GetVerificationURIComplete returns the response's complete verification uri if set func (d *DeviceResponse) GetVerificationURIComplete() string { - return d.verificationURIComplete + return d.deviceResponse.VerificationURIComplete } func (d *DeviceResponse) SetVerificationURIComplete(uri string) { - d.verificationURIComplete = uri + 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.expiresIn + return d.deviceResponse.ExpiresIn } func (d *DeviceResponse) SetExpiresIn(seconds int64) { - d.expiresIn = seconds + d.deviceResponse.ExpiresIn = seconds } // GetInterval returns the response's polling interval if set func (d *DeviceResponse) GetInterval() int { - return d.interval + return d.deviceResponse.Interval } func (d *DeviceResponse) SetInterval(seconds int) { - d.interval = seconds + d.deviceResponse.Interval = seconds } func (a *DeviceResponse) GetHeader() http.Header { - return a.Header + return a.deviceResponse.Header } func (a *DeviceResponse) AddHeader(key, value string) { - a.Header.Add(key, value) + 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_write.go b/device_write.go index a9abb57f8..f227bd32d 100644 --- a/device_write.go +++ b/device_write.go @@ -5,7 +5,6 @@ package fosite import ( "context" - "encoding/json" "net/http" ) @@ -23,19 +22,16 @@ func (f *Fosite) WriteDeviceResponse(ctx context.Context, rw http.ResponseWriter rw.Header().Set("Cache-Control", "no-store") rw.Header().Set("Pragma", "no-cache") - _ = json.NewEncoder(rw).Encode(struct { - 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"` - }{ - DeviceCode: responder.GetDeviceCode(), - UserCode: responder.GetUserCode(), - VerificationURI: responder.GetVerificationURI(), - VerificationURIComplete: responder.GetVerificationURIComplete(), - ExpiresIn: responder.GetExpiresIn(), - Interval: responder.GetInterval(), - }) + 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 index 05b5c6a70..12721b226 100644 --- a/device_write_test.go +++ b/device_write_test.go @@ -5,7 +5,6 @@ package fosite_test import ( "context" - "encoding/json" "net/http/httptest" "testing" "time" @@ -40,23 +39,17 @@ func TestWriteDeviceAuthorizeResponse(t *testing.T) { ) oauth2.WriteDeviceResponse(context.Background(), rw, ar, resp) - var params struct { - 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"` - } assert.Equal(t, 200, rw.Code) - err := json.NewDecoder(rw.Body).Decode(¶ms) + + wroteDeviceResponse := DeviceResponse{} + err := wroteDeviceResponse.FromJson(rw.Body) require.NoError(t, err) - assert.Equal(t, resp.GetUserCode(), params.UserCode) - assert.Equal(t, resp.GetDeviceCode(), params.DeviceCode) - assert.Equal(t, resp.GetVerificationURI(), params.VerificationURI) - assert.Equal(t, resp.GetVerificationURIComplete(), params.VerificationURIComplete) - assert.Equal(t, resp.GetInterval(), params.Interval) - assert.Equal(t, resp.GetExpiresIn(), params.ExpiresIn) + 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) } From 9279963627162eddd54d65168c078efb7d2d1974 Mon Sep 17 00:00:00 2001 From: Romain Caire Date: Mon, 9 Jan 2023 11:23:15 +0100 Subject: [PATCH 32/49] Use RandX instead of custom code to generate the Random UserCode --- handler/rfc8628/strategy_hmacsha.go | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/handler/rfc8628/strategy_hmacsha.go b/handler/rfc8628/strategy_hmacsha.go index 2f184aa76..1bb12b287 100644 --- a/handler/rfc8628/strategy_hmacsha.go +++ b/handler/rfc8628/strategy_hmacsha.go @@ -5,12 +5,11 @@ package rfc8628 import ( "context" - "crypto/rand" - "math/big" "strings" "time" "github.com/ory/x/errorsx" + "github.com/ory/x/randx" "github.com/ory/fosite" enigma "github.com/ory/fosite/token/hmac" @@ -25,26 +24,12 @@ type DefaultDeviceStrategy struct { } } -func (h *DefaultDeviceStrategy) generateRandomString(length int) (token string, err error) { - chars := [20]byte{'B', 'C', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'X', 'Z'} - chars_length := int64(len(chars)) - - code := make([]byte, length) - for i := 0; i < length; i++ { - num, err := rand.Int(rand.Reader, big.NewInt(chars_length)) - if err != nil { - return "", err - } - code[i] = chars[num.Int64()] - } - return string(code), nil -} - func (h *DefaultDeviceStrategy) GenerateUserCode(ctx context.Context) (token string, signature string, err error) { - userCode, err := h.generateRandomString(8) + seq, err := randx.RuneSequence(8, []rune("BCDFGHJKLMNPQRSTVWXZ")) if err != nil { return "", "", err } + userCode := string(seq) signUserCode, signErr := h.UserCodeSignature(ctx, userCode) if signErr != nil { return "", "", err From e60949d62456b8b5414c9ede68d1e94ffa664bfd Mon Sep 17 00:00:00 2001 From: Romain Caire Date: Mon, 9 Jan 2023 12:04:31 +0100 Subject: [PATCH 33/49] Remove padding from HMAC generation to avoid confusion in URLs --- token/hmac/hmacsha.go | 2 +- token/hmac/hmacsha_test.go | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/token/hmac/hmacsha.go b/token/hmac/hmacsha.go index 35d300259..54b31eea4 100644 --- a/token/hmac/hmacsha.go +++ b/token/hmac/hmacsha.go @@ -186,7 +186,7 @@ func (c *HMACStrategy) GenerateHMACForString(ctx context.Context, text string) ( bytes := []byte(text) hashBytes := c.generateHMAC(ctx, bytes, &signingKey) - b64 := base64.URLEncoding.EncodeToString(hashBytes) + b64 := base64.RawURLEncoding.EncodeToString(hashBytes) return b64, nil } diff --git a/token/hmac/hmacsha_test.go b/token/hmac/hmacsha_test.go index 19c6b11ac..b77eb6af9 100644 --- a/token/hmac/hmacsha_test.go +++ b/token/hmac/hmacsha_test.go @@ -144,19 +144,19 @@ func TestGenerateFromString(t *testing.T) { }{ { text: "", - hash: "-n7EqD-bXkY3yYMH-ctEAGV8XLkU7Y6Bo6pbyT1agGA=", + hash: "-n7EqD-bXkY3yYMH-ctEAGV8XLkU7Y6Bo6pbyT1agGA", }, { text: " ", - hash: "zXJvonHTNSOOGj_QKl4RpIX_zXgD2YfXUfwuDKaTTIg=", + hash: "zXJvonHTNSOOGj_QKl4RpIX_zXgD2YfXUfwuDKaTTIg", }, { text: "Test", - hash: "TMeEaHS-cDC2nijiesCNtsOyBqHHtzWqAcWvceQT50g=", + hash: "TMeEaHS-cDC2nijiesCNtsOyBqHHtzWqAcWvceQT50g", }, { text: "AnotherTest1234", - hash: "zHYDOZGjzhVjx5r8RlBhpnJemX5JxEEBUjVT01n3IFM=", + hash: "zHYDOZGjzhVjx5r8RlBhpnJemX5JxEEBUjVT01n3IFM", }, } { hash, _ := cg.GenerateHMACForString(context.Background(), c.text) From ad37f5d825500b9b333e71d10045840a0689d100 Mon Sep 17 00:00:00 2001 From: Romain Caire Date: Mon, 9 Jan 2023 12:11:44 +0100 Subject: [PATCH 34/49] Use GrantType const string instead of raw string --- device_authorize_request_handler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/device_authorize_request_handler.go b/device_authorize_request_handler.go index d11bab4f4..4898fdac0 100644 --- a/device_authorize_request_handler.go +++ b/device_authorize_request_handler.go @@ -28,7 +28,7 @@ func (f *Fosite) NewDeviceAuthorizeRequest(ctx context.Context, r *http.Request) } request.Client = client - if !client.GetGrantTypes().Has("urn:ietf:params:oauth:grant-type:device_code") { + 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.")) } } From ea146be63dc1b10c299f1330ee289dfc56be3936 Mon Sep 17 00:00:00 2001 From: Romain Caire Date: Thu, 19 Jan 2023 17:24:57 +0100 Subject: [PATCH 35/49] Split DeviceAuthorizeHandler and CodeAuthorizeHandler --- compose/compose.go | 4 +- compose/compose_oauth2.go | 19 +- compose/compose_openid.go | 6 +- compose/compose_rfc8628.go | 16 + fosite_test.go | 10 +- handler/oauth2/flow_authorize_code_auth.go | 33 +- .../oauth2/flow_authorize_code_auth_test.go | 10 +- handler/oauth2/flow_authorize_code_token.go | 251 +----- .../oauth2/flow_authorize_code_token_test.go | 718 ++---------------- handler/oauth2/flow_generic_code_token.go | 221 ++++++ handler/oauth2/storage.go | 1 - handler/oauth2/strategy_hmacsha_test.go | 8 - handler/openid/flow_hybrid.go | 8 +- handler/openid/flow_hybrid_test.go | 6 +- handler/rfc8628/auth_handler_test.go | 3 +- handler/rfc8628/token_endpoint_handler.go | 57 ++ .../rfc8628/token_endpoint_handler_test.go | 674 ++++++++++++++++ ...rize_code_grant_public_client_pkce_test.go | 2 +- ...authorize_code_grant_public_client_test.go | 2 +- integration/authorize_code_grant_test.go | 4 +- .../pushed_authorize_code_grant_test.go | 2 +- 21 files changed, 1098 insertions(+), 957 deletions(-) create mode 100644 handler/oauth2/flow_generic_code_token.go create mode 100644 handler/rfc8628/token_endpoint_handler.go create mode 100644 handler/rfc8628/token_endpoint_handler_test.go diff --git a/compose/compose.go b/compose/compose.go index 0548aba5a..bcec8e53e 100644 --- a/compose/compose.go +++ b/compose/compose.go @@ -78,7 +78,9 @@ func ComposeAllEnabled(config *fosite.Config, storage interface{}, key interface OpenIDConnectTokenStrategy: NewOpenIDConnectStrategy(keyGetter, config), Signer: &jwt.DefaultSigner{GetPrivateKey: keyGetter}, }, - OAuth2AuthorizeExplicitFactory, + OAuth2AuthorizeExplicitAuthFactory, + OAuth2AuthorizeExplicitTokenFactory, + OAuth2DeviceAuthorizationTokenFactory, OAuth2AuthorizeImplicitFactory, OAuth2ClientCredentialsGrantFactory, OAuth2RefreshTokenGrantFactory, diff --git a/compose/compose_oauth2.go b/compose/compose_oauth2.go index 2ec77b5d3..c0a757ab8 100644 --- a/compose/compose_oauth2.go +++ b/compose/compose_oauth2.go @@ -6,21 +6,28 @@ package compose import ( "github.com/ory/fosite" "github.com/ory/fosite/handler/oauth2" - "github.com/ory/fosite/handler/rfc8628" "github.com/ory/fosite/token/jwt" ) // 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{ +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.GenericCodeTokenEndpointHandler{ + CodeTokenEndpointHandler: &oauth2.AuthorizeExplicitGrantTokenHandler{ + AuthorizeCodeStrategy: strategy.(oauth2.AuthorizeCodeStrategy), + AuthorizeCodeStorage: storage.(oauth2.AuthorizeCodeStorage), + }, AccessTokenStrategy: strategy.(oauth2.AccessTokenStrategy), RefreshTokenStrategy: strategy.(oauth2.RefreshTokenStrategy), - AuthorizeCodeStrategy: strategy.(oauth2.AuthorizeCodeStrategy), - DeviceStrategy: strategy.(rfc8628.RFC8628CodeStrategy), CoreStorage: storage.(oauth2.CoreStorage), TokenRevocationStorage: storage.(oauth2.TokenRevocationStorage), - DeviceStorage: storage.(rfc8628.RFC8628CodeStorage), Config: config, } } diff --git a/compose/compose_openid.go b/compose/compose_openid.go index 6dba5917c..6e1805395 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, diff --git a/compose/compose_rfc8628.go b/compose/compose_rfc8628.go index 4bceeed5e..a46bf840f 100644 --- a/compose/compose_rfc8628.go +++ b/compose/compose_rfc8628.go @@ -5,6 +5,7 @@ package compose import ( "github.com/ory/fosite" + "github.com/ory/fosite/handler/oauth2" "github.com/ory/fosite/handler/rfc8628" ) @@ -17,3 +18,18 @@ func RFC8628DeviceFactory(config fosite.Configurator, storage interface{}, strat Config: config, } } + +// OAuth2DeviceCodeFactory creates an OAuth2 device authorization grant ("device authorization flow") handler and registers +// an access token, refresh token and authorize code validator. +func OAuth2DeviceAuthorizationTokenFactory(config fosite.Configurator, storage interface{}, strategy interface{}) interface{} { + return &oauth2.GenericCodeTokenEndpointHandler{ + CodeTokenEndpointHandler: &rfc8628.DeviceAuthorizeHandler{ + DeviceStrategy: strategy.(rfc8628.DeviceCodeStrategy), + DeviceStorage: storage.(rfc8628.DeviceCodeStorage), + }, + AccessTokenStrategy: strategy.(oauth2.AccessTokenStrategy), + RefreshTokenStrategy: strategy.(oauth2.RefreshTokenStrategy), + TokenRevocationStorage: storage.(oauth2.TokenRevocationStorage), + Config: config, + } +} diff --git a/fosite_test.go b/fosite_test.go index 66c6e9e06..13e34604e 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/handler/oauth2/flow_authorize_code_auth.go b/handler/oauth2/flow_authorize_code_auth.go index d4b6325b8..84c8b3975 100644 --- a/handler/oauth2/flow_authorize_code_auth.go +++ b/handler/oauth2/flow_authorize_code_auth.go @@ -9,46 +9,37 @@ import ( "strings" "time" - "github.com/ory/fosite/handler/rfc8628" "github.com/ory/x/errorsx" "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 - DeviceStrategy rfc8628.RFC8628CodeStrategy - DeviceStorage rfc8628.RFC8628CodeStorage - 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 @@ -79,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())) } @@ -100,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 edbaabe88..133b27d5c 100644 --- a/handler/oauth2/flow_authorize_code_auth_test.go +++ b/handler/oauth2/flow_authorize_code_auth_test.go @@ -27,16 +27,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 @@ -121,9 +121,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 6695f9424..1a5ca147a 100644 --- a/handler/oauth2/flow_authorize_code_token.go +++ b/handler/oauth2/flow_authorize_code_token.go @@ -5,254 +5,49 @@ 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 isAuthorizationCode(request) { - 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\".")) - } - } else if isDeviceCode(request) { - if !request.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\".")) - } - } - - 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) { - 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())) - } - - if isAuthorizationCode(request) { - // 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())) - } - } else if isDeviceCode(request) { - // The authorization server MUST verify that the device code is valid - // This needs to happen after store retrieval for the session to be hydrated properly - if err := c.DeviceStrategy.ValidateDeviceCode(ctx, request, code); 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 +// 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 } -func (*AuthorizeExplicitGrantHandler) 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 *AuthorizeExplicitGrantHandler) 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 isAuthorizationCode(requester) { - 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())) - } - } else if isDeviceCode(requester) { - if err := c.DeviceStrategy.ValidateDeviceCode(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) - } +var _ CodeTokenEndpointHandler = (*AuthorizeExplicitGrantTokenHandler)(nil) - 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, 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 isAuthorizationCode(requester) { - if err = c.CoreStorage.InvalidateAuthorizeCodeSession(ctx, signature); err != nil { - return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) - } - } else if isDeviceCode(requester) { - if err = c.DeviceStorage.InvalidateDeviceCodeSession(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())) +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 (c *AuthorizeExplicitGrantHandler) CanSkipClientAuth(ctx context.Context, requester fosite.AccessRequester) bool { - return isDeviceCode(requester) +func (c *AuthorizeExplicitGrantTokenHandler) ValidateCode(ctx context.Context, request fosite.AccessRequester, code string) error { + return c.AuthorizeCodeStrategy.ValidateAuthorizeCode(ctx, request, code) } -func (c *AuthorizeExplicitGrantHandler) CanHandleTokenEndpointRequest(ctx context.Context, requester fosite.AccessRequester) bool { - return isDeviceCode(requester) || isAuthorizationCode(requester) +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) + req, err := c.AuthorizeCodeStorage.GetAuthorizeCodeSession(ctx, signature, requester.GetSession()) + return code, signature, req, err } -func (c *AuthorizeExplicitGrantHandler) getCodeAndSession(ctx context.Context, requester fosite.AccessRequester) (code string, signature string, request fosite.Requester, err error) { - if isAuthorizationCode(requester) { - code := requester.GetRequestForm().Get("code") - signature := c.AuthorizeCodeStrategy.AuthorizeCodeSignature(ctx, code) - req, err := c.CoreStorage.GetAuthorizeCodeSession(ctx, signature, requester.GetSession()) - return code, signature, req, err - } else if isDeviceCode(requester) { - code := requester.GetRequestForm().Get("device_code") - 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 - } - - // We should never fall here - return "", "", nil, errorsx.WithStack(fosite.ErrAccessDenied.WithHint("This OAuth 2.0 request could not be identified")) +func (c *AuthorizeExplicitGrantTokenHandler) InvalidateSession(ctx context.Context, signature string) error { + return c.AuthorizeCodeStorage.InvalidateAuthorizeCodeSession(ctx, signature) } -func isDeviceCode(requester fosite.AccessRequester) bool { - // grant_type REQUIRED. - // Value MUST be set to "urn:ietf:params:oauth:grant-type:device_code" - return requester.GetGrantTypes().ExactOne(string(fosite.GrantTypeDeviceCode)) // && len(requester.GetRequestForm().Get("device_code")) > 0 +// implement TokenEndpointHandler +func (c *AuthorizeExplicitGrantTokenHandler) CanSkipClientAuth(ctx context.Context, requester fosite.AccessRequester) bool { + return false } -func isAuthorizationCode(requester fosite.AccessRequester) bool { - // grant_type REQUIRED. - // Value MUST be set to "authorization_code" - return requester.GetGrantTypes().ExactOne("authorization_code") // && len(requester.GetRequestForm().Get("code")) > 0 +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 1bc0ceadd..1675c2cc0 100644 --- a/handler/oauth2/flow_authorize_code_token_test.go +++ b/handler/oauth2/flow_authorize_code_token_test.go @@ -11,7 +11,6 @@ import ( "github.com/golang/mock/gomock" - "github.com/ory/fosite/handler/rfc8628" "github.com/ory/fosite/internal" //"github.com/golang/mock/gomock" @@ -31,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 @@ -210,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 { @@ -246,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 @@ -446,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"}, @@ -470,6 +475,11 @@ func TestAuthorizeCodeTransactional_HandleTokenEndpointRequest(t *testing.T) { CoreStorage } + type authorizeTransactionalStore struct { + storage.Transactional + AuthorizeCodeStorage + } + for _, testCase := range []struct { description string setup func() @@ -478,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). @@ -487,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). @@ -512,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). @@ -521,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!")). @@ -537,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). @@ -546,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). @@ -567,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). @@ -582,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). @@ -591,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!")). @@ -607,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). @@ -616,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). @@ -651,650 +661,28 @@ func TestAuthorizeCodeTransactional_HandleTokenEndpointRequest(t *testing.T) { mockTransactional = internal.NewMockTransactional(ctrl) mockCoreStore = internal.NewMockCoreStorage(ctrl) + mockAuthorizeStore = internal.NewMockAuthorizeCodeStorage(ctrl) testCase.setup() - - handler := AuthorizeExplicitGrantHandler{ - CoreStorage: transactionalStore{ - mockTransactional, - mockCoreStore, - }, - AccessTokenStrategy: &strategy, - RefreshTokenStrategy: &strategy, - AuthorizeCodeStrategy: &strategy, - Config: &fosite.Config{ - ScopeStrategy: fosite.HierarchicScopeStrategy, - AudienceMatchingStrategy: fosite.DefaultAudienceMatchingStrategy, - AuthorizeCodeLifespan: time.Minute, - }, - } - - if err := handler.PopulateTokenEndpointResponse(propagatedContext, request, response); testCase.expectError != nil { - assert.EqualError(t, err, testCase.expectError.Error()) - } - }) - } -} - -func TestDeviceAuthorizeCode_PopulateTokenEndpointResponse(t *testing.T) { - for k, strategy := range map[string]struct { - CoreStrategy - rfc8628.RFC8628CodeStrategy - }{ - "hmac": {&hmacshaStrategy, &RFC8628HMACSHAStrategy}, - } { - t.Run("strategy="+k, func(t *testing.T) { - store := storage.NewMemoryStore() - - var h AuthorizeExplicitGrantHandler - 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 = AuthorizeExplicitGrantHandler{ - CoreStorage: store, - DeviceStorage: store, - AuthorizeCodeStrategy: strategy.CoreStrategy, - AccessTokenStrategy: strategy.CoreStrategy, - RefreshTokenStrategy: strategy.CoreStrategy, - DeviceStrategy: strategy.RFC8628CodeStrategy, - Config: config, - } - - 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 TestDeviceAuthorizeCode_HandleTokenEndpointRequest(t *testing.T) { - for k, strategy := range map[string]struct { - CoreStrategy - rfc8628.RFC8628CodeStrategy - }{ - "hmac": {&hmacshaStrategy, &RFC8628HMACSHAStrategy}, - } { - t.Run("strategy="+k, func(t *testing.T) { - store := storage.NewMemoryStore() - - h := AuthorizeExplicitGrantHandler{ - CoreStorage: store, - DeviceStorage: store, - AuthorizeCodeStrategy: strategy.CoreStrategy, - AccessTokenStrategy: strategy.CoreStrategy, - RefreshTokenStrategy: strategy.CoreStrategy, - DeviceStrategy: strategy.RFC8628CodeStrategy, - Config: &fosite.Config{ - ScopeStrategy: fosite.HierarchicScopeStrategy, - AudienceMatchingStrategy: fosite.DefaultAudienceMatchingStrategy, - AuthorizeCodeLifespan: time.Minute, - }, + config := &fosite.Config{ + ScopeStrategy: fosite.HierarchicScopeStrategy, + AudienceMatchingStrategy: fosite.DefaultAudienceMatchingStrategy, + AuthorizeCodeLifespan: time.Minute, } - for i, c := range []struct { - areq *fosite.AccessRequest - authreq *fosite.DeviceAuthorizeRequest - description string - setup func(t *testing.T, areq *fosite.AccessRequest, authreq *fosite.DeviceAuthorizeRequest) - check func(t *testing.T, areq *fosite.AccessRequest, authreq *fosite.DeviceAuthorizeRequest) - 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.DeviceAuthorizeRequest) { - 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.DeviceAuthorizeRequest{ - 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.DeviceAuthorizeRequest) { - 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.DeviceAuthorizeRequest{ - 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.DeviceAuthorizeRequest) { - 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.DeviceAuthorizeRequest) { - 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.DeviceAuthorizeRequest) { - 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)) + handler := GenericCodeTokenEndpointHandler{ + CodeTokenEndpointHandler: &AuthorizeExplicitGrantTokenHandler{ + AuthorizeCodeStrategy: &strategy, + AuthorizeCodeStorage: authorizeTransactionalStore{ + mockTransactional, + mockAuthorizeStore, }, - 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 TestDeviceAuthorizeCodeTransactional_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 - CoreStorage - } - - type deviceTransactionalStore struct { - storage.Transactional - rfc8628.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 := AuthorizeExplicitGrantHandler{ - CoreStorage: coreTransactionalStore{ + CoreStorage: transactionalStore{ mockTransactional, mockCoreStore, }, - DeviceStorage: deviceTransactionalStore{ - mockTransactional, - mockDeviceStore, - }, - AuthorizeCodeStrategy: &strategy, - AccessTokenStrategy: &strategy, - RefreshTokenStrategy: &strategy, - DeviceStrategy: &deviceStrategy, - Config: &fosite.Config{ - ScopeStrategy: fosite.HierarchicScopeStrategy, - AudienceMatchingStrategy: fosite.DefaultAudienceMatchingStrategy, - DeviceAndUserCodeLifespan: 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..7784dbe1e --- /dev/null +++ b/handler/oauth2/flow_generic_code_token.go @@ -0,0 +1,221 @@ +// Copyright © 2022 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) { + 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 1a2860635..ae5c14b33 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/oauth2/strategy_hmacsha_test.go b/handler/oauth2/strategy_hmacsha_test.go index 04008fefe..0d72d0a4f 100644 --- a/handler/oauth2/strategy_hmacsha_test.go +++ b/handler/oauth2/strategy_hmacsha_test.go @@ -12,7 +12,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/ory/fosite" - "github.com/ory/fosite/handler/rfc8628" "github.com/ory/fosite/token/hmac" ) @@ -24,13 +23,6 @@ var hmacshaStrategy = HMACSHAStrategy{ }, } -var RFC8628HMACSHAStrategy = rfc8628.DefaultDeviceStrategy{ - Enigma: &hmac.HMACStrategy{Config: &fosite.Config{GlobalSecret: []byte("foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobar")}}, - Config: &fosite.Config{ - DeviceAndUserCodeLifespan: time.Hour * 24, - }, -} - var hmacExpiredCase = fosite.Request{ Client: &fosite.DefaultClient{ Secret: []byte("foobarfoobarfoobarfoobar"), diff --git a/handler/openid/flow_hybrid.go b/handler/openid/flow_hybrid.go index e0bdf181c..f86f5d9e4 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 4cba91839..ae2b18678 100644 --- a/handler/openid/flow_hybrid_test.go +++ b/handler/openid/flow_hybrid_test.go @@ -70,11 +70,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/rfc8628/auth_handler_test.go b/handler/rfc8628/auth_handler_test.go index 505a3fb0b..7430c5c10 100644 --- a/handler/rfc8628/auth_handler_test.go +++ b/handler/rfc8628/auth_handler_test.go @@ -4,6 +4,7 @@ package rfc8628_test import ( + "context" "testing" "time" @@ -38,7 +39,7 @@ func Test_HandleDeviceEndpointRequest(t *testing.T) { } resp := &fosite.DeviceResponse{} - handler.HandleDeviceEndpointRequest(nil, req, resp) + handler.HandleDeviceEndpointRequest(context.TODO(), req, resp) assert.NotEmpty(t, resp.GetDeviceCode()) assert.NotEmpty(t, resp.GetUserCode()) diff --git a/handler/rfc8628/token_endpoint_handler.go b/handler/rfc8628/token_endpoint_handler.go new file mode 100644 index 000000000..e07c8a4d9 --- /dev/null +++ b/handler/rfc8628/token_endpoint_handler.go @@ -0,0 +1,57 @@ +// Copyright © 2022 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" +) + +var _ oauth2.CodeTokenEndpointHandler = (*DeviceAuthorizeHandler)(nil) + +// DeviceAuthorizeHandler is a response handler for the Device UserCode introduced in the Device Authorize Grant +// as defined in https://www.rfc-editor.org/rfc/rfc8628 +type DeviceAuthorizeHandler struct { + DeviceStrategy DeviceCodeStrategy + DeviceStorage DeviceCodeStorage +} + +func (c *DeviceAuthorizeHandler) 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 *DeviceAuthorizeHandler) ValidateCode(ctx context.Context, request fosite.AccessRequester, code string) error { + return c.DeviceStrategy.ValidateDeviceCode(ctx, request, code) +} + +func (c *DeviceAuthorizeHandler) GetCodeAndSession(ctx context.Context, requester fosite.AccessRequester) (code string, signature string, request fosite.Requester, err error) { + code = requester.GetRequestForm().Get("device_code") + 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 *DeviceAuthorizeHandler) InvalidateSession(ctx context.Context, signature string) error { + return c.DeviceStorage.InvalidateDeviceCodeSession(ctx, signature) +} + +// implement TokenEndpointHandler +func (c *DeviceAuthorizeHandler) CanSkipClientAuth(ctx context.Context, requester fosite.AccessRequester) bool { + return requester.GetGrantTypes().ExactOne(string(fosite.GrantTypeDeviceCode)) +} + +func (c *DeviceAuthorizeHandler) 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..e0c52d256 --- /dev/null +++ b/handler/rfc8628/token_endpoint_handler_test.go @@ -0,0 +1,674 @@ +// Copyright © 2022 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 TestDeviceAuthorizeCode_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: &DeviceAuthorizeHandler{ + 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 TestDeviceAuthorizeCode_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: &DeviceAuthorizeHandler{ + 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.DeviceAuthorizeRequest + description string + setup func(t *testing.T, areq *fosite.AccessRequest, authreq *fosite.DeviceAuthorizeRequest) + check func(t *testing.T, areq *fosite.AccessRequest, authreq *fosite.DeviceAuthorizeRequest) + 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.DeviceAuthorizeRequest) { + 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.DeviceAuthorizeRequest{ + 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.DeviceAuthorizeRequest) { + 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.DeviceAuthorizeRequest{ + 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.DeviceAuthorizeRequest) { + 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.DeviceAuthorizeRequest) { + 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.DeviceAuthorizeRequest) { + 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 TestDeviceAuthorizeCodeTransactional_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: &DeviceAuthorizeHandler{ + 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 03581182e..aefabe81b 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 581812dc2..aa95146d4 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 0c56b4f8b..2fece59ee 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/pushed_authorize_code_grant_test.go b/integration/pushed_authorize_code_grant_test.go index 555a05232..1e5f59dd3 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() From 1b306bbb0e4edccac00a1feff4af0882ea919ff1 Mon Sep 17 00:00:00 2001 From: Romain Caire Date: Wed, 1 Feb 2023 10:55:10 +0100 Subject: [PATCH 36/49] Update Copyright header --- compose/compose_rfc8628.go | 2 +- device_authorize_request.go | 2 +- device_authorize_request_handler.go | 2 +- device_authorize_request_handler_test.go | 2 +- device_authorize_request_test.go | 2 +- device_authorize_response.go | 2 +- device_authorize_response_writer.go | 2 +- device_authorize_writer.go | 2 +- device_request.go | 2 +- device_request_handler.go | 2 +- device_request_handler_test.go | 2 +- device_response.go | 2 +- device_response_writer.go | 2 +- device_response_writer_test.go | 2 +- device_write.go | 2 +- device_write_test.go | 2 +- handler/oauth2/flow_generic_code_token.go | 2 +- handler/openid/flow_device_auth.go | 2 +- handler/openid/flow_device_token.go | 2 +- handler/pkce/handler_device_test.go | 2 +- handler/rfc8628/auth_handler.go | 2 +- handler/rfc8628/auth_handler_test.go | 2 +- handler/rfc8628/storage.go | 2 +- handler/rfc8628/strategy.go | 2 +- handler/rfc8628/strategy_hmacsha.go | 2 +- handler/rfc8628/strategy_hmacsha_test.go | 2 +- handler/rfc8628/token_endpoint_handler.go | 2 +- handler/rfc8628/token_endpoint_handler_test.go | 2 +- internal/device_authorize_handler.go | 2 +- internal/device_authorize_request.go | 2 +- internal/device_authorize_response.go | 2 +- internal/device_code_storage.go | 2 +- internal/device_handler.go | 2 +- internal/device_request.go | 2 +- internal/device_response.go | 2 +- internal/oauth2_auth_device_storage.go | 2 +- internal/user_code_storage.go | 2 +- 37 files changed, 37 insertions(+), 37 deletions(-) diff --git a/compose/compose_rfc8628.go b/compose/compose_rfc8628.go index a46bf840f..33e7a82e2 100644 --- a/compose/compose_rfc8628.go +++ b/compose/compose_rfc8628.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Ory Corp +// Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 package compose diff --git a/device_authorize_request.go b/device_authorize_request.go index e2f0d7140..d3802d174 100644 --- a/device_authorize_request.go +++ b/device_authorize_request.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Ory Corp +// Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 package fosite diff --git a/device_authorize_request_handler.go b/device_authorize_request_handler.go index 4898fdac0..7155f12b1 100644 --- a/device_authorize_request_handler.go +++ b/device_authorize_request_handler.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Ory Corp +// Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 package fosite diff --git a/device_authorize_request_handler_test.go b/device_authorize_request_handler_test.go index 6ec74e9e0..8922fd717 100644 --- a/device_authorize_request_handler_test.go +++ b/device_authorize_request_handler_test.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Ory Corp +// Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 package fosite_test diff --git a/device_authorize_request_test.go b/device_authorize_request_test.go index e0710cc60..34e28b36c 100644 --- a/device_authorize_request_test.go +++ b/device_authorize_request_test.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Ory Corp +// Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 package fosite diff --git a/device_authorize_response.go b/device_authorize_response.go index 274d28039..58bd44886 100644 --- a/device_authorize_response.go +++ b/device_authorize_response.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Ory Corp +// Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 package fosite diff --git a/device_authorize_response_writer.go b/device_authorize_response_writer.go index 493dc5555..954d66f7d 100644 --- a/device_authorize_response_writer.go +++ b/device_authorize_response_writer.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Ory Corp +// Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 package fosite diff --git a/device_authorize_writer.go b/device_authorize_writer.go index e1ef62d55..474235ef3 100644 --- a/device_authorize_writer.go +++ b/device_authorize_writer.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Ory Corp +// Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 /* diff --git a/device_request.go b/device_request.go index 3c521a113..3c2df0636 100644 --- a/device_request.go +++ b/device_request.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Ory Corp +// Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 package fosite diff --git a/device_request_handler.go b/device_request_handler.go index 389b64813..2167ad6f0 100644 --- a/device_request_handler.go +++ b/device_request_handler.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Ory Corp +// Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 /* diff --git a/device_request_handler_test.go b/device_request_handler_test.go index 13ef52cb8..e1dd52392 100644 --- a/device_request_handler_test.go +++ b/device_request_handler_test.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Ory Corp +// Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 package fosite_test diff --git a/device_response.go b/device_response.go index a2844feca..4f1c3899c 100644 --- a/device_response.go +++ b/device_response.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Ory Corp +// Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 package fosite diff --git a/device_response_writer.go b/device_response_writer.go index 5080f7635..1cf9cda04 100644 --- a/device_response_writer.go +++ b/device_response_writer.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Ory Corp +// Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 package fosite diff --git a/device_response_writer_test.go b/device_response_writer_test.go index b8eca533f..6f81afcbb 100644 --- a/device_response_writer_test.go +++ b/device_response_writer_test.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Ory Corp +// Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 package fosite_test diff --git a/device_write.go b/device_write.go index f227bd32d..0e8fa77b5 100644 --- a/device_write.go +++ b/device_write.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Ory Corp +// Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 package fosite diff --git a/device_write_test.go b/device_write_test.go index 12721b226..d4e9437df 100644 --- a/device_write_test.go +++ b/device_write_test.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Ory Corp +// Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 package fosite_test diff --git a/handler/oauth2/flow_generic_code_token.go b/handler/oauth2/flow_generic_code_token.go index 7784dbe1e..6fefc6791 100644 --- a/handler/oauth2/flow_generic_code_token.go +++ b/handler/oauth2/flow_generic_code_token.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Ory Corp +// Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 package oauth2 diff --git a/handler/openid/flow_device_auth.go b/handler/openid/flow_device_auth.go index a3b3ec201..0c063eb19 100644 --- a/handler/openid/flow_device_auth.go +++ b/handler/openid/flow_device_auth.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Ory Corp +// Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 package openid diff --git a/handler/openid/flow_device_token.go b/handler/openid/flow_device_token.go index ee1a69e7f..081d26f35 100644 --- a/handler/openid/flow_device_token.go +++ b/handler/openid/flow_device_token.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Ory Corp +// Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 package openid diff --git a/handler/pkce/handler_device_test.go b/handler/pkce/handler_device_test.go index a3108ebfc..414effc18 100644 --- a/handler/pkce/handler_device_test.go +++ b/handler/pkce/handler_device_test.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Ory Corp +// Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 package pkce diff --git a/handler/rfc8628/auth_handler.go b/handler/rfc8628/auth_handler.go index 3b84544cd..af3fc00db 100644 --- a/handler/rfc8628/auth_handler.go +++ b/handler/rfc8628/auth_handler.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Ory Corp +// Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 package rfc8628 diff --git a/handler/rfc8628/auth_handler_test.go b/handler/rfc8628/auth_handler_test.go index 7430c5c10..600fa56e7 100644 --- a/handler/rfc8628/auth_handler_test.go +++ b/handler/rfc8628/auth_handler_test.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Ory Corp +// Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 package rfc8628_test diff --git a/handler/rfc8628/storage.go b/handler/rfc8628/storage.go index b520dab8a..1bc22c853 100644 --- a/handler/rfc8628/storage.go +++ b/handler/rfc8628/storage.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Ory Corp +// Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 package rfc8628 diff --git a/handler/rfc8628/strategy.go b/handler/rfc8628/strategy.go index 7c525be2b..43a1cca32 100644 --- a/handler/rfc8628/strategy.go +++ b/handler/rfc8628/strategy.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Ory Corp +// Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 package rfc8628 diff --git a/handler/rfc8628/strategy_hmacsha.go b/handler/rfc8628/strategy_hmacsha.go index 1bb12b287..d61ce52d5 100644 --- a/handler/rfc8628/strategy_hmacsha.go +++ b/handler/rfc8628/strategy_hmacsha.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Ory Corp +// Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 package rfc8628 diff --git a/handler/rfc8628/strategy_hmacsha_test.go b/handler/rfc8628/strategy_hmacsha_test.go index 8d604b43c..57639aad0 100644 --- a/handler/rfc8628/strategy_hmacsha_test.go +++ b/handler/rfc8628/strategy_hmacsha_test.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Ory Corp +// Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 package rfc8628_test diff --git a/handler/rfc8628/token_endpoint_handler.go b/handler/rfc8628/token_endpoint_handler.go index e07c8a4d9..fa1339168 100644 --- a/handler/rfc8628/token_endpoint_handler.go +++ b/handler/rfc8628/token_endpoint_handler.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Ory Corp +// Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 package rfc8628 diff --git a/handler/rfc8628/token_endpoint_handler_test.go b/handler/rfc8628/token_endpoint_handler_test.go index e0c52d256..26f5c0c5a 100644 --- a/handler/rfc8628/token_endpoint_handler_test.go +++ b/handler/rfc8628/token_endpoint_handler_test.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Ory Corp +// Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 package rfc8628 diff --git a/internal/device_authorize_handler.go b/internal/device_authorize_handler.go index 354070dcc..f7f3e5f87 100644 --- a/internal/device_authorize_handler.go +++ b/internal/device_authorize_handler.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Ory Corp +// Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 // Code generated by MockGen. DO NOT EDIT. diff --git a/internal/device_authorize_request.go b/internal/device_authorize_request.go index 759fbeb80..33e318a1d 100644 --- a/internal/device_authorize_request.go +++ b/internal/device_authorize_request.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Ory Corp +// Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 // Code generated by MockGen. DO NOT EDIT. diff --git a/internal/device_authorize_response.go b/internal/device_authorize_response.go index 25d40310c..54aadbbc0 100644 --- a/internal/device_authorize_response.go +++ b/internal/device_authorize_response.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Ory Corp +// Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 // Code generated by MockGen. DO NOT EDIT. diff --git a/internal/device_code_storage.go b/internal/device_code_storage.go index a5c60ba3f..3c337e555 100644 --- a/internal/device_code_storage.go +++ b/internal/device_code_storage.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Ory Corp +// Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 // Code generated by MockGen. DO NOT EDIT. diff --git a/internal/device_handler.go b/internal/device_handler.go index bd3709f78..28ba82336 100644 --- a/internal/device_handler.go +++ b/internal/device_handler.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Ory Corp +// Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 // Code generated by MockGen. DO NOT EDIT. diff --git a/internal/device_request.go b/internal/device_request.go index 4a79697d4..35ffb6365 100644 --- a/internal/device_request.go +++ b/internal/device_request.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Ory Corp +// Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 // Code generated by MockGen. DO NOT EDIT. diff --git a/internal/device_response.go b/internal/device_response.go index 641785354..889ed5ec9 100644 --- a/internal/device_response.go +++ b/internal/device_response.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Ory Corp +// Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 // Code generated by MockGen. DO NOT EDIT. diff --git a/internal/oauth2_auth_device_storage.go b/internal/oauth2_auth_device_storage.go index 59fb51a75..84f54c4a4 100644 --- a/internal/oauth2_auth_device_storage.go +++ b/internal/oauth2_auth_device_storage.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Ory Corp +// Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 // Code generated by MockGen. DO NOT EDIT. diff --git a/internal/user_code_storage.go b/internal/user_code_storage.go index 2928a8db1..70c2748d6 100644 --- a/internal/user_code_storage.go +++ b/internal/user_code_storage.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Ory Corp +// Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 // Code generated by MockGen. DO NOT EDIT. From 8a2cf5c8eb9881e77b7babe25411fe1180b6cdac Mon Sep 17 00:00:00 2001 From: Romain Caire Date: Tue, 18 Apr 2023 09:32:44 +0200 Subject: [PATCH 37/49] Update fosite refactoring --- compose/compose.go | 2 +- compose/compose_oauth2.go | 20 +++++++++++--------- compose/compose_rfc8628.go | 21 ++++++++++++--------- handler/oauth2/flow_authorize_code_token.go | 5 +++++ handler/rfc8628/token_endpoint_handler.go | 11 ++++++++--- 5 files changed, 37 insertions(+), 22 deletions(-) diff --git a/compose/compose.go b/compose/compose.go index c5a311faf..94cf44e89 100644 --- a/compose/compose.go +++ b/compose/compose.go @@ -80,13 +80,13 @@ func ComposeAllEnabled(config *fosite.Config, storage interface{}, key interface }, OAuth2AuthorizeExplicitAuthFactory, OAuth2AuthorizeExplicitTokenFactory, - OAuth2DeviceAuthorizationTokenFactory, OAuth2AuthorizeImplicitFactory, OAuth2ClientCredentialsGrantFactory, OAuth2RefreshTokenGrantFactory, OAuth2ResourceOwnerPasswordCredentialsFactory, RFC7523AssertionGrantFactory, RFC8628DeviceFactory, + RFC8628DeviceAuthorizationTokenFactory, OpenIDConnectExplicitFactory, OpenIDConnectImplicitFactory, diff --git a/compose/compose_oauth2.go b/compose/compose_oauth2.go index 39b1d3912..813eef1aa 100644 --- a/compose/compose_oauth2.go +++ b/compose/compose_oauth2.go @@ -19,16 +19,18 @@ func OAuth2AuthorizeExplicitAuthFactory(config fosite.Configurator, storage inte } } func OAuth2AuthorizeExplicitTokenFactory(config fosite.Configurator, storage interface{}, strategy interface{}) interface{} { - return &oauth2.GenericCodeTokenEndpointHandler{ - CodeTokenEndpointHandler: &oauth2.AuthorizeExplicitGrantTokenHandler{ - AuthorizeCodeStrategy: strategy.(oauth2.AuthorizeCodeStrategy), - AuthorizeCodeStorage: storage.(oauth2.AuthorizeCodeStorage), + 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, }, - AccessTokenStrategy: strategy.(oauth2.AccessTokenStrategy), - RefreshTokenStrategy: strategy.(oauth2.RefreshTokenStrategy), - CoreStorage: storage.(oauth2.CoreStorage), - TokenRevocationStorage: storage.(oauth2.TokenRevocationStorage), - Config: config, } } diff --git a/compose/compose_rfc8628.go b/compose/compose_rfc8628.go index 33e7a82e2..b3574f0aa 100644 --- a/compose/compose_rfc8628.go +++ b/compose/compose_rfc8628.go @@ -21,15 +21,18 @@ func RFC8628DeviceFactory(config fosite.Configurator, storage interface{}, strat // OAuth2DeviceCodeFactory creates an OAuth2 device authorization grant ("device authorization flow") handler and registers // an access token, refresh token and authorize code validator. -func OAuth2DeviceAuthorizationTokenFactory(config fosite.Configurator, storage interface{}, strategy interface{}) interface{} { - return &oauth2.GenericCodeTokenEndpointHandler{ - CodeTokenEndpointHandler: &rfc8628.DeviceAuthorizeHandler{ - DeviceStrategy: strategy.(rfc8628.DeviceCodeStrategy), - DeviceStorage: storage.(rfc8628.DeviceCodeStorage), +func RFC8628DeviceAuthorizationTokenFactory(config fosite.Configurator, storage interface{}, strategy interface{}) interface{} { + return &rfc8628.DeviceCodeTokenEndpointHandler{ + GenericCodeTokenEndpointHandler: oauth2.GenericCodeTokenEndpointHandler{ + CodeTokenEndpointHandler: &rfc8628.DeviceAuthorizeHandler{ + 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, }, - AccessTokenStrategy: strategy.(oauth2.AccessTokenStrategy), - RefreshTokenStrategy: strategy.(oauth2.RefreshTokenStrategy), - TokenRevocationStorage: storage.(oauth2.TokenRevocationStorage), - Config: config, } } diff --git a/handler/oauth2/flow_authorize_code_token.go b/handler/oauth2/flow_authorize_code_token.go index 680725b68..20213f15a 100644 --- a/handler/oauth2/flow_authorize_code_token.go +++ b/handler/oauth2/flow_authorize_code_token.go @@ -18,7 +18,12 @@ type AuthorizeExplicitGrantTokenHandler struct { AuthorizeCodeStorage AuthorizeCodeStorage } +type AuthorizeExplicitTokenEndpointHandler struct { + GenericCodeTokenEndpointHandler +} + var _ CodeTokenEndpointHandler = (*AuthorizeExplicitGrantTokenHandler)(nil) +var _ fosite.TokenEndpointHandler = (*AuthorizeExplicitTokenEndpointHandler)(nil) func (c *AuthorizeExplicitGrantTokenHandler) ValidateGrantTypes(ctx context.Context, requester fosite.AccessRequester) error { if !requester.GetClient().GetGrantTypes().Has("authorization_code") { diff --git a/handler/rfc8628/token_endpoint_handler.go b/handler/rfc8628/token_endpoint_handler.go index fa1339168..b916c5344 100644 --- a/handler/rfc8628/token_endpoint_handler.go +++ b/handler/rfc8628/token_endpoint_handler.go @@ -12,15 +12,20 @@ import ( "github.com/ory/fosite" ) -var _ oauth2.CodeTokenEndpointHandler = (*DeviceAuthorizeHandler)(nil) - -// DeviceAuthorizeHandler is a response handler for the Device UserCode introduced in the Device Authorize Grant +// DeviceAuthorizeHandler is a response handler for the Device Code introduced in the Device Authorize Grant // as defined in https://www.rfc-editor.org/rfc/rfc8628 type DeviceAuthorizeHandler struct { DeviceStrategy DeviceCodeStrategy DeviceStorage DeviceCodeStorage } +type DeviceCodeTokenEndpointHandler struct { + oauth2.GenericCodeTokenEndpointHandler +} + +var _ oauth2.CodeTokenEndpointHandler = (*DeviceAuthorizeHandler)(nil) +var _ fosite.TokenEndpointHandler = (*DeviceCodeTokenEndpointHandler)(nil) + func (c *DeviceAuthorizeHandler) 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\".")) From d2316f409350f6d85ae6046b58a21ae1778b0706 Mon Sep 17 00:00:00 2001 From: Romain Caire Date: Tue, 10 Oct 2023 09:23:43 +0200 Subject: [PATCH 38/49] Fix some typo in config storage --- config_default.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config_default.go b/config_default.go index 1f272f12a..0ec165c45 100644 --- a/config_default.go +++ b/config_default.go @@ -388,7 +388,7 @@ func (c *Config) GetAuthorizeCodeLifespan(_ context.Context) time.Duration { } func (c *Config) GetDeviceAndUserCodeLifespan(_ context.Context) time.Duration { - if c.AuthorizeCodeLifespan == 0 { + if c.DeviceAndUserCodeLifespan == 0 { return time.Minute * 10 } return c.DeviceAndUserCodeLifespan From 084703654bd735ce708a9fd43699f5df8162500b Mon Sep 17 00:00:00 2001 From: Romain Caire Date: Mon, 27 Nov 2023 21:01:30 +0100 Subject: [PATCH 39/49] As per spec, set polling to 5s (from 10s) --- config_default.go | 2 +- handler/rfc8628/auth_handler_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config_default.go b/config_default.go index 7df3cf696..54b9b860b 100644 --- a/config_default.go +++ b/config_default.go @@ -544,7 +544,7 @@ func (c *Config) GetDeviceVerificationURL(ctx context.Context) string { func (c *Config) GetDeviceAuthTokenPollingInterval(ctx context.Context) time.Duration { if c.DeviceAuthTokenPollingInterval == 0 { - return time.Second * 10 + return time.Second * 5 } return c.DeviceAuthTokenPollingInterval } diff --git a/handler/rfc8628/auth_handler_test.go b/handler/rfc8628/auth_handler_test.go index 600fa56e7..ef59b97f1 100644 --- a/handler/rfc8628/auth_handler_test.go +++ b/handler/rfc8628/auth_handler_test.go @@ -24,7 +24,7 @@ func Test_HandleDeviceEndpointRequest(t *testing.T) { Strategy: &hmacshaStrategy, Config: &fosite.Config{ DeviceAndUserCodeLifespan: time.Minute * 10, - DeviceAuthTokenPollingInterval: time.Second * 10, + DeviceAuthTokenPollingInterval: time.Second * 5, DeviceVerificationURL: "www.test.com", AccessTokenLifespan: time.Hour, RefreshTokenLifespan: time.Hour, From 5b41e88bda4ae8a0f77486d41b0dfb56631e0582 Mon Sep 17 00:00:00 2001 From: Romain Caire Date: Mon, 27 Nov 2023 21:01:41 +0100 Subject: [PATCH 40/49] Fix typo in comment --- oauth2.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oauth2.go b/oauth2.go index 2839b132c..7fe198883 100644 --- a/oauth2.go +++ b/oauth2.go @@ -136,7 +136,7 @@ type OAuth2Provider interface { // time NewDeviceResponse(ctx context.Context, requester DeviceRequester) (DeviceResponder, error) - // WriteDeviceAuthorizeResponse return to the user both codes and + // 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: From 5f9b0b1d591f92e0fa0303dce5646067f77a170e Mon Sep 17 00:00:00 2001 From: Romain Caire Date: Mon, 27 Nov 2023 22:27:51 +0100 Subject: [PATCH 41/49] Add tracer as per AuthorizeRequest --- device_authorize_request_handler.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/device_authorize_request_handler.go b/device_authorize_request_handler.go index 7155f12b1..715579abd 100644 --- a/device_authorize_request_handler.go +++ b/device_authorize_request_handler.go @@ -9,9 +9,18 @@ import ( "github.com/ory/fosite/i18n" "github.com/ory/x/errorsx" + "github.com/ory/x/otelx" + "go.opentelemetry.io/otel/trace" ) -func (f *Fosite) NewDeviceAuthorizeRequest(ctx context.Context, r *http.Request) (DeviceAuthorizeRequester, error) { +func (f *Fosite) NewDeviceAuthorizeRequest(ctx context.Context, r *http.Request) (_ DeviceAuthorizeRequester, err error) { + ctx, span := trace.SpanFromContext(ctx).TracerProvider().Tracer("github.com/ory/fosite").Start(ctx, "Fosite.NewDeviceAuthorizeRequest") + defer otelx.End(span, &err) + + return f.newDeviceAuthorizeRequest(ctx, r) +} + +func (f *Fosite) newDeviceAuthorizeRequest(ctx context.Context, r *http.Request) (DeviceAuthorizeRequester, error) { request := NewDeviceAuthorizeRequest() request.Lang = i18n.GetLangFromRequest(f.Config.GetMessageCatalog(ctx), r) From bc7013888d9a26052b5b24e9d5fc8739f1faf6fc Mon Sep 17 00:00:00 2001 From: Romain Caire Date: Mon, 27 Nov 2023 22:31:55 +0100 Subject: [PATCH 42/49] Fix comment in typo --- device_response.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/device_response.go b/device_response.go index 4f1c3899c..b9b9d655e 100644 --- a/device_response.go +++ b/device_response.go @@ -31,7 +31,7 @@ func (d *DeviceResponse) GetDeviceCode() string { return d.deviceResponse.DeviceCode } -// GetUserCode returns the response's user code +// SetDeviceCode returns the response's user code func (d *DeviceResponse) SetDeviceCode(code string) { d.deviceResponse.DeviceCode = code } From c4b608ae1e3609945b3ed1976712b03c4c28fe25 Mon Sep 17 00:00:00 2001 From: Romain Caire Date: Wed, 29 Nov 2023 13:32:53 +0100 Subject: [PATCH 43/49] use randx runes defined in ory/x --- handler/rfc8628/strategy_hmacsha.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/handler/rfc8628/strategy_hmacsha.go b/handler/rfc8628/strategy_hmacsha.go index d61ce52d5..a15648bbb 100644 --- a/handler/rfc8628/strategy_hmacsha.go +++ b/handler/rfc8628/strategy_hmacsha.go @@ -25,7 +25,7 @@ type DefaultDeviceStrategy struct { } func (h *DefaultDeviceStrategy) GenerateUserCode(ctx context.Context) (token string, signature string, err error) { - seq, err := randx.RuneSequence(8, []rune("BCDFGHJKLMNPQRSTVWXZ")) + seq, err := randx.RuneSequence(8, []rune(randx.AlphaUpperNoVowels)) if err != nil { return "", "", err } From 6cded098892c3aeb4bbc6daea77a78623ea0ae83 Mon Sep 17 00:00:00 2001 From: Romain Caire Date: Thu, 30 Nov 2023 14:11:46 +0100 Subject: [PATCH 44/49] Remove PKCE --- compose/compose_pkce.go | 2 - handler/pkce/handler.go | 75 ++---- handler/pkce/handler_device_test.go | 353 ---------------------------- handler/pkce/handler_test.go | 1 - 4 files changed, 15 insertions(+), 416 deletions(-) delete mode 100644 handler/pkce/handler_device_test.go diff --git a/compose/compose_pkce.go b/compose/compose_pkce.go index cd0e0a138..d87a53b6f 100644 --- a/compose/compose_pkce.go +++ b/compose/compose_pkce.go @@ -7,14 +7,12 @@ import ( "github.com/ory/fosite" "github.com/ory/fosite/handler/oauth2" "github.com/ory/fosite/handler/pkce" - "github.com/ory/fosite/handler/rfc8628" ) // OAuth2PKCEFactory creates a PKCE handler. func OAuth2PKCEFactory(config fosite.Configurator, storage interface{}, strategy interface{}) interface{} { return &pkce.Handler{ AuthorizeCodeStrategy: strategy.(oauth2.AuthorizeCodeStrategy), - DeviceCodeStrategy: strategy.(rfc8628.DeviceCodeStrategy), Storage: storage.(pkce.PKCERequestStorage), Config: config, } diff --git a/handler/pkce/handler.go b/handler/pkce/handler.go index dc527839a..f457b8bea 100644 --- a/handler/pkce/handler.go +++ b/handler/pkce/handler.go @@ -15,14 +15,12 @@ import ( "github.com/ory/fosite" "github.com/ory/fosite/handler/oauth2" - "github.com/ory/fosite/handler/rfc8628" ) var _ fosite.TokenEndpointHandler = (*Handler)(nil) type Handler struct { AuthorizeCodeStrategy oauth2.AuthorizeCodeStrategy - DeviceCodeStrategy rfc8628.DeviceCodeStrategy Storage PKCERequestStorage Config interface { fosite.EnforcePKCEProvider @@ -35,51 +33,27 @@ var _ fosite.TokenEndpointHandler = (*Handler)(nil) var verifierWrongFormat = regexp.MustCompile("[^\\w\\.\\-~]") -func (c *Handler) HandleDeviceEndpointRequest(ctx context.Context, dr fosite.DeviceRequester, resp fosite.DeviceResponder) error { - return c.handlePkceEndpointRequest(ctx, dr, resp) -} - func (c *Handler) HandleAuthorizeEndpointRequest(ctx context.Context, ar fosite.AuthorizeRequester, resp fosite.AuthorizeResponder) error { - return c.handlePkceEndpointRequest(ctx, ar, resp) -} - -func (c *Handler) handlePkceEndpointRequest(ctx context.Context, r fosite.Requester, resp fosite.Responder) error { // This let's us define multiple response types, for example open id connect's id_token - if !(isAuthorizationCode(r) || isDeviceCode(r)) { + if !ar.GetResponseTypes().Has("code") { return nil } - challenge := r.GetRequestForm().Get("code_challenge") - method := r.GetRequestForm().Get("code_challenge_method") - client := r.GetClient() + challenge := ar.GetRequestForm().Get("code_challenge") + method := ar.GetRequestForm().Get("code_challenge_method") + client := ar.GetClient() if err := c.validate(ctx, challenge, method, client); err != nil { return err } - var signature string - if authorizeResp, ok := resp.(fosite.AuthorizeResponder); ok { - code := authorizeResp.GetCode() - if len(code) == 0 { - return errorsx.WithStack(fosite.ErrServerError.WithDebug("The PKCE handler must be loaded after the authorize/device code handler.")) - } - signature = c.AuthorizeCodeStrategy.AuthorizeCodeSignature(ctx, code) - } else if deviceResp, ok := resp.(fosite.DeviceResponder); ok { - code := deviceResp.GetDeviceCode() - if len(code) == 0 { - return errorsx.WithStack(fosite.ErrServerError.WithDebug("The PKCE handler must be loaded after the device code handler.")) - } - - var err error - signature, err = c.DeviceCodeStrategy.DeviceCodeSignature(ctx, code) - if err != nil { - return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) - } - } else { - return errorsx.WithStack(fosite.ErrServerError.WithDebug("This PKCE handle could not find the proper response type")) + code := resp.GetCode() + if len(code) == 0 { + return errorsx.WithStack(fosite.ErrServerError.WithDebug("The PKCE handler must be loaded after the authorize code handler.")) } - if err := c.Storage.CreatePKCERequestSession(ctx, signature, r.Sanitize([]string{ + signature := c.AuthorizeCodeStrategy.AuthorizeCodeSignature(ctx, code) + if err := c.Storage.CreatePKCERequestSession(ctx, signature, ar.Sanitize([]string{ "code_challenge", "code_challenge_method", })); err != nil { @@ -89,7 +63,7 @@ func (c *Handler) handlePkceEndpointRequest(ctx context.Context, r fosite.Reques return nil } -func (c *Handler) validate(ctx context.Context, challenge string, method string, client fosite.Client) error { +func (c *Handler) validate(ctx context.Context, challenge, method string, client fosite.Client) error { if challenge == "" { // If the server requires Proof Key for Code Exchange (PKCE) by OAuth // clients and the client does not send the "code_challenge" in @@ -147,19 +121,8 @@ func (c *Handler) HandleTokenEndpointRequest(ctx context.Context, request fosite // endpoint MUST use to verify the "code_verifier". verifier := request.GetRequestForm().Get("code_verifier") - var signature string - if request.GetGrantTypes().ExactOne("authorization_code") { - code := request.GetRequestForm().Get("code") - signature = c.AuthorizeCodeStrategy.AuthorizeCodeSignature(ctx, code) - } else if request.GetGrantTypes().ExactOne(string(fosite.GrantTypeDeviceCode)) { - var err error - code := request.GetRequestForm().Get("device_code") - signature, err = c.DeviceCodeStrategy.DeviceCodeSignature(ctx, code) - if err != nil { - return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) - } - } - + code := request.GetRequestForm().Get("code") + signature := c.AuthorizeCodeStrategy.AuthorizeCodeSignature(ctx, code) authorizeRequest, err := c.Storage.GetPKCERequestSession(ctx, signature, request.GetSession()) if errors.Is(err, fosite.ErrNotFound) { return errorsx.WithStack(fosite.ErrInvalidGrant.WithHint("Unable to find initial PKCE data tied to this request").WithWrap(err).WithDebug(err.Error())) @@ -254,15 +217,7 @@ func (c *Handler) CanSkipClientAuth(ctx context.Context, requester fosite.Access } func (c *Handler) CanHandleTokenEndpointRequest(ctx context.Context, requester fosite.AccessRequester) bool { - return requester.GetGrantTypes().ExactOne("authorization_code") || - requester.GetGrantTypes().ExactOne(string(fosite.GrantTypeDeviceCode)) -} - -func isDeviceCode(r fosite.Requester) bool { - return r.GetClient().GetGrantTypes().Has(string(fosite.GrantTypeDeviceCode)) -} - -func isAuthorizationCode(r fosite.Requester) bool { - ar, ok := r.(*fosite.AuthorizeRequest) - return ok && ar.GetResponseTypes().Has("code") + // grant_type REQUIRED. + // Value MUST be set to "authorization_code" + return requester.GetGrantTypes().ExactOne("authorization_code") } diff --git a/handler/pkce/handler_device_test.go b/handler/pkce/handler_device_test.go deleted file mode 100644 index 414effc18..000000000 --- a/handler/pkce/handler_device_test.go +++ /dev/null @@ -1,353 +0,0 @@ -// Copyright © 2023 Ory Corp -// SPDX-License-Identifier: Apache-2.0 - -package pkce - -import ( - "context" - "crypto/sha256" - "encoding/base64" - "fmt" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/ory/fosite" - "github.com/ory/fosite/handler/rfc8628" - "github.com/ory/fosite/storage" -) - -type mockDeviceCodeStrategy struct { - signature string -} - -func (m *mockDeviceCodeStrategy) DeviceCodeSignature(ctx context.Context, token string) (signature string, err error) { - return m.signature, nil -} - -func (m *mockDeviceCodeStrategy) GenerateDeviceCode(ctx context.Context) (token string, signature string, err error) { - return "", "", nil -} - -func (m *mockDeviceCodeStrategy) ValidateDeviceCode(ctx context.Context, requester fosite.Requester, token string) (err error) { - return nil -} - -func TestPKCEHandlerDevice_HandleAuthorizeEndpointRequest(t *testing.T) { - var config fosite.Config - h := &Handler{ - Storage: storage.NewMemoryStore(), - DeviceCodeStrategy: new(rfc8628.DefaultDeviceStrategy), - Config: &config, - } - w := fosite.NewDeviceResponse() - r := fosite.NewDeviceRequest() - config.GlobalSecret = []byte("thisissecret") - - w.SetDeviceCode("foo") - - r.Form.Add("code_challenge", "challenge") - r.Form.Add("code_challenge_method", "plain") - - c := &fosite.DefaultClient{} - r.Client = c - require.NoError(t, h.HandleDeviceEndpointRequest(context.Background(), r, w)) - - c = &fosite.DefaultClient{ - GrantTypes: []string{"urn:ietf:params:oauth:grant-type:device_code"}, - } - r.Client = c - require.Error(t, h.HandleDeviceEndpointRequest(context.Background(), r, w)) - - c.Public = true - config.EnablePKCEPlainChallengeMethod = true - require.NoError(t, h.HandleDeviceEndpointRequest(context.Background(), r, w)) - - c.Public = false - config.EnablePKCEPlainChallengeMethod = true - require.NoError(t, h.HandleDeviceEndpointRequest(context.Background(), r, w)) - - config.EnablePKCEPlainChallengeMethod = false - require.Error(t, h.HandleDeviceEndpointRequest(context.Background(), r, w)) - - r.Form.Set("code_challenge_method", "S256") - r.Form.Set("code_challenge", "") - config.EnforcePKCE = true - require.Error(t, h.HandleDeviceEndpointRequest(context.Background(), r, w)) - - r.Form.Set("code_challenge", "challenge") - require.NoError(t, h.HandleDeviceEndpointRequest(context.Background(), r, w)) -} - -func TestPKCEHandlerDevice_HandlerValidate(t *testing.T) { - s := storage.NewMemoryStore() - ds := &mockDeviceCodeStrategy{} - config := &fosite.Config{} - h := &Handler{ - Storage: s, - DeviceCodeStrategy: ds, - Config: config, - } - pc := &fosite.DefaultClient{Public: true} - - s256verifier := "KGCt4m8AmjUvIR5ArTByrmehjtbxn1A49YpTZhsH8N7fhDr7LQayn9xx6mck" - hash := sha256.New() - hash.Write([]byte(s256verifier)) - s256challenge := base64.RawURLEncoding.EncodeToString(hash.Sum([]byte{})) - - for k, tc := range []struct { - d string - grant string - force bool - enablePlain bool - challenge string - method string - verifier string - code string - expectErr error - client *fosite.DefaultClient - }{ - { - d: "fails because not auth code flow", - grant: "not_urn:ietf:params:oauth:grant-type:device_code", - expectErr: fosite.ErrUnknownRequest, - client: &fosite.DefaultClient{Public: false}, - }, - { - d: "passes with private client", - grant: "urn:ietf:params:oauth:grant-type:device_code", - challenge: "foofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoo", - verifier: "foofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoo", - method: "plain", - client: &fosite.DefaultClient{Public: false}, - enablePlain: true, - force: true, - code: "valid-code-1", - }, - { - d: "fails because invalid code", - grant: "urn:ietf:params:oauth:grant-type:device_code", - expectErr: fosite.ErrInvalidGrant, - client: pc, - code: "invalid-code-2", - }, - { - d: "passes because auth code flow but pkce is not forced and no challenge given", - grant: "urn:ietf:params:oauth:grant-type:device_code", - client: pc, - code: "valid-code-3", - }, - { - d: "fails because auth code flow and pkce challenge given but plain is disabled", - grant: "urn:ietf:params:oauth:grant-type:device_code", - challenge: "foo", - client: pc, - expectErr: fosite.ErrInvalidRequest, - code: "valid-code-4", - }, - { - d: "passes", - grant: "urn:ietf:params:oauth:grant-type:device_code", - challenge: "foofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoo", - verifier: "foofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoo", - client: pc, - enablePlain: true, - force: true, - code: "valid-code-5", - }, - { - d: "passes", - grant: "urn:ietf:params:oauth:grant-type:device_code", - challenge: "foofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoo", - verifier: "foofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoo", - method: "plain", - client: pc, - enablePlain: true, - force: true, - code: "valid-code-6", - }, - { - d: "fails because challenge and verifier do not match", - grant: "urn:ietf:params:oauth:grant-type:device_code", - challenge: "not-foo", - verifier: "foofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoo", - method: "plain", - client: pc, - enablePlain: true, - code: "valid-code-7", - expectErr: fosite.ErrInvalidGrant, - }, - { - d: "fails because challenge and verifier do not match", - grant: "urn:ietf:params:oauth:grant-type:device_code", - challenge: "not-foonot-foonot-foonot-foonot-foonot-foonot-foonot-foo", - verifier: "foofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoo", - client: pc, - enablePlain: true, - code: "valid-code-8", - expectErr: fosite.ErrInvalidGrant, - }, - { - d: "fails because verifier is too short", - grant: "urn:ietf:params:oauth:grant-type:device_code", - challenge: "foo", - verifier: "foo", - method: "S256", - client: pc, - force: true, - code: "valid-code-9a", - expectErr: fosite.ErrInvalidGrant, - }, - { - d: "fails because verifier is too long", - grant: "urn:ietf:params:oauth:grant-type:device_code", - challenge: "foo", - verifier: "foofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoo", - method: "S256", - client: pc, - force: true, - code: "valid-code-10", - expectErr: fosite.ErrInvalidGrant, - }, - { - d: "fails because verifier is malformed", - grant: "urn:ietf:params:oauth:grant-type:device_code", - challenge: "foo", - verifier: `(!"/$%Z&$T()/)OUZI>$"&=/T(PUOI>"%/)TUOI&/(O/()RGTE>=/(%"/()="$/)(=()=/R/()=))`, - method: "S256", - client: pc, - force: true, - code: "valid-code-11", - expectErr: fosite.ErrInvalidGrant, - }, - { - d: "fails because challenge and verifier do not match", - grant: "urn:ietf:params:oauth:grant-type:device_code", - challenge: "Zm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9v", - verifier: "Zm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9vZm9v", - method: "S256", - client: pc, - force: true, - code: "valid-code-12", - expectErr: fosite.ErrInvalidGrant, - }, - { - d: "passes because challenge and verifier match", - grant: "urn:ietf:params:oauth:grant-type:device_code", - challenge: s256challenge, - verifier: s256verifier, - method: "S256", - client: pc, - force: true, - code: "valid-code-13", - }, - } { - t.Run(fmt.Sprintf("case=%d/description=%s", k, tc.d), func(t *testing.T) { - config.EnablePKCEPlainChallengeMethod = tc.enablePlain - config.EnforcePKCE = tc.force - ds.signature = tc.code - ar := fosite.NewAuthorizeRequest() - ar.Form.Add("code_challenge", tc.challenge) - ar.Form.Add("code_challenge_method", tc.method) - require.NoError(t, s.CreatePKCERequestSession(context.TODO(), fmt.Sprintf("valid-code-%d", k), ar)) - - r := fosite.NewAccessRequest(nil) - r.Client = tc.client - r.GrantTypes = fosite.Arguments{tc.grant} - r.Form.Add("code_verifier", tc.verifier) - r.Form.Add("device_code", tc.code) - if tc.expectErr == nil { - require.NoError(t, h.HandleTokenEndpointRequest(context.Background(), r)) - } else { - err := h.HandleTokenEndpointRequest(context.Background(), r) - require.EqualError(t, err, tc.expectErr.Error(), "%+v", err) - } - }) - } -} - -func TestPKCEHandlerDevice_HandleTokenEndpointRequest(t *testing.T) { - for k, tc := range []struct { - d string - force bool - forcePublic bool - enablePlain bool - challenge string - method string - expectErr bool - client *fosite.DefaultClient - }{ - { - d: "should pass because pkce is not enforced", - }, - { - d: "should fail because plain is not enabled and method is empty which defaults to plain", - expectErr: true, - force: true, - }, - { - d: "should fail because force is enabled and no challenge was given", - force: true, - enablePlain: true, - expectErr: true, - method: "S256", - }, - { - d: "should fail because forcePublic is enabled, the client is public, and no challenge was given", - forcePublic: true, - client: &fosite.DefaultClient{Public: true}, - expectErr: true, - method: "S256", - }, - { - d: "should fail because although force is enabled and a challenge was given, plain is disabled", - force: true, - expectErr: true, - method: "plain", - challenge: "challenge", - }, - { - d: "should fail because although force is enabled and a challenge was given, plain is disabled and method is empty", - force: true, - expectErr: true, - challenge: "challenge", - }, - { - d: "should fail because invalid challenge method", - force: true, - expectErr: true, - method: "invalid", - challenge: "challenge", - }, - { - d: "should pass because force is enabled with challenge given and method is S256", - force: true, - method: "S256", - challenge: "challenge", - }, - { - d: "should pass because forcePublic is enabled with challenge given and method is S256", - forcePublic: true, - client: &fosite.DefaultClient{Public: true}, - method: "S256", - challenge: "challenge", - }, - } { - t.Run(fmt.Sprintf("case=%d/description=%s", k, tc.d), func(t *testing.T) { - h := &Handler{ - Config: &fosite.Config{ - EnforcePKCE: tc.force, - EnforcePKCEForPublicClients: tc.forcePublic, - EnablePKCEPlainChallengeMethod: tc.enablePlain, - }, - } - - if tc.expectErr { - assert.Error(t, h.validate(context.Background(), tc.challenge, tc.method, tc.client)) - } else { - assert.NoError(t, h.validate(context.Background(), tc.challenge, tc.method, tc.client)) - } - }) - } -} diff --git a/handler/pkce/handler_test.go b/handler/pkce/handler_test.go index 774d90196..68c42b438 100644 --- a/handler/pkce/handler_test.go +++ b/handler/pkce/handler_test.go @@ -251,7 +251,6 @@ func TestPKCEHandlerValidate(t *testing.T) { r.Client = tc.client r.GrantTypes = fosite.Arguments{tc.grant} r.Form.Add("code_verifier", tc.verifier) - r.Form.Add("code", tc.code) if tc.expectErr == nil { require.NoError(t, h.HandleTokenEndpointRequest(context.Background(), r)) } else { From 7ad38513fc5f99ce086bc7a4d89511182339228e Mon Sep 17 00:00:00 2001 From: Romain Caire Date: Tue, 5 Dec 2023 14:27:21 +0100 Subject: [PATCH 45/49] Rename DeviceAuthorizeXXX to DeviceUserXXX --- compose/compose.go | 4 +- compose/compose_rfc8628.go | 2 +- config.go | 10 +- config_default.go | 8 +- device_authorize_request.go | 24 --- device_authorize_response.go | 22 --- device_authorize_response_writer.go | 21 --- device_request_handler_test.go | 4 +- device_response_writer_test.go | 2 +- device_user_request.go | 24 +++ ...ndler.go => device_user_request_handler.go | 10 +- ....go => device_user_request_handler_test.go | 12 +- ...est_test.go => device_user_request_test.go | 14 +- device_user_response.go | 22 +++ device_user_response_writer.go | 21 +++ ...thorize_writer.go => device_user_writer.go | 6 +- device_write_test.go | 2 +- fosite.go | 12 +- generate-mocks.sh | 6 +- generate.go | 6 +- handler.go | 8 +- handler/openid/flow_device_auth.go | 2 +- handler/openid/validator_test.go | 2 +- handler/rfc8628/token_endpoint_handler.go | 18 +-- .../rfc8628/token_endpoint_handler_test.go | 32 ++-- internal/device_authorize_handler.go | 36 ++--- internal/device_authorize_request.go | 144 +++++++++--------- internal/device_authorize_response.go | 36 ++--- oauth2.go | 18 +-- 29 files changed, 264 insertions(+), 264 deletions(-) delete mode 100644 device_authorize_request.go delete mode 100644 device_authorize_response.go delete mode 100644 device_authorize_response_writer.go create mode 100644 device_user_request.go rename device_authorize_request_handler.go => device_user_request_handler.go (76%) rename device_authorize_request_handler_test.go => device_user_request_handler_test.go (93%) rename device_authorize_request_test.go => device_user_request_test.go (83%) create mode 100644 device_user_response.go create mode 100644 device_user_response_writer.go rename device_authorize_writer.go => device_user_writer.go (80%) diff --git a/compose/compose.go b/compose/compose.go index 94cf44e89..c79553aea 100644 --- a/compose/compose.go +++ b/compose/compose.go @@ -56,8 +56,8 @@ func Compose(config *fosite.Config, storage interface{}, strategy interface{}, f if dh, ok := res.(fosite.DeviceEndpointHandler); ok { config.DeviceEndpointHandlers.Append(dh) } - if dah, ok := res.(fosite.DeviceAuthorizeEndpointHandler); ok { - config.DeviceAuthorizeEndpointHandlers.Append(dah) + if dah, ok := res.(fosite.DeviceUserEndpointHandler); ok { + config.DeviceUserEndpointHandlers.Append(dah) } } diff --git a/compose/compose_rfc8628.go b/compose/compose_rfc8628.go index b3574f0aa..37b8503fd 100644 --- a/compose/compose_rfc8628.go +++ b/compose/compose_rfc8628.go @@ -24,7 +24,7 @@ func RFC8628DeviceFactory(config fosite.Configurator, storage interface{}, strat func RFC8628DeviceAuthorizationTokenFactory(config fosite.Configurator, storage interface{}, strategy interface{}) interface{} { return &rfc8628.DeviceCodeTokenEndpointHandler{ GenericCodeTokenEndpointHandler: oauth2.GenericCodeTokenEndpointHandler{ - CodeTokenEndpointHandler: &rfc8628.DeviceAuthorizeHandler{ + CodeTokenEndpointHandler: &rfc8628.DeviceUserHandler{ DeviceStrategy: strategy.(rfc8628.DeviceCodeStrategy), DeviceStorage: storage.(rfc8628.DeviceCodeStorage), }, diff --git a/config.go b/config.go index 36dce6a52..cb027c182 100644 --- a/config.go +++ b/config.go @@ -26,7 +26,7 @@ type DeviceAndUserCodeLifespanProvider interface { GetDeviceAndUserCodeLifespan(ctx context.Context) time.Duration } -type DeviceAuthorizeProvider interface { +type DeviceUserProvider interface { GetDeviceDone(ctx context.Context) string } @@ -300,10 +300,10 @@ type DeviceEndpointHandlersProvider interface { GetDeviceEndpointHandlers(ctx context.Context) DeviceEndpointHandlers } -// DeviceAuthorizeEndpointHandlersProvider returns the provider for setting up the Device Authorize handlers. -type DeviceAuthorizeEndpointHandlersProvider interface { - // GetDeviceAuthorizeEndpointHandlers returns the handlers. - GetDeviceAuthorizeEndpointHandlers(ctx context.Context) DeviceAuthorizeEndpointHandlers +// 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. diff --git a/config_default.go b/config_default.go index 54b9b860b..2d81cc571 100644 --- a/config_default.go +++ b/config_default.go @@ -213,8 +213,8 @@ type Config struct { // DeviceEndpointHandlers is a list of handlers that are called before the device endpoint is served. DeviceEndpointHandlers DeviceEndpointHandlers - // DeviceAuthorizeEndpointHandlers is a list of handlers that are called before the device authorize endpoint is served. - DeviceAuthorizeEndpointHandlers DeviceAuthorizeEndpointHandlers + // 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 @@ -268,8 +268,8 @@ func (c *Config) GetDeviceEndpointHandlers(ctx context.Context) DeviceEndpointHa return c.DeviceEndpointHandlers } -func (c *Config) GetDeviceAuthorizeEndpointHandlers(ctx context.Context) DeviceAuthorizeEndpointHandlers { - return c.DeviceAuthorizeEndpointHandlers +func (c *Config) GetDeviceUserEndpointHandlers(ctx context.Context) DeviceUserEndpointHandlers { + return c.DeviceUserEndpointHandlers } func (c *Config) GetRevocationHandlers(ctx context.Context) RevocationHandlers { diff --git a/device_authorize_request.go b/device_authorize_request.go deleted file mode 100644 index d3802d174..000000000 --- a/device_authorize_request.go +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright © 2023 Ory Corp -// SPDX-License-Identifier: Apache-2.0 - -package fosite - -// DeviceAuthorizeRequest is an implementation of DeviceAuthorizeRequester -type DeviceAuthorizeRequest struct { - signature string - Request -} - -func (d *DeviceAuthorizeRequest) GetDeviceCodeSignature() string { - return d.signature -} - -func (d *DeviceAuthorizeRequest) SetDeviceCodeSignature(signature string) { - d.signature = signature -} - -func NewDeviceAuthorizeRequest() *DeviceAuthorizeRequest { - return &DeviceAuthorizeRequest{ - Request: *NewRequest(), - } -} diff --git a/device_authorize_response.go b/device_authorize_response.go deleted file mode 100644 index 58bd44886..000000000 --- a/device_authorize_response.go +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright © 2023 Ory Corp -// SPDX-License-Identifier: Apache-2.0 - -package fosite - -import "net/http" - -type DeviceAuthorizeResponse struct { - Header http.Header -} - -func NewDeviceAuthorizeResponse() *DeviceAuthorizeResponse { - return &DeviceAuthorizeResponse{} -} - -func (a *DeviceAuthorizeResponse) GetHeader() http.Header { - return a.Header -} - -func (a *DeviceAuthorizeResponse) AddHeader(key, value string) { - a.Header.Add(key, value) -} diff --git a/device_authorize_response_writer.go b/device_authorize_response_writer.go deleted file mode 100644 index 954d66f7d..000000000 --- a/device_authorize_response_writer.go +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright © 2023 Ory Corp -// SPDX-License-Identifier: Apache-2.0 - -package fosite - -import ( - "context" -) - -func (f *Fosite) NewDeviceAuthorizeResponse(ctx context.Context, dar DeviceAuthorizeRequester, session Session) (DeviceAuthorizeResponder, error) { - var resp = &DeviceAuthorizeResponse{} - - dar.SetSession(session) - for _, h := range f.Config.GetDeviceAuthorizeEndpointHandlers(ctx) { - if err := h.HandleDeviceAuthorizeEndpointRequest(ctx, dar, resp); err != nil { - return nil, err - } - } - - return resp, nil -} diff --git a/device_request_handler_test.go b/device_request_handler_test.go index e1dd52392..6d6f69fe4 100644 --- a/device_request_handler_test.go +++ b/device_request_handler_test.go @@ -28,7 +28,7 @@ func TestNewDeviceRequest(t *testing.T) { query url.Values expectedError error mock func() - expect *DeviceAuthorizeRequest + expect *DeviceUserRequest }{ /* empty request */ { @@ -88,7 +88,7 @@ func TestNewDeviceRequest(t *testing.T) { GrantTypes: []string{"urn:ietf:params:oauth:grant-type:device_code"}, }, nil) }, - expect: &DeviceAuthorizeRequest{ + expect: &DeviceUserRequest{ Request: Request{ Client: &DefaultClient{ Scopes: []string{"foo", "bar"}, diff --git a/device_response_writer_test.go b/device_response_writer_test.go index 6f81afcbb..88c23222f 100644 --- a/device_response_writer_test.go +++ b/device_response_writer_test.go @@ -18,7 +18,7 @@ import ( func TestNewDeviceResponse(t *testing.T) { ctrl := gomock.NewController(t) handlers := []*MockDeviceEndpointHandler{NewMockDeviceEndpointHandler(ctrl)} - dar := NewMockDeviceAuthorizeRequester(ctrl) + dar := NewMockDeviceUserRequester(ctrl) defer ctrl.Finish() ctx := context.Background() 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_authorize_request_handler.go b/device_user_request_handler.go similarity index 76% rename from device_authorize_request_handler.go rename to device_user_request_handler.go index 715579abd..9367de5b4 100644 --- a/device_authorize_request_handler.go +++ b/device_user_request_handler.go @@ -13,15 +13,15 @@ import ( "go.opentelemetry.io/otel/trace" ) -func (f *Fosite) NewDeviceAuthorizeRequest(ctx context.Context, r *http.Request) (_ DeviceAuthorizeRequester, err error) { - ctx, span := trace.SpanFromContext(ctx).TracerProvider().Tracer("github.com/ory/fosite").Start(ctx, "Fosite.NewDeviceAuthorizeRequest") +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.newDeviceAuthorizeRequest(ctx, r) + return f.newDeviceUserRequest(ctx, r) } -func (f *Fosite) newDeviceAuthorizeRequest(ctx context.Context, r *http.Request) (DeviceAuthorizeRequester, error) { - request := NewDeviceAuthorizeRequest() +func (f *Fosite) newDeviceUserRequest(ctx context.Context, r *http.Request) (DeviceUserRequester, error) { + request := NewDeviceUserRequest() request.Lang = i18n.GetLangFromRequest(f.Config.GetMessageCatalog(ctx), r) if err := r.ParseForm(); err != nil { diff --git a/device_authorize_request_handler_test.go b/device_user_request_handler_test.go similarity index 93% rename from device_authorize_request_handler_test.go rename to device_user_request_handler_test.go index 8922fd717..df937a16c 100644 --- a/device_authorize_request_handler_test.go +++ b/device_user_request_handler_test.go @@ -21,7 +21,7 @@ import ( . "github.com/ory/fosite/internal" ) -func TestNewDeviceAuthorizeRequest(t *testing.T) { +func TestNewDeviceUserRequest(t *testing.T) { var store *MockStorage for k, c := range []struct { desc string @@ -31,7 +31,7 @@ func TestNewDeviceAuthorizeRequest(t *testing.T) { form url.Values expectedError error mock func() - expect *DeviceAuthorizeRequest + expect *DeviceUserRequest }{ /* invalid client */ { @@ -49,7 +49,7 @@ func TestNewDeviceAuthorizeRequest(t *testing.T) { conf: &Fosite{Store: store, Config: &Config{ScopeStrategy: ExactScopeStrategy, AudienceMatchingStrategy: DefaultAudienceMatchingStrategy}}, r: &http.Request{}, mock: func() {}, - expect: &DeviceAuthorizeRequest{ + expect: &DeviceUserRequest{ Request: Request{}, }, }, @@ -65,7 +65,7 @@ func TestNewDeviceAuthorizeRequest(t *testing.T) { GrantTypes: []string{"urn:ietf:params:oauth:grant-type:device_code"}, }, nil) }, - expect: &DeviceAuthorizeRequest{ + expect: &DeviceUserRequest{ Request: Request{ Client: &DefaultClient{ GrantTypes: []string{"urn:ietf:params:oauth:grant-type:device_code"}, @@ -85,7 +85,7 @@ func TestNewDeviceAuthorizeRequest(t *testing.T) { GrantTypes: []string{"urn:ietf:params:oauth:grant-type:device_code"}, }, nil) }, - expect: &DeviceAuthorizeRequest{ + expect: &DeviceUserRequest{ Request: Request{ Client: &DefaultClient{ GrantTypes: []string{"urn:ietf:params:oauth:grant-type:device_code"}, @@ -125,7 +125,7 @@ func TestNewDeviceAuthorizeRequest(t *testing.T) { } c.conf.Store = store - ar, err := c.conf.NewDeviceAuthorizeRequest(context.Background(), c.r) + ar, err := c.conf.NewDeviceUserRequest(context.Background(), c.r) if c.expectedError != nil { assert.EqualError(t, err, c.expectedError.Error()) } else { diff --git a/device_authorize_request_test.go b/device_user_request_test.go similarity index 83% rename from device_authorize_request_test.go rename to device_user_request_test.go index 34e28b36c..473c1a561 100644 --- a/device_authorize_request_test.go +++ b/device_user_request_test.go @@ -10,25 +10,25 @@ import ( "github.com/stretchr/testify/assert" ) -func TestDeviceAuthorizeRequest(t *testing.T) { +func TestDeviceUserRequest(t *testing.T) { for k, c := range []struct { - ar *DeviceAuthorizeRequest + ar *DeviceUserRequest }{ { - ar: NewDeviceAuthorizeRequest(), + ar: NewDeviceUserRequest(), }, { - ar: &DeviceAuthorizeRequest{}, + ar: &DeviceUserRequest{}, }, { - ar: &DeviceAuthorizeRequest{ + ar: &DeviceUserRequest{ Request: Request{ Client: &DefaultClient{RedirectURIs: []string{""}}, }, }, }, { - ar: &DeviceAuthorizeRequest{ + ar: &DeviceUserRequest{ signature: "AAAA", Request: Request{ Client: &DefaultClient{RedirectURIs: []string{""}}, @@ -36,7 +36,7 @@ func TestDeviceAuthorizeRequest(t *testing.T) { }, }, { - ar: &DeviceAuthorizeRequest{ + ar: &DeviceUserRequest{ Request: Request{ Client: &DefaultClient{RedirectURIs: []string{"https://foobar.com/cb"}}, RequestedAt: time.Now().UTC(), 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..cd473c441 --- /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, dar DeviceUserRequester, session Session) (DeviceUserResponder, error) { + var resp = &DeviceUserResponse{} + + dar.SetSession(session) + for _, h := range f.Config.GetDeviceUserEndpointHandlers(ctx) { + if err := h.HandleDeviceUserEndpointRequest(ctx, dar, resp); err != nil { + return nil, err + } + } + + return resp, nil +} diff --git a/device_authorize_writer.go b/device_user_writer.go similarity index 80% rename from device_authorize_writer.go rename to device_user_writer.go index 474235ef3..0c75cfc0e 100644 --- a/device_authorize_writer.go +++ b/device_user_writer.go @@ -29,9 +29,9 @@ import ( "net/http" ) -// Once the user has approved the grant he will be redirected on his loggin machine +// 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 and return to his non-interactive device; -func (f *Fosite) WriteDeviceAuthorizeResponse(ctx context.Context, r *http.Request, rw http.ResponseWriter, requester DeviceAuthorizeRequester, responder DeviceAuthorizeResponder) { +// 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_test.go b/device_write_test.go index d4e9437df..0ed418cb7 100644 --- a/device_write_test.go +++ b/device_write_test.go @@ -15,7 +15,7 @@ import ( . "github.com/ory/fosite" ) -func TestWriteDeviceAuthorizeResponse(t *testing.T) { +func TestWriteDeviceUserResponse(t *testing.T) { oauth2 := &Fosite{Config: &Config{ DeviceAndUserCodeLifespan: time.Minute, DeviceAuthTokenPollingInterval: time.Minute, diff --git a/fosite.go b/fosite.go index 4d709443d..e5d30fb77 100644 --- a/fosite.go +++ b/fosite.go @@ -96,11 +96,11 @@ func (a *DeviceEndpointHandlers) Append(h DeviceEndpointHandler) { *a = append(*a, h) } -// DeviceAuthorizeEndpointHandlers is a list of DeviceAuthorizeEndpointHandler -type DeviceAuthorizeEndpointHandlers []DeviceAuthorizeEndpointHandler +// DeviceUserEndpointHandlers is a list of DeviceUserEndpointHandler +type DeviceUserEndpointHandlers []DeviceUserEndpointHandler -// Append adds an DeviceAuthorizeEndpointHandlers to this list. Ignores duplicates based on reflect.TypeOf. -func (a *DeviceAuthorizeEndpointHandlers) Append(h DeviceAuthorizeEndpointHandler) { +// 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 @@ -162,9 +162,9 @@ type Configurator interface { RevocationHandlersProvider UseLegacyErrorFormatProvider DeviceEndpointHandlersProvider - DeviceAuthorizeEndpointHandlersProvider + DeviceUserEndpointHandlersProvider DeviceProvider - DeviceAuthorizeProvider + DeviceUserProvider } func NewOAuth2Provider(s Storage, c Configurator) *Fosite { diff --git a/generate-mocks.sh b/generate-mocks.sh index c0ca047d5..c56a5b781 100755 --- a/generate-mocks.sh +++ b/generate-mocks.sh @@ -23,7 +23,7 @@ mockgen -package internal -destination internal/id_token_strategy.go github.com/ 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_authorize_handler.go github.com/ory/fosite DeviceAuthorizeEndpointHandler +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 @@ -33,8 +33,8 @@ 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_authorize_request.go github.com/ory/fosite DeviceAuthorizeRequester -mockgen -package internal -destination internal/device_authorize_response.go github.com/ory/fosite DeviceAuthorizeResponder +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 diff --git a/generate.go b/generate.go index f11b2ab64..43e211ce8 100644 --- a/generate.go +++ b/generate.go @@ -25,7 +25,7 @@ package fosite //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_authorize_handler.go github.com/ory/fosite DeviceAuthorizeEndpointHandler +//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 @@ -35,7 +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_authorize_request.go github.com/ory/fosite DeviceAuthorizeRequester -//go:generate go run github.com/golang/mock/mockgen -package internal -destination internal/device_authorize_response.go github.com/ory/fosite DeviceAuthorizeResponder +//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/handler.go b/handler.go index f4ebd19b5..d0a91497f 100644 --- a/handler.go +++ b/handler.go @@ -72,15 +72,15 @@ type DeviceEndpointHandler interface { // 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 HandleDeviceAuthorizeRequest should do. + // 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 DeviceAuthorizeEndpointHandler interface { - // HandleDeviceAuthorizeEndpointRequest handles a device authorize endpoint request. +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. - HandleDeviceAuthorizeEndpointRequest(ctx context.Context, requester DeviceAuthorizeRequester, responder DeviceAuthorizeResponder) error + HandleDeviceUserEndpointRequest(ctx context.Context, requester DeviceUserRequester, responder DeviceUserResponder) error } diff --git a/handler/openid/flow_device_auth.go b/handler/openid/flow_device_auth.go index 0c063eb19..56c4b0acc 100644 --- a/handler/openid/flow_device_auth.go +++ b/handler/openid/flow_device_auth.go @@ -22,7 +22,7 @@ type OpenIDConnectDeviceHandler struct { *IDTokenHandleHelper } -func (c *OpenIDConnectDeviceHandler) HandleDeviceAuthorizeEndpointRequest(ctx context.Context, ar fosite.DeviceAuthorizeRequester, resp fosite.DeviceAuthorizeResponder) error { +func (c *OpenIDConnectDeviceHandler) HandleDeviceUserEndpointRequest(ctx context.Context, ar fosite.DeviceUserRequester, resp fosite.DeviceUserResponder) error { if !(ar.GetGrantedScopes().Has("openid")) { return nil } diff --git a/handler/openid/validator_test.go b/handler/openid/validator_test.go index e571bd3d9..3ae72a026 100644 --- a/handler/openid/validator_test.go +++ b/handler/openid/validator_test.go @@ -498,7 +498,7 @@ func TestDeviceValidatePrompt(t *testing.T) { } { 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.DeviceAuthorizeRequest{ + 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}, diff --git a/handler/rfc8628/token_endpoint_handler.go b/handler/rfc8628/token_endpoint_handler.go index b916c5344..fc1f71a1b 100644 --- a/handler/rfc8628/token_endpoint_handler.go +++ b/handler/rfc8628/token_endpoint_handler.go @@ -12,9 +12,9 @@ import ( "github.com/ory/fosite" ) -// DeviceAuthorizeHandler is a response handler for the Device Code introduced in the Device Authorize Grant +// DeviceUserHandler is a response handler for the Device Code introduced in the Device Authorize Grant // as defined in https://www.rfc-editor.org/rfc/rfc8628 -type DeviceAuthorizeHandler struct { +type DeviceUserHandler struct { DeviceStrategy DeviceCodeStrategy DeviceStorage DeviceCodeStorage } @@ -23,10 +23,10 @@ type DeviceCodeTokenEndpointHandler struct { oauth2.GenericCodeTokenEndpointHandler } -var _ oauth2.CodeTokenEndpointHandler = (*DeviceAuthorizeHandler)(nil) +var _ oauth2.CodeTokenEndpointHandler = (*DeviceUserHandler)(nil) var _ fosite.TokenEndpointHandler = (*DeviceCodeTokenEndpointHandler)(nil) -func (c *DeviceAuthorizeHandler) ValidateGrantTypes(ctx context.Context, requester fosite.AccessRequester) error { +func (c *DeviceUserHandler) 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\".")) } @@ -34,11 +34,11 @@ func (c *DeviceAuthorizeHandler) ValidateGrantTypes(ctx context.Context, request return nil } -func (c *DeviceAuthorizeHandler) ValidateCode(ctx context.Context, request fosite.AccessRequester, code string) error { +func (c *DeviceUserHandler) ValidateCode(ctx context.Context, request fosite.AccessRequester, code string) error { return c.DeviceStrategy.ValidateDeviceCode(ctx, request, code) } -func (c *DeviceAuthorizeHandler) GetCodeAndSession(ctx context.Context, requester fosite.AccessRequester) (code string, signature string, request fosite.Requester, err error) { +func (c *DeviceUserHandler) GetCodeAndSession(ctx context.Context, requester fosite.AccessRequester) (code string, signature string, request fosite.Requester, err error) { code = requester.GetRequestForm().Get("device_code") signature, err = c.DeviceStrategy.DeviceCodeSignature(ctx, code) if err != nil { @@ -48,15 +48,15 @@ func (c *DeviceAuthorizeHandler) GetCodeAndSession(ctx context.Context, requeste return code, signature, req, err } -func (c *DeviceAuthorizeHandler) InvalidateSession(ctx context.Context, signature string) error { +func (c *DeviceUserHandler) InvalidateSession(ctx context.Context, signature string) error { return c.DeviceStorage.InvalidateDeviceCodeSession(ctx, signature) } // implement TokenEndpointHandler -func (c *DeviceAuthorizeHandler) CanSkipClientAuth(ctx context.Context, requester fosite.AccessRequester) bool { +func (c *DeviceUserHandler) CanSkipClientAuth(ctx context.Context, requester fosite.AccessRequester) bool { return requester.GetGrantTypes().ExactOne(string(fosite.GrantTypeDeviceCode)) } -func (c *DeviceAuthorizeHandler) CanHandleTokenEndpointRequest(ctx context.Context, requester fosite.AccessRequester) bool { +func (c *DeviceUserHandler) 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 index 26f5c0c5a..9a9ed6686 100644 --- a/handler/rfc8628/token_endpoint_handler_test.go +++ b/handler/rfc8628/token_endpoint_handler_test.go @@ -40,7 +40,7 @@ var RFC8628HMACSHAStrategy = DefaultDeviceStrategy{ }, } -func TestDeviceAuthorizeCode_PopulateTokenEndpointResponse(t *testing.T) { +func TestDeviceUserCode_PopulateTokenEndpointResponse(t *testing.T) { for k, strategy := range map[string]struct { oauth2.CoreStrategy RFC8628CodeStrategy @@ -212,7 +212,7 @@ func TestDeviceAuthorizeCode_PopulateTokenEndpointResponse(t *testing.T) { RefreshTokenScopes: []string{"offline"}, } h = oauth2.GenericCodeTokenEndpointHandler{ - CodeTokenEndpointHandler: &DeviceAuthorizeHandler{ + CodeTokenEndpointHandler: &DeviceUserHandler{ DeviceStrategy: strategy, DeviceStorage: store, }, @@ -245,7 +245,7 @@ func TestDeviceAuthorizeCode_PopulateTokenEndpointResponse(t *testing.T) { } } -func TestDeviceAuthorizeCode_HandleTokenEndpointRequest(t *testing.T) { +func TestDeviceUserCode_HandleTokenEndpointRequest(t *testing.T) { for k, strategy := range map[string]struct { oauth2.CoreStrategy RFC8628CodeStrategy @@ -256,7 +256,7 @@ func TestDeviceAuthorizeCode_HandleTokenEndpointRequest(t *testing.T) { store := storage.NewMemoryStore() h := oauth2.GenericCodeTokenEndpointHandler{ - CodeTokenEndpointHandler: &DeviceAuthorizeHandler{ + CodeTokenEndpointHandler: &DeviceUserHandler{ DeviceStrategy: strategy.RFC8628CodeStrategy, DeviceStorage: store, }, @@ -271,10 +271,10 @@ func TestDeviceAuthorizeCode_HandleTokenEndpointRequest(t *testing.T) { } for i, c := range []struct { areq *fosite.AccessRequest - authreq *fosite.DeviceAuthorizeRequest + authreq *fosite.DeviceUserRequest description string - setup func(t *testing.T, areq *fosite.AccessRequest, authreq *fosite.DeviceAuthorizeRequest) - check func(t *testing.T, areq *fosite.AccessRequest, authreq *fosite.DeviceAuthorizeRequest) + setup func(t *testing.T, areq *fosite.AccessRequest, authreq *fosite.DeviceUserRequest) + check func(t *testing.T, areq *fosite.AccessRequest, authreq *fosite.DeviceUserRequest) expectErr error }{ { @@ -306,7 +306,7 @@ func TestDeviceAuthorizeCode_HandleTokenEndpointRequest(t *testing.T) { }, }, description: "should fail because device code could not be retrieved", - setup: func(t *testing.T, areq *fosite.AccessRequest, authreq *fosite.DeviceAuthorizeRequest) { + 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}} @@ -335,14 +335,14 @@ func TestDeviceAuthorizeCode_HandleTokenEndpointRequest(t *testing.T) { RequestedAt: time.Now().UTC(), }, }, - authreq: &fosite.DeviceAuthorizeRequest{ + 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.DeviceAuthorizeRequest) { + 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}} @@ -360,7 +360,7 @@ func TestDeviceAuthorizeCode_HandleTokenEndpointRequest(t *testing.T) { RequestedAt: time.Now().UTC(), }, }, - authreq: &fosite.DeviceAuthorizeRequest{ + authreq: &fosite.DeviceUserRequest{ Request: fosite.Request{ Client: &fosite.DefaultClient{ID: "foo", GrantTypes: []string{"urn:ietf:params:oauth:grant-type:device_code"}}, Session: &fosite.DefaultSession{}, @@ -369,7 +369,7 @@ func TestDeviceAuthorizeCode_HandleTokenEndpointRequest(t *testing.T) { }, }, description: "should pass", - setup: func(t *testing.T, areq *fosite.AccessRequest, authreq *fosite.DeviceAuthorizeRequest) { + setup: func(t *testing.T, areq *fosite.AccessRequest, authreq *fosite.DeviceUserRequest) { token, signature, err := strategy.GenerateDeviceCode(context.TODO()) require.NoError(t, err) @@ -390,11 +390,11 @@ func TestDeviceAuthorizeCode_HandleTokenEndpointRequest(t *testing.T) { RequestedAt: time.Now().UTC(), }, }, - check: func(t *testing.T, areq *fosite.AccessRequest, authreq *fosite.DeviceAuthorizeRequest) { + 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.DeviceAuthorizeRequest) { + 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) @@ -427,7 +427,7 @@ func TestDeviceAuthorizeCode_HandleTokenEndpointRequest(t *testing.T) { } } -func TestDeviceAuthorizeCodeTransactional_HandleTokenEndpointRequest(t *testing.T) { +func TestDeviceUserCodeTransactional_HandleTokenEndpointRequest(t *testing.T) { var mockTransactional *internal.MockTransactional var mockCoreStore *internal.MockCoreStorage var mockDeviceStore *internal.MockRFC8628CodeStorage @@ -646,7 +646,7 @@ func TestDeviceAuthorizeCodeTransactional_HandleTokenEndpointRequest(t *testing. testCase.setup() handler := oauth2.GenericCodeTokenEndpointHandler{ - CodeTokenEndpointHandler: &DeviceAuthorizeHandler{ + CodeTokenEndpointHandler: &DeviceUserHandler{ DeviceStrategy: &deviceStrategy, DeviceStorage: deviceTransactionalStore{ mockTransactional, diff --git a/internal/device_authorize_handler.go b/internal/device_authorize_handler.go index f7f3e5f87..ebe2ad150 100644 --- a/internal/device_authorize_handler.go +++ b/internal/device_authorize_handler.go @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/ory/fosite (interfaces: DeviceAuthorizeEndpointHandler) +// Source: github.com/ory/fosite (interfaces: DeviceUserEndpointHandler) // Package internal is a generated GoMock package. package internal @@ -15,39 +15,39 @@ import ( fosite "github.com/ory/fosite" ) -// MockDeviceAuthorizeEndpointHandler is a mock of DeviceAuthorizeEndpointHandler interface. -type MockDeviceAuthorizeEndpointHandler struct { +// MockDeviceUserEndpointHandler is a mock of DeviceUserEndpointHandler interface. +type MockDeviceUserEndpointHandler struct { ctrl *gomock.Controller - recorder *MockDeviceAuthorizeEndpointHandlerMockRecorder + recorder *MockDeviceUserEndpointHandlerMockRecorder } -// MockDeviceAuthorizeEndpointHandlerMockRecorder is the mock recorder for MockDeviceAuthorizeEndpointHandler. -type MockDeviceAuthorizeEndpointHandlerMockRecorder struct { - mock *MockDeviceAuthorizeEndpointHandler +// MockDeviceUserEndpointHandlerMockRecorder is the mock recorder for MockDeviceUserEndpointHandler. +type MockDeviceUserEndpointHandlerMockRecorder struct { + mock *MockDeviceUserEndpointHandler } -// NewMockDeviceAuthorizeEndpointHandler creates a new mock instance. -func NewMockDeviceAuthorizeEndpointHandler(ctrl *gomock.Controller) *MockDeviceAuthorizeEndpointHandler { - mock := &MockDeviceAuthorizeEndpointHandler{ctrl: ctrl} - mock.recorder = &MockDeviceAuthorizeEndpointHandlerMockRecorder{mock} +// 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 *MockDeviceAuthorizeEndpointHandler) EXPECT() *MockDeviceAuthorizeEndpointHandlerMockRecorder { +func (m *MockDeviceUserEndpointHandler) EXPECT() *MockDeviceUserEndpointHandlerMockRecorder { return m.recorder } -// HandleDeviceAuthorizeEndpointRequest mocks base method. -func (m *MockDeviceAuthorizeEndpointHandler) HandleDeviceAuthorizeEndpointRequest(arg0 context.Context, arg1 fosite.DeviceAuthorizeRequester, arg2 fosite.DeviceAuthorizeResponder) error { +// 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, "HandleDeviceAuthorizeEndpointRequest", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "HandleDeviceUserEndpointRequest", arg0, arg1, arg2) ret0, _ := ret[0].(error) return ret0 } -// HandleDeviceAuthorizeEndpointRequest indicates an expected call of HandleDeviceAuthorizeEndpointRequest. -func (mr *MockDeviceAuthorizeEndpointHandlerMockRecorder) HandleDeviceAuthorizeEndpointRequest(arg0, arg1, arg2 interface{}) *gomock.Call { +// 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, "HandleDeviceAuthorizeEndpointRequest", reflect.TypeOf((*MockDeviceAuthorizeEndpointHandler)(nil).HandleDeviceAuthorizeEndpointRequest), arg0, arg1, arg2) + 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 index 33e318a1d..ce01302de 100644 --- a/internal/device_authorize_request.go +++ b/internal/device_authorize_request.go @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/ory/fosite (interfaces: DeviceAuthorizeRequester) +// Source: github.com/ory/fosite (interfaces: DeviceUserRequester) // Package internal is a generated GoMock package. package internal @@ -16,43 +16,43 @@ import ( fosite "github.com/ory/fosite" ) -// MockDeviceAuthorizeRequester is a mock of DeviceAuthorizeRequester interface. -type MockDeviceAuthorizeRequester struct { +// MockDeviceUserRequester is a mock of DeviceUserRequester interface. +type MockDeviceUserRequester struct { ctrl *gomock.Controller - recorder *MockDeviceAuthorizeRequesterMockRecorder + recorder *MockDeviceUserRequesterMockRecorder } -// MockDeviceAuthorizeRequesterMockRecorder is the mock recorder for MockDeviceAuthorizeRequester. -type MockDeviceAuthorizeRequesterMockRecorder struct { - mock *MockDeviceAuthorizeRequester +// MockDeviceUserRequesterMockRecorder is the mock recorder for MockDeviceUserRequester. +type MockDeviceUserRequesterMockRecorder struct { + mock *MockDeviceUserRequester } -// NewMockDeviceAuthorizeRequester creates a new mock instance. -func NewMockDeviceAuthorizeRequester(ctrl *gomock.Controller) *MockDeviceAuthorizeRequester { - mock := &MockDeviceAuthorizeRequester{ctrl: ctrl} - mock.recorder = &MockDeviceAuthorizeRequesterMockRecorder{mock} +// 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 *MockDeviceAuthorizeRequester) EXPECT() *MockDeviceAuthorizeRequesterMockRecorder { +func (m *MockDeviceUserRequester) EXPECT() *MockDeviceUserRequesterMockRecorder { return m.recorder } // AppendRequestedScope mocks base method. -func (m *MockDeviceAuthorizeRequester) AppendRequestedScope(arg0 string) { +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 *MockDeviceAuthorizeRequesterMockRecorder) AppendRequestedScope(arg0 interface{}) *gomock.Call { +func (mr *MockDeviceUserRequesterMockRecorder) AppendRequestedScope(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AppendRequestedScope", reflect.TypeOf((*MockDeviceAuthorizeRequester)(nil).AppendRequestedScope), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AppendRequestedScope", reflect.TypeOf((*MockDeviceUserRequester)(nil).AppendRequestedScope), arg0) } // GetClient mocks base method. -func (m *MockDeviceAuthorizeRequester) GetClient() fosite.Client { +func (m *MockDeviceUserRequester) GetClient() fosite.Client { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetClient") ret0, _ := ret[0].(fosite.Client) @@ -60,13 +60,13 @@ func (m *MockDeviceAuthorizeRequester) GetClient() fosite.Client { } // GetClient indicates an expected call of GetClient. -func (mr *MockDeviceAuthorizeRequesterMockRecorder) GetClient() *gomock.Call { +func (mr *MockDeviceUserRequesterMockRecorder) GetClient() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClient", reflect.TypeOf((*MockDeviceAuthorizeRequester)(nil).GetClient)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClient", reflect.TypeOf((*MockDeviceUserRequester)(nil).GetClient)) } // GetDeviceCodeSignature mocks base method. -func (m *MockDeviceAuthorizeRequester) GetDeviceCodeSignature() string { +func (m *MockDeviceUserRequester) GetDeviceCodeSignature() string { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetDeviceCodeSignature") ret0, _ := ret[0].(string) @@ -74,13 +74,13 @@ func (m *MockDeviceAuthorizeRequester) GetDeviceCodeSignature() string { } // GetDeviceCodeSignature indicates an expected call of GetDeviceCodeSignature. -func (mr *MockDeviceAuthorizeRequesterMockRecorder) GetDeviceCodeSignature() *gomock.Call { +func (mr *MockDeviceUserRequesterMockRecorder) GetDeviceCodeSignature() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDeviceCodeSignature", reflect.TypeOf((*MockDeviceAuthorizeRequester)(nil).GetDeviceCodeSignature)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDeviceCodeSignature", reflect.TypeOf((*MockDeviceUserRequester)(nil).GetDeviceCodeSignature)) } // GetGrantedAudience mocks base method. -func (m *MockDeviceAuthorizeRequester) GetGrantedAudience() fosite.Arguments { +func (m *MockDeviceUserRequester) GetGrantedAudience() fosite.Arguments { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetGrantedAudience") ret0, _ := ret[0].(fosite.Arguments) @@ -88,13 +88,13 @@ func (m *MockDeviceAuthorizeRequester) GetGrantedAudience() fosite.Arguments { } // GetGrantedAudience indicates an expected call of GetGrantedAudience. -func (mr *MockDeviceAuthorizeRequesterMockRecorder) GetGrantedAudience() *gomock.Call { +func (mr *MockDeviceUserRequesterMockRecorder) GetGrantedAudience() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGrantedAudience", reflect.TypeOf((*MockDeviceAuthorizeRequester)(nil).GetGrantedAudience)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGrantedAudience", reflect.TypeOf((*MockDeviceUserRequester)(nil).GetGrantedAudience)) } // GetGrantedScopes mocks base method. -func (m *MockDeviceAuthorizeRequester) GetGrantedScopes() fosite.Arguments { +func (m *MockDeviceUserRequester) GetGrantedScopes() fosite.Arguments { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetGrantedScopes") ret0, _ := ret[0].(fosite.Arguments) @@ -102,13 +102,13 @@ func (m *MockDeviceAuthorizeRequester) GetGrantedScopes() fosite.Arguments { } // GetGrantedScopes indicates an expected call of GetGrantedScopes. -func (mr *MockDeviceAuthorizeRequesterMockRecorder) GetGrantedScopes() *gomock.Call { +func (mr *MockDeviceUserRequesterMockRecorder) GetGrantedScopes() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGrantedScopes", reflect.TypeOf((*MockDeviceAuthorizeRequester)(nil).GetGrantedScopes)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGrantedScopes", reflect.TypeOf((*MockDeviceUserRequester)(nil).GetGrantedScopes)) } // GetID mocks base method. -func (m *MockDeviceAuthorizeRequester) GetID() string { +func (m *MockDeviceUserRequester) GetID() string { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetID") ret0, _ := ret[0].(string) @@ -116,13 +116,13 @@ func (m *MockDeviceAuthorizeRequester) GetID() string { } // GetID indicates an expected call of GetID. -func (mr *MockDeviceAuthorizeRequesterMockRecorder) GetID() *gomock.Call { +func (mr *MockDeviceUserRequesterMockRecorder) GetID() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetID", reflect.TypeOf((*MockDeviceAuthorizeRequester)(nil).GetID)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetID", reflect.TypeOf((*MockDeviceUserRequester)(nil).GetID)) } // GetRequestForm mocks base method. -func (m *MockDeviceAuthorizeRequester) GetRequestForm() url.Values { +func (m *MockDeviceUserRequester) GetRequestForm() url.Values { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetRequestForm") ret0, _ := ret[0].(url.Values) @@ -130,13 +130,13 @@ func (m *MockDeviceAuthorizeRequester) GetRequestForm() url.Values { } // GetRequestForm indicates an expected call of GetRequestForm. -func (mr *MockDeviceAuthorizeRequesterMockRecorder) GetRequestForm() *gomock.Call { +func (mr *MockDeviceUserRequesterMockRecorder) GetRequestForm() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRequestForm", reflect.TypeOf((*MockDeviceAuthorizeRequester)(nil).GetRequestForm)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRequestForm", reflect.TypeOf((*MockDeviceUserRequester)(nil).GetRequestForm)) } // GetRequestedAt mocks base method. -func (m *MockDeviceAuthorizeRequester) GetRequestedAt() time.Time { +func (m *MockDeviceUserRequester) GetRequestedAt() time.Time { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetRequestedAt") ret0, _ := ret[0].(time.Time) @@ -144,13 +144,13 @@ func (m *MockDeviceAuthorizeRequester) GetRequestedAt() time.Time { } // GetRequestedAt indicates an expected call of GetRequestedAt. -func (mr *MockDeviceAuthorizeRequesterMockRecorder) GetRequestedAt() *gomock.Call { +func (mr *MockDeviceUserRequesterMockRecorder) GetRequestedAt() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRequestedAt", reflect.TypeOf((*MockDeviceAuthorizeRequester)(nil).GetRequestedAt)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRequestedAt", reflect.TypeOf((*MockDeviceUserRequester)(nil).GetRequestedAt)) } // GetRequestedAudience mocks base method. -func (m *MockDeviceAuthorizeRequester) GetRequestedAudience() fosite.Arguments { +func (m *MockDeviceUserRequester) GetRequestedAudience() fosite.Arguments { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetRequestedAudience") ret0, _ := ret[0].(fosite.Arguments) @@ -158,13 +158,13 @@ func (m *MockDeviceAuthorizeRequester) GetRequestedAudience() fosite.Arguments { } // GetRequestedAudience indicates an expected call of GetRequestedAudience. -func (mr *MockDeviceAuthorizeRequesterMockRecorder) GetRequestedAudience() *gomock.Call { +func (mr *MockDeviceUserRequesterMockRecorder) GetRequestedAudience() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRequestedAudience", reflect.TypeOf((*MockDeviceAuthorizeRequester)(nil).GetRequestedAudience)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRequestedAudience", reflect.TypeOf((*MockDeviceUserRequester)(nil).GetRequestedAudience)) } // GetRequestedScopes mocks base method. -func (m *MockDeviceAuthorizeRequester) GetRequestedScopes() fosite.Arguments { +func (m *MockDeviceUserRequester) GetRequestedScopes() fosite.Arguments { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetRequestedScopes") ret0, _ := ret[0].(fosite.Arguments) @@ -172,13 +172,13 @@ func (m *MockDeviceAuthorizeRequester) GetRequestedScopes() fosite.Arguments { } // GetRequestedScopes indicates an expected call of GetRequestedScopes. -func (mr *MockDeviceAuthorizeRequesterMockRecorder) GetRequestedScopes() *gomock.Call { +func (mr *MockDeviceUserRequesterMockRecorder) GetRequestedScopes() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRequestedScopes", reflect.TypeOf((*MockDeviceAuthorizeRequester)(nil).GetRequestedScopes)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRequestedScopes", reflect.TypeOf((*MockDeviceUserRequester)(nil).GetRequestedScopes)) } // GetSession mocks base method. -func (m *MockDeviceAuthorizeRequester) GetSession() fosite.Session { +func (m *MockDeviceUserRequester) GetSession() fosite.Session { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetSession") ret0, _ := ret[0].(fosite.Session) @@ -186,49 +186,49 @@ func (m *MockDeviceAuthorizeRequester) GetSession() fosite.Session { } // GetSession indicates an expected call of GetSession. -func (mr *MockDeviceAuthorizeRequesterMockRecorder) GetSession() *gomock.Call { +func (mr *MockDeviceUserRequesterMockRecorder) GetSession() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSession", reflect.TypeOf((*MockDeviceAuthorizeRequester)(nil).GetSession)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSession", reflect.TypeOf((*MockDeviceUserRequester)(nil).GetSession)) } // GrantAudience mocks base method. -func (m *MockDeviceAuthorizeRequester) GrantAudience(arg0 string) { +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 *MockDeviceAuthorizeRequesterMockRecorder) GrantAudience(arg0 interface{}) *gomock.Call { +func (mr *MockDeviceUserRequesterMockRecorder) GrantAudience(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GrantAudience", reflect.TypeOf((*MockDeviceAuthorizeRequester)(nil).GrantAudience), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GrantAudience", reflect.TypeOf((*MockDeviceUserRequester)(nil).GrantAudience), arg0) } // GrantScope mocks base method. -func (m *MockDeviceAuthorizeRequester) GrantScope(arg0 string) { +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 *MockDeviceAuthorizeRequesterMockRecorder) GrantScope(arg0 interface{}) *gomock.Call { +func (mr *MockDeviceUserRequesterMockRecorder) GrantScope(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GrantScope", reflect.TypeOf((*MockDeviceAuthorizeRequester)(nil).GrantScope), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GrantScope", reflect.TypeOf((*MockDeviceUserRequester)(nil).GrantScope), arg0) } // Merge mocks base method. -func (m *MockDeviceAuthorizeRequester) Merge(arg0 fosite.Requester) { +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 *MockDeviceAuthorizeRequesterMockRecorder) Merge(arg0 interface{}) *gomock.Call { +func (mr *MockDeviceUserRequesterMockRecorder) Merge(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Merge", reflect.TypeOf((*MockDeviceAuthorizeRequester)(nil).Merge), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Merge", reflect.TypeOf((*MockDeviceUserRequester)(nil).Merge), arg0) } // Sanitize mocks base method. -func (m *MockDeviceAuthorizeRequester) Sanitize(arg0 []string) fosite.Requester { +func (m *MockDeviceUserRequester) Sanitize(arg0 []string) fosite.Requester { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Sanitize", arg0) ret0, _ := ret[0].(fosite.Requester) @@ -236,67 +236,67 @@ func (m *MockDeviceAuthorizeRequester) Sanitize(arg0 []string) fosite.Requester } // Sanitize indicates an expected call of Sanitize. -func (mr *MockDeviceAuthorizeRequesterMockRecorder) Sanitize(arg0 interface{}) *gomock.Call { +func (mr *MockDeviceUserRequesterMockRecorder) Sanitize(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Sanitize", reflect.TypeOf((*MockDeviceAuthorizeRequester)(nil).Sanitize), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Sanitize", reflect.TypeOf((*MockDeviceUserRequester)(nil).Sanitize), arg0) } // SetDeviceCodeSignature mocks base method. -func (m *MockDeviceAuthorizeRequester) SetDeviceCodeSignature(arg0 string) { +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 *MockDeviceAuthorizeRequesterMockRecorder) SetDeviceCodeSignature(arg0 interface{}) *gomock.Call { +func (mr *MockDeviceUserRequesterMockRecorder) SetDeviceCodeSignature(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDeviceCodeSignature", reflect.TypeOf((*MockDeviceAuthorizeRequester)(nil).SetDeviceCodeSignature), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDeviceCodeSignature", reflect.TypeOf((*MockDeviceUserRequester)(nil).SetDeviceCodeSignature), arg0) } // SetID mocks base method. -func (m *MockDeviceAuthorizeRequester) SetID(arg0 string) { +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 *MockDeviceAuthorizeRequesterMockRecorder) SetID(arg0 interface{}) *gomock.Call { +func (mr *MockDeviceUserRequesterMockRecorder) SetID(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetID", reflect.TypeOf((*MockDeviceAuthorizeRequester)(nil).SetID), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetID", reflect.TypeOf((*MockDeviceUserRequester)(nil).SetID), arg0) } // SetRequestedAudience mocks base method. -func (m *MockDeviceAuthorizeRequester) SetRequestedAudience(arg0 fosite.Arguments) { +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 *MockDeviceAuthorizeRequesterMockRecorder) SetRequestedAudience(arg0 interface{}) *gomock.Call { +func (mr *MockDeviceUserRequesterMockRecorder) SetRequestedAudience(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetRequestedAudience", reflect.TypeOf((*MockDeviceAuthorizeRequester)(nil).SetRequestedAudience), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetRequestedAudience", reflect.TypeOf((*MockDeviceUserRequester)(nil).SetRequestedAudience), arg0) } // SetRequestedScopes mocks base method. -func (m *MockDeviceAuthorizeRequester) SetRequestedScopes(arg0 fosite.Arguments) { +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 *MockDeviceAuthorizeRequesterMockRecorder) SetRequestedScopes(arg0 interface{}) *gomock.Call { +func (mr *MockDeviceUserRequesterMockRecorder) SetRequestedScopes(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetRequestedScopes", reflect.TypeOf((*MockDeviceAuthorizeRequester)(nil).SetRequestedScopes), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetRequestedScopes", reflect.TypeOf((*MockDeviceUserRequester)(nil).SetRequestedScopes), arg0) } // SetSession mocks base method. -func (m *MockDeviceAuthorizeRequester) SetSession(arg0 fosite.Session) { +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 *MockDeviceAuthorizeRequesterMockRecorder) SetSession(arg0 interface{}) *gomock.Call { +func (mr *MockDeviceUserRequesterMockRecorder) SetSession(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetSession", reflect.TypeOf((*MockDeviceAuthorizeRequester)(nil).SetSession), arg0) + 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 index 54aadbbc0..9a77bcdbb 100644 --- a/internal/device_authorize_response.go +++ b/internal/device_authorize_response.go @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/ory/fosite (interfaces: DeviceAuthorizeResponder) +// Source: github.com/ory/fosite (interfaces: DeviceUserResponder) // Package internal is a generated GoMock package. package internal @@ -14,43 +14,43 @@ import ( gomock "github.com/golang/mock/gomock" ) -// MockDeviceAuthorizeResponder is a mock of DeviceAuthorizeResponder interface. -type MockDeviceAuthorizeResponder struct { +// MockDeviceUserResponder is a mock of DeviceUserResponder interface. +type MockDeviceUserResponder struct { ctrl *gomock.Controller - recorder *MockDeviceAuthorizeResponderMockRecorder + recorder *MockDeviceUserResponderMockRecorder } -// MockDeviceAuthorizeResponderMockRecorder is the mock recorder for MockDeviceAuthorizeResponder. -type MockDeviceAuthorizeResponderMockRecorder struct { - mock *MockDeviceAuthorizeResponder +// MockDeviceUserResponderMockRecorder is the mock recorder for MockDeviceUserResponder. +type MockDeviceUserResponderMockRecorder struct { + mock *MockDeviceUserResponder } -// NewMockDeviceAuthorizeResponder creates a new mock instance. -func NewMockDeviceAuthorizeResponder(ctrl *gomock.Controller) *MockDeviceAuthorizeResponder { - mock := &MockDeviceAuthorizeResponder{ctrl: ctrl} - mock.recorder = &MockDeviceAuthorizeResponderMockRecorder{mock} +// 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 *MockDeviceAuthorizeResponder) EXPECT() *MockDeviceAuthorizeResponderMockRecorder { +func (m *MockDeviceUserResponder) EXPECT() *MockDeviceUserResponderMockRecorder { return m.recorder } // AddHeader mocks base method. -func (m *MockDeviceAuthorizeResponder) AddHeader(arg0, arg1 string) { +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 *MockDeviceAuthorizeResponderMockRecorder) AddHeader(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockDeviceUserResponderMockRecorder) AddHeader(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddHeader", reflect.TypeOf((*MockDeviceAuthorizeResponder)(nil).AddHeader), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddHeader", reflect.TypeOf((*MockDeviceUserResponder)(nil).AddHeader), arg0, arg1) } // GetHeader mocks base method. -func (m *MockDeviceAuthorizeResponder) GetHeader() http.Header { +func (m *MockDeviceUserResponder) GetHeader() http.Header { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetHeader") ret0, _ := ret[0].(http.Header) @@ -58,7 +58,7 @@ func (m *MockDeviceAuthorizeResponder) GetHeader() http.Header { } // GetHeader indicates an expected call of GetHeader. -func (mr *MockDeviceAuthorizeResponderMockRecorder) GetHeader() *gomock.Call { +func (mr *MockDeviceUserResponderMockRecorder) GetHeader() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetHeader", reflect.TypeOf((*MockDeviceAuthorizeResponder)(nil).GetHeader)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetHeader", reflect.TypeOf((*MockDeviceUserResponder)(nil).GetHeader)) } diff --git a/oauth2.go b/oauth2.go index 7fe198883..196177bfd 100644 --- a/oauth2.go +++ b/oauth2.go @@ -103,19 +103,19 @@ 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) - // NewDeviceAuthorizeRequest returns an DeviceAuthorizeRequest. + // 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 - NewDeviceAuthorizeRequest(ctx context.Context, req *http.Request) (DeviceAuthorizeRequester, error) + NewDeviceUserRequest(ctx context.Context, req *http.Request) (DeviceUserRequester, error) - // NewDeviceAuthorizeResponse + // 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 - NewDeviceAuthorizeResponse(ctx context.Context, requester DeviceAuthorizeRequester, session Session) (DeviceAuthorizeResponder, error) + NewDeviceUserResponse(ctx context.Context, requester DeviceUserRequester, session Session) (DeviceUserResponder, error) - // WriteDeviceAuthorizeResponse + // WriteDeviceUserResponse // Once the user is authorized, it is being redirect to the login page; - WriteDeviceAuthorizeResponse(ctx context.Context, r *http.Request, rw http.ResponseWriter, requester DeviceAuthorizeRequester, responder DeviceAuthorizeResponder) + WriteDeviceUserResponse(ctx context.Context, r *http.Request, rw http.ResponseWriter, requester DeviceUserRequester, responder DeviceUserResponder) // NewDeviceRequest validate the OAuth 2.0 Device Authorization Flow Request // @@ -303,8 +303,8 @@ type DeviceRequester interface { Requester } -// DeviceAuthorizeRequester is an device authorize endpoint's request context. -type DeviceAuthorizeRequester interface { +// DeviceUserRequester is an device authorize endpoint's request context. +type DeviceUserRequester interface { // SetDeviceCodeSignature set the device code signature SetDeviceCodeSignature(signature string) @@ -427,7 +427,7 @@ type G11NContext interface { GetLang() language.Tag } -type DeviceAuthorizeResponder interface { +type DeviceUserResponder interface { Responder } From 10419a9c3f873ed9d9f578bc0da0cf7272c8c3bf Mon Sep 17 00:00:00 2001 From: Romain Caire Date: Tue, 5 Dec 2023 14:27:42 +0100 Subject: [PATCH 46/49] Refactor the way we query the URL Query params --- device_user_request_handler.go | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/device_user_request_handler.go b/device_user_request_handler.go index 9367de5b4..e294e400c 100644 --- a/device_user_request_handler.go +++ b/device_user_request_handler.go @@ -23,15 +23,10 @@ func (f *Fosite) NewDeviceUserRequest(ctx context.Context, r *http.Request) (_ D 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 err := r.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 = r.Form - - verifier := request.GetRequestForm().Get("device_verifier") - if verifier != "" { - client, err := f.Store.GetClient(ctx, request.GetRequestForm().Get("client_id")) + 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())) } From 5bc878357c29157d2d1d4db5824f11213ad9a250 Mon Sep 17 00:00:00 2001 From: Romain Caire Date: Thu, 7 Dec 2023 13:39:43 +0100 Subject: [PATCH 47/49] Don't send stacktrace when err is AuthorizationPending --- handler/oauth2/flow_generic_code_token.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/handler/oauth2/flow_generic_code_token.go b/handler/oauth2/flow_generic_code_token.go index 6fefc6791..c9191827f 100644 --- a/handler/oauth2/flow_generic_code_token.go +++ b/handler/oauth2/flow_generic_code_token.go @@ -72,7 +72,8 @@ func (c *GenericCodeTokenEndpointHandler) HandleTokenEndpointRequest(ctx context } return errorsx.WithStack(fosite.ErrInvalidGrant.WithHint(hint).WithDebug(debug)) } else if errors.Is(err, fosite.ErrAuthorizationPending) { - return errorsx.WithStack(err) + // Don't print a stacktrace as it spams logs + return err } else if err != nil && errors.Is(err, fosite.ErrNotFound) { return errorsx.WithStack(fosite.ErrInvalidGrant.WithWrap(err).WithDebug(err.Error())) } else if err != nil { From e8b16541fe525bec4e258e14dd9b01ed46266599 Mon Sep 17 00:00:00 2001 From: Romain Caire Date: Thu, 4 Jan 2024 16:35:10 +0100 Subject: [PATCH 48/49] Add RateLimit to Device Polling endpoint --- compose/compose.go | 4 +- compose/compose_rfc8628.go | 9 +- compose/compose_strategy.go | 5 + device_user_response_writer.go | 6 +- errors.go | 7 + go.mod | 69 +++++---- go.sum | 141 +++++++++--------- handler/oauth2/flow_generic_code_token.go | 2 + handler/rfc8628/strategy.go | 5 + handler/rfc8628/strategy_hmacsha.go | 33 +++- handler/rfc8628/strategy_hmacsha_test.go | 26 +++- handler/rfc8628/token_endpoint_handler.go | 29 ++-- .../rfc8628/token_endpoint_handler_test.go | 6 +- oauth2.go | 5 + 14 files changed, 214 insertions(+), 133 deletions(-) diff --git a/compose/compose.go b/compose/compose.go index c79553aea..9f5b95f54 100644 --- a/compose/compose.go +++ b/compose/compose.go @@ -56,8 +56,8 @@ func Compose(config *fosite.Config, storage interface{}, strategy interface{}, f if dh, ok := res.(fosite.DeviceEndpointHandler); ok { config.DeviceEndpointHandlers.Append(dh) } - if dah, ok := res.(fosite.DeviceUserEndpointHandler); ok { - config.DeviceUserEndpointHandlers.Append(dah) + if duh, ok := res.(fosite.DeviceUserEndpointHandler); ok { + config.DeviceUserEndpointHandlers.Append(duh) } } diff --git a/compose/compose_rfc8628.go b/compose/compose_rfc8628.go index 37b8503fd..bbdfcf8df 100644 --- a/compose/compose_rfc8628.go +++ b/compose/compose_rfc8628.go @@ -19,14 +19,15 @@ func RFC8628DeviceFactory(config fosite.Configurator, storage interface{}, strat } } -// OAuth2DeviceCodeFactory creates an OAuth2 device authorization grant ("device authorization flow") handler and registers +// 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.DeviceUserHandler{ - DeviceStrategy: strategy.(rfc8628.DeviceCodeStrategy), - DeviceStorage: storage.(rfc8628.DeviceCodeStorage), + CodeTokenEndpointHandler: &rfc8628.DeviceHandler{ + DeviceRateLimitStrategy: strategy.(rfc8628.DeviceRateLimitStrategy), + DeviceStrategy: strategy.(rfc8628.DeviceCodeStrategy), + DeviceStorage: storage.(rfc8628.DeviceCodeStorage), }, AccessTokenStrategy: strategy.(oauth2.AccessTokenStrategy), RefreshTokenStrategy: strategy.(oauth2.RefreshTokenStrategy), diff --git a/compose/compose_strategy.go b/compose/compose_strategy.go index be9dcb537..748d8eab4 100644 --- a/compose/compose_strategy.go +++ b/compose/compose_strategy.go @@ -12,6 +12,7 @@ import ( "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 { @@ -57,6 +58,10 @@ func NewOpenIDConnectStrategy(keyGetter func(context.Context) (interface{}, erro 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/device_user_response_writer.go b/device_user_response_writer.go index cd473c441..4c9c370b5 100644 --- a/device_user_response_writer.go +++ b/device_user_response_writer.go @@ -7,12 +7,12 @@ import ( "context" ) -func (f *Fosite) NewDeviceUserResponse(ctx context.Context, dar DeviceUserRequester, session Session) (DeviceUserResponder, error) { +func (f *Fosite) NewDeviceUserResponse(ctx context.Context, dur DeviceUserRequester, session Session) (DeviceUserResponder, error) { var resp = &DeviceUserResponse{} - dar.SetSession(session) + dur.SetSession(session) for _, h := range f.Config.GetDeviceUserEndpointHandlers(ctx) { - if err := h.HandleDeviceUserEndpointRequest(ctx, dar, resp); err != nil { + if err := h.HandleDeviceUserEndpointRequest(ctx, dur, resp); err != nil { return nil, err } } diff --git a/errors.go b/errors.go index 3ca3bc693..f276e1d33 100644 --- a/errors.go +++ b/errors.go @@ -211,6 +211,12 @@ var ( 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, @@ -254,6 +260,7 @@ const ( errRegistrationNotSupportedName = "registration_not_supported" errJTIKnownName = "jti_known" errAuthorizationPending = "authorization_pending" + errPollingIntervalRateLimited = "polling_interval_rate_limited" errDeviceExpiredToken = "expired_token" ) diff --git a/go.mod b/go.mod index ad75e99a3..59c522797 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,8 @@ replace github.com/gobuffalo/packr => github.com/gobuffalo/packr v1.30.1 replace github.com/gorilla/sessions => github.com/gorilla/sessions v1.2.1 +replace github.com/ory/x => ../x + require ( github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 github.com/cristalhq/jwt/v4 v4.0.2 @@ -13,7 +15,7 @@ require ( github.com/ecordell/optgen v0.0.9 github.com/go-jose/go-jose/v3 v3.0.0 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 +25,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.0-00010101000000-000000000000 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.19.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 +48,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 +77,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.45.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 // indirect + go.opentelemetry.io/contrib/propagators/b3 v1.20.0 // indirect + go.opentelemetry.io/contrib/propagators/jaeger v1.20.0 // indirect + go.opentelemetry.io/contrib/samplers/jaegerremote v0.14.0 // indirect + go.opentelemetry.io/otel v1.19.0 // indirect + go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 // indirect + go.opentelemetry.io/otel/exporters/zipkin v1.19.0 // indirect + go.opentelemetry.io/otel/metric v1.19.0 // indirect + go.opentelemetry.io/otel/sdk v1.19.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..3ff0fed8e 100644 --- a/go.sum +++ b/go.sum @@ -92,8 +92,8 @@ 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= @@ -105,8 +105,8 @@ github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxF 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,18 @@ 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/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 +339,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 +414,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.45.0 h1:2ea0IkZBsWH+HA2GkD+7+hRw2u97jzdFyRtXuO14a1s= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.45.0/go.mod h1:4m3RnBBb+7dB9d21y510oO1pdB1V4J6smNf14WXcBFQ= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 h1:x8Z78aZx8cOF0+Kkazoc7lwUNMGy0LrzEMxTm4BbTxg= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0/go.mod h1:62CPTSry9QZtOaSsE3tOzhx6LzDhHnXJ6xHeMNNiM6Q= +go.opentelemetry.io/contrib/propagators/b3 v1.20.0 h1:Yty9Vs4F3D6/liF1o6FNt0PvN85h/BJJ6DQKJ3nrcM0= +go.opentelemetry.io/contrib/propagators/b3 v1.20.0/go.mod h1:On4VgbkqYL18kbJlWsa18+cMNe6rYpBnPi1ARI/BrsU= +go.opentelemetry.io/contrib/propagators/jaeger v1.20.0 h1:iVhNKkMIpzyZqxk8jkDU2n4DFTD+FbpGacvooxEvyyc= +go.opentelemetry.io/contrib/propagators/jaeger v1.20.0/go.mod h1:cpSABr0cm/AH/HhbJjn+AudBVUMgZWdfN3Gb+ZqxSZc= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.14.0 h1:Xg9iU9DF9V9zC6NI8sJthYqHlSWsWAQMTXM8QIErKlc= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.14.0/go.mod h1:ExRuq62/gYluX5fzTTZif5WujyG51ail4APTbBUu+S4= +go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs= +go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY= +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.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= +go.opentelemetry.io/otel/exporters/zipkin v1.19.0 h1:EGY0h5mGliP9o/nIkVuLI0vRiQqmsYOcbwCuotksO1o= +go.opentelemetry.io/otel/exporters/zipkin v1.19.0/go.mod h1:JQgTGJP11yi3o4GHzIWYodhPisxANdqxF1eHwDSnJrI= +go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPiOKwvpE= +go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8= +go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= +go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= +go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg= +go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo= 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 +467,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 +506,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 +547,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 +558,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 +574,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 +628,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 +644,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 +713,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 +746,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 +785,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 +807,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/oauth2/flow_generic_code_token.go b/handler/oauth2/flow_generic_code_token.go index c9191827f..22e92190f 100644 --- a/handler/oauth2/flow_generic_code_token.go +++ b/handler/oauth2/flow_generic_code_token.go @@ -74,6 +74,8 @@ func (c *GenericCodeTokenEndpointHandler) HandleTokenEndpointRequest(ctx context } 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 { diff --git a/handler/rfc8628/strategy.go b/handler/rfc8628/strategy.go index 43a1cca32..3b0df7a71 100644 --- a/handler/rfc8628/strategy.go +++ b/handler/rfc8628/strategy.go @@ -10,10 +10,15 @@ import ( ) 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) diff --git a/handler/rfc8628/strategy_hmacsha.go b/handler/rfc8628/strategy_hmacsha.go index a15648bbb..b45d7f8a1 100644 --- a/handler/rfc8628/strategy_hmacsha.go +++ b/handler/rfc8628/strategy_hmacsha.go @@ -5,25 +5,30 @@ 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 { - RFC8628CodeStrategy - - Enigma *enigma.HMACStrategy - Config interface { + 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 { @@ -77,3 +82,23 @@ func (h *DefaultDeviceStrategy) ValidateDeviceCode(ctx context.Context, r fosite 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 index 57639aad0..8181921c3 100644 --- a/handler/rfc8628/strategy_hmacsha_test.go +++ b/handler/rfc8628/strategy_hmacsha_test.go @@ -11,6 +11,7 @@ import ( "testing" "time" + "github.com/patrickmn/go-cache" "github.com/stretchr/testify/assert" "github.com/ory/fosite" @@ -19,11 +20,13 @@ import ( ) var hmacshaStrategy = DefaultDeviceStrategy{ - Enigma: &hmac.HMACStrategy{Config: &fosite.Config{GlobalSecret: []byte("foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobar")}}, + 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, + AccessTokenLifespan: time.Minute * 24, + AuthorizeCodeLifespan: time.Minute * 24, + DeviceAndUserCodeLifespan: time.Minute * 24, + DeviceAuthTokenPollingInterval: 400 * time.Millisecond, }, } @@ -148,3 +151,18 @@ func TestHMACDeviceCode(t *testing.T) { }) } } + +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 index fc1f71a1b..e1f581f92 100644 --- a/handler/rfc8628/token_endpoint_handler.go +++ b/handler/rfc8628/token_endpoint_handler.go @@ -12,21 +12,22 @@ import ( "github.com/ory/fosite" ) -// DeviceUserHandler is a response handler for the Device Code introduced in the Device Authorize Grant +// 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 DeviceUserHandler struct { - DeviceStrategy DeviceCodeStrategy - DeviceStorage DeviceCodeStorage +type DeviceHandler struct { + DeviceRateLimitStrategy DeviceRateLimitStrategy + DeviceStrategy DeviceCodeStrategy + DeviceStorage DeviceCodeStorage } type DeviceCodeTokenEndpointHandler struct { oauth2.GenericCodeTokenEndpointHandler } -var _ oauth2.CodeTokenEndpointHandler = (*DeviceUserHandler)(nil) +var _ oauth2.CodeTokenEndpointHandler = (*DeviceHandler)(nil) var _ fosite.TokenEndpointHandler = (*DeviceCodeTokenEndpointHandler)(nil) -func (c *DeviceUserHandler) ValidateGrantTypes(ctx context.Context, requester fosite.AccessRequester) error { +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\".")) } @@ -34,29 +35,35 @@ func (c *DeviceUserHandler) ValidateGrantTypes(ctx context.Context, requester fo return nil } -func (c *DeviceUserHandler) ValidateCode(ctx context.Context, request fosite.AccessRequester, code string) error { +func (c *DeviceHandler) ValidateCode(ctx context.Context, request fosite.AccessRequester, code string) error { return c.DeviceStrategy.ValidateDeviceCode(ctx, request, code) } -func (c *DeviceUserHandler) GetCodeAndSession(ctx context.Context, requester fosite.AccessRequester) (code string, signature string, request fosite.Requester, err error) { +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 *DeviceUserHandler) InvalidateSession(ctx context.Context, signature string) error { +func (c *DeviceHandler) InvalidateSession(ctx context.Context, signature string) error { return c.DeviceStorage.InvalidateDeviceCodeSession(ctx, signature) } // implement TokenEndpointHandler -func (c *DeviceUserHandler) CanSkipClientAuth(ctx context.Context, requester fosite.AccessRequester) bool { +func (c *DeviceHandler) CanSkipClientAuth(ctx context.Context, requester fosite.AccessRequester) bool { return requester.GetGrantTypes().ExactOne(string(fosite.GrantTypeDeviceCode)) } -func (c *DeviceUserHandler) CanHandleTokenEndpointRequest(ctx context.Context, requester fosite.AccessRequester) bool { +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 index 9a9ed6686..fa0452e99 100644 --- a/handler/rfc8628/token_endpoint_handler_test.go +++ b/handler/rfc8628/token_endpoint_handler_test.go @@ -212,7 +212,7 @@ func TestDeviceUserCode_PopulateTokenEndpointResponse(t *testing.T) { RefreshTokenScopes: []string{"offline"}, } h = oauth2.GenericCodeTokenEndpointHandler{ - CodeTokenEndpointHandler: &DeviceUserHandler{ + CodeTokenEndpointHandler: &DeviceHandler{ DeviceStrategy: strategy, DeviceStorage: store, }, @@ -256,7 +256,7 @@ func TestDeviceUserCode_HandleTokenEndpointRequest(t *testing.T) { store := storage.NewMemoryStore() h := oauth2.GenericCodeTokenEndpointHandler{ - CodeTokenEndpointHandler: &DeviceUserHandler{ + CodeTokenEndpointHandler: &DeviceHandler{ DeviceStrategy: strategy.RFC8628CodeStrategy, DeviceStorage: store, }, @@ -646,7 +646,7 @@ func TestDeviceUserCodeTransactional_HandleTokenEndpointRequest(t *testing.T) { testCase.setup() handler := oauth2.GenericCodeTokenEndpointHandler{ - CodeTokenEndpointHandler: &DeviceUserHandler{ + CodeTokenEndpointHandler: &DeviceHandler{ DeviceStrategy: &deviceStrategy, DeviceStorage: deviceTransactionalStore{ mockTransactional, diff --git a/oauth2.go b/oauth2.go index 196177bfd..5b129dd63 100644 --- a/oauth2.go +++ b/oauth2.go @@ -298,6 +298,11 @@ 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 From 193f7d7a5e5e70109948cf16edcef4009bd2bb33 Mon Sep 17 00:00:00 2001 From: Romain Caire Date: Thu, 4 Jan 2024 16:42:48 +0100 Subject: [PATCH 49/49] Bump ory/x version to latest --- go.mod | 30 ++++++++++++++---------------- go.sum | 54 ++++++++++++++++++++++++++++-------------------------- 2 files changed, 42 insertions(+), 42 deletions(-) diff --git a/go.mod b/go.mod index 59c522797..1fed8f122 100644 --- a/go.mod +++ b/go.mod @@ -6,14 +6,12 @@ replace github.com/gobuffalo/packr => github.com/gobuffalo/packr v1.30.1 replace github.com/gorilla/sessions => github.com/gorilla/sessions v1.2.1 -replace github.com/ory/x => ../x - require ( github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 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.1 github.com/gorilla/mux v1.8.0 @@ -25,13 +23,13 @@ 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.0-00010101000000-000000000000 + 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.19.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 @@ -77,18 +75,18 @@ 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.45.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 // indirect - go.opentelemetry.io/contrib/propagators/b3 v1.20.0 // indirect - go.opentelemetry.io/contrib/propagators/jaeger v1.20.0 // indirect - go.opentelemetry.io/contrib/samplers/jaegerremote v0.14.0 // indirect - go.opentelemetry.io/otel v1.19.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.19.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 // indirect - go.opentelemetry.io/otel/exporters/zipkin v1.19.0 // indirect - go.opentelemetry.io/otel/metric v1.19.0 // indirect - go.opentelemetry.io/otel/sdk v1.19.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.14.0 // indirect golang.org/x/sys v0.14.0 // indirect diff --git a/go.sum b/go.sum index 3ff0fed8e..fc2f9d8e9 100644 --- a/go.sum +++ b/go.sum @@ -100,8 +100,8 @@ github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbS 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= @@ -323,6 +323,8 @@ github.com/ory/go-convenience v0.1.0 h1:zouLKfF2GoSGnJwGq+PE/nJAE6dj2Zj5QlTgmMTs github.com/ory/go-convenience v0.1.0/go.mod h1:uEY/a60PL5c12nYz4V5cHY03IBmwIAEm8TWB0yn9KNs= 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.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= @@ -414,32 +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.45.0 h1:2ea0IkZBsWH+HA2GkD+7+hRw2u97jzdFyRtXuO14a1s= -go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.45.0/go.mod h1:4m3RnBBb+7dB9d21y510oO1pdB1V4J6smNf14WXcBFQ= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 h1:x8Z78aZx8cOF0+Kkazoc7lwUNMGy0LrzEMxTm4BbTxg= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0/go.mod h1:62CPTSry9QZtOaSsE3tOzhx6LzDhHnXJ6xHeMNNiM6Q= -go.opentelemetry.io/contrib/propagators/b3 v1.20.0 h1:Yty9Vs4F3D6/liF1o6FNt0PvN85h/BJJ6DQKJ3nrcM0= -go.opentelemetry.io/contrib/propagators/b3 v1.20.0/go.mod h1:On4VgbkqYL18kbJlWsa18+cMNe6rYpBnPi1ARI/BrsU= -go.opentelemetry.io/contrib/propagators/jaeger v1.20.0 h1:iVhNKkMIpzyZqxk8jkDU2n4DFTD+FbpGacvooxEvyyc= -go.opentelemetry.io/contrib/propagators/jaeger v1.20.0/go.mod h1:cpSABr0cm/AH/HhbJjn+AudBVUMgZWdfN3Gb+ZqxSZc= -go.opentelemetry.io/contrib/samplers/jaegerremote v0.14.0 h1:Xg9iU9DF9V9zC6NI8sJthYqHlSWsWAQMTXM8QIErKlc= -go.opentelemetry.io/contrib/samplers/jaegerremote v0.14.0/go.mod h1:ExRuq62/gYluX5fzTTZif5WujyG51ail4APTbBUu+S4= -go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs= -go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY= +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.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= -go.opentelemetry.io/otel/exporters/zipkin v1.19.0 h1:EGY0h5mGliP9o/nIkVuLI0vRiQqmsYOcbwCuotksO1o= -go.opentelemetry.io/otel/exporters/zipkin v1.19.0/go.mod h1:JQgTGJP11yi3o4GHzIWYodhPisxANdqxF1eHwDSnJrI= -go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPiOKwvpE= -go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8= -go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= -go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= -go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg= -go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo= +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=