Skip to content

Commit

Permalink
feat: token request handling for device flow
Browse files Browse the repository at this point in the history
  • Loading branch information
wood-push-melon committed Mar 25, 2024
1 parent 0393fd7 commit c7fa549
Show file tree
Hide file tree
Showing 23 changed files with 229 additions and 24 deletions.
4 changes: 3 additions & 1 deletion fositex/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ type Config struct {

var defaultResponseModeHandler = fosite.NewDefaultResponseModeHandler()
var defaultFactories = []Factory{
compose.OAuth2AuthorizeExplicitFactory,
compose.OAuth2AuthorizeExplicitAuthFactory,
compose.Oauth2AuthorizeExplicitTokenFactory,
compose.OAuth2AuthorizeImplicitFactory,
compose.OAuth2ClientCredentialsGrantFactory,
compose.OAuth2RefreshTokenGrantFactory,
Expand All @@ -63,6 +64,7 @@ var defaultFactories = []Factory{
compose.RFC7523AssertionGrantFactory,
compose.OIDCUserinfoVerifiableCredentialFactory,
compose.RFC8628DeviceFactory,
compose.RFC8628DeviceAuthorizationTokenFactory,
}

func NewConfig(deps configDependencies) *Config {
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -256,4 +256,4 @@ require (
gopkg.in/yaml.v3 v3.0.1 // indirect
)

replace github.com/ory/fosite => github.com/canonical/fosite v0.0.0-20240227091618-fee676b7da75
replace github.com/ory/fosite => github.com/canonical/fosite v0.0.0-20240325192919-3c931a9affa9
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4Yn
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M=
github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0=
github.com/canonical/fosite v0.0.0-20240227091618-fee676b7da75 h1:LkVhc3oBLhYdNKhfbf4988ayFZf+kdN9bmy8NLQ0jnQ=
github.com/canonical/fosite v0.0.0-20240227091618-fee676b7da75/go.mod h1:G0cHiNH8Q4zdddEWosFlZDBjcphr8nlNSUMfz3iSzgo=
github.com/canonical/fosite v0.0.0-20240325192919-3c931a9affa9 h1:o20vsxPInTwm4//fTfA2WaFrV+BI5uGt2IvK/p1xLgs=
github.com/canonical/fosite v0.0.0-20240325192919-3c931a9affa9/go.mod h1:G0cHiNH8Q4zdddEWosFlZDBjcphr8nlNSUMfz3iSzgo=
github.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M=
github.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs=
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
"consent_challenge": "",
"exclude_not_before_claim": false,
"allowed_top_level_claims": [],
"mirror_top_level_claims": true
"mirror_top_level_claims": true,
"browser_flow_completed": false
},
"requester": {
"client_id": "app-client",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
"consent_challenge": "",
"exclude_not_before_claim": false,
"allowed_top_level_claims": [],
"mirror_top_level_claims": true
"mirror_top_level_claims": true,
"browser_flow_completed": false
},
"request": {
"client_id": "app-client",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
"consent_challenge": "",
"exclude_not_before_claim": false,
"allowed_top_level_claims": [],
"mirror_top_level_claims": true
"mirror_top_level_claims": true,
"browser_flow_completed": false
},
"requester": {
"client_id": "app-client",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
"consent_challenge": "",
"exclude_not_before_claim": false,
"allowed_top_level_claims": [],
"mirror_top_level_claims": true
"mirror_top_level_claims": true,
"browser_flow_completed": false
},
"request": {
"client_id": "app-client",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
"consent_challenge": "",
"exclude_not_before_claim": false,
"allowed_top_level_claims": [],
"mirror_top_level_claims": true
"mirror_top_level_claims": true,
"browser_flow_completed": false
},
"requester": {
"client_id": "app-client",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
"consent_challenge": "",
"exclude_not_before_claim": false,
"allowed_top_level_claims": [],
"mirror_top_level_claims": true
"mirror_top_level_claims": true,
"browser_flow_completed": false
},
"request": {
"client_id": "app-client",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
"consent_challenge": "",
"exclude_not_before_claim": false,
"allowed_top_level_claims": [],
"mirror_top_level_claims": true
"mirror_top_level_claims": true,
"browser_flow_completed": false
},
"requester": {
"client_id": "app-client",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
"consent_challenge": "",
"exclude_not_before_claim": false,
"allowed_top_level_claims": [],
"mirror_top_level_claims": true
"mirror_top_level_claims": true,
"browser_flow_completed": false
},
"request": {
"client_id": "app-client",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
"consent_challenge": "",
"exclude_not_before_claim": false,
"allowed_top_level_claims": [],
"mirror_top_level_claims": true
"mirror_top_level_claims": true,
"browser_flow_completed": false
},
"requester": {
"client_id": "app-client",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
"consent_challenge": "",
"exclude_not_before_claim": false,
"allowed_top_level_claims": [],
"mirror_top_level_claims": true
"mirror_top_level_claims": true,
"browser_flow_completed": false
},
"request": {
"client_id": "app-client",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
"consent_challenge": "",
"exclude_not_before_claim": false,
"allowed_top_level_claims": [],
"mirror_top_level_claims": true
"mirror_top_level_claims": true,
"browser_flow_completed": false
},
"requester": {
"client_id": "app-client",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
"consent_challenge": "",
"exclude_not_before_claim": false,
"allowed_top_level_claims": [],
"mirror_top_level_claims": true
"mirror_top_level_claims": true,
"browser_flow_completed": false
},
"request": {
"client_id": "app-client",
Expand Down
3 changes: 2 additions & 1 deletion oauth2/.snapshots/TestUnmarshalSession-v1.11.8.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,6 @@
"zone",
"login_session_id"
],
"mirror_top_level_claims": false
"mirror_top_level_claims": false,
"browser_flow_completed": false
}
3 changes: 2 additions & 1 deletion oauth2/.snapshots/TestUnmarshalSession-v1.11.9.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,6 @@
"zone",
"login_session_id"
],
"mirror_top_level_claims": false
"mirror_top_level_claims": false,
"browser_flow_completed": false
}
3 changes: 2 additions & 1 deletion oauth2/fixtures/v1.11.8-session.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,6 @@
"market",
"zone",
"login_session_id"
]
],
"BrowserFlowCompleted": false
}
3 changes: 2 additions & 1 deletion oauth2/fixtures/v1.11.9-session.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,6 @@
"market",
"zone",
"login_session_id"
]
],
"browser_flow_completed": false
}
14 changes: 11 additions & 3 deletions oauth2/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -832,7 +832,9 @@ func (h *Handler) oAuth2DeviceFlow(w http.ResponseWriter, r *http.Request) {
// device_challenge and device_verifier
var session = &Session{
DefaultSession: &openid.DefaultSession{
Headers: &jwt.Headers{}},
Headers: &jwt.Headers{},
},
BrowserFlowCompleted: false,
}

