From c7fa549a906443f37cba51f143df0453240d1648 Mon Sep 17 00:00:00 2001 From: dushu Date: Sat, 23 Mar 2024 16:54:13 -0600 Subject: [PATCH] feat: token request handling for device flow --- fositex/config.go | 4 +- go.mod | 2 +- go.sum | 4 +- ..._token_hook_if_configured-hook=legacy.json | 3 +- ...esh_token_hook_if_configured-hook=new.json | 3 +- ..._token_hook_if_configured-hook=legacy.json | 3 +- ...esh_token_hook_if_configured-hook=new.json | 3 +- ..._token_hook_if_configured-hook=legacy.json | 3 +- ...esh_token_hook_if_configured-hook=new.json | 3 +- ..._token_hook_if_configured-hook=legacy.json | 3 +- ...esh_token_hook_if_configured-hook=new.json | 3 +- ..._token_hook_if_configured-hook=legacy.json | 3 +- ...esh_token_hook_if_configured-hook=new.json | 3 +- ..._token_hook_if_configured-hook=legacy.json | 3 +- ...esh_token_hook_if_configured-hook=new.json | 3 +- .../TestUnmarshalSession-v1.11.8.json | 3 +- .../TestUnmarshalSession-v1.11.9.json | 3 +- oauth2/fixtures/v1.11.8-session.json | 3 +- oauth2/fixtures/v1.11.9-session.json | 3 +- oauth2/handler.go | 14 +- oauth2/oauth2_auth_code_test.go | 2 +- oauth2/oauth2_device_code_test.go | 166 ++++++++++++++++++ oauth2/session.go | 13 ++ 23 files changed, 229 insertions(+), 24 deletions(-) create mode 100644 oauth2/oauth2_device_code_test.go diff --git a/fositex/config.go b/fositex/config.go index 4300077ea99..bec3770af61 100644 --- a/fositex/config.go +++ b/fositex/config.go @@ -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, @@ -63,6 +64,7 @@ var defaultFactories = []Factory{ compose.RFC7523AssertionGrantFactory, compose.OIDCUserinfoVerifiableCredentialFactory, compose.RFC8628DeviceFactory, + compose.RFC8628DeviceAuthorizationTokenFactory, } func NewConfig(deps configDependencies) *Config { diff --git a/go.mod b/go.mod index bca9c41a482..4820db8cd7c 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index cfcfaddd6df..c0a483755e2 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=jwt-case=0-description=should_pass_request_if_strategy_passes-should_call_refresh_token_hook_if_configured-hook=legacy.json b/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=jwt-case=0-description=should_pass_request_if_strategy_passes-should_call_refresh_token_hook_if_configured-hook=legacy.json index 61dfba78726..a687c788d99 100644 --- a/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=jwt-case=0-description=should_pass_request_if_strategy_passes-should_call_refresh_token_hook_if_configured-hook=legacy.json +++ b/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=jwt-case=0-description=should_pass_request_if_strategy_passes-should_call_refresh_token_hook_if_configured-hook=legacy.json @@ -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", diff --git a/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=jwt-case=0-description=should_pass_request_if_strategy_passes-should_call_refresh_token_hook_if_configured-hook=new.json b/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=jwt-case=0-description=should_pass_request_if_strategy_passes-should_call_refresh_token_hook_if_configured-hook=new.json index 1133cbe7f21..80b5ab3031c 100644 --- a/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=jwt-case=0-description=should_pass_request_if_strategy_passes-should_call_refresh_token_hook_if_configured-hook=new.json +++ b/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=jwt-case=0-description=should_pass_request_if_strategy_passes-should_call_refresh_token_hook_if_configured-hook=new.json @@ -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", diff --git a/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=jwt-case=2-description=should_pass_because_prompt=none_and_max_age_is_less_than_auth_time-should_call_refresh_token_hook_if_configured-hook=legacy.json b/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=jwt-case=2-description=should_pass_because_prompt=none_and_max_age_is_less_than_auth_time-should_call_refresh_token_hook_if_configured-hook=legacy.json index 61dfba78726..a687c788d99 100644 --- a/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=jwt-case=2-description=should_pass_because_prompt=none_and_max_age_is_less_than_auth_time-should_call_refresh_token_hook_if_configured-hook=legacy.json +++ b/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=jwt-case=2-description=should_pass_because_prompt=none_and_max_age_is_less_than_auth_time-should_call_refresh_token_hook_if_configured-hook=legacy.json @@ -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", diff --git a/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=jwt-case=2-description=should_pass_because_prompt=none_and_max_age_is_less_than_auth_time-should_call_refresh_token_hook_if_configured-hook=new.json b/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=jwt-case=2-description=should_pass_because_prompt=none_and_max_age_is_less_than_auth_time-should_call_refresh_token_hook_if_configured-hook=new.json index 1133cbe7f21..80b5ab3031c 100644 --- a/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=jwt-case=2-description=should_pass_because_prompt=none_and_max_age_is_less_than_auth_time-should_call_refresh_token_hook_if_configured-hook=new.json +++ b/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=jwt-case=2-description=should_pass_because_prompt=none_and_max_age_is_less_than_auth_time-should_call_refresh_token_hook_if_configured-hook=new.json @@ -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", diff --git a/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=jwt-case=5-description=should_pass_with_prompt=login_when_authentication_time_is_recent-should_call_refresh_token_hook_if_configured-hook=legacy.json b/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=jwt-case=5-description=should_pass_with_prompt=login_when_authentication_time_is_recent-should_call_refresh_token_hook_if_configured-hook=legacy.json index 61dfba78726..a687c788d99 100644 --- a/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=jwt-case=5-description=should_pass_with_prompt=login_when_authentication_time_is_recent-should_call_refresh_token_hook_if_configured-hook=legacy.json +++ b/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=jwt-case=5-description=should_pass_with_prompt=login_when_authentication_time_is_recent-should_call_refresh_token_hook_if_configured-hook=legacy.json @@ -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", diff --git a/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=jwt-case=5-description=should_pass_with_prompt=login_when_authentication_time_is_recent-should_call_refresh_token_hook_if_configured-hook=new.json b/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=jwt-case=5-description=should_pass_with_prompt=login_when_authentication_time_is_recent-should_call_refresh_token_hook_if_configured-hook=new.json index 1133cbe7f21..80b5ab3031c 100644 --- a/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=jwt-case=5-description=should_pass_with_prompt=login_when_authentication_time_is_recent-should_call_refresh_token_hook_if_configured-hook=new.json +++ b/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=jwt-case=5-description=should_pass_with_prompt=login_when_authentication_time_is_recent-should_call_refresh_token_hook_if_configured-hook=new.json @@ -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", diff --git a/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=opaque-case=0-description=should_pass_request_if_strategy_passes-should_call_refresh_token_hook_if_configured-hook=legacy.json b/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=opaque-case=0-description=should_pass_request_if_strategy_passes-should_call_refresh_token_hook_if_configured-hook=legacy.json index 61dfba78726..a687c788d99 100644 --- a/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=opaque-case=0-description=should_pass_request_if_strategy_passes-should_call_refresh_token_hook_if_configured-hook=legacy.json +++ b/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=opaque-case=0-description=should_pass_request_if_strategy_passes-should_call_refresh_token_hook_if_configured-hook=legacy.json @@ -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", diff --git a/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=opaque-case=0-description=should_pass_request_if_strategy_passes-should_call_refresh_token_hook_if_configured-hook=new.json b/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=opaque-case=0-description=should_pass_request_if_strategy_passes-should_call_refresh_token_hook_if_configured-hook=new.json index 1133cbe7f21..80b5ab3031c 100644 --- a/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=opaque-case=0-description=should_pass_request_if_strategy_passes-should_call_refresh_token_hook_if_configured-hook=new.json +++ b/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=opaque-case=0-description=should_pass_request_if_strategy_passes-should_call_refresh_token_hook_if_configured-hook=new.json @@ -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", diff --git a/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=opaque-case=2-description=should_pass_because_prompt=none_and_max_age_is_less_than_auth_time-should_call_refresh_token_hook_if_configured-hook=legacy.json b/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=opaque-case=2-description=should_pass_because_prompt=none_and_max_age_is_less_than_auth_time-should_call_refresh_token_hook_if_configured-hook=legacy.json index 61dfba78726..a687c788d99 100644 --- a/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=opaque-case=2-description=should_pass_because_prompt=none_and_max_age_is_less_than_auth_time-should_call_refresh_token_hook_if_configured-hook=legacy.json +++ b/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=opaque-case=2-description=should_pass_because_prompt=none_and_max_age_is_less_than_auth_time-should_call_refresh_token_hook_if_configured-hook=legacy.json @@ -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", diff --git a/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=opaque-case=2-description=should_pass_because_prompt=none_and_max_age_is_less_than_auth_time-should_call_refresh_token_hook_if_configured-hook=new.json b/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=opaque-case=2-description=should_pass_because_prompt=none_and_max_age_is_less_than_auth_time-should_call_refresh_token_hook_if_configured-hook=new.json index 1133cbe7f21..80b5ab3031c 100644 --- a/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=opaque-case=2-description=should_pass_because_prompt=none_and_max_age_is_less_than_auth_time-should_call_refresh_token_hook_if_configured-hook=new.json +++ b/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=opaque-case=2-description=should_pass_because_prompt=none_and_max_age_is_less_than_auth_time-should_call_refresh_token_hook_if_configured-hook=new.json @@ -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", diff --git a/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=opaque-case=5-description=should_pass_with_prompt=login_when_authentication_time_is_recent-should_call_refresh_token_hook_if_configured-hook=legacy.json b/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=opaque-case=5-description=should_pass_with_prompt=login_when_authentication_time_is_recent-should_call_refresh_token_hook_if_configured-hook=legacy.json index 61dfba78726..a687c788d99 100644 --- a/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=opaque-case=5-description=should_pass_with_prompt=login_when_authentication_time_is_recent-should_call_refresh_token_hook_if_configured-hook=legacy.json +++ b/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=opaque-case=5-description=should_pass_with_prompt=login_when_authentication_time_is_recent-should_call_refresh_token_hook_if_configured-hook=legacy.json @@ -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", diff --git a/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=opaque-case=5-description=should_pass_with_prompt=login_when_authentication_time_is_recent-should_call_refresh_token_hook_if_configured-hook=new.json b/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=opaque-case=5-description=should_pass_with_prompt=login_when_authentication_time_is_recent-should_call_refresh_token_hook_if_configured-hook=new.json index 1133cbe7f21..80b5ab3031c 100644 --- a/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=opaque-case=5-description=should_pass_with_prompt=login_when_authentication_time_is_recent-should_call_refresh_token_hook_if_configured-hook=new.json +++ b/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=opaque-case=5-description=should_pass_with_prompt=login_when_authentication_time_is_recent-should_call_refresh_token_hook_if_configured-hook=new.json @@ -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", diff --git a/oauth2/.snapshots/TestUnmarshalSession-v1.11.8.json b/oauth2/.snapshots/TestUnmarshalSession-v1.11.8.json index d57fb916727..6852d3ea459 100644 --- a/oauth2/.snapshots/TestUnmarshalSession-v1.11.8.json +++ b/oauth2/.snapshots/TestUnmarshalSession-v1.11.8.json @@ -46,5 +46,6 @@ "zone", "login_session_id" ], - "mirror_top_level_claims": false + "mirror_top_level_claims": false, + "browser_flow_completed": false } diff --git a/oauth2/.snapshots/TestUnmarshalSession-v1.11.9.json b/oauth2/.snapshots/TestUnmarshalSession-v1.11.9.json index d57fb916727..6852d3ea459 100644 --- a/oauth2/.snapshots/TestUnmarshalSession-v1.11.9.json +++ b/oauth2/.snapshots/TestUnmarshalSession-v1.11.9.json @@ -46,5 +46,6 @@ "zone", "login_session_id" ], - "mirror_top_level_claims": false + "mirror_top_level_claims": false, + "browser_flow_completed": false } diff --git a/oauth2/fixtures/v1.11.8-session.json b/oauth2/fixtures/v1.11.8-session.json index a7070d03c32..45f06f2e88e 100644 --- a/oauth2/fixtures/v1.11.8-session.json +++ b/oauth2/fixtures/v1.11.8-session.json @@ -43,5 +43,6 @@ "market", "zone", "login_session_id" - ] + ], + "BrowserFlowCompleted": false } diff --git a/oauth2/fixtures/v1.11.9-session.json b/oauth2/fixtures/v1.11.9-session.json index 2ded034a556..e3ad1c20778 100644 --- a/oauth2/fixtures/v1.11.9-session.json +++ b/oauth2/fixtures/v1.11.9-session.json @@ -43,5 +43,6 @@ "market", "zone", "login_session_id" - ] + ], + "browser_flow_completed": false } diff --git a/oauth2/handler.go b/oauth2/handler.go index 7c6d8384f48..c9e4abcacef 100644 --- a/oauth2/handler.go +++ b/oauth2/handler.go @@ -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) @@ -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{}{ @@ -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) { diff --git a/oauth2/oauth2_auth_code_test.go b/oauth2/oauth2_auth_code_test.go index 69e9d42e427..8f33b16f835 100644 --- a/oauth2/oauth2_auth_code_test.go +++ b/oauth2/oauth2_auth_code_test.go @@ -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 diff --git a/oauth2/oauth2_device_code_test.go b/oauth2/oauth2_device_code_test.go new file mode 100644 index 00000000000..022b79f45f6 --- /dev/null +++ b/oauth2/oauth2_device_code_test.go @@ -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() + } + }) + } +} diff --git a/oauth2/session.go b/oauth2/session.go index 7908246029f..808a519a825 100644 --- a/oauth2/session.go +++ b/oauth2/session.go @@ -34,6 +34,7 @@ type Session struct { ExcludeNotBeforeClaim bool `json:"exclude_not_before_claim"` AllowedTopLevelClaims []string `json:"allowed_top_level_claims"` MirrorTopLevelClaims bool `json:"mirror_top_level_claims"` + BrowserFlowCompleted bool `json:"browser_flow_completed"` Flow *flow.Flow `json:"-"` } @@ -192,3 +193,15 @@ func (s *Session) UnmarshalJSON(original []byte) (err error) { return nil } + +func (s *Session) GetBrowserFlowCompleted() bool { + if s == nil { + return false + } + + return s.BrowserFlowCompleted +} + +func (s *Session) SetBrowserFlowCompleted(flag bool) { + s.BrowserFlowCompleted = flag +}