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 3 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
36 changes: 26 additions & 10 deletions compose/compose_oauth2.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,32 @@ 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{
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,
},
}
}

Expand Down Expand Up @@ -96,7 +112,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
23 changes: 22 additions & 1 deletion compose/compose_rfc8628.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,36 @@ 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{
CodeTokenEndpointHandler: &rfc8628.DeviceTokenHandler{
DeviceRateLimitStrategy: strategy.(rfc8628.DeviceRateLimitStrategy),
DeviceCodeStrategy: strategy.(rfc8628.DeviceCodeStrategy),
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
24 changes: 24 additions & 0 deletions device_user_request.go
wood-push-melon marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright © 2024 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(),
}
}
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
11 changes: 6 additions & 5 deletions fosite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,24 @@ 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.AuthorizeExplicitGrantHandler{}
wood-push-melon marked this conversation as resolved.
Show resolved Hide resolved
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
8 changes: 4 additions & 4 deletions handler.go
wood-push-melon marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
)

type AuthorizeEndpointHandler interface {
// HandleAuthorizeRequest handles an authorize endpoint request. To extend the handler's capabilities, the http request
// HandleAuthorizeEndpointRequest handles an 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.
//
Expand All @@ -31,12 +31,12 @@ type TokenEndpointHandler interface {
// the request, this method should return ErrUnknownRequest and otherwise handle the request.
HandleTokenEndpointRequest(ctx context.Context, requester AccessRequester) error

// CanSkipClientAuth indicates if client authentication can be skipped. By default it MUST be false, unless you are
// CanSkipClientAuth indicates if client authentication can be skipped. By default, it MUST be false, unless you are
// implementing extension grant type, which allows unauthenticated client. CanSkipClientAuth must be called
// before HandleTokenEndpointRequest to decide, if AccessRequester will contain authenticated client.
CanSkipClientAuth(ctx context.Context, requester AccessRequester) bool

// CanHandleRequest indicates, if TokenEndpointHandler can handle this request or not. If true,
// CanHandleTokenEndpointRequest indicates, if TokenEndpointHandler can handle this request or not. If true,
// HandleTokenEndpointRequest can be called.
CanHandleTokenEndpointRequest(ctx context.Context, requester AccessRequester) bool
}
Expand All @@ -61,7 +61,7 @@ type RevocationHandler interface {

// PushedAuthorizeEndpointHandler is the interface that handles PAR (https://datatracker.ietf.org/doc/html/rfc9126)
type PushedAuthorizeEndpointHandler interface {
// HandlePushedAuthorizeRequest handles a pushed authorize endpoint request. To extend the handler's capabilities, the http request
// HandlePushedAuthorizeEndpointRequest handles a pushed 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 pushed authorize request, he must return nil and NOT modify session nor responder neither requester.
HandlePushedAuthorizeEndpointRequest(ctx context.Context, requester AuthorizeRequester, responder PushedAuthorizeResponder) error
Expand Down
Loading
Loading