resp, err := h.r.OAuth2Provider().NewDeviceResponse(ctx, request, session)
Expand Down Expand Up @@ -1354,7 +1356,7 @@ func (h *Handler) updateSessionWithRequest(ctx context.Context, session *flow.Ac
}
claims.Add("sid", session.ConsentRequest.LoginSessionID)

return &Session{
s := &Session{
DefaultSession: &openid.DefaultSession{
Claims: claims,
Headers: &jwt.Headers{Extra: map[string]interface{}{
Expand All @@ -1371,7 +1373,13 @@ func (h *Handler) updateSessionWithRequest(ctx context.Context, session *flow.Ac
AllowedTopLevelClaims: h.c.AllowedTopLevelClaims(ctx),
MirrorTopLevelClaims: h.c.MirrorTopLevelClaims(ctx),
Flow: flow,
}, nil
}

if _, ok := request.(*fosite.DeviceRequest); ok {
s.BrowserFlowCompleted = true
}

return s, nil
}

func (h *Handler) logOrAudit(err error, r *http.Request) {
Expand Down
2 changes: 1 addition & 1 deletion oauth2/oauth2_auth_code_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1345,7 +1345,7 @@ func TestAuthCodeWithMockStrategy(t *testing.T) {
TokenURL: ts.URL + "/oauth2/token",
},
RedirectURL: ts.URL + "/callback",
Scopes: []string{"hydra.*", "offline", "openid"},
Scopes: []string{"offline", "openid", "hydra.*"},
}

var code string
Expand Down
166 changes: 166 additions & 0 deletions oauth2/oauth2_device_code_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
// Copyright © 2024 Ory Corp
// SPDX-License-Identifier: Apache-2.0

package oauth2_test

import (
"context"
"strings"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/oauth2"

"github.com/ory/fosite"
"github.com/ory/fosite/handler/openid"
"github.com/ory/hydra/v2/client"
"github.com/ory/hydra/v2/internal"
"github.com/ory/hydra/v2/internal/testhelpers"
hydraoauth2 "github.com/ory/hydra/v2/oauth2"
"github.com/ory/x/contextx"
)

func TestDeviceAuthRequest(t *testing.T) {
ctx := context.Background()
reg := internal.NewMockedRegistry(t, &contextx.Default{})
testhelpers.NewOAuth2Server(ctx, t, reg)

c := &client.Client{
ResponseTypes: []string{"id_token", "code", "token"},
GrantTypes: []string{
string(fosite.GrantTypeDeviceCode),
},
Scope: "hydra offline openid",
Audience: []string{"https://api.ory.sh/"},
TokenEndpointAuthMethod: "none",
}
require.NoError(t, reg.ClientManager().CreateClient(ctx, c))

oauthClient := &oauth2.Config{
ClientID: c.GetID(),
Endpoint: oauth2.Endpoint{
DeviceAuthURL: reg.Config().OAuth2DeviceAuthorisationURL(ctx).String(),
TokenURL: reg.Config().OAuth2TokenURL(ctx).String(),
},
Scopes: strings.Split(c.Scope, " "),
}

testCases := []struct {
description string
setUp func()
check func(t *testing.T, resp *oauth2.DeviceAuthResponse, err error)
cleanUp func()
}{
{
description: "should pass",
check: func(t *testing.T, resp *oauth2.DeviceAuthResponse, _ error) {
assert.NotEmpty(t, resp.DeviceCode)
assert.NotEmpty(t, resp.UserCode)
assert.NotEmpty(t, resp.Interval)
assert.NotEmpty(t, resp.VerificationURI)
assert.NotEmpty(t, resp.VerificationURIComplete)
},
},
}

for _, testCase := range testCases {
t.Run("case="+testCase.description, func(t *testing.T) {
if testCase.setUp != nil {
testCase.setUp()
}

resp, err := oauthClient.DeviceAuth(context.Background())

if testCase.check != nil {
testCase.check(t, resp, err)
}

if testCase.cleanUp != nil {
testCase.cleanUp()
}
})
}
}

func TestDeviceTokenRequest(t *testing.T) {
ctx := context.Background()
reg := internal.NewMockedRegistry(t, &contextx.Default{})
testhelpers.NewOAuth2Server(ctx, t, reg)

c := &client.Client{
GrantTypes: []string{
string(fosite.GrantTypeDeviceCode),
},
Scope: "hydra offline openid",
Audience: []string{"https://api.ory.sh/"},
TokenEndpointAuthMethod: "none",
}
require.NoError(t, reg.ClientManager().CreateClient(ctx, c))

oauthClient := &oauth2.Config{
ClientID: c.GetID(),
Endpoint: oauth2.Endpoint{
DeviceAuthURL: reg.Config().OAuth2DeviceAuthorisationURL(ctx).String(),
TokenURL: reg.Config().OAuth2TokenURL(ctx).String(),
},
Scopes: strings.Split(c.Scope, " "),
}

var code, signature string
var err error
code, signature, err = reg.RFC8628HMACStrategy().GenerateDeviceCode(context.TODO())
require.NoError(t, err)

testCases := []struct {
description string
setUp func()
check func(t *testing.T, token *oauth2.Token, err error)
cleanUp func()
}{
{
description: "should pass",
setUp: func() {
authreq := &fosite.DeviceRequest{
Request: fosite.Request{
Client: &fosite.DefaultClient{ID: c.GetID(), GrantTypes: []string{string(fosite.GrantTypeDeviceCode)}},
Session: &hydraoauth2.Session{
DefaultSession: &openid.DefaultSession{
ExpiresAt: map[fosite.TokenType]time.Time{
fosite.DeviceCode: time.Now().Add(time.Hour).UTC(),
},
},
BrowserFlowCompleted: true,
},
RequestedAt: time.Now(),
},
}

require.NoError(t, reg.OAuth2Storage().CreateDeviceCodeSession(context.TODO(), signature, authreq))
},
check: func(t *testing.T, token *oauth2.Token, err error) {
assert.NotEmpty(t, token.AccessToken)
},
},
}

for _, testCase := range testCases {
t.Run("case="+testCase.description, func(t *testing.T) {
if testCase.setUp != nil {
testCase.setUp()
}

var token *oauth2.Token
token, err = oauthClient.DeviceAccessToken(context.Background(), &oauth2.DeviceAuthResponse{DeviceCode: code})

if testCase.check != nil {
testCase.check(t, token, err)
}

if testCase.cleanUp != nil {
testCase.cleanUp()
}
})
}
}
Loading

0 comments on commit c7fa549

Please sign in to comment.