Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement the access token handling for device authorization flow #5

Merged
merged 6 commits into from
Mar 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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{})
wood-push-melon marked this conversation as resolved.
Show resolved Hide resolved
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
Loading