forked from ory/fosite
-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: add the OIDC handler for device flow
- Loading branch information
1 parent
3be7722
commit 8b3fbb6
Showing
11 changed files
with
830 additions
and
35 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
// Copyright © 2024 Ory Corp | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package openid | ||
|
||
import ( | ||
"context" | ||
|
||
"github.com/ory/fosite/handler/rfc8628" | ||
|
||
"github.com/ory/fosite" | ||
"github.com/ory/x/errorsx" | ||
) | ||
|
||
// OpenIDConnectDeviceHandler a response handler for the Device Authorization Grant with OpenID Connect identity layer | ||
type OpenIDConnectDeviceHandler struct { | ||
OpenIDConnectRequestStorage OpenIDConnectRequestStorage | ||
OpenIDConnectRequestValidator *OpenIDConnectRequestValidator | ||
DeviceCodeStrategy rfc8628.DeviceCodeStrategy | ||
|
||
Config interface { | ||
fosite.IDTokenLifespanProvider | ||
} | ||
|
||
*IDTokenHandleHelper | ||
} | ||
|
||
func (c *OpenIDConnectDeviceHandler) HandleDeviceEndpointRequest(ctx context.Context, dar fosite.DeviceRequester, resp fosite.DeviceResponder) error { | ||
if !(dar.GetGrantedScopes().Has("openid")) { | ||
return nil | ||
} | ||
|
||
if !dar.GetClient().GetGrantTypes().Has(string(fosite.GrantTypeDeviceCode)) { | ||
return nil | ||
} | ||
|
||
if len(resp.GetDeviceCode()) == 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, dar); err != nil { | ||
return err | ||
} | ||
|
||
signature, err := c.DeviceCodeStrategy.DeviceCodeSignature(ctx, resp.GetDeviceCode()) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if err := c.OpenIDConnectRequestStorage.CreateOpenIDConnectSession(ctx, signature, dar.Sanitize(oidcParameters)); err != nil { | ||
return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) | ||
} | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
// Copyright © 2024 Ory Corp | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package openid | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"testing" | ||
"time" | ||
|
||
"github.com/golang/mock/gomock" | ||
"github.com/ory/fosite/internal" | ||
"github.com/pkg/errors" | ||
|
||
"github.com/stretchr/testify/require" | ||
|
||
"github.com/coocood/freecache" | ||
|
||
"github.com/ory/fosite" | ||
"github.com/ory/fosite/handler/rfc8628" | ||
"github.com/ory/fosite/token/hmac" | ||
"github.com/ory/fosite/token/jwt" | ||
) | ||
|
||
func TestDeviceAuth_HandleDeviceEndpointRequest(t *testing.T) { | ||
ctrl := gomock.NewController(t) | ||
defer ctrl.Finish() | ||
store := internal.NewMockOpenIDConnectRequestStorage(ctrl) | ||
|
||
config := &fosite.Config{ | ||
MinParameterEntropy: fosite.MinParameterEntropy, | ||
DeviceAndUserCodeLifespan: time.Hour * 24, | ||
} | ||
|
||
signer := &jwt.DefaultSigner{ | ||
GetPrivateKey: func(ctx context.Context) (interface{}, error) { | ||
return key, nil | ||
}, | ||
} | ||
|
||
h := OpenIDConnectDeviceHandler{ | ||
OpenIDConnectRequestStorage: store, | ||
OpenIDConnectRequestValidator: NewOpenIDConnectRequestValidator(signer, config), | ||
DeviceCodeStrategy: &rfc8628.DefaultDeviceStrategy{ | ||
Enigma: &hmac.HMACStrategy{Config: &fosite.Config{GlobalSecret: []byte("foobar")}}, | ||
RateLimiterCache: freecache.NewCache(1024 * 1024), | ||
Config: config, | ||
}, | ||
Config: config, | ||
IDTokenHandleHelper: &IDTokenHandleHelper{ | ||
IDTokenStrategy: &DefaultStrategy{ | ||
Signer: signer, | ||
Config: config, | ||
}, | ||
}, | ||
} | ||
|
||
session := &DefaultSession{ | ||
Claims: &jwt.IDTokenClaims{ | ||
Subject: "foo", | ||
}, | ||
Headers: &jwt.Headers{}, | ||
} | ||
|
||
client := &fosite.DefaultClient{ | ||
ID: "foo", | ||
GrantTypes: fosite.Arguments{string(fosite.GrantTypeDeviceCode)}, | ||
} | ||
|
||
testCases := []struct { | ||
description string | ||
authreq *fosite.DeviceRequest | ||
authresp *fosite.DeviceResponse | ||
setup func(authreq *fosite.DeviceRequest) | ||
expectErr error | ||
}{ | ||
{ | ||
description: "should ignore because scope openid is not set", | ||
authreq: &fosite.DeviceRequest{ | ||
Request: fosite.Request{ | ||
GrantedScope: fosite.Arguments{"email"}, | ||
}, | ||
}, | ||
}, | ||
{ | ||
description: "should ignore because client grant type is invalid", | ||
authreq: &fosite.DeviceRequest{ | ||
Request: fosite.Request{ | ||
GrantedScope: fosite.Arguments{"openid", "email"}, | ||
Client: &fosite.DefaultClient{ | ||
GrantTypes: []string{string(fosite.GrantTypeAuthorizationCode)}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
{ | ||
description: "should fail because device code is not issued", | ||
authreq: &fosite.DeviceRequest{ | ||
Request: fosite.Request{ | ||
GrantedScope: fosite.Arguments{"openid", "email"}, | ||
Client: client, | ||
}, | ||
}, | ||
authresp: &fosite.DeviceResponse{}, | ||
expectErr: fosite.ErrMisconfiguration, | ||
}, | ||
{ | ||
description: "should fail because cannot create session", | ||
authreq: &fosite.DeviceRequest{ | ||
Request: fosite.Request{ | ||
GrantedScope: fosite.Arguments{"openid", "email"}, | ||
Client: client, | ||
Session: session, | ||
}, | ||
}, | ||
authresp: &fosite.DeviceResponse{ | ||
DeviceCode: "device_code", | ||
}, | ||
setup: func(authreq *fosite.DeviceRequest) { | ||
store. | ||
EXPECT(). | ||
CreateOpenIDConnectSession(gomock.Any(), gomock.Any(), gomock.Eq(authreq.Sanitize(oidcParameters))). | ||
Return(errors.New("")) | ||
}, | ||
expectErr: fosite.ErrServerError, | ||
}, | ||
{ | ||
description: "should pass", | ||
authreq: &fosite.DeviceRequest{ | ||
Request: fosite.Request{ | ||
GrantedScope: fosite.Arguments{"openid", "email"}, | ||
Client: client, | ||
Session: session, | ||
}, | ||
}, | ||
authresp: &fosite.DeviceResponse{ | ||
DeviceCode: "device_code", | ||
}, | ||
setup: func(authreq *fosite.DeviceRequest) { | ||
store. | ||
EXPECT(). | ||
CreateOpenIDConnectSession(gomock.Any(), gomock.Any(), gomock.Eq(authreq.Sanitize(oidcParameters))). | ||
Return(nil) | ||
}, | ||
}, | ||
} | ||
|
||
for i, testCase := range testCases { | ||
t.Run(fmt.Sprintf("case=%d/description=%s", i, testCase.description), func(t *testing.T) { | ||
if testCase.setup != nil { | ||
testCase.setup(testCase.authreq) | ||
} | ||
|
||
err := h.HandleDeviceEndpointRequest(context.Background(), testCase.authreq, testCase.authresp) | ||
if testCase.expectErr != nil { | ||
require.EqualError(t, err, testCase.expectErr.Error(), "%+v", err) | ||
} else { | ||
require.NoError(t, err, "%+v", err) | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
// Copyright © 2024 Ory Corp | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package openid | ||
|
||
import ( | ||
"context" | ||
|
||
"github.com/pkg/errors" | ||
|
||
"github.com/ory/fosite" | ||
"github.com/ory/x/errorsx" | ||
) | ||
|
||
func (c *OpenIDConnectDeviceHandler) HandleTokenEndpointRequest(ctx context.Context, requester 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) | ||
} | ||
|
||
deviceCode := requester.GetRequestForm().Get("device_code") | ||
signature, err := c.DeviceCodeStrategy.DeviceCodeSignature(ctx, deviceCode) | ||
ar, err := c.OpenIDConnectRequestStorage.GetOpenIDConnectSession(ctx, signature, requester) | ||
if errors.Is(err, ErrNoSessionFound) { | ||
return errorsx.WithStack(fosite.ErrUnknownRequest.WithWrap(err).WithDebug(err.Error())) | ||
} | ||
if err != nil { | ||
return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) | ||
} | ||
|
||
if !ar.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(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\".")) | ||
} | ||
|
||
session, ok := ar.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 := session.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) | ||
|
||
idTokenLifespan := fosite.GetEffectiveLifespan(requester.GetClient(), fosite.GrantTypeDeviceCode, fosite.IDToken, c.Config.GetIDTokenLifespan(ctx)) | ||
return c.IssueExplicitIDToken(ctx, idTokenLifespan, ar, 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(string(fosite.GrantTypeDeviceCode)) | ||
} |
Oops, something went wrong.