Skip to content

Commit

Permalink
Device grant flow (migrate to master)
Browse files Browse the repository at this point in the history
  • Loading branch information
BuzzBumbleBee committed Sep 4, 2022
1 parent 575ae6d commit eaca767
Show file tree
Hide file tree
Showing 54 changed files with 1,840 additions and 70 deletions.
34 changes: 19 additions & 15 deletions compose/compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -91,20 +91,24 @@ func ComposeAllEnabled(config *fosite.Config, storage interface{}, key interface
},
OAuth2AuthorizeExplicitFactory,
OAuth2AuthorizeImplicitFactory,
OAuth2AuthorizeDeviceFactory,
OAuth2ClientCredentialsGrantFactory,
OAuth2RefreshTokenGrantFactory,
OAuth2DeviceAuthorizeFactory,
OAuth2ResourceOwnerPasswordCredentialsFactory,
RFC7523AssertionGrantFactory,

OpenIDConnectExplicitFactory,
OpenIDConnectImplicitFactory,
OpenIDConnectHybridFactory,
OpenIDConnectRefreshFactory,
OpenIDConnectDeviceFactory,

OAuth2TokenIntrospectionFactory,
OAuth2TokenRevocationFactory,

OAuth2PKCEFactory,
PushedAuthorizeHandlerFactory,
OAuth2DevicePKCEFactory,
)
}
22 changes: 22 additions & 0 deletions compose/compose_oauth2.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}
20 changes: 19 additions & 1 deletion compose/compose_openid.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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,
}
}
11 changes: 11 additions & 0 deletions compose/compose_pkce.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}
1 change: 1 addition & 0 deletions compose/compose_strategy.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ type HMACSHAStrategyConfigurator interface {
fosite.GlobalSecretProvider
fosite.RotatedGlobalSecretsProvider
fosite.HMACHashingProvider
fosite.DeviceAndUserCodeLifespanProvider
}

func NewOAuth2HMACStrategy(config HMACSHAStrategyConfigurator) *oauth2.HMACSHAStrategy {
Expand Down
13 changes: 13 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
34 changes: 34 additions & 0 deletions config_default.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ var (
_ RevocationHandlersProvider = (*Config)(nil)
_ PushedAuthorizeRequestHandlersProvider = (*Config)(nil)
_ PushedAuthorizeRequestConfigProvider = (*Config)(nil)
_ DeviceAuthorizeEndpointHandlersProvider = (*Config)(nil)
)

type Config struct {
Expand All @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
10 changes: 10 additions & 0 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -258,6 +267,7 @@ const (
errRequestURINotSupportedName = "request_uri_not_supported"
errRegistrationNotSupportedName = "registration_not_supported"
errJTIKnownName = "jti_known"
errAuthorizationPending = "authorization_pending"
)

type (
Expand Down
17 changes: 17 additions & 0 deletions fosite.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -125,6 +139,7 @@ type Configurator interface {
AccessTokenLifespanProvider
RefreshTokenLifespanProvider
AuthorizeCodeLifespanProvider
DeviceAndUserCodeLifespanProvider
TokenEntropyProvider
RotatedGlobalSecretsProvider
GlobalSecretProvider
Expand All @@ -149,6 +164,8 @@ type Configurator interface {
TokenIntrospectionHandlersProvider
RevocationHandlersProvider
UseLegacyErrorFormatProvider
DeviceAuthorizeEndpointHandlersProvider
DeviceUriProvider
}

func NewOAuth2Provider(s Storage, c Configurator) *Fosite {
Expand Down
10 changes: 10 additions & 0 deletions handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
60 changes: 60 additions & 0 deletions handler/oauth2/device_authorization.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit eaca767

Please sign in to comment.