Skip to content

Commit

Permalink
feat: implement the access token handling for device authorization flow
Browse files Browse the repository at this point in the history
feat: add the access token endpoint handling for device authorization flow
  • Loading branch information
wood-push-melon authored Mar 15, 2024
1 parent fee676b commit 222821e
Show file tree
Hide file tree
Showing 32 changed files with 1,620 additions and 323 deletions.
4 changes: 3 additions & 1 deletion compose/compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ func ComposeAllEnabled(config *fosite.Config, storage interface{}, key interface
OpenIDConnectTokenStrategy: NewOpenIDConnectStrategy(keyGetter, config),
Signer: &jwt.DefaultSigner{GetPrivateKey: keyGetter},
},
OAuth2AuthorizeExplicitFactory,
OAuth2AuthorizeExplicitAuthFactory,
Oauth2AuthorizeExplicitTokenFactory,
OAuth2AuthorizeImplicitFactory,
OAuth2ClientCredentialsGrantFactory,
OAuth2RefreshTokenGrantFactory,
Expand All @@ -91,6 +92,7 @@ func ComposeAllEnabled(config *fosite.Config, storage interface{}, key interface
OAuth2TokenRevocationFactory,

RFC8628DeviceFactory,
RFC8628DeviceAuthorizationTokenFactory,

OAuth2PKCEFactory,
PushedAuthorizeHandlerFactory,
Expand Down
39 changes: 29 additions & 10 deletions compose/compose_oauth2.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,35 @@ import (
"github.com/ory/fosite/token/jwt"
)

// OAuth2AuthorizeExplicitFactory creates an OAuth2 authorize code grant ("authorize explicit flow") handler and registers
// OAuth2AuthorizeExplicitAuthFactory creates an OAuth2 authorize code grant ("authorize explicit flow") handler and registers
// an access token, refresh token and authorize code validator.
func OAuth2AuthorizeExplicitFactory(config fosite.Configurator, storage interface{}, strategy interface{}) interface{} {
return &oauth2.AuthorizeExplicitGrantHandler{
AccessTokenStrategy: strategy.(oauth2.AccessTokenStrategy),
RefreshTokenStrategy: strategy.(oauth2.RefreshTokenStrategy),
AuthorizeCodeStrategy: strategy.(oauth2.AuthorizeCodeStrategy),
CoreStorage: storage.(oauth2.CoreStorage),
TokenRevocationStorage: storage.(oauth2.TokenRevocationStorage),
Config: config,
func OAuth2AuthorizeExplicitAuthFactory(config fosite.Configurator, storage interface{}, strategy interface{}) interface{} {
return &oauth2.AuthorizeExplicitGrantAuthHandler{
AuthorizeCodeStrategy: strategy.(oauth2.AuthorizeCodeStrategy),
AuthorizeCodeStorage: storage.(oauth2.AuthorizeCodeStorage),
Config: config,
}
}

// Oauth2AuthorizeExplicitTokenFactory creates an OAuth2 authorize code grant ("authorize explicit flow") token handler and registers
// an access token, refresh token and authorize code validator.
func Oauth2AuthorizeExplicitTokenFactory(config fosite.Configurator, storage interface{}, strategy interface{}) interface{} {
return &oauth2.AuthorizeExplicitTokenEndpointHandler{
GenericCodeTokenEndpointHandler: oauth2.GenericCodeTokenEndpointHandler{
AccessRequestValidator: &oauth2.AuthorizeExplicitGrantAccessRequestValidator{},
CodeHandler: &oauth2.AuthorizeCodeHandler{
AuthorizeCodeStrategy: strategy.(oauth2.AuthorizeCodeStrategy),
},
SessionHandler: &oauth2.AuthorizeExplicitGrantSessionHandler{
AuthorizeCodeStorage: storage.(oauth2.AuthorizeCodeStorage),
},

AccessTokenStrategy: strategy.(oauth2.AccessTokenStrategy),
RefreshTokenStrategy: strategy.(oauth2.RefreshTokenStrategy),
CoreStorage: storage.(oauth2.CoreStorage),
TokenRevocationStorage: storage.(oauth2.TokenRevocationStorage),
Config: config,
},
}
}

Expand Down Expand Up @@ -96,7 +115,7 @@ func OAuth2TokenIntrospectionFactory(config fosite.Configurator, storage interfa

// OAuth2StatelessJWTIntrospectionFactory creates an OAuth2 token introspection handler and
// registers an access token validator. This can only be used to validate JWTs and does so
// statelessly, meaning it uses only the data available in the JWT itself, and does not access the
// stateless, meaning it uses only the data available in the JWT itself, and does not access the
// storage implementation at all.
//
// Due to the stateless nature of this factory, THE BUILT-IN REVOCATION MECHANISMS WILL NOT WORK.
Expand Down
6 changes: 2 additions & 4 deletions compose/compose_openid.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
26 changes: 25 additions & 1 deletion compose/compose_rfc8628.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,39 @@ package compose

import (
"github.com/ory/fosite"
"github.com/ory/fosite/handler/oauth2"
"github.com/ory/fosite/handler/rfc8628"
)

// RFC8628DeviceFactory creates an OAuth2 device code grant ("Device Authorization Grant") handler and registers
// an user code, device code, access token and a refresh token validator.
// a 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.RFC8628CoreStorage),
Config: config,
}
}

// RFC8628DeviceAuthorizationTokenFactory creates an OAuth2 device authorization grant ("Device Authorization Grant") handler and registers
// an access token, refresh token and authorize code validator.
func RFC8628DeviceAuthorizationTokenFactory(config fosite.Configurator, storage interface{}, strategy interface{}) interface{} {
return &rfc8628.DeviceCodeTokenEndpointHandler{
GenericCodeTokenEndpointHandler: oauth2.GenericCodeTokenEndpointHandler{
AccessRequestValidator: &rfc8628.DeviceAccessRequestValidator{},
CodeHandler: &rfc8628.DeviceCodeHandler{
DeviceRateLimitStrategy: strategy.(rfc8628.DeviceRateLimitStrategy),
DeviceCodeStrategy: strategy.(rfc8628.DeviceCodeStrategy),
},
SessionHandler: &rfc8628.DeviceSessionHandler{
DeviceCodeStorage: storage.(rfc8628.DeviceCodeStorage),
},

AccessTokenStrategy: strategy.(oauth2.AccessTokenStrategy),
RefreshTokenStrategy: strategy.(oauth2.RefreshTokenStrategy),
CoreStorage: storage.(oauth2.CoreStorage),
TokenRevocationStorage: storage.(oauth2.TokenRevocationStorage),
Config: config,
},
}
}
2 changes: 1 addition & 1 deletion device_response_writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (

// NewDeviceResponse returns a new DeviceResponder
func (f *Fosite) NewDeviceResponse(ctx context.Context, r DeviceRequester, session Session) (DeviceResponder, error) {
var resp = &DeviceResponse{}
resp := &DeviceResponse{}

r.SetSession(session)
for _, h := range f.Config.GetDeviceEndpointHandlers(ctx) {
Expand Down
21 changes: 21 additions & 0 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ 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 benn used previously.
ErrInvalidatedDeviceCode = errors.New("Device 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 @@ -202,6 +204,22 @@ var (
ErrorField: errJTIKnownName,
CodeField: http.StatusBadRequest,
}
ErrAuthorizationPending = &RFC6749Error{
DescriptionField: "The authorization request is still pending as the end user hasn't yet completed the user-interaction steps.",
ErrorField: errAuthorizationPending,
CodeField: http.StatusBadRequest,
}
ErrPollingRateLimited = &RFC6749Error{
DescriptionField: "The authorization request was rate-limited to prevent system overload.",
HintField: "Ensure that you don't call the token endpoint sooner than the polling interval",
ErrorField: errPollingIntervalRateLimited,
CodeField: http.StatusTooManyRequests,
}
ErrDeviceExpiredToken = &RFC6749Error{
DescriptionField: "The device_code has expired, and the device authorization session has concluded.",
ErrorField: errDeviceExpiredToken,
CodeField: http.StatusBadRequest,
}
)

const (
Expand Down Expand Up @@ -239,6 +257,9 @@ const (
errRequestURINotSupportedName = "request_uri_not_supported"
errRegistrationNotSupportedName = "registration_not_supported"
errJTIKnownName = "jti_known"
errAuthorizationPending = "authorization_pending"
errPollingIntervalRateLimited = "polling_interval_rate_limited"
errDeviceExpiredToken = "expired_token"
)

type (
Expand Down
10 changes: 5 additions & 5 deletions fosite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 3 additions & 1 deletion generate-mocks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ mockgen -package internal -destination internal/transactional.go github.com/ory/
mockgen -package internal -destination internal/oauth2_storage.go github.com/ory/fosite/handler/oauth2 CoreStorage
mockgen -package internal -destination internal/oauth2_strategy.go github.com/ory/fosite/handler/oauth2 CoreStrategy
mockgen -package internal -destination internal/authorize_code_storage.go github.com/ory/fosite/handler/oauth2 AuthorizeCodeStorage
mockgen -package internal -destination internal/device_code_storage.go github.com/ory/fosite/handler/rfc8628 DeviceCodeStorage
mockgen -package internal -destination internal/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
Expand All @@ -16,6 +17,7 @@ mockgen -package internal -destination internal/openid_id_token_storage.go githu
mockgen -package internal -destination internal/access_token_strategy.go github.com/ory/fosite/handler/oauth2 AccessTokenStrategy
mockgen -package internal -destination internal/refresh_token_strategy.go github.com/ory/fosite/handler/oauth2 RefreshTokenStrategy
mockgen -package internal -destination internal/authorize_code_strategy.go github.com/ory/fosite/handler/oauth2 AuthorizeCodeStrategy
mockgen -package internal -destination internal/device_code_rate_limit_strategy.go github.com/ory/fosite/handler/rfc8628 DeviceRateLimitStrategy
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
Expand All @@ -29,4 +31,4 @@ mockgen -package internal -destination internal/access_response.go github.com/or
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

goimports -w internal/
goimports -w internal/
2 changes: 2 additions & 0 deletions generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ 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/rfc8628 DeviceCodeStorage
//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
Expand All @@ -19,6 +20,7 @@ package fosite
//go:generate go run github.com/golang/mock/mockgen -package internal -destination internal/access_token_strategy.go github.com/ory/fosite/handler/oauth2 AccessTokenStrategy
//go:generate go run github.com/golang/mock/mockgen -package internal -destination internal/refresh_token_strategy.go github.com/ory/fosite/handler/oauth2 RefreshTokenStrategy
//go:generate go run github.com/golang/mock/mockgen -package internal -destination internal/authorize_code_strategy.go github.com/ory/fosite/handler/oauth2 AuthorizeCodeStrategy
//go:generate go run github.com/golang/mock/mockgen -package internal -destination internal/device_code_rate_limit_strategy.go github.com/ory/fosite/handler/rfc8628 DeviceRateLimitStrategy
//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
Expand Down
2 changes: 1 addition & 1 deletion handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ 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 HandleDeviceUserRequest should do.
// The following spec is a good example of what HandleDeviceEndpointRequest should do.
// * https://tools.ietf.org/html/rfc8628#section-3.2
HandleDeviceEndpointRequest(ctx context.Context, requester DeviceRequester, responder DeviceResponder) error
}
37 changes: 13 additions & 24 deletions handler/oauth2/flow_authorize_code_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,50 +14,39 @@ import (
"github.com/ory/fosite"
)

var _ fosite.AuthorizeEndpointHandler = (*AuthorizeExplicitGrantHandler)(nil)
var _ fosite.TokenEndpointHandler = (*AuthorizeExplicitGrantHandler)(nil)
var _ fosite.AuthorizeEndpointHandler = (*AuthorizeExplicitGrantAuthHandler)(nil)

// AuthorizeExplicitGrantHandler is a response handler for the Authorize Code grant using the explicit grant type
// AuthorizeExplicitGrantAuthHandler is a response handler for the Authorize Code grant using the explicit grant type
// as defined in https://tools.ietf.org/html/rfc6749#section-4.1
type AuthorizeExplicitGrantHandler struct {
AccessTokenStrategy AccessTokenStrategy
RefreshTokenStrategy RefreshTokenStrategy
AuthorizeCodeStrategy AuthorizeCodeStrategy
CoreStorage CoreStorage
TokenRevocationStorage TokenRevocationStorage
Config interface {
type AuthorizeExplicitGrantAuthHandler struct {
AuthorizeCodeStrategy AuthorizeCodeStrategy
AuthorizeCodeStorage AuthorizeCodeStorage

Config interface {
fosite.AuthorizeCodeLifespanProvider
fosite.AccessTokenLifespanProvider
fosite.RefreshTokenLifespanProvider
fosite.ScopeStrategyProvider
fosite.AudienceStrategyProvider
fosite.RedirectSecureCheckerProvider
fosite.RefreshTokenScopesProvider
fosite.OmitRedirectScopeParamProvider
fosite.SanitationAllowedProvider
}
}

func (c *AuthorizeExplicitGrantHandler) secureChecker(ctx context.Context) func(context.Context, *url.URL) bool {
func (c *AuthorizeExplicitGrantAuthHandler) secureChecker(ctx context.Context) func(context.Context, *url.URL) bool {
if c.Config.GetRedirectSecureChecker(ctx) == nil {
return fosite.IsRedirectURISecure
}
return c.Config.GetRedirectSecureChecker(ctx)
}

func (c *AuthorizeExplicitGrantHandler) HandleAuthorizeEndpointRequest(ctx context.Context, ar fosite.AuthorizeRequester, resp fosite.AuthorizeResponder) error {
// This let's us define multiple response types, for example open id connect's id_token
func (c *AuthorizeExplicitGrantAuthHandler) HandleAuthorizeEndpointRequest(ctx context.Context, ar fosite.AuthorizeRequester, resp fosite.AuthorizeResponder) error {
// This allows to define multiple response types, for example OpenID Connect `id_token`
if !ar.GetResponseTypes().ExactOne("code") {
return nil
}

ar.SetDefaultResponseMode(fosite.ResponseModeQuery)

// Disabled because this is already handled at the authorize_request_handler
// if !ar.GetClient().GetResponseTypes().Has("code") {
// return errorsx.WithStack(fosite.ErrInvalidGrant)
// }

if !c.secureChecker(ctx)(ctx, ar.GetRedirectURI()) {
return errorsx.WithStack(fosite.ErrInvalidRequest.WithHint("Redirect URL is using an insecure protocol, http is only allowed for hosts with suffix 'localhost', for example: http://myapp.localhost/."))
}
Expand All @@ -76,14 +65,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()))
}

Expand All @@ -97,7 +86,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
}
Expand Down
Loading

0 comments on commit 222821e

Please sign in to comment.