From 522fb432a62058a0cd0258f52b45c87ab617d995 Mon Sep 17 00:00:00 2001 From: Nikos Date: Wed, 25 Sep 2024 15:02:39 +0300 Subject: [PATCH 01/52] chore: install fosite from commit --- go.mod | 3 +++ go.sum | 6 ++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 2a9413c0ea5..5141495cf69 100644 --- a/go.mod +++ b/go.mod @@ -232,6 +232,7 @@ require ( go.opentelemetry.io/otel/exporters/zipkin v1.32.0 // indirect go.opentelemetry.io/otel/metric v1.32.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect + go.uber.org/mock v0.5.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/mod v0.19.0 // indirect golang.org/x/net v0.33.0 // indirect @@ -247,3 +248,5 @@ require ( gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) + +replace github.com/ory/fosite => github.com/canonical/fosite v0.0.0-20250124155649-b77efc392574 diff --git a/go.sum b/go.sum index f621e7822b7..83edb2dde7f 100644 --- a/go.sum +++ b/go.sum @@ -47,6 +47,8 @@ github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dR 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/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= +github.com/canonical/fosite v0.0.0-20250124155649-b77efc392574 h1:ZH9mv26aX0FTVXM6jenoVZfsBBxKxnjiq8c44sMMAUE= +github.com/canonical/fosite v0.0.0-20250124155649-b77efc392574/go.mod h1:rwKPYm0bLjxRQ+J7/CVAIwpnwRMaRrwRzdGqgbjwNK8= 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.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= @@ -378,8 +380,6 @@ github.com/ory/analytics-go/v5 v5.0.1 h1:LX8T5B9FN8KZXOtxgN+R3I4THRRVB6+28IKgKBp github.com/ory/analytics-go/v5 v5.0.1/go.mod h1:lWCiCjAaJkKfgR/BN5DCLMol8BjKS1x+4jxBxff/FF0= github.com/ory/dockertest/v3 v3.10.1-0.20240704115616-d229e74b748d h1:By96ZSVuH5LyjXLVVMfvJoLVGHaT96LdOnwgFSLVf0E= github.com/ory/dockertest/v3 v3.10.1-0.20240704115616-d229e74b748d/go.mod h1:F2FIjwwAk6CsNAs//B8+aPFQF0t84pbM8oliyNXwQrk= -github.com/ory/fosite v0.49.0 h1:KNqO7RVt/1X8F08/UI0Y+GRvcpscCWgjqvpLBQPRovo= -github.com/ory/fosite v0.49.0/go.mod h1:FAn7IY+I6DjT1r29wMouPeRYq63DWUuBj++96uOS4mE= github.com/ory/go-acc v0.2.9-0.20230103102148-6b1c9a70dbbe h1:rvu4obdvqR0fkSIJ8IfgzKOWwZ5kOT2UNfLq81Qk7rc= github.com/ory/go-acc v0.2.9-0.20230103102148-6b1c9a70dbbe/go.mod h1:z4n3u6as84LbV4YmgjHhnwtccQqzf4cZlSk9f1FhygI= github.com/ory/go-convenience v0.1.0 h1:zouLKfF2GoSGnJwGq+PE/nJAE6dj2Zj5QlTgmMTsTS8= @@ -560,6 +560,8 @@ go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8= go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0= go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= From 3795e257902c563bcdf2ca798a2141abd4aea57f Mon Sep 17 00:00:00 2001 From: Nikos Date: Wed, 25 Sep 2024 21:40:34 +0300 Subject: [PATCH 02/52] fix: set utc expires_at --- persistence/sql/persister_nid_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/persistence/sql/persister_nid_test.go b/persistence/sql/persister_nid_test.go index 99090a47690..836b3e561fe 100644 --- a/persistence/sql/persister_nid_test.go +++ b/persistence/sql/persister_nid_test.go @@ -2176,7 +2176,7 @@ func (s *PersisterTestSuite) TestVerifyAndInvalidateLogoutRequest() { t.Run("case=logout request that expired returns error", func(t *testing.T) { lr := newLogoutRequest() - lr.ExpiresAt = sqlxx.NullTime(time.Now().Add(-time.Hour)) + lr.ExpiresAt = sqlxx.NullTime(time.Now().UTC().Add(-time.Hour)) lr.Verifier = uuid.Must(uuid.NewV4()).String() lr.Accepted = true lr.Rejected = false From ff9bebcbb603fd32e0849b1fc25b2df9c642b0cc Mon Sep 17 00:00:00 2001 From: Nikos Date: Wed, 25 Sep 2024 22:14:35 +0300 Subject: [PATCH 03/52] fix: add redirect_uri to test --- consent/strategy_default_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/consent/strategy_default_test.go b/consent/strategy_default_test.go index c4ba3d97107..628542a0e81 100644 --- a/consent/strategy_default_test.go +++ b/consent/strategy_default_test.go @@ -65,6 +65,7 @@ func makeOAuth2Request(t *testing.T, reg driver.Registry, hc *http.Client, oc *c values.Add("response_type", "code") values.Add("state", uuid.New().String()) values.Add("client_id", oc.GetID()) + values.Add("redirect_uri", oc.GetRedirectURIs()[0]) res, err := hc.Get(urlx.CopyWithQuery(reg.Config().OAuth2AuthURL(ctx), values).String()) require.NoError(t, err) defer res.Body.Close() From 4c4a8691d9130f6cff9b7e042f0b7d6128234b94 Mon Sep 17 00:00:00 2001 From: Nikos Date: Fri, 9 Feb 2024 17:01:36 +0200 Subject: [PATCH 04/52] fix: add rfc8628 providers to registry --- .schema/config.schema.json | 51 +++++++++++++++++++++++++++++ client/client.go | 1 + driver/config/provider.go | 31 ++++++++++++++++++ driver/config/provider_test.go | 8 +++++ fositex/config.go | 13 ++++++++ internal/mock/config_cookie.go | 5 +-- jwk/registry_mock_test.go | 6 +--- oauth2/oauth2_provider_mock_test.go | 46 +++++++++++++++++++++++--- oauth2/registry.go | 2 ++ spec/config.json | 47 ++++++++++++++++++++++++++ 10 files changed, 197 insertions(+), 13 deletions(-) diff --git a/.schema/config.schema.json b/.schema/config.schema.json index 252433161c0..df510c34645 100644 --- a/.schema/config.schema.json +++ b/.schema/config.schema.json @@ -464,6 +464,11 @@ "description": "Sets the session cookie name. Use with care!", "type": "object", "properties": { + "device_csrf": { + "type": "string", + "title": "CSRF Cookie Name", + "default": "ory_hydra_device_csrf" + }, "login_csrf": { "type": "string", "title": "CSRF Cookie Name", @@ -645,6 +650,14 @@ "examples": [ "https://example.org/my-custom-userinfo-endpoint" ] + }, + "device_authorization_url": { + "type": "string", + "description": "A URL of the device authorization endpoint to be advertised at the OpenID Connect Discovery endpoint /.well-known/openid-configuration. Defaults to Ory Hydra's device authorizatoin endpoint at /oauth2/device/auth. Set this value if you want to handle this endpoint yourself.", + "format": "uri-reference", + "examples": [ + "https://example.org/oauth2/device/auth" + ] } } } @@ -803,6 +816,24 @@ "/ui/logout" ] }, + "device_verification": { + "type": "string", + "description": "Sets the device user code verification endpoint. Defaults to an internal fallback URL showing an error.", + "format": "uri-reference", + "examples": [ + "https://my-logout.app/device_verification", + "/ui/device_verification" + ] + }, + "post_device_done": { + "type": "string", + "description": "Sets the post device authentication endpoint. Defaults to an internal fallback URL showing an error.", + "format": "uri-reference", + "examples": [ + "https://my-logout.app/device_done", + "/ui/device_done" + ] + }, "error": { "type": "string", "description": "Sets the error endpoint. The error ui will be shown when an OAuth2 error occurs that which can not be sent back to the client. Defaults to an internal fallback URL showing an error.", @@ -947,6 +978,15 @@ "$ref": "#/definitions/duration" } ] + }, + "device_user_code": { + "description": "Configures how long device and user codes are valid.", + "default": "15m", + "allOf": [ + { + "$ref": "#/definitions/duration" + } + ] } } }, @@ -1064,6 +1104,17 @@ } } }, + "device_authorization": { + "token_polling_interval": { + "description": "Sets the starting token polling interval.", + "default": "5s", + "allOf": [ + { + "$ref": "#/definitions/duration" + } + ] + } + }, "grant": { "type": "object", "additionalProperties": false, diff --git a/client/client.go b/client/client.go index 3394dd51c79..1d78e7add34 100644 --- a/client/client.go +++ b/client/client.go @@ -79,6 +79,7 @@ type Client struct { // - OpenID Connect Implicit Grant (deprecated!): `implicit` // - Refresh Token Grant: `refresh_token` // - OAuth 2.0 Token Exchange: `urn:ietf:params:oauth:grant-type:jwt-bearer` + // - OAuth 2.0 Device Code Grant: `urn:ietf:params:oauth:grant-type:device_code` GrantTypes sqlxx.StringSliceJSONFormat `json:"grant_types" db:"grant_types"` // OAuth 2.0 Client Response Types diff --git a/driver/config/provider.go b/driver/config/provider.go index b02d0ae1da4..5957b4b3877 100644 --- a/driver/config/provider.go +++ b/driver/config/provider.go @@ -15,6 +15,7 @@ import ( "github.com/pkg/errors" "github.com/ory/x/hasherx" + "github.com/ory/x/randx" "github.com/gofrs/uuid" @@ -50,6 +51,7 @@ const ( KeyOIDCDiscoverySupportedClaims = "webfinger.oidc_discovery.supported_claims" KeyOIDCDiscoverySupportedScope = "webfinger.oidc_discovery.supported_scope" KeyOIDCDiscoveryUserinfoEndpoint = "webfinger.oidc_discovery.userinfo_url" + KeyOAuth2DeviceAuthorisationURL = "webfinger.oidc_discovery.device_authorization_url" KeySubjectTypesSupported = "oidc.subject_identifiers.supported_types" KeyDefaultClientScope = "oidc.dynamic_client_registration.default_scope" KeyDSN = "dsn" @@ -73,6 +75,7 @@ const ( KeyVerifiableCredentialsNonceLifespan = "ttl.vc_nonce" // #nosec G101 KeyIDTokenLifespan = "ttl.id_token" // #nosec G101 KeyAuthCodeLifespan = "ttl.auth_code" + KeyDeviceAndUserCodeLifespan = "ttl.device_user_code" KeyScopeStrategy = "strategies.scope" KeyGetCookieSecrets = "secrets.cookie" KeyGetSystemSecret = "secrets.system" @@ -82,6 +85,7 @@ const ( KeyLogoutURL = "urls.logout" KeyConsentURL = "urls.consent" KeyErrorURL = "urls.error" + KeyDeviceVerificationURL = "urls.device_verification" KeyPublicURL = "urls.self.public" KeyAdminURL = "urls.self.admin" KeyIssuerURL = "urls.self.issuer" @@ -93,6 +97,7 @@ const ( KeyDBIgnoreUnknownTableColumns = "db.ignore_unknown_table_columns" KeySubjectIdentifierAlgorithmSalt = "oidc.subject_identifiers.pairwise.salt" KeyPublicAllowDynamicRegistration = "oidc.dynamic_client_registration.enabled" + KeyDeviceAuthTokenPollingInterval = "oauth2.device_authorization.token_polling_interval" // #nosec G101 KeyPKCEEnforced = "oauth2.pkce.enforced" KeyPKCEEnforcedForPublicClients = "oauth2.pkce.enforced_for_public_clients" KeyLogLevel = "log.level" @@ -397,6 +402,24 @@ func (p *DefaultProvider) fallbackURL(ctx context.Context, path string, host str return &u } +func (p *DefaultProvider) GetDeviceAndUserCodeLifespan(ctx context.Context) time.Duration { + return p.p.DurationF(KeyDeviceAndUserCodeLifespan, time.Minute*15) +} + +func (p *DefaultProvider) GetDeviceAuthTokenPollingInterval(ctx context.Context) time.Duration { + return p.p.DurationF(KeyDeviceAuthTokenPollingInterval, time.Second*5) +} + +// GetUserCodeLength returns configured user_code length +func (c *DefaultProvider) GetUserCodeLength(ctx context.Context) int { + return 8 +} + +// GetDeviceAuthTokenPollingInterval returns configured user_code allowed symbols +func (c *DefaultProvider) GetUserCodeSymbols(ctx context.Context) []rune { + return []rune(randx.AlphaUpper) +} + func (p *DefaultProvider) LoginURL(ctx context.Context) *url.URL { return urlRoot(p.getProvider(ctx).URIF(KeyLoginURL, p.publicFallbackURL(ctx, "oauth2/fallbacks/login"))) } @@ -417,6 +440,10 @@ func (p *DefaultProvider) ErrorURL(ctx context.Context) *url.URL { return urlRoot(p.getProvider(ctx).RequestURIF(KeyErrorURL, p.publicFallbackURL(ctx, "oauth2/fallbacks/error"))) } +func (p *DefaultProvider) DeviceVerificationURL(ctx context.Context) *url.URL { + return urlRoot(p.getProvider(ctx).URIF(KeyDeviceVerificationURL, p.publicFallbackURL(ctx, "oauth2/fallbacks/device"))) +} + func (p *DefaultProvider) PublicURL(ctx context.Context) *url.URL { return urlRoot(p.getProvider(ctx).RequestURIF(KeyPublicURL, p.IssuerURL(ctx))) } @@ -474,6 +501,10 @@ func (p *DefaultProvider) OAuth2AuthURL(ctx context.Context) *url.URL { return p.getProvider(ctx).RequestURIF(KeyOAuth2AuthURL, urlx.AppendPaths(p.PublicURL(ctx), "/oauth2/auth")) } +func (p *DefaultProvider) OAuth2DeviceAuthorisationURL(ctx context.Context) *url.URL { + return p.getProvider(ctx).RequestURIF(KeyOAuth2DeviceAuthorisationURL, urlx.AppendPaths(p.PublicURL(ctx), "/oauth2/device/auth")) +} + func (p *DefaultProvider) JWKSURL(ctx context.Context) *url.URL { return p.getProvider(ctx).RequestURIF(KeyJWKSURL, urlx.AppendPaths(p.IssuerURL(ctx), "/.well-known/jwks.json")) } diff --git a/driver/config/provider_test.go b/driver/config/provider_test.go index 7ec1dce8df9..e6d1c7f9e16 100644 --- a/driver/config/provider_test.go +++ b/driver/config/provider_test.go @@ -279,6 +279,7 @@ func TestViperProviderValidates(t *testing.T) { // webfinger assert.Equal(t, []string{"hydra.openid.id-token", "hydra.jwt.access-token"}, c.WellKnownKeys(ctx)) assert.Equal(t, urlx.ParseOrPanic("https://example.com"), c.OAuth2ClientRegistrationURL(ctx)) + assert.Equal(t, urlx.ParseOrPanic("https://example.com/device_authorization"), c.OAuth2DeviceAuthorisationURL(ctx)) assert.Equal(t, urlx.ParseOrPanic("https://example.com/jwks.json"), c.JWKSURL(ctx)) assert.Equal(t, urlx.ParseOrPanic("https://example.com/auth"), c.OAuth2AuthURL(ctx)) assert.Equal(t, urlx.ParseOrPanic("https://example.com/token"), c.OAuth2TokenURL(ctx)) @@ -304,6 +305,7 @@ func TestViperProviderValidates(t *testing.T) { assert.Equal(t, urlx.ParseOrPanic("https://admin/"), c.AdminURL(ctx)) assert.Equal(t, urlx.ParseOrPanic("https://login/"), c.LoginURL(ctx)) assert.Equal(t, urlx.ParseOrPanic("https://consent/"), c.ConsentURL(ctx)) + assert.Equal(t, urlx.ParseOrPanic("https://device/"), c.DeviceVerificationURL(ctx)) assert.Equal(t, urlx.ParseOrPanic("https://logout/"), c.LogoutURL(ctx)) assert.Equal(t, urlx.ParseOrPanic("https://error/"), c.ErrorURL(ctx)) assert.Equal(t, urlx.ParseOrPanic("https://post_logout/"), c.LogoutRedirectURL(ctx)) @@ -321,12 +323,14 @@ func TestViperProviderValidates(t *testing.T) { assert.Equal(t, 2*time.Hour, c.GetRefreshTokenLifespan(ctx)) assert.Equal(t, 2*time.Hour, c.GetIDTokenLifespan(ctx)) assert.Equal(t, 2*time.Hour, c.GetAuthorizeCodeLifespan(ctx)) + assert.Equal(t, 2*time.Hour, c.GetDeviceAndUserCodeLifespan(ctx)) // oauth2 assert.Equal(t, true, c.GetSendDebugMessagesToClients(ctx)) assert.Equal(t, 20, c.GetBCryptCost(ctx)) assert.Equal(t, true, c.GetEnforcePKCE(ctx)) assert.Equal(t, true, c.GetEnforcePKCEForPublicClients(ctx)) + assert.Equal(t, 2*time.Hour, c.GetDeviceAuthTokenPollingInterval(ctx)) // secrets secret, err := c.GetGlobalSecret(ctx) @@ -395,16 +399,20 @@ func TestLoginConsentURL(t *testing.T) { p := MustNew(context.Background(), l) p.MustSet(ctx, KeyLoginURL, "http://localhost:8080/oauth/login") p.MustSet(ctx, KeyConsentURL, "http://localhost:8080/oauth/consent") + p.MustSet(ctx, KeyDeviceVerificationURL, "http://localhost:8080/oauth/device") assert.Equal(t, "http://localhost:8080/oauth/login", p.LoginURL(ctx).String()) assert.Equal(t, "http://localhost:8080/oauth/consent", p.ConsentURL(ctx).String()) + assert.Equal(t, "http://localhost:8080/oauth/device", p.DeviceVerificationURL(ctx).String()) p2 := MustNew(context.Background(), l) p2.MustSet(ctx, KeyLoginURL, "http://localhost:3000/#/oauth/login") p2.MustSet(ctx, KeyConsentURL, "http://localhost:3000/#/oauth/consent") + p2.MustSet(ctx, KeyDeviceVerificationURL, "http://localhost:3000/#/oauth/device") assert.Equal(t, "http://localhost:3000/#/oauth/login", p2.LoginURL(ctx).String()) assert.Equal(t, "http://localhost:3000/#/oauth/consent", p2.ConsentURL(ctx).String()) + assert.Equal(t, "http://localhost:3000/#/oauth/device", p2.DeviceVerificationURL(ctx).String()) } func TestInfinitRefreshTokenTTL(t *testing.T) { diff --git a/fositex/config.go b/fositex/config.go index 4377efb1f6d..7c2018971f6 100644 --- a/fositex/config.go +++ b/fositex/config.go @@ -42,6 +42,7 @@ type Config struct { tokenEndpointHandlers fosite.TokenEndpointHandlers tokenIntrospectionHandlers fosite.TokenIntrospectionHandlers revocationHandlers fosite.RevocationHandlers + deviceEndpointHandlers fosite.DeviceEndpointHandlers *config.DefaultProvider } @@ -61,6 +62,7 @@ var defaultFactories = []Factory{ compose.OAuth2PKCEFactory, compose.RFC7523AssertionGrantFactory, compose.OIDCUserinfoVerifiableCredentialFactory, + compose.RFC8628DeviceFactory, } func NewConfig(deps configDependencies) *Config { @@ -87,6 +89,9 @@ func (c *Config) LoadDefaultHandlers(strategy interface{}) { if rh, ok := res.(fosite.RevocationHandler); ok { c.revocationHandlers.Append(rh) } + if dh, ok := res.(fosite.DeviceEndpointHandler); ok { + c.deviceEndpointHandlers.Append(dh) + } } } @@ -114,6 +119,10 @@ func (c *Config) GetRevocationHandlers(context.Context) fosite.RevocationHandler return c.revocationHandlers } +func (c *Config) GetDeviceEndpointHandlers(ctx context.Context) fosite.DeviceEndpointHandlers { + return c.deviceEndpointHandlers +} + func (c *Config) GetGrantTypeJWTBearerCanSkipClientAuth(context.Context) bool { return false } @@ -206,3 +215,7 @@ func (c *Config) GetTokenURLs(ctx context.Context) []string { urlx.AppendPaths(c.deps.Config().PublicURL(ctx), oauth2.TokenPath).String(), }) } + +func (c *Config) GetDeviceVerificationURL(ctx context.Context) string { + return urlx.AppendPaths(c.deps.Config().PublicURL(ctx), oauth2.DeviceAuthPath).String() +} diff --git a/internal/mock/config_cookie.go b/internal/mock/config_cookie.go index 5fab6d1d7dc..d6898a7b8d8 100644 --- a/internal/mock/config_cookie.go +++ b/internal/mock/config_cookie.go @@ -1,8 +1,5 @@ -// Copyright © 2022 Ory Corp -// SPDX-License-Identifier: Apache-2.0 - // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/ory/hydra/x (interfaces: CookieConfigProvider) +// Source: github.com/ory/hydra/v2/x (interfaces: CookieConfigProvider) // Package mock is a generated GoMock package. package mock diff --git a/jwk/registry_mock_test.go b/jwk/registry_mock_test.go index c305fd18167..68de41ca30b 100644 --- a/jwk/registry_mock_test.go +++ b/jwk/registry_mock_test.go @@ -1,6 +1,3 @@ -// Copyright © 2022 Ory Corp -// SPDX-License-Identifier: Apache-2.0 - // Code generated by MockGen. DO NOT EDIT. // Source: jwk/registry.go @@ -11,9 +8,8 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" - herodot "github.com/ory/herodot" - "github.com/ory/hydra/v2/aead" + aead "github.com/ory/hydra/v2/aead" config "github.com/ory/hydra/v2/driver/config" jwk "github.com/ory/hydra/v2/jwk" logrusx "github.com/ory/x/logrusx" diff --git a/oauth2/oauth2_provider_mock_test.go b/oauth2/oauth2_provider_mock_test.go index 83d584eb12f..e99c959fc4f 100644 --- a/oauth2/oauth2_provider_mock_test.go +++ b/oauth2/oauth2_provider_mock_test.go @@ -1,6 +1,3 @@ -// Copyright © 2022 Ory Corp -// SPDX-License-Identifier: Apache-2.0 - // Code generated by MockGen. DO NOT EDIT. // Source: github.com/ory/fosite (interfaces: OAuth2Provider) @@ -13,7 +10,6 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" - fosite "github.com/ory/fosite" ) @@ -121,6 +117,36 @@ func (mr *MockOAuth2ProviderMockRecorder) NewAuthorizeResponse(arg0, arg1, arg2 return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewAuthorizeResponse", reflect.TypeOf((*MockOAuth2Provider)(nil).NewAuthorizeResponse), arg0, arg1, arg2) } +// NewDeviceRequest mocks base method. +func (m *MockOAuth2Provider) NewDeviceRequest(arg0 context.Context, arg1 *http.Request) (fosite.DeviceRequester, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewDeviceRequest", arg0, arg1) + ret0, _ := ret[0].(fosite.DeviceRequester) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// NewDeviceRequest indicates an expected call of NewDeviceRequest. +func (mr *MockOAuth2ProviderMockRecorder) NewDeviceRequest(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewDeviceRequest", reflect.TypeOf((*MockOAuth2Provider)(nil).NewDeviceRequest), arg0, arg1) +} + +// NewDeviceResponse mocks base method. +func (m *MockOAuth2Provider) NewDeviceResponse(arg0 context.Context, arg1 fosite.DeviceRequester) (fosite.DeviceResponder, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewDeviceResponse", arg0, arg1) + ret0, _ := ret[0].(fosite.DeviceResponder) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// NewDeviceResponse indicates an expected call of NewDeviceResponse. +func (mr *MockOAuth2ProviderMockRecorder) NewDeviceResponse(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewDeviceResponse", reflect.TypeOf((*MockOAuth2Provider)(nil).NewDeviceResponse), arg0, arg1) +} + // NewIntrospectionRequest mocks base method. func (m *MockOAuth2Provider) NewIntrospectionRequest(arg0 context.Context, arg1 *http.Request, arg2 fosite.Session) (fosite.IntrospectionResponder, error) { m.ctrl.T.Helper() @@ -228,6 +254,18 @@ func (mr *MockOAuth2ProviderMockRecorder) WriteAuthorizeResponse(arg0, arg1, arg return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WriteAuthorizeResponse", reflect.TypeOf((*MockOAuth2Provider)(nil).WriteAuthorizeResponse), arg0, arg1, arg2, arg3) } +// WriteDeviceResponse mocks base method. +func (m *MockOAuth2Provider) WriteDeviceResponse(arg0 context.Context, arg1 http.ResponseWriter, arg2 fosite.DeviceRequester, arg3 fosite.DeviceResponder) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "WriteDeviceResponse", arg0, arg1, arg2, arg3) +} + +// WriteDeviceResponse indicates an expected call of WriteDeviceResponse. +func (mr *MockOAuth2ProviderMockRecorder) WriteDeviceResponse(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WriteDeviceResponse", reflect.TypeOf((*MockOAuth2Provider)(nil).WriteDeviceResponse), arg0, arg1, arg2, arg3) +} + // WriteIntrospectionError mocks base method. func (m *MockOAuth2Provider) WriteIntrospectionError(arg0 context.Context, arg1 http.ResponseWriter, arg2 error) { m.ctrl.T.Helper() diff --git a/oauth2/registry.go b/oauth2/registry.go index 0e7cdfae81a..ffb7b642541 100644 --- a/oauth2/registry.go +++ b/oauth2/registry.go @@ -6,6 +6,7 @@ package oauth2 import ( "github.com/ory/fosite" "github.com/ory/fosite/handler/openid" + "github.com/ory/fosite/handler/rfc8628" "github.com/ory/hydra/v2/aead" "github.com/ory/hydra/v2/client" "github.com/ory/hydra/v2/consent" @@ -35,4 +36,5 @@ type Registry interface { OpenIDConnectRequestValidator() *openid.OpenIDConnectRequestValidator AccessRequestHooks() []AccessRequestHook OAuth2ProviderConfig() fosite.Configurator + RFC8628HMACStrategy() rfc8628.RFC8628CodeStrategy } diff --git a/spec/config.json b/spec/config.json index f4f5dcbd60a..821ace2cf76 100644 --- a/spec/config.json +++ b/spec/config.json @@ -464,6 +464,11 @@ "description": "Sets the session cookie name. Use with care!", "type": "object", "properties": { + "device_csrf": { + "type": "string", + "title": "CSRF Cookie Name", + "default": "ory_hydra_device_csrf" + }, "login_csrf": { "type": "string", "title": "CSRF Cookie Name", @@ -614,6 +619,14 @@ "https://my-service.com/oauth2/auth" ] }, + "device_authorization_url": { + "type": "string", + "description": "Overwrites the OAuth2 Device Auth URL", + "format": "uri-reference", + "examples": [ + "https://my-service.com/oauth2/device/auth" + ] + }, "client_registration_url": { "description": "Sets the OpenID Connect Dynamic Client Registration Endpoint", "type": "string", @@ -803,6 +816,15 @@ "/ui/logout" ] }, + "device_verification": { + "type": "string", + "description": "Sets the device verification URL. Defaults to an internal fallback URL showing an error.", + "format": "uri-reference", + "examples": [ + "https://my-app/device", + "/ui/device" + ] + }, "error": { "type": "string", "description": "Sets the error endpoint. The error ui will be shown when an OAuth2 error occurs that which can not be sent back to the client. Defaults to an internal fallback URL showing an error.", @@ -947,6 +969,15 @@ "$ref": "#/definitions/duration" } ] + }, + "device_user_code": { + "description": "Configures how long device & user codes are valid.", + "default": "10m", + "allOf": [ + { + "$ref": "#/definitions/duration" + } + ] } } }, @@ -1124,6 +1155,22 @@ } ] }, + "device_authorization": { + "type": "object", + "additionalProperties": false, + "properties": { + "token_polling_interval": { + "allOf": [ + { + "$ref": "#/definitions/duration" + } + ], + "default": "5s", + "description": "configure how often a non-interactive device should poll the device token endpoint", + "examples": ["5s", "15s", "1m"] + } + } + }, "token_hook": { "description": "Sets the token hook endpoint for all grant types. If set it will be called while providing token to customize claims.", "examples": ["https://my-example.app/token-hook"], From 95164828d51e53bead2267fc6a1bdaf01376fb96 Mon Sep 17 00:00:00 2001 From: Nikos Date: Fri, 9 Feb 2024 17:02:28 +0200 Subject: [PATCH 05/52] fix: update database schema --- .../fixtures/hydra_client/client-22.json | 12 +++ .../hydra_oauth2_flow/challenge-0001.json | 3 + .../hydra_oauth2_flow/challenge-0002.json | 3 + .../hydra_oauth2_flow/challenge-0003.json | 3 + .../hydra_oauth2_flow/challenge-0004.json | 3 + .../hydra_oauth2_flow/challenge-0005.json | 3 + .../hydra_oauth2_flow/challenge-0006.json | 3 + .../hydra_oauth2_flow/challenge-0007.json | 3 + .../hydra_oauth2_flow/challenge-0008.json | 3 + .../hydra_oauth2_flow/challenge-0009.json | 3 + .../hydra_oauth2_flow/challenge-0010.json | 3 + .../hydra_oauth2_flow/challenge-0011.json | 3 + .../hydra_oauth2_flow/challenge-0012.json | 3 + .../hydra_oauth2_flow/challenge-0013.json | 3 + .../hydra_oauth2_flow/challenge-0014.json | 3 + .../hydra_oauth2_flow/challenge-0015.json | 3 + .../hydra_oauth2_flow/challenge-0016.json | 3 + .../hydra_oauth2_flow/challenge-0017.json | 3 + .../hydra_oauth2_flow/challenge-0018.json | 79 +++++++++++++++++++ ...9000001000000_device_flow.cockroach.up.sql | 70 ++++++++++++++++ .../20241609000001000000_device_flow.down.sql | 27 +++++++ ...41609000001000000_device_flow.mysql.up.sql | 70 ++++++++++++++++ ...09000001000000_device_flow.postgres.up.sql | 68 ++++++++++++++++ .../20241609000001000000_device_flow.up.sql | 60 ++++++++++++++ ...9000001000000_device_flow.cockroach.up.sql | 68 ++++++++++++++++ .../20241609000001000000_device_flow.down.sql | 25 ++++++ ...41609000001000000_device_flow.mysql.up.sql | 68 ++++++++++++++++ ...09000001000000_device_flow.postgres.up.sql | 66 ++++++++++++++++ .../20241609000001000000_device_flow.up.sql | 58 ++++++++++++++ 29 files changed, 722 insertions(+) create mode 100644 persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0018.json create mode 100644 persistence/sql/migrations/20241609000001000000_device_flow.cockroach.up.sql create mode 100644 persistence/sql/migrations/20241609000001000000_device_flow.down.sql create mode 100644 persistence/sql/migrations/20241609000001000000_device_flow.mysql.up.sql create mode 100644 persistence/sql/migrations/20241609000001000000_device_flow.postgres.up.sql create mode 100644 persistence/sql/migrations/20241609000001000000_device_flow.up.sql create mode 100644 persistence/sql/src/YYYYMMDD000001_device_flow/20241609000001000000_device_flow.cockroach.up.sql create mode 100644 persistence/sql/src/YYYYMMDD000001_device_flow/20241609000001000000_device_flow.down.sql create mode 100644 persistence/sql/src/YYYYMMDD000001_device_flow/20241609000001000000_device_flow.mysql.up.sql create mode 100644 persistence/sql/src/YYYYMMDD000001_device_flow/20241609000001000000_device_flow.postgres.up.sql create mode 100644 persistence/sql/src/YYYYMMDD000001_device_flow/20241609000001000000_device_flow.up.sql diff --git a/persistence/sql/migratest/fixtures/hydra_client/client-22.json b/persistence/sql/migratest/fixtures/hydra_client/client-22.json index 13a940c8416..49c9fb5ea91 100644 --- a/persistence/sql/migratest/fixtures/hydra_client/client-22.json +++ b/persistence/sql/migratest/fixtures/hydra_client/client-22.json @@ -44,6 +44,18 @@ "Duration": 0, "Valid": false }, + "DeviceAuthorizationGrantAccessTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantIDTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantRefreshTokenLifespan": { + "Duration": 0, + "Valid": false + }, "ImplicitGrantAccessTokenLifespan": { "Duration": 0, "Valid": false diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0001.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0001.json index fae8513d60a..1431c94066c 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0001.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0001.json @@ -29,6 +29,9 @@ "valid": false }, "la": null, + "da": null, + "du": null, + "dh": null, "cc": "challenge-0001", "cs": true, "cv": "verifier-0001", diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0002.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0002.json index bc73e23fc21..e454e282434 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0002.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0002.json @@ -30,6 +30,9 @@ "valid": false }, "la": null, + "da": null, + "du": null, + "dh": null, "cc": "challenge-0002", "cs": true, "cv": "verifier-0002", diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0003.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0003.json index f04dee37267..aa4d250f5c2 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0003.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0003.json @@ -31,6 +31,9 @@ "valid": false }, "la": null, + "da": null, + "du": null, + "dh": null, "cc": "challenge-0003", "cs": true, "cv": "verifier-0003", diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0004.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0004.json index e3b5d630dd2..c95e9dd963c 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0004.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0004.json @@ -34,6 +34,9 @@ "valid": false }, "la": null, + "da": null, + "du": null, + "dh": null, "cc": "challenge-0004", "cs": true, "cv": "verifier-0004", diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0005.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0005.json index db4e0787291..14fa4483bdd 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0005.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0005.json @@ -34,6 +34,9 @@ "valid": false }, "la": null, + "da": null, + "du": null, + "dh": null, "cc": "challenge-0005", "cs": true, "cv": "verifier-0005", diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0006.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0006.json index 7a8b9fd8890..12157ef0300 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0006.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0006.json @@ -34,6 +34,9 @@ "valid": false }, "la": null, + "da": null, + "du": null, + "dh": null, "cc": "challenge-0006", "cs": true, "cv": "verifier-0006", diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0007.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0007.json index b5f6814ea47..9efbdcc49b3 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0007.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0007.json @@ -34,6 +34,9 @@ "valid": false }, "la": null, + "da": null, + "du": null, + "dh": null, "cc": "challenge-0007", "cs": true, "cv": "verifier-0007", diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0008.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0008.json index e821518707f..b240dce7127 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0008.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0008.json @@ -36,6 +36,9 @@ "valid": false }, "la": null, + "da": null, + "du": null, + "dh": null, "cc": "challenge-0008", "cs": true, "cv": "verifier-0008", diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0009.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0009.json index be51195ca6a..1887b28b1f1 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0009.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0009.json @@ -36,6 +36,9 @@ "valid": false }, "la": null, + "da": null, + "du": null, + "dh": null, "cc": "challenge-0009", "cs": true, "cv": "verifier-0009", diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0010.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0010.json index 353ed37ffe5..06922c8709f 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0010.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0010.json @@ -36,6 +36,9 @@ "valid": false }, "la": null, + "da": null, + "du": null, + "dh": null, "cc": "challenge-0010", "cs": true, "cv": "verifier-0010", diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0011.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0011.json index ed92bbce294..8298eea26c1 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0011.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0011.json @@ -36,6 +36,9 @@ "valid": false }, "la": null, + "da": null, + "du": null, + "dh": null, "cc": "challenge-0011", "cs": true, "cv": "verifier-0011", diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0012.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0012.json index 6375e369280..689bf6cec8f 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0012.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0012.json @@ -36,6 +36,9 @@ "valid": false }, "la": null, + "da": null, + "du": null, + "dh": null, "cc": "challenge-0012", "cs": true, "cv": "verifier-0012", diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0013.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0013.json index 3939f00e959..5c7db729136 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0013.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0013.json @@ -36,6 +36,9 @@ "valid": false }, "la": null, + "da": null, + "du": null, + "dh": null, "cc": "challenge-0013", "cs": true, "cv": "verifier-0013", diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0014.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0014.json index 38e0af54056..596894f09d9 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0014.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0014.json @@ -36,6 +36,9 @@ "valid": false }, "la": null, + "da": null, + "du": null, + "dh": null, "cc": "challenge-0014", "cs": true, "cv": "verifier-0014", diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0015.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0015.json index f55d9d59c0a..be20015e244 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0015.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0015.json @@ -42,6 +42,9 @@ "valid": false }, "la": null, + "da": null, + "du": null, + "dh": null, "cc": "challenge-0015", "cs": true, "cv": "verifier-0015", diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0016.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0016.json index be6ca67a2d1..5e8d25b2c76 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0016.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0016.json @@ -43,6 +43,9 @@ "valid": false }, "la": null, + "da": null, + "du": null, + "dh": null, "cc": "challenge-0016", "cs": true, "cv": "verifier-0016", diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0017.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0017.json index e8f9235696b..1e26b6038b3 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0017.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0017.json @@ -44,6 +44,9 @@ "valid": false }, "la": null, + "da": null, + "du": null, + "dh": null, "cc": "challenge-0017", "cs": true, "cv": "verifier-0017", diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0018.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0018.json new file mode 100644 index 00000000000..b21344dfcbf --- /dev/null +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0018.json @@ -0,0 +1,79 @@ +{ + "i": "challenge-0018", + "n": "00000000-0000-0000-0000-000000000000", + "rs": [ + "requested_scope-0018_1", + "requested_scope-0018_2" + ], + "ra": [ + "requested_audience-0018_1", + "requested_audience-0018_2" + ], + "ls": false, + "s": "subject-0018", + "oc": {}, + "r": "http://request/0018", + "si": "auth_session-0018", + "lv": "verifier-0018", + "lc": "csrf-0018", + "li": null, + "ia": "0001-01-01T00:00:00Z", + "q": 128, + "lr": true, + "lf": 15, + "ll": true, + "a": "acr-0018", + "am": [], + "fs": "force_subject_id-0018", + "ct": { + "context": "0018" + }, + "lu": true, + "le": { + "error": "", + "error_description": "", + "error_hint": "", + "status_code": 0, + "error_debug": "", + "valid": false + }, + "la": null, + "di": "challenge-0018", + "dr": "request-0018", + "dv": "verifier-0018", + "dc": "csrf-0018", + "da": "0001-01-01T00:00:00Z", + "du": true, + "dh": "0001-01-01T00:00:00Z", + "de": null, + "cc": "challenge-0018", + "cs": true, + "cv": "verifier-0018", + "cr": "csrf-0018", + "gs": [ + "granted_scope-0018_1", + "granted_scope-0018_2" + ], + "ga": [ + "granted_audience-0018_1", + "granted_audience-0018_2" + ], + "ce": true, + "cf": 15, + "ch": null, + "cw": true, + "cx": { + "error": "", + "error_description": "", + "error_hint": "", + "status_code": 0, + "error_debug": "", + "valid": false + }, + "st": { + "session_id_token-0018": "0018" + }, + "sa": { + "session_access_token-0018": "0018" + } +} diff --git a/persistence/sql/migrations/20241609000001000000_device_flow.cockroach.up.sql b/persistence/sql/migrations/20241609000001000000_device_flow.cockroach.up.sql new file mode 100644 index 00000000000..69d926ba289 --- /dev/null +++ b/persistence/sql/migrations/20241609000001000000_device_flow.cockroach.up.sql @@ -0,0 +1,70 @@ +-- Migration generated by the command below; DO NOT EDIT. +-- hydra:generate hydra migrate gen +CREATE TABLE IF NOT EXISTS hydra_oauth2_device_code +( + signature VARCHAR(255) NOT NULL PRIMARY KEY, + request_id VARCHAR(40) NOT NULL DEFAULT '', + requested_at TIMESTAMP NOT NULL DEFAULT NOW(), + client_id VARCHAR(255) NOT NULL DEFAULT '', + scope TEXT NOT NULL, + granted_scope TEXT NOT NULL, + form_data TEXT NOT NULL, + session_data TEXT NOT NULL, + subject VARCHAR(255) NOT NULL DEFAULT '', + active BOOL NOT NULL DEFAULT true, + requested_audience TEXT NOT NULL, + granted_audience TEXT NOT NULL, + challenge_id VARCHAR(40) NULL, + expires_at TIMESTAMP NULL, + nid UUID NOT NULL, + + FOREIGN KEY (client_id, nid) REFERENCES hydra_client(id, nid) ON DELETE CASCADE, + FOREIGN KEY (nid) REFERENCES networks(id) ON UPDATE RESTRICT ON DELETE CASCADE +); + +CREATE INDEX hydra_oauth2_device_code_request_id_idx ON hydra_oauth2_device_code (request_id, nid); +CREATE INDEX hydra_oauth2_device_code_client_id_idx ON hydra_oauth2_device_code (client_id, nid); +CREATE INDEX hydra_oauth2_device_code_challenge_id_idx ON hydra_oauth2_device_code (challenge_id); +CREATE INDEX hydra_oauth2_device_code_expires_at_idx ON hydra_oauth2_device_code (expires_at); + +CREATE TABLE IF NOT EXISTS hydra_oauth2_user_code +( + signature VARCHAR(255) NOT NULL PRIMARY KEY, + request_id VARCHAR(40) NOT NULL DEFAULT '', + requested_at TIMESTAMP NOT NULL DEFAULT NOW(), + client_id VARCHAR(255) NOT NULL DEFAULT '', + scope TEXT NOT NULL, + granted_scope TEXT NOT NULL, + form_data TEXT NOT NULL, + session_data TEXT NOT NULL, + subject VARCHAR(255) NOT NULL DEFAULT '', + active BOOL NOT NULL DEFAULT true, + requested_audience TEXT NOT NULL, + granted_audience TEXT NOT NULL, + challenge_id VARCHAR(40) NULL, + expires_at TIMESTAMP NULL, + nid UUID NOT NULL, + + FOREIGN KEY (client_id, nid) REFERENCES hydra_client(id, nid) ON DELETE CASCADE, + FOREIGN KEY (nid) REFERENCES networks(id) ON UPDATE RESTRICT ON DELETE CASCADE +); + +CREATE INDEX hydra_oauth2_user_code_request_id_idx ON hydra_oauth2_user_code (request_id, nid); +CREATE INDEX hydra_oauth2_user_code_client_id_idx ON hydra_oauth2_user_code (client_id, nid); +CREATE INDEX hydra_oauth2_user_code_challenge_id_idx ON hydra_oauth2_user_code (challenge_id); +CREATE INDEX hydra_oauth2_user_code_expires_at_idx ON hydra_oauth2_device_code (expires_at); + +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_challenge_id VARCHAR(255) NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_code_request_id VARCHAR(255) NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_verifier VARCHAR(40) NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_csrf VARCHAR(40) NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_user_code_accepted_at TIMESTAMP NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_was_used BOOL NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_handled_at TIMESTAMP NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_error TEXT NULL; + +CREATE INDEX hydra_oauth2_flow_device_challenge_idx ON hydra_oauth2_flow (device_challenge_id); + +ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_id_token_lifespan BIGINT NULL DEFAULT NULL; +ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_access_token_lifespan BIGINT NULL DEFAULT NULL; +ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_refresh_token_lifespan BIGINT NULL DEFAULT NULL; diff --git a/persistence/sql/migrations/20241609000001000000_device_flow.down.sql b/persistence/sql/migrations/20241609000001000000_device_flow.down.sql new file mode 100644 index 00000000000..54ceefff83b --- /dev/null +++ b/persistence/sql/migrations/20241609000001000000_device_flow.down.sql @@ -0,0 +1,27 @@ +-- Migration generated by the command below; DO NOT EDIT. +-- hydra:generate hydra migrate gen +ALTER TABLE hydra_oauth2_device_code DROP FOREIGN KEY IF EXISTS hydra_oauth2_device_code_challenge_id_fk; +ALTER TABLE hydra_oauth2_device_code DROP FOREIGN KEY IF EXISTS hydra_oauth2_device_code_client_id_fk; +ALTER TABLE hydra_oauth2_device_code DROP FOREIGN KEY IF EXISTS hydra_oauth2_device_code_nid_fk_idx; + +DROP TABLE IF EXISTS hydra_oauth2_device_code; + +ALTER TABLE hydra_oauth2_user_code DROP FOREIGN KEY IF EXISTS hydra_oauth2_user_code_challenge_id_fk; +ALTER TABLE hydra_oauth2_user_code DROP FOREIGN KEY IF EXISTS hydra_oauth2_user_code_client_id_fk; +ALTER TABLE hydra_oauth2_user_code DROP FOREIGN KEY IF EXISTS hydra_oauth2_user_code_nid_fk_idx; + +DROP TABLE IF EXISTS hydra_oauth2_user_code; + +ALTER TABLE hydra_oauth2_flow DROP COLUMN IF EXISTS device_challenge_id; +ALTER TABLE hydra_oauth2_flow DROP COLUMN IF EXISTS device_code_request_id; +ALTER TABLE hydra_oauth2_flow DROP COLUMN IF EXISTS device_verifier; +ALTER TABLE hydra_oauth2_flow DROP COLUMN IF EXISTS device_csrf; +ALTER TABLE hydra_oauth2_flow DROP COLUMN IF EXISTS device_user_code_accepted_at; +ALTER TABLE hydra_oauth2_flow DROP COLUMN IF EXISTS device_was_used; +ALTER TABLE hydra_oauth2_flow DROP COLUMN IF EXISTS device_handled_at; +ALTER TABLE hydra_oauth2_flow DROP COLUMN IF EXISTS device_error; + + +ALTER TABLE hydra_client DROP COLUMN device_authorization_grant_id_token_lifespan; +ALTER TABLE hydra_client DROP COLUMN device_authorization_grant_access_token_lifespan; +ALTER TABLE hydra_client DROP COLUMN device_authorization_grant_refresh_token_lifespan; \ No newline at end of file diff --git a/persistence/sql/migrations/20241609000001000000_device_flow.mysql.up.sql b/persistence/sql/migrations/20241609000001000000_device_flow.mysql.up.sql new file mode 100644 index 00000000000..c0b6446a0ec --- /dev/null +++ b/persistence/sql/migrations/20241609000001000000_device_flow.mysql.up.sql @@ -0,0 +1,70 @@ +-- Migration generated by the command below; DO NOT EDIT. +-- hydra:generate hydra migrate gen +CREATE TABLE IF NOT EXISTS hydra_oauth2_device_code +( + signature VARCHAR(255) NOT NULL PRIMARY KEY, + request_id VARCHAR(40) NOT NULL DEFAULT '', + requested_at TIMESTAMP NOT NULL DEFAULT NOW(), + client_id VARCHAR(255) NOT NULL DEFAULT '', + scope TEXT NOT NULL, + granted_scope TEXT NOT NULL, + form_data TEXT NOT NULL, + session_data TEXT NOT NULL, + subject VARCHAR(255) NOT NULL DEFAULT '', + active BOOL NOT NULL DEFAULT true, + requested_audience TEXT NOT NULL, + granted_audience TEXT NOT NULL, + challenge_id VARCHAR(40) NULL, + expires_at TIMESTAMP NULL, + nid CHAR(36) NOT NULL, + + FOREIGN KEY (client_id, nid) REFERENCES hydra_client(id, nid) ON DELETE CASCADE, + FOREIGN KEY (nid) REFERENCES networks(id) ON UPDATE RESTRICT ON DELETE CASCADE +); + +CREATE INDEX hydra_oauth2_device_code_request_id_idx ON hydra_oauth2_device_code (request_id, nid); +CREATE INDEX hydra_oauth2_device_code_client_id_idx ON hydra_oauth2_device_code (client_id, nid); +CREATE INDEX hydra_oauth2_device_code_challenge_id_idx ON hydra_oauth2_device_code (challenge_id); +CREATE INDEX hydra_oauth2_device_code_expires_at_idx ON hydra_oauth2_device_code (expires_at); + +CREATE TABLE IF NOT EXISTS hydra_oauth2_user_code +( + signature VARCHAR(255) NOT NULL PRIMARY KEY, + request_id VARCHAR(40) NOT NULL DEFAULT '', + requested_at TIMESTAMP NOT NULL DEFAULT NOW(), + client_id VARCHAR(255) NOT NULL DEFAULT '', + scope TEXT NOT NULL, + granted_scope TEXT NOT NULL, + form_data TEXT NOT NULL, + session_data TEXT NOT NULL, + subject VARCHAR(255) NOT NULL DEFAULT '', + active BOOL NOT NULL DEFAULT true, + requested_audience TEXT NOT NULL, + granted_audience TEXT NOT NULL, + challenge_id VARCHAR(40) NULL, + expires_at TIMESTAMP NULL, + nid CHAR(36) NOT NULL, + + FOREIGN KEY (client_id, nid) REFERENCES hydra_client(id, nid) ON DELETE CASCADE, + FOREIGN KEY (nid) REFERENCES networks(id) ON UPDATE RESTRICT ON DELETE CASCADE +); + +CREATE INDEX hydra_oauth2_user_code_request_id_idx ON hydra_oauth2_user_code (request_id, nid); +CREATE INDEX hydra_oauth2_user_code_client_id_idx ON hydra_oauth2_user_code (client_id, nid); +CREATE INDEX hydra_oauth2_user_code_challenge_id_idx ON hydra_oauth2_user_code (challenge_id); +CREATE INDEX hydra_oauth2_user_code_expires_at_idx ON hydra_oauth2_device_code (expires_at); + +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_challenge_id VARCHAR(255) NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_code_request_id VARCHAR(255) NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_verifier VARCHAR(40) NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_csrf VARCHAR(40) NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_user_code_accepted_at TIMESTAMP NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_was_used BOOL NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_handled_at TIMESTAMP NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_error TEXT NULL; + +CREATE INDEX hydra_oauth2_flow_device_challenge_idx ON hydra_oauth2_flow (device_challenge_id); + +ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_id_token_lifespan BIGINT NULL DEFAULT NULL; +ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_access_token_lifespan BIGINT NULL DEFAULT NULL; +ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_refresh_token_lifespan BIGINT NULL DEFAULT NULL; diff --git a/persistence/sql/migrations/20241609000001000000_device_flow.postgres.up.sql b/persistence/sql/migrations/20241609000001000000_device_flow.postgres.up.sql new file mode 100644 index 00000000000..29bc24f69e3 --- /dev/null +++ b/persistence/sql/migrations/20241609000001000000_device_flow.postgres.up.sql @@ -0,0 +1,68 @@ +-- Migration generated by the command below; DO NOT EDIT. +-- hydra:generate hydra migrate gen +CREATE TABLE IF NOT EXISTS hydra_oauth2_device_code ( + signature VARCHAR(255) NOT NULL PRIMARY KEY, + request_id VARCHAR(40) NOT NULL, + requested_at TIMESTAMP NOT NULL DEFAULT NOW(), + client_id VARCHAR(255) NOT NULL, + scope TEXT NOT NULL, + granted_scope TEXT NOT NULL, + form_data TEXT NOT NULL, + session_data TEXT NOT NULL, + subject VARCHAR(255) NOT NULL DEFAULT '', + active BOOL NOT NULL DEFAULT true, + requested_audience TEXT NULL DEFAULT '', + granted_audience TEXT NULL DEFAULT '', + challenge_id VARCHAR(40) NULL, + expires_at TIMESTAMP NULL, + nid UUID NULL, + + FOREIGN KEY (client_id, nid) REFERENCES hydra_client(id, nid) ON DELETE CASCADE, + FOREIGN KEY (nid) REFERENCES networks(id) ON UPDATE RESTRICT ON DELETE CASCADE +); + +CREATE INDEX hydra_oauth2_device_code_request_id_idx ON hydra_oauth2_device_code (request_id, nid); +CREATE INDEX hydra_oauth2_device_code_client_id_idx ON hydra_oauth2_device_code (client_id, nid); +CREATE INDEX hydra_oauth2_device_code_challenge_id_idx ON hydra_oauth2_device_code (challenge_id); +CREATE INDEX hydra_oauth2_device_code_expires_at_idx ON hydra_oauth2_device_code (expires_at); + +CREATE TABLE IF NOT EXISTS hydra_oauth2_user_code ( + signature VARCHAR(255) NOT NULL PRIMARY KEY, + request_id VARCHAR(40) NOT NULL, + requested_at TIMESTAMP NOT NULL DEFAULT NOW(), + client_id VARCHAR(255) NOT NULL, + scope TEXT NOT NULL, + granted_scope TEXT NOT NULL, + form_data TEXT NOT NULL, + session_data TEXT NOT NULL, + subject VARCHAR(255) NOT NULL DEFAULT '', + active BOOL NOT NULL DEFAULT true, + requested_audience TEXT NULL DEFAULT '', + granted_audience TEXT NULL DEFAULT '', + challenge_id VARCHAR(40) NULL, + expires_at TIMESTAMP NULL, + nid UUID NULL, + + FOREIGN KEY (client_id, nid) REFERENCES hydra_client(id, nid) ON DELETE CASCADE, + FOREIGN KEY (nid) REFERENCES networks(id) ON UPDATE RESTRICT ON DELETE CASCADE +); + +CREATE INDEX hydra_oauth2_user_code_request_id_idx ON hydra_oauth2_user_code (request_id, nid); +CREATE INDEX hydra_oauth2_user_code_client_id_idx ON hydra_oauth2_user_code (client_id, nid); +CREATE INDEX hydra_oauth2_user_code_challenge_id_idx ON hydra_oauth2_user_code (challenge_id); +CREATE INDEX hydra_oauth2_user_code_expires_at_idx ON hydra_oauth2_device_code (expires_at); + +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_challenge_id VARCHAR(255) NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_code_request_id VARCHAR(255) NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_verifier VARCHAR(40) NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_csrf VARCHAR(40) NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_user_code_accepted_at TIMESTAMP NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_was_used BOOLEAN NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_handled_at TIMESTAMP NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_error TEXT NULL; + +CREATE INDEX hydra_oauth2_flow_device_challenge_idx ON hydra_oauth2_flow (device_challenge_id); + +ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_id_token_lifespan BIGINT NULL DEFAULT NULL; +ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_access_token_lifespan BIGINT NULL DEFAULT NULL; +ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_refresh_token_lifespan BIGINT NULL DEFAULT NULL; diff --git a/persistence/sql/migrations/20241609000001000000_device_flow.up.sql b/persistence/sql/migrations/20241609000001000000_device_flow.up.sql new file mode 100644 index 00000000000..ceb69a1078b --- /dev/null +++ b/persistence/sql/migrations/20241609000001000000_device_flow.up.sql @@ -0,0 +1,60 @@ +-- Migration generated by the command below; DO NOT EDIT. +-- hydra:generate hydra migrate gen +CREATE TABLE IF NOT EXISTS hydra_oauth2_device_code +( + signature VARCHAR(255) NOT NULL PRIMARY KEY, + request_id VARCHAR(40) NOT NULL, + requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + client_id VARCHAR(255) NOT NULL, + scope TEXT NOT NULL, + granted_scope TEXT NOT NULL, + form_data TEXT NOT NULL, + session_data TEXT NOT NULL, + subject VARCHAR(255) NOT NULL DEFAULT '', + active BOOL NOT NULL DEFAULT true, + requested_audience TEXT NULL DEFAULT '', + granted_audience TEXT NULL DEFAULT '', + challenge_id VARCHAR(40) NULL, + expires_at TIMESTAMP NULL, + nid UUID NULL +); +CREATE INDEX hydra_oauth2_device_code_request_id_idx ON hydra_oauth2_device_code (request_id, nid); +CREATE INDEX hydra_oauth2_device_code_client_id_idx ON hydra_oauth2_device_code (client_id, nid); +CREATE INDEX hydra_oauth2_device_code_challenge_id_idx ON hydra_oauth2_device_code (challenge_id); + +CREATE TABLE IF NOT EXISTS hydra_oauth2_user_code +( + signature VARCHAR(255) NOT NULL PRIMARY KEY, + request_id VARCHAR(40) NOT NULL, + requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + client_id VARCHAR(255) NOT NULL, + scope TEXT NOT NULL, + granted_scope TEXT NOT NULL, + form_data TEXT NOT NULL, + session_data TEXT NOT NULL, + subject VARCHAR(255) NOT NULL DEFAULT '', + active BOOL NOT NULL DEFAULT true, + requested_audience TEXT NULL DEFAULT '', + granted_audience TEXT NULL DEFAULT '', + challenge_id VARCHAR(40) NULL, + expires_at TIMESTAMP NULL, + nid UUID NULL +); +CREATE INDEX hydra_oauth2_user_code_request_id_idx ON hydra_oauth2_user_code (request_id, nid); +CREATE INDEX hydra_oauth2_user_code_client_id_idx ON hydra_oauth2_user_code (client_id, nid); +CREATE INDEX hydra_oauth2_user_code_challenge_id_idx ON hydra_oauth2_user_code (challenge_id); + +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_challenge_id VARCHAR(255) NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_code_request_id VARCHAR(255) NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_verifier VARCHAR(40) NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_csrf VARCHAR(40) NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_user_code_accepted_at TIMESTAMP NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_was_used BOOLEAN NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_handled_at TIMESTAMP NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_error TEXT NULL; + +CREATE INDEX hydra_oauth2_flow_device_challenge_idx ON hydra_oauth2_flow (device_challenge_id); + +ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_id_token_lifespan BIGINT NULL DEFAULT NULL; +ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_access_token_lifespan BIGINT NULL DEFAULT NULL; +ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_refresh_token_lifespan BIGINT NULL DEFAULT NULL; diff --git a/persistence/sql/src/YYYYMMDD000001_device_flow/20241609000001000000_device_flow.cockroach.up.sql b/persistence/sql/src/YYYYMMDD000001_device_flow/20241609000001000000_device_flow.cockroach.up.sql new file mode 100644 index 00000000000..59fae8ea86f --- /dev/null +++ b/persistence/sql/src/YYYYMMDD000001_device_flow/20241609000001000000_device_flow.cockroach.up.sql @@ -0,0 +1,68 @@ +CREATE TABLE IF NOT EXISTS hydra_oauth2_device_code +( + signature VARCHAR(255) NOT NULL PRIMARY KEY, + request_id VARCHAR(40) NOT NULL DEFAULT '', + requested_at TIMESTAMP NOT NULL DEFAULT NOW(), + client_id VARCHAR(255) NOT NULL DEFAULT '', + scope TEXT NOT NULL, + granted_scope TEXT NOT NULL, + form_data TEXT NOT NULL, + session_data TEXT NOT NULL, + subject VARCHAR(255) NOT NULL DEFAULT '', + active BOOL NOT NULL DEFAULT true, + requested_audience TEXT NOT NULL, + granted_audience TEXT NOT NULL, + challenge_id VARCHAR(40) NULL, + expires_at TIMESTAMP NULL, + nid UUID NOT NULL, + + FOREIGN KEY (client_id, nid) REFERENCES hydra_client(id, nid) ON DELETE CASCADE, + FOREIGN KEY (nid) REFERENCES networks(id) ON UPDATE RESTRICT ON DELETE CASCADE +); + +CREATE INDEX hydra_oauth2_device_code_request_id_idx ON hydra_oauth2_device_code (request_id, nid); +CREATE INDEX hydra_oauth2_device_code_client_id_idx ON hydra_oauth2_device_code (client_id, nid); +CREATE INDEX hydra_oauth2_device_code_challenge_id_idx ON hydra_oauth2_device_code (challenge_id); +CREATE INDEX hydra_oauth2_device_code_expires_at_idx ON hydra_oauth2_device_code (expires_at); + +CREATE TABLE IF NOT EXISTS hydra_oauth2_user_code +( + signature VARCHAR(255) NOT NULL PRIMARY KEY, + request_id VARCHAR(40) NOT NULL DEFAULT '', + requested_at TIMESTAMP NOT NULL DEFAULT NOW(), + client_id VARCHAR(255) NOT NULL DEFAULT '', + scope TEXT NOT NULL, + granted_scope TEXT NOT NULL, + form_data TEXT NOT NULL, + session_data TEXT NOT NULL, + subject VARCHAR(255) NOT NULL DEFAULT '', + active BOOL NOT NULL DEFAULT true, + requested_audience TEXT NOT NULL, + granted_audience TEXT NOT NULL, + challenge_id VARCHAR(40) NULL, + expires_at TIMESTAMP NULL, + nid UUID NOT NULL, + + FOREIGN KEY (client_id, nid) REFERENCES hydra_client(id, nid) ON DELETE CASCADE, + FOREIGN KEY (nid) REFERENCES networks(id) ON UPDATE RESTRICT ON DELETE CASCADE +); + +CREATE INDEX hydra_oauth2_user_code_request_id_idx ON hydra_oauth2_user_code (request_id, nid); +CREATE INDEX hydra_oauth2_user_code_client_id_idx ON hydra_oauth2_user_code (client_id, nid); +CREATE INDEX hydra_oauth2_user_code_challenge_id_idx ON hydra_oauth2_user_code (challenge_id); +CREATE INDEX hydra_oauth2_user_code_expires_at_idx ON hydra_oauth2_device_code (expires_at); + +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_challenge_id VARCHAR(255) NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_code_request_id VARCHAR(255) NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_verifier VARCHAR(40) NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_csrf VARCHAR(40) NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_user_code_accepted_at TIMESTAMP NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_was_used BOOL NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_handled_at TIMESTAMP NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_error TEXT NULL; + +CREATE INDEX hydra_oauth2_flow_device_challenge_idx ON hydra_oauth2_flow (device_challenge_id); + +ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_id_token_lifespan BIGINT NULL DEFAULT NULL; +ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_access_token_lifespan BIGINT NULL DEFAULT NULL; +ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_refresh_token_lifespan BIGINT NULL DEFAULT NULL; diff --git a/persistence/sql/src/YYYYMMDD000001_device_flow/20241609000001000000_device_flow.down.sql b/persistence/sql/src/YYYYMMDD000001_device_flow/20241609000001000000_device_flow.down.sql new file mode 100644 index 00000000000..2547f10e767 --- /dev/null +++ b/persistence/sql/src/YYYYMMDD000001_device_flow/20241609000001000000_device_flow.down.sql @@ -0,0 +1,25 @@ +ALTER TABLE hydra_oauth2_device_code DROP FOREIGN KEY IF EXISTS hydra_oauth2_device_code_challenge_id_fk; +ALTER TABLE hydra_oauth2_device_code DROP FOREIGN KEY IF EXISTS hydra_oauth2_device_code_client_id_fk; +ALTER TABLE hydra_oauth2_device_code DROP FOREIGN KEY IF EXISTS hydra_oauth2_device_code_nid_fk_idx; + +DROP TABLE IF EXISTS hydra_oauth2_device_code; + +ALTER TABLE hydra_oauth2_user_code DROP FOREIGN KEY IF EXISTS hydra_oauth2_user_code_challenge_id_fk; +ALTER TABLE hydra_oauth2_user_code DROP FOREIGN KEY IF EXISTS hydra_oauth2_user_code_client_id_fk; +ALTER TABLE hydra_oauth2_user_code DROP FOREIGN KEY IF EXISTS hydra_oauth2_user_code_nid_fk_idx; + +DROP TABLE IF EXISTS hydra_oauth2_user_code; + +ALTER TABLE hydra_oauth2_flow DROP COLUMN IF EXISTS device_challenge_id; +ALTER TABLE hydra_oauth2_flow DROP COLUMN IF EXISTS device_code_request_id; +ALTER TABLE hydra_oauth2_flow DROP COLUMN IF EXISTS device_verifier; +ALTER TABLE hydra_oauth2_flow DROP COLUMN IF EXISTS device_csrf; +ALTER TABLE hydra_oauth2_flow DROP COLUMN IF EXISTS device_user_code_accepted_at; +ALTER TABLE hydra_oauth2_flow DROP COLUMN IF EXISTS device_was_used; +ALTER TABLE hydra_oauth2_flow DROP COLUMN IF EXISTS device_handled_at; +ALTER TABLE hydra_oauth2_flow DROP COLUMN IF EXISTS device_error; + + +ALTER TABLE hydra_client DROP COLUMN device_authorization_grant_id_token_lifespan; +ALTER TABLE hydra_client DROP COLUMN device_authorization_grant_access_token_lifespan; +ALTER TABLE hydra_client DROP COLUMN device_authorization_grant_refresh_token_lifespan; \ No newline at end of file diff --git a/persistence/sql/src/YYYYMMDD000001_device_flow/20241609000001000000_device_flow.mysql.up.sql b/persistence/sql/src/YYYYMMDD000001_device_flow/20241609000001000000_device_flow.mysql.up.sql new file mode 100644 index 00000000000..19892b22d87 --- /dev/null +++ b/persistence/sql/src/YYYYMMDD000001_device_flow/20241609000001000000_device_flow.mysql.up.sql @@ -0,0 +1,68 @@ +CREATE TABLE IF NOT EXISTS hydra_oauth2_device_code +( + signature VARCHAR(255) NOT NULL PRIMARY KEY, + request_id VARCHAR(40) NOT NULL DEFAULT '', + requested_at TIMESTAMP NOT NULL DEFAULT NOW(), + client_id VARCHAR(255) NOT NULL DEFAULT '', + scope TEXT NOT NULL, + granted_scope TEXT NOT NULL, + form_data TEXT NOT NULL, + session_data TEXT NOT NULL, + subject VARCHAR(255) NOT NULL DEFAULT '', + active BOOL NOT NULL DEFAULT true, + requested_audience TEXT NOT NULL, + granted_audience TEXT NOT NULL, + challenge_id VARCHAR(40) NULL, + expires_at TIMESTAMP NULL, + nid CHAR(36) NOT NULL, + + FOREIGN KEY (client_id, nid) REFERENCES hydra_client(id, nid) ON DELETE CASCADE, + FOREIGN KEY (nid) REFERENCES networks(id) ON UPDATE RESTRICT ON DELETE CASCADE +); + +CREATE INDEX hydra_oauth2_device_code_request_id_idx ON hydra_oauth2_device_code (request_id, nid); +CREATE INDEX hydra_oauth2_device_code_client_id_idx ON hydra_oauth2_device_code (client_id, nid); +CREATE INDEX hydra_oauth2_device_code_challenge_id_idx ON hydra_oauth2_device_code (challenge_id); +CREATE INDEX hydra_oauth2_device_code_expires_at_idx ON hydra_oauth2_device_code (expires_at); + +CREATE TABLE IF NOT EXISTS hydra_oauth2_user_code +( + signature VARCHAR(255) NOT NULL PRIMARY KEY, + request_id VARCHAR(40) NOT NULL DEFAULT '', + requested_at TIMESTAMP NOT NULL DEFAULT NOW(), + client_id VARCHAR(255) NOT NULL DEFAULT '', + scope TEXT NOT NULL, + granted_scope TEXT NOT NULL, + form_data TEXT NOT NULL, + session_data TEXT NOT NULL, + subject VARCHAR(255) NOT NULL DEFAULT '', + active BOOL NOT NULL DEFAULT true, + requested_audience TEXT NOT NULL, + granted_audience TEXT NOT NULL, + challenge_id VARCHAR(40) NULL, + expires_at TIMESTAMP NULL, + nid CHAR(36) NOT NULL, + + FOREIGN KEY (client_id, nid) REFERENCES hydra_client(id, nid) ON DELETE CASCADE, + FOREIGN KEY (nid) REFERENCES networks(id) ON UPDATE RESTRICT ON DELETE CASCADE +); + +CREATE INDEX hydra_oauth2_user_code_request_id_idx ON hydra_oauth2_user_code (request_id, nid); +CREATE INDEX hydra_oauth2_user_code_client_id_idx ON hydra_oauth2_user_code (client_id, nid); +CREATE INDEX hydra_oauth2_user_code_challenge_id_idx ON hydra_oauth2_user_code (challenge_id); +CREATE INDEX hydra_oauth2_user_code_expires_at_idx ON hydra_oauth2_device_code (expires_at); + +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_challenge_id VARCHAR(255) NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_code_request_id VARCHAR(255) NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_verifier VARCHAR(40) NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_csrf VARCHAR(40) NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_user_code_accepted_at TIMESTAMP NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_was_used BOOL NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_handled_at TIMESTAMP NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_error TEXT NULL; + +CREATE INDEX hydra_oauth2_flow_device_challenge_idx ON hydra_oauth2_flow (device_challenge_id); + +ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_id_token_lifespan BIGINT NULL DEFAULT NULL; +ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_access_token_lifespan BIGINT NULL DEFAULT NULL; +ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_refresh_token_lifespan BIGINT NULL DEFAULT NULL; diff --git a/persistence/sql/src/YYYYMMDD000001_device_flow/20241609000001000000_device_flow.postgres.up.sql b/persistence/sql/src/YYYYMMDD000001_device_flow/20241609000001000000_device_flow.postgres.up.sql new file mode 100644 index 00000000000..62b894fd6b8 --- /dev/null +++ b/persistence/sql/src/YYYYMMDD000001_device_flow/20241609000001000000_device_flow.postgres.up.sql @@ -0,0 +1,66 @@ +CREATE TABLE IF NOT EXISTS hydra_oauth2_device_code ( + signature VARCHAR(255) NOT NULL PRIMARY KEY, + request_id VARCHAR(40) NOT NULL, + requested_at TIMESTAMP NOT NULL DEFAULT NOW(), + client_id VARCHAR(255) NOT NULL, + scope TEXT NOT NULL, + granted_scope TEXT NOT NULL, + form_data TEXT NOT NULL, + session_data TEXT NOT NULL, + subject VARCHAR(255) NOT NULL DEFAULT '', + active BOOL NOT NULL DEFAULT true, + requested_audience TEXT NULL DEFAULT '', + granted_audience TEXT NULL DEFAULT '', + challenge_id VARCHAR(40) NULL, + expires_at TIMESTAMP NULL, + nid UUID NULL, + + FOREIGN KEY (client_id, nid) REFERENCES hydra_client(id, nid) ON DELETE CASCADE, + FOREIGN KEY (nid) REFERENCES networks(id) ON UPDATE RESTRICT ON DELETE CASCADE +); + +CREATE INDEX hydra_oauth2_device_code_request_id_idx ON hydra_oauth2_device_code (request_id, nid); +CREATE INDEX hydra_oauth2_device_code_client_id_idx ON hydra_oauth2_device_code (client_id, nid); +CREATE INDEX hydra_oauth2_device_code_challenge_id_idx ON hydra_oauth2_device_code (challenge_id); +CREATE INDEX hydra_oauth2_device_code_expires_at_idx ON hydra_oauth2_device_code (expires_at); + +CREATE TABLE IF NOT EXISTS hydra_oauth2_user_code ( + signature VARCHAR(255) NOT NULL PRIMARY KEY, + request_id VARCHAR(40) NOT NULL, + requested_at TIMESTAMP NOT NULL DEFAULT NOW(), + client_id VARCHAR(255) NOT NULL, + scope TEXT NOT NULL, + granted_scope TEXT NOT NULL, + form_data TEXT NOT NULL, + session_data TEXT NOT NULL, + subject VARCHAR(255) NOT NULL DEFAULT '', + active BOOL NOT NULL DEFAULT true, + requested_audience TEXT NULL DEFAULT '', + granted_audience TEXT NULL DEFAULT '', + challenge_id VARCHAR(40) NULL, + expires_at TIMESTAMP NULL, + nid UUID NULL, + + FOREIGN KEY (client_id, nid) REFERENCES hydra_client(id, nid) ON DELETE CASCADE, + FOREIGN KEY (nid) REFERENCES networks(id) ON UPDATE RESTRICT ON DELETE CASCADE +); + +CREATE INDEX hydra_oauth2_user_code_request_id_idx ON hydra_oauth2_user_code (request_id, nid); +CREATE INDEX hydra_oauth2_user_code_client_id_idx ON hydra_oauth2_user_code (client_id, nid); +CREATE INDEX hydra_oauth2_user_code_challenge_id_idx ON hydra_oauth2_user_code (challenge_id); +CREATE INDEX hydra_oauth2_user_code_expires_at_idx ON hydra_oauth2_device_code (expires_at); + +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_challenge_id VARCHAR(255) NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_code_request_id VARCHAR(255) NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_verifier VARCHAR(40) NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_csrf VARCHAR(40) NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_user_code_accepted_at TIMESTAMP NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_was_used BOOLEAN NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_handled_at TIMESTAMP NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_error TEXT NULL; + +CREATE INDEX hydra_oauth2_flow_device_challenge_idx ON hydra_oauth2_flow (device_challenge_id); + +ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_id_token_lifespan BIGINT NULL DEFAULT NULL; +ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_access_token_lifespan BIGINT NULL DEFAULT NULL; +ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_refresh_token_lifespan BIGINT NULL DEFAULT NULL; diff --git a/persistence/sql/src/YYYYMMDD000001_device_flow/20241609000001000000_device_flow.up.sql b/persistence/sql/src/YYYYMMDD000001_device_flow/20241609000001000000_device_flow.up.sql new file mode 100644 index 00000000000..15f7417c626 --- /dev/null +++ b/persistence/sql/src/YYYYMMDD000001_device_flow/20241609000001000000_device_flow.up.sql @@ -0,0 +1,58 @@ +CREATE TABLE IF NOT EXISTS hydra_oauth2_device_code +( + signature VARCHAR(255) NOT NULL PRIMARY KEY, + request_id VARCHAR(40) NOT NULL, + requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + client_id VARCHAR(255) NOT NULL, + scope TEXT NOT NULL, + granted_scope TEXT NOT NULL, + form_data TEXT NOT NULL, + session_data TEXT NOT NULL, + subject VARCHAR(255) NOT NULL DEFAULT '', + active BOOL NOT NULL DEFAULT true, + requested_audience TEXT NULL DEFAULT '', + granted_audience TEXT NULL DEFAULT '', + challenge_id VARCHAR(40) NULL, + expires_at TIMESTAMP NULL, + nid UUID NULL +); +CREATE INDEX hydra_oauth2_device_code_request_id_idx ON hydra_oauth2_device_code (request_id, nid); +CREATE INDEX hydra_oauth2_device_code_client_id_idx ON hydra_oauth2_device_code (client_id, nid); +CREATE INDEX hydra_oauth2_device_code_challenge_id_idx ON hydra_oauth2_device_code (challenge_id); + +CREATE TABLE IF NOT EXISTS hydra_oauth2_user_code +( + signature VARCHAR(255) NOT NULL PRIMARY KEY, + request_id VARCHAR(40) NOT NULL, + requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + client_id VARCHAR(255) NOT NULL, + scope TEXT NOT NULL, + granted_scope TEXT NOT NULL, + form_data TEXT NOT NULL, + session_data TEXT NOT NULL, + subject VARCHAR(255) NOT NULL DEFAULT '', + active BOOL NOT NULL DEFAULT true, + requested_audience TEXT NULL DEFAULT '', + granted_audience TEXT NULL DEFAULT '', + challenge_id VARCHAR(40) NULL, + expires_at TIMESTAMP NULL, + nid UUID NULL +); +CREATE INDEX hydra_oauth2_user_code_request_id_idx ON hydra_oauth2_user_code (request_id, nid); +CREATE INDEX hydra_oauth2_user_code_client_id_idx ON hydra_oauth2_user_code (client_id, nid); +CREATE INDEX hydra_oauth2_user_code_challenge_id_idx ON hydra_oauth2_user_code (challenge_id); + +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_challenge_id VARCHAR(255) NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_code_request_id VARCHAR(255) NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_verifier VARCHAR(40) NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_csrf VARCHAR(40) NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_user_code_accepted_at TIMESTAMP NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_was_used BOOLEAN NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_handled_at TIMESTAMP NULL; +ALTER TABLE hydra_oauth2_flow ADD COLUMN device_error TEXT NULL; + +CREATE INDEX hydra_oauth2_flow_device_challenge_idx ON hydra_oauth2_flow (device_challenge_id); + +ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_id_token_lifespan BIGINT NULL DEFAULT NULL; +ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_access_token_lifespan BIGINT NULL DEFAULT NULL; +ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_refresh_token_lifespan BIGINT NULL DEFAULT NULL; From 7ef66de4f47d01d3f5bfdc5a9a906f4dc19e06c3 Mon Sep 17 00:00:00 2001 From: Nikos Date: Tue, 24 Sep 2024 13:17:55 +0300 Subject: [PATCH 06/52] fix: update oauth persister logic --- driver/config/provider.go | 5 ++ driver/registry_sql.go | 14 ++++ flow/consent_types.go | 65 ++++++++++++++++ flow/flow.go | 102 +++++++++++++++++++++++++ oauth2/flowctx/encoding.go | 4 + persistence/sql/persister_consent.go | 98 +++++++++++++++++++++++- persistence/sql/persister_oauth2.go | 110 +++++++++++++++++++++++++-- x/clean_sql.go | 4 + 8 files changed, 395 insertions(+), 7 deletions(-) diff --git a/driver/config/provider.go b/driver/config/provider.go index 5957b4b3877..740e82cade2 100644 --- a/driver/config/provider.go +++ b/driver/config/provider.go @@ -66,6 +66,7 @@ const ( KeyCookieDomain = "serve.cookies.domain" KeyCookieSecure = "serve.cookies.secure" KeyCookieLoginCSRFName = "serve.cookies.names.login_csrf" + KeyCookieDeviceCSRFName = "serve.cookies.names.device_csrf" KeyCookieConsentCSRFName = "serve.cookies.names.consent_csrf" KeyCookieSessionName = "serve.cookies.names.session" KeyCookieSessionPath = "serve.cookies.paths.session" @@ -693,6 +694,10 @@ func (p *DefaultProvider) CookieNameLoginCSRF(ctx context.Context) string { return p.cookieSuffix(ctx, KeyCookieLoginCSRFName) } +func (p *DefaultProvider) CookieNameDeviceCSRF(ctx context.Context) string { + return p.cookieSuffix(ctx, KeyCookieDeviceCSRFName) +} + func (p *DefaultProvider) CookieNameConsentCSRF(ctx context.Context) string { return p.cookieSuffix(ctx, KeyCookieConsentCSRFName) } diff --git a/driver/registry_sql.go b/driver/registry_sql.go index 7cef650f955..07832c3e022 100644 --- a/driver/registry_sql.go +++ b/driver/registry_sql.go @@ -22,6 +22,7 @@ import ( "github.com/ory/fosite/compose" foauth2 "github.com/ory/fosite/handler/oauth2" "github.com/ory/fosite/handler/openid" + "github.com/ory/fosite/handler/rfc8628" "github.com/ory/fosite/token/hmac" "github.com/ory/herodot" "github.com/ory/hydra/v2/aead" @@ -99,6 +100,7 @@ type RegistrySQL struct { ats jwk.JWTSigner hmacs foauth2.CoreStrategy enigmaHMAC *hmac.HMACStrategy + deviceHmac rfc8628.RFC8628CodeStrategy fc *fositex.Config publicCORS *cors.Cors kratos kratos.Client @@ -592,6 +594,16 @@ func (m *RegistrySQL) OAuth2HMACStrategy() foauth2.CoreStrategy { return m.hmacs } +// RFC8628HMACStrategy returns the rfc8628 strategy +func (m *RegistrySQL) RFC8628HMACStrategy() rfc8628.RFC8628CodeStrategy { + if m.deviceHmac != nil { + return m.deviceHmac + } + + m.deviceHmac = compose.NewDeviceStrategy(m.OAuth2Config()) + return m.deviceHmac +} + func (m *RegistrySQL) OAuth2Config() *fositex.Config { if m.fc != nil { return m.fc @@ -618,6 +630,7 @@ func (m *RegistrySQL) OAuth2ProviderConfig() fosite.Configurator { conf := m.OAuth2Config() hmacAtStrategy := m.OAuth2HMACStrategy() + deviceHmacAtStrategy := m.RFC8628HMACStrategy() oidcSigner := m.OpenIDJWTStrategy() atSigner := m.AccessTokenJWTStrategy() jwtAtStrategy := &foauth2.DefaultJWTStrategy{ @@ -632,6 +645,7 @@ func (m *RegistrySQL) OAuth2ProviderConfig() fosite.Configurator { HMACSHAStrategy: hmacAtStrategy, Config: conf, }), + RFC8628CodeStrategy: deviceHmacAtStrategy, OpenIDConnectTokenStrategy: &openid.DefaultStrategy{ Config: conf, Signer: oidcSigner, diff --git a/flow/consent_types.go b/flow/consent_types.go index 1216760118f..e1d708d10e9 100644 --- a/flow/consent_types.go +++ b/flow/consent_types.go @@ -23,6 +23,7 @@ import ( ) const ( + DeviceRequestDeniedErrorName = "device request denied" ConsentRequestDeniedErrorName = "consent request denied" LoginRequestDeniedErrorName = "login request denied" ) @@ -539,6 +540,70 @@ type LogoutResult struct { FrontChannelLogoutURLs []string } +// Contains information on an ongoing device grant request. +// +// swagger:model DeviceUserAuthRequest +type DeviceUserAuthRequest struct { + // ID is the identifier ("device challenge") of the device grant request. It is used to + // identify the session. + // + // required: true + ID string `json:"challenge"` + NID uuid.UUID `json:"-"` + + // RequestedScope contains the OAuth 2.0 Scope requested by the OAuth 2.0 Client. + RequestedScope sqlxx.StringSliceJSONFormat `json:"requested_scope"` + + // RequestedAudience contains the access token audience as requested by the OAuth 2.0 Client. + RequestedAudience sqlxx.StringSliceJSONFormat `json:"requested_access_token_audience"` + + // RequestURL is the original Device Grant URL requested. + RequestURL string `json:"request_url"` + // SessionID is the login session ID. If the user-agent reuses a login session (via cookie / remember flag) + // this ID will remain the same. If the user-agent did not have an existing authentication session (e.g. remember is false) + // this will be a new random value. This value is used as the "sid" parameter in the ID Token and in OIDC Front-/Back- + // channel logout. It's value can generally be used to associate consecutive login requests by a certain user. + SessionID sqlxx.NullString `json:"session_id"` + + // Client is the OAuth 2.0 Client that initiated the request. + // + // required: true + Client *client.Client `json:"client"` + ClientID string `json:"-"` + + // DeviceCodeSignature is the OAuth 2.0 Device Authorization Grant Device Code Signature + // + // required: true + DeviceCodeSignature sqlxx.NullString `json:"-"` + + CSRF string `json:"-"` + Verifier string `json:"-"` + + Accepted bool `json:"-"` + AcceptedAt sqlxx.NullTime `json:"handled_at"` + RequestedAt time.Time `json:"-"` +} + +// HandledDeviceUserAuthRequest is the request payload used to accept a device user_code. +// +// swagger:model verifyUserCodeRequest +type HandledDeviceUserAuthRequest struct { + // ID is the identifier ("device challenge") of the device request. It is used to + // identify the session. + // + // required: true + ID string `json:"challenge"` + UserCode string `json:"user_code"` + HandledAt sqlxx.NullTime `json:"handled_at"` + WasHandled bool `json:"-"` + DeviceRequest *DeviceUserAuthRequest `json:"-" faker:"-"` + Error *RequestDeniedError `json:"-"` +} + +func (r *HandledDeviceUserAuthRequest) HasError() bool { + return r.Error.IsError() +} + // Contains information on an ongoing login request. // // swagger:model oAuth2LoginRequest diff --git a/flow/flow.go b/flow/flow.go index b44e14322bb..ff46088e7c9 100644 --- a/flow/flow.go +++ b/flow/flow.go @@ -24,6 +24,10 @@ import ( // // graph TD // +// DEVICE_INITIALIZED --> DEVICE_UNUSED +// DEVICE_UNUSED --> DEVICE_USED +// DEVICE_UNUSED --> DEVICE_ERROR +// DEVICE_USED --> LOGIN_INITIALIZED // LOGIN_INITIALIZED --> LOGIN_UNUSED // LOGIN_UNUSED --> LOGIN_USED // LOGIN_UNUSED --> LOGIN_ERROR @@ -54,6 +58,20 @@ const ( FlowStateConsentUnused = int16(5) FlowStateConsentUsed = int16(6) + // FlowStateLoginInitialized applies before the login app either + // accepts or rejects the login request. + FlowStateDeviceInitialized = int16(7) + + // FlowStateDeviceUnused indicates that the login has been authenticated, but + // the User Agent hasn't picked up the result yet. + FlowStateDeviceUnused = int16(8) + + // FlowStateDeviceUsed indicates that the User Agent is requesting consent and + // Hydra has invalidated the login request. This is a short-lived state + // because the transition to FlowStateConsentInitialized should happen while + // handling the request that triggered the transition to FlowStateDeviceUsed. + FlowStateDeviceUsed = int16(9) + // TODO: Refactor error handling to persist error codes instead of JSON // strings. Currently we persist errors as JSON strings in the LoginError // and ConsentError fields. This shouldn't be necessary because the different @@ -65,6 +83,7 @@ const ( // If the above is implemented, merge the LoginError and ConsentError fields // and use the following FlowStates when converting to/from // [Handled]{Login|Consent}Request: + FlowStateDeviceError = int16(127) FlowStateLoginError = int16(128) FlowStateConsentError = int16(129) ) @@ -211,6 +230,22 @@ type Flow struct { // The database column should be named `consent_request_id`, but is not for historical reasons. ConsentRequestID sqlxx.NullString `db:"consent_challenge_id" json:"cc,omitempty"` + // DeviceChallengeID is the identifier ("authorization challenge") of the consent authorization request. It is used to + // identify the session. + // + // required: true + DeviceChallengeID sqlxx.NullString `db:"device_challenge_id"` + + DeviceVerifier string `db:"device_verifier"` + DeviceCSRF string `db:"device_csrf"` + + // The user_code was already handled. + // TODO(nsklikas): Is this needed? + DeviceUserCodeWasUsed bool `db:"device_user_code_was_used"` + // DeviceHandledAt contains the timestamp the device user verification request was handled. + DeviceUserCodeHandledAt sqlxx.NullTime `db:"device_user_code_handled_at"` + DeviceError *RequestDeniedError `db:"device_error"` + // ConsentSkip, if true, implies that the client has requested the same scopes from the same user previously. // If true, you must not ask the user to grant the requested scopes. You must however either allow or deny the // consent request using the usual API call. @@ -266,6 +301,22 @@ func NewFlow(r *LoginRequest) *Flow { } } +func NewDeviceFlow(r *DeviceUserAuthRequest) *Flow { + return &Flow{ + ID: r.ID, + RequestedScope: r.RequestedScope, + RequestedAudience: r.RequestedAudience, + Client: r.Client, + ClientID: r.ClientID, + RequestURL: r.RequestURL, + SessionID: r.SessionID, + LoginVerifier: r.Verifier, + LoginCSRF: r.CSRF, + RequestedAt: r.RequestedAt, + State: FlowStateDeviceInitialized, + } +} + func (f *Flow) HandleLoginRequest(h *HandledLoginRequest) error { if f.LoginWasUsed { return errors.WithStack(x.ErrConflict.WithHint("The login request was already used and can no longer be changed.")) @@ -368,6 +419,47 @@ func (f *Flow) InvalidateLoginRequest() error { return nil } +func (f *Flow) GetDeviceUserAuthRequest() *DeviceUserAuthRequest { + return &DeviceUserAuthRequest{ + ID: f.ID, + RequestedScope: f.RequestedScope, + RequestedAudience: f.RequestedAudience, + Client: f.Client, + ClientID: f.ClientID, + RequestURL: f.RequestURL, + SessionID: f.SessionID, + Verifier: f.LoginVerifier, + CSRF: f.LoginCSRF, + RequestedAt: f.RequestedAt, + } +} + +func (f *Flow) HandleDeviceUserAuthRequest(h *HandledDeviceUserAuthRequest) error { + if f.DeviceUserCodeWasUsed { + return errors.WithStack(x.ErrConflict.WithHint("The user_code was already used and can no longer be changed.")) + } + + if f.State != FlowStateDeviceInitialized && f.State != FlowStateDeviceUnused && f.State != FlowStateDeviceError { + return errors.Errorf("invalid flow state: expected %d/%d/%d, got %d", FlowStateDeviceInitialized, FlowStateDeviceUnused, FlowStateDeviceError, f.State) + } + + if f.ID != h.ID { + return errors.Errorf("flow device challenge ID %s does not match HandledDeviceUserAuthRequest ID %s", f.ID, h.ID) + } + + if h.Error != nil { + f.State = FlowStateDeviceError + } else { + f.State = FlowStateDeviceUnused + } + f.DeviceError = h.Error + f.DeviceUserCodeHandledAt = h.HandledAt + f.DeviceUserCodeWasUsed = h.WasHandled + f.DeviceError = h.Error + + return nil +} + func (f *Flow) HandleConsentRequest(r *AcceptOAuth2ConsentRequest) error { if time.Time(r.HandledAt).IsZero() { return errors.New("refusing to handle a consent request with null HandledAt") @@ -513,6 +605,16 @@ type CipherProvider interface { FlowCipher() *aead.XChaCha20Poly1305 } +// ToDeviceChallenge converts the flow into a device challenge. +func (f *Flow) ToDeviceChallenge(ctx context.Context, cipherProvider CipherProvider) (string, error) { + return flowctx.Encode(ctx, cipherProvider.FlowCipher(), f, flowctx.AsDeviceChallenge) +} + +// ToDeviceVerifier converts the flow into a device verifier. +func (f *Flow) ToDeviceVerifier(ctx context.Context, cipherProvider CipherProvider) (string, error) { + return flowctx.Encode(ctx, cipherProvider.FlowCipher(), f, flowctx.AsDeviceVerifier) +} + // ToLoginChallenge converts the flow into a login challenge. func (f Flow) ToLoginChallenge(ctx context.Context, cipherProvider CipherProvider) (challenge string, err error) { if f.Client != nil { diff --git a/oauth2/flowctx/encoding.go b/oauth2/flowctx/encoding.go index 8a1f8cbf270..8c659ad724e 100644 --- a/oauth2/flowctx/encoding.go +++ b/oauth2/flowctx/encoding.go @@ -25,6 +25,8 @@ type ( const ( loginChallenge purpose = iota loginVerifier + deviceChallenge + deviceVerifier consentChallenge consentVerifier ) @@ -34,6 +36,8 @@ func withPurpose(purpose purpose) CodecOption { return func(ad *data) { ad.Purpo var ( AsLoginChallenge = withPurpose(loginChallenge) AsLoginVerifier = withPurpose(loginVerifier) + AsDeviceChallenge = withPurpose(deviceChallenge) + AsDeviceVerifier = withPurpose(deviceVerifier) AsConsentChallenge = withPurpose(consentChallenge) AsConsentVerifier = withPurpose(consentVerifier) ) diff --git a/persistence/sql/persister_consent.go b/persistence/sql/persister_consent.go index 7b314f275f0..e8f91d72882 100644 --- a/persistence/sql/persister_consent.go +++ b/persistence/sql/persister_consent.go @@ -224,11 +224,105 @@ func (p *Persister) GetConsentRequest(ctx context.Context, challenge string) (_ return f.GetConsentRequest(challenge), nil } -func (p *Persister) CreateLoginRequest(ctx context.Context, req *flow.LoginRequest) (_ *flow.Flow, err error) { +// CreateDeviceUserAuthRequest creates a new flow from a DeviceUserAuthRequest. +func (p *Persister) CreateDeviceUserAuthRequest(ctx context.Context, req *flow.DeviceUserAuthRequest) (*flow.Flow, error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateDeviceUserAuthRequest") + defer span.End() + + nid := p.NetworkID(ctx) + if nid == uuid.Nil { + return nil, errorsx.WithStack(x.ErrNotFound) + } + f := flow.NewDeviceFlow(req) + f.NID = nid + + return f, nil +} + +// GetDeviceUserAuthRequest decodes a challenge into a new DeviceUserAuthRequest. +func (p *Persister) GetDeviceUserAuthRequest(ctx context.Context, challenge string) (*flow.DeviceUserAuthRequest, error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.GetDeviceUserAuthRequest") + defer span.End() + + f, err := flowctx.Decode[flow.Flow](ctx, p.r.FlowCipher(), challenge, flowctx.AsDeviceChallenge) + if err != nil { + return nil, errorsx.WithStack(x.ErrNotFound.WithWrap(err)) + } + if f.NID != p.NetworkID(ctx) { + return nil, errorsx.WithStack(x.ErrNotFound) + } + if f.RequestedAt.Add(p.config.ConsentRequestMaxAge(ctx)).Before(time.Now()) { + return nil, errorsx.WithStack(fosite.ErrRequestUnauthorized.WithHint("The device request has expired, please try again.")) + } + dr := f.GetDeviceUserAuthRequest() + + return dr, nil +} + +// HandleDeviceUserAuthRequest uses a HandledDeviceUserAuthRequest to update the flow and returns a DeviceUserAuthRequest. +func (p *Persister) HandleDeviceUserAuthRequest(ctx context.Context, f *flow.Flow, challenge string, r *flow.HandledDeviceUserAuthRequest) (*flow.DeviceUserAuthRequest, error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.HandleDeviceUserAuthRequest") + defer span.End() + + if f == nil { + return nil, errorsx.WithStack(fosite.ErrInvalidRequest.WithDebug("Flow was nil")) + } + if f.NID != p.NetworkID(ctx) { + return nil, errorsx.WithStack(x.ErrNotFound) + } + err := f.HandleDeviceUserAuthRequest(r) + if err != nil { + return nil, err + } + + return p.GetDeviceUserAuthRequest(ctx, challenge) +} + +// VerifyAndInvalidateDeviceUserAuthRequest verifies a verifier and invalidates the flow. +func (p *Persister) VerifyAndInvalidateDeviceUserAuthRequest(ctx context.Context, verifier string) (*flow.HandledDeviceUserAuthRequest, error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.VerifyAndInvalidateDeviceUserAuthRequest") + defer span.End() + + f, err := flowctx.Decode[flow.Flow](ctx, p.r.FlowCipher(), verifier, flowctx.AsDeviceVerifier) + if err != nil { + return nil, errorsx.WithStack(fosite.ErrAccessDenied.WithHint("The device verifier has already been used, has not been granted, or is invalid.")) + } + if f.NID != p.NetworkID(ctx) { + return nil, errorsx.WithStack(sqlcon.ErrNoRows) + } + + if err = f.InvalidateDeviceRequest(); err != nil { + return nil, errorsx.WithStack(fosite.ErrInvalidRequest.WithDebug(err.Error())) + } + + return f.GetHandledDeviceUserAuthRequest(), nil +} + +func (p *Persister) CreateLoginRequest(ctx context.Context, f *flow.Flow, req *flow.LoginRequest) (_ *flow.Flow, err error) { ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateLoginRequest") defer otelx.End(span, &err) - f := flow.NewFlow(req) + if f == nil { + f = flow.NewFlow(req) + } else { + f.ID = req.ID + f.RequestedScope = req.RequestedScope + f.RequestedAudience = req.RequestedAudience + f.LoginSkip = req.Skip + f.Subject = req.Subject + f.OpenIDConnectContext = req.OpenIDConnectContext + f.Client = req.Client + f.ClientID = req.ClientID + f.RequestURL = req.RequestURL + f.SessionID = req.SessionID + f.LoginWasUsed = req.WasHandled + f.ForceSubjectIdentifier = req.ForceSubjectIdentifier + f.LoginVerifier = req.Verifier + f.LoginCSRF = req.CSRF + f.LoginAuthenticatedAt = req.AuthenticatedAt + f.RequestedAt = req.RequestedAt + f.State = flow.FlowStateLoginInitialized + } nid := p.NetworkID(ctx) if nid == uuid.Nil { return nil, errorsx.WithStack(x.ErrNotFound) diff --git a/persistence/sql/persister_oauth2.go b/persistence/sql/persister_oauth2.go index 851367e3411..4e2fb26610e 100644 --- a/persistence/sql/persister_oauth2.go +++ b/persistence/sql/persister_oauth2.go @@ -65,11 +65,13 @@ type ( ) const ( - sqlTableOpenID tableName = "oidc" - sqlTableAccess tableName = "access" - sqlTableRefresh tableName = "refresh" - sqlTableCode tableName = "code" - sqlTablePKCE tableName = "pkce" + sqlTableOpenID tableName = "oidc" + sqlTableAccess tableName = "access" + sqlTableRefresh tableName = "refresh" + sqlTableCode tableName = "code" + sqlTablePKCE tableName = "pkce" + sqlTableDeviceCode tableName = "device_code" + sqlTableUserCode tableName = "user_code" ) func (r OAuth2RequestSQL) TableName() string { @@ -753,3 +755,101 @@ func (p *Persister) RotateRefreshToken(ctx context.Context, requestID string, re return handleRetryError(p.strictRefreshRotation(ctx, requestID)) } + +func (p *Persister) CreateDeviceCodeSession(ctx context.Context, signature string, requester fosite.Requester) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateDeviceCodeSession") + defer otelx.End(span, &err) + return p.createSession(ctx, signature, requester, sqlTableDeviceCode, requester.GetSession().GetExpiresAt(fosite.DeviceCode).UTC()) +} + +// UpdateDeviceCodeSession updates a device code session by requestID +func (p *Persister) UpdateDeviceCodeSessionByRequestID(ctx context.Context, requestID string, requester fosite.Requester) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.UpdateDeviceCodeSessionByRequestID") + defer otelx.End(span, &err) + + req, err := p.sqlSchemaFromRequest(ctx, requestID, requester, sqlTableDeviceCode, requester.GetSession().GetExpiresAt(fosite.DeviceCode).UTC()) + if err != nil { + return + } + + /* #nosec G201 table is static */ + return sqlcon.HandleError( + p.Connection(ctx). + RawQuery( + fmt.Sprintf("UPDATE %s SET session_data=? WHERE request_id=? AND nid = ?", OAuth2RequestSQL{Table: sqlTableDeviceCode}.TableName()), + req.Session, + requestID, + p.NetworkID(ctx), + ). + Exec(), + ) +} + +func (p *Persister) GetDeviceCodeSession(ctx context.Context, signature string, session fosite.Session) (_ fosite.Requester, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.GetDeviceCodeSession") + defer otelx.End(span, &err) + return p.findSessionBySignature(ctx, signature, session, sqlTableDeviceCode) +} + +// InvalidateDeviceCodeSession invalidates a device code session +func (p *Persister) InvalidateDeviceCodeSession(ctx context.Context, signature string) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.InvalidateDeviceCodeSession") + defer otelx.End(span, &err) + + /* #nosec G201 table is static */ + return sqlcon.HandleError( + p.Connection(ctx). + RawQuery( + fmt.Sprintf("UPDATE %s SET active=false WHERE signature=? AND nid = ?", OAuth2RequestSQL{Table: sqlTableDeviceCode}.TableName()), + signature, + p.NetworkID(ctx), + ). + Exec(), + ) +} + +func (p *Persister) CreateUserCodeSession(ctx context.Context, signature string, requester fosite.Requester) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateUserCodeSession") + defer otelx.End(span, &err) + return p.createSession(ctx, signature, requester, sqlTableUserCode, requester.GetSession().GetExpiresAt(fosite.UserCode).UTC()) +} + +func (p *Persister) GetUserCodeSession(ctx context.Context, signature string, session fosite.Session) (_ fosite.Requester, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.GetUserCodeSession") + defer otelx.End(span, &err) + return p.findSessionBySignature(ctx, signature, session, sqlTableUserCode) +} + +func (p *Persister) InvalidateUserCodeSession(ctx context.Context, signature string) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.InvalidateUserCodeSession") + defer otelx.End(span, &err) + + /* #nosec G201 table is static */ + return sqlcon.HandleError( + p.Connection(ctx). + RawQuery( + fmt.Sprintf("UPDATE %s SET active=false WHERE signature=? AND nid = ?", OAuth2RequestSQL{Table: sqlTableUserCode}.TableName()), + signature, + p.NetworkID(ctx), + ). + Exec(), + ) +} + +// UpdateAndInvalidateUserCodeSession invalidates a user code session and connects it with the device flow challenge ID +func (p *Persister) UpdateAndInvalidateUserCodeSession(ctx context.Context, signature, challenge_id string) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.UpdateAndInvalidateUserCodeSession") + defer otelx.End(span, &err) + + /* #nosec G201 table is static */ + return sqlcon.HandleError( + p.Connection(ctx). + RawQuery( + fmt.Sprintf("UPDATE %s SET active=false, challenge_id=? WHERE signature=? AND nid = ?", OAuth2RequestSQL{Table: sqlTableUserCode}.TableName()), + challenge_id, + signature, + p.NetworkID(ctx), + ). + Exec(), + ) +} diff --git a/x/clean_sql.go b/x/clean_sql.go index a02a9a054ce..243d65033d8 100644 --- a/x/clean_sql.go +++ b/x/clean_sql.go @@ -16,6 +16,8 @@ func DeleteHydraRows(t *testing.T, c *pop.Connection) { "hydra_oauth2_code", "hydra_oauth2_oidc", "hydra_oauth2_pkce", + "hydra_oauth2_device_code", + "hydra_oauth2_user_code", "hydra_oauth2_flow", "hydra_oauth2_authentication_session", "hydra_oauth2_obfuscated_authentication_session", @@ -39,6 +41,8 @@ func CleanSQLPop(t *testing.T, c *pop.Connection) { "hydra_oauth2_code", "hydra_oauth2_oidc", "hydra_oauth2_pkce", + "hydra_oauth2_device_code", + "hydra_oauth2_user_code", "hydra_oauth2_flow", "hydra_oauth2_authentication_session", "hydra_oauth2_obfuscated_authentication_session", From accccf15b2221b0acd5ca324096715b783ebd5e5 Mon Sep 17 00:00:00 2001 From: Nikos Date: Fri, 9 Feb 2024 17:04:19 +0200 Subject: [PATCH 07/52] feat: add device authorization endpoint handler --- driver/config/provider.go | 4 ++ fositex/config.go | 2 + internal/.hydra.yaml | 5 ++ oauth2/handler.go | 89 +++++++++++++++++++++++++++++ oauth2/oauth2_provider_mock_test.go | 10 ++-- persistence/sql/persister_oauth2.go | 7 ++- spec/swagger.json | 39 +++++++++++++ 7 files changed, 150 insertions(+), 6 deletions(-) diff --git a/driver/config/provider.go b/driver/config/provider.go index 740e82cade2..b3f7c464de6 100644 --- a/driver/config/provider.go +++ b/driver/config/provider.go @@ -403,10 +403,12 @@ func (p *DefaultProvider) fallbackURL(ctx context.Context, path string, host str return &u } +// GetDeviceAndUserCodeLifespan returns the device_code and user_code lifespan. Defaults to 15 minutes. func (p *DefaultProvider) GetDeviceAndUserCodeLifespan(ctx context.Context) time.Duration { return p.p.DurationF(KeyDeviceAndUserCodeLifespan, time.Minute*15) } +// GetDeviceAuthTokenPollingInterval returns device grant token endpoint polling interval. Defaults to 5 seconds. func (p *DefaultProvider) GetDeviceAuthTokenPollingInterval(ctx context.Context) time.Duration { return p.p.DurationF(KeyDeviceAuthTokenPollingInterval, time.Second*5) } @@ -441,6 +443,7 @@ func (p *DefaultProvider) ErrorURL(ctx context.Context) *url.URL { return urlRoot(p.getProvider(ctx).RequestURIF(KeyErrorURL, p.publicFallbackURL(ctx, "oauth2/fallbacks/error"))) } +// DeviceVerificationURL returns user_code verification page URL. Defaults to "oauth2/fallbacks/device". func (p *DefaultProvider) DeviceVerificationURL(ctx context.Context) *url.URL { return urlRoot(p.getProvider(ctx).URIF(KeyDeviceVerificationURL, p.publicFallbackURL(ctx, "oauth2/fallbacks/device"))) } @@ -502,6 +505,7 @@ func (p *DefaultProvider) OAuth2AuthURL(ctx context.Context) *url.URL { return p.getProvider(ctx).RequestURIF(KeyOAuth2AuthURL, urlx.AppendPaths(p.PublicURL(ctx), "/oauth2/auth")) } +// OAuth2DeviceAuthorisationURL returns device authorization endpoint. Defaults to "/oauth2/device/auth". func (p *DefaultProvider) OAuth2DeviceAuthorisationURL(ctx context.Context) *url.URL { return p.getProvider(ctx).RequestURIF(KeyOAuth2DeviceAuthorisationURL, urlx.AppendPaths(p.PublicURL(ctx), "/oauth2/device/auth")) } diff --git a/fositex/config.go b/fositex/config.go index 7c2018971f6..f699eb5ab5e 100644 --- a/fositex/config.go +++ b/fositex/config.go @@ -119,6 +119,7 @@ func (c *Config) GetRevocationHandlers(context.Context) fosite.RevocationHandler return c.revocationHandlers } +// GetDeviceEndpointHandlers returns the deviceEndpointHandlers func (c *Config) GetDeviceEndpointHandlers(ctx context.Context) fosite.DeviceEndpointHandlers { return c.deviceEndpointHandlers } @@ -216,6 +217,7 @@ func (c *Config) GetTokenURLs(ctx context.Context) []string { }) } +// GetDeviceVerificationURL returns the device verification url func (c *Config) GetDeviceVerificationURL(ctx context.Context) string { return urlx.AppendPaths(c.deps.Config().PublicURL(ctx), oauth2.DeviceAuthPath).String() } diff --git a/internal/.hydra.yaml b/internal/.hydra.yaml index bb02d986ad6..7442fe036f2 100644 --- a/internal/.hydra.yaml +++ b/internal/.hydra.yaml @@ -74,6 +74,7 @@ webfinger: auth_url: https://example.com/auth token_url: https://example.com/token client_registration_url: https://example.com + device_authorization_url: https://example.com/device_authorization supported_claims: - username supported_scope: @@ -100,6 +101,7 @@ urls: consent: https://consent logout: https://logout error: https://error + device_verification: https://device post_logout_redirect: https://post_logout strategies: @@ -112,12 +114,15 @@ ttl: refresh_token: 2h id_token: 2h auth_code: 2h + device_user_code: 2h oauth2: expose_internal_errors: true hashers: bcrypt: cost: 20 + device_authorization: + token_polling_interval: 2h pkce: enforced: true enforced_for_public_clients: true diff --git a/oauth2/handler.go b/oauth2/handler.go index 61c6ae4ddd1..00d9e16fd35 100644 --- a/oauth2/handler.go +++ b/oauth2/handler.go @@ -61,6 +61,9 @@ const ( IntrospectPath = "/oauth2/introspect" RevocationPath = "/oauth2/revoke" DeleteTokensPath = "/oauth2/tokens" // #nosec G101 + + // Device authorization endpoint + DeviceAuthPath = "/oauth2/device/auth" ) type Handler struct { @@ -106,6 +109,8 @@ func (h *Handler) SetRoutes(admin *httprouterx.RouterAdmin, public *httprouterx. public.Handler("OPTIONS", VerifiableCredentialsPath, corsMiddleware(http.HandlerFunc(h.handleOptions))) public.Handler("POST", VerifiableCredentialsPath, corsMiddleware(http.HandlerFunc(h.createVerifiableCredential))) + public.Handler("POST", DeviceAuthPath, http.HandlerFunc(h.performOAuth2DeviceFlow)) + admin.POST(IntrospectPath, h.introspectOAuth2Token) admin.DELETE(DeleteTokensPath, h.deleteOAuth2Token) } @@ -689,6 +694,90 @@ func (h *Handler) getOidcUserInfo(w http.ResponseWriter, r *http.Request) { } } +// OAuth2 Device Flow +// +// # Ory's OAuth 2.0 Device Authorization API +// +// swagger:model deviceAuthorization +type deviceAuthorization struct { + // The device verification code. + // + // example: ory_dc_smldfksmdfkl.mslkmlkmlk + DeviceCode string `json:"device_code"` + + // The end-user verification code. + // + // example: AAAAAA + UserCode string `json:"user_code"` + + // The end-user verification URI on the authorization + // server. The URI should be short and easy to remember as end users + // will be asked to manually type it into their user agent. + // + // example: https://auth.ory.sh/tv + VerificationUri string `json:"verification_uri"` + + // A verification URI that includes the "user_code" (or + // other information with the same function as the "user_code"), + // which is designed for non-textual transmission. + // + // example: https://auth.ory.sh/tv?user_code=AAAAAA + VerificationUriComplete string `json:"verification_uri_complete"` + + // The lifetime in seconds of the "device_code" and "user_code". + // + // example: 16830 + ExpiresIn int `json:"expires_in"` + + // The minimum amount of time in seconds that the client + // SHOULD wait between polling requests to the token endpoint. If no + // value is provided, clients MUST use 5 as the default. + // + // example: 5 + Interval int `json:"interval"` +} + +// swagger:route POST /oauth2/device/auth oauth performOAuth2DeviceFlow +// +// # The OAuth 2.0 Device Authorize Endpoint +// +// This endpoint is not documented here because you should never use your own implementation to perform OAuth2 flows. +// OAuth2 is a very popular protocol and a library for your programming language will exists. +// +// To learn more about this flow please refer to the specification: https://tools.ietf.org/html/rfc8628 +// +// Consumes: +// - application/x-www-form-urlencoded +// +// Schemes: http, https +// +// Responses: +// 200: deviceAuthorization +// default: errorOAuth2 +func (h *Handler) performOAuth2DeviceFlow(w http.ResponseWriter, r *http.Request) { + var ctx = r.Context() + request, err := h.r.OAuth2Provider().NewDeviceRequest(ctx, r) + if err != nil { + h.r.OAuth2Provider().WriteAccessError(ctx, w, request, err) + return + } + + // TODO: We need to call the consent manager here to create a new loginFlow with the + // device_challenge and device_verifier + var session = &Session{ + DefaultSession: &openid.DefaultSession{ + Headers: &jwt.Headers{}}, + } + + resp, err := h.r.OAuth2Provider().NewDeviceResponse(ctx, request, session) + if err != nil { + h.r.OAuth2Provider().WriteAccessError(ctx, w, request, err) + return + } + + h.r.OAuth2Provider().WriteDeviceResponse(ctx, w, request, resp) +} + // Revoke OAuth 2.0 Access or Refresh Token Request // // swagger:parameters revokeOAuth2Token diff --git a/oauth2/oauth2_provider_mock_test.go b/oauth2/oauth2_provider_mock_test.go index e99c959fc4f..8149e206b28 100644 --- a/oauth2/oauth2_provider_mock_test.go +++ b/oauth2/oauth2_provider_mock_test.go @@ -133,18 +133,18 @@ func (mr *MockOAuth2ProviderMockRecorder) NewDeviceRequest(arg0, arg1 interface{ } // NewDeviceResponse mocks base method. -func (m *MockOAuth2Provider) NewDeviceResponse(arg0 context.Context, arg1 fosite.DeviceRequester) (fosite.DeviceResponder, error) { +func (m *MockOAuth2Provider) NewDeviceResponse(arg0 context.Context, arg1 fosite.DeviceRequester, arg2 fosite.Session) (fosite.DeviceResponder, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "NewDeviceResponse", arg0, arg1) + ret := m.ctrl.Call(m, "NewDeviceResponse", arg0, arg1, arg2) ret0, _ := ret[0].(fosite.DeviceResponder) ret1, _ := ret[1].(error) return ret0, ret1 } // NewDeviceResponse indicates an expected call of NewDeviceResponse. -func (mr *MockOAuth2ProviderMockRecorder) NewDeviceResponse(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockOAuth2ProviderMockRecorder) NewDeviceResponse(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewDeviceResponse", reflect.TypeOf((*MockOAuth2Provider)(nil).NewDeviceResponse), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewDeviceResponse", reflect.TypeOf((*MockOAuth2Provider)(nil).NewDeviceResponse), arg0, arg1, arg2) } // NewIntrospectionRequest mocks base method. @@ -207,7 +207,7 @@ func (mr *MockOAuth2ProviderMockRecorder) NewRevocationRequest(arg0, arg1 interf } // WriteAccessError mocks base method. -func (m *MockOAuth2Provider) WriteAccessError(arg0 context.Context, arg1 http.ResponseWriter, arg2 fosite.AccessRequester, arg3 error) { +func (m *MockOAuth2Provider) WriteAccessError(arg0 context.Context, arg1 http.ResponseWriter, arg2 fosite.Requester, arg3 error) { m.ctrl.T.Helper() m.ctrl.Call(m, "WriteAccessError", arg0, arg1, arg2, arg3) } diff --git a/persistence/sql/persister_oauth2.go b/persistence/sql/persister_oauth2.go index 4e2fb26610e..a326ca78191 100644 --- a/persistence/sql/persister_oauth2.go +++ b/persistence/sql/persister_oauth2.go @@ -756,13 +756,14 @@ func (p *Persister) RotateRefreshToken(ctx context.Context, requestID string, re return handleRetryError(p.strictRefreshRotation(ctx, requestID)) } +// CreateDeviceCodeSession creates a new device code session and stores it in the database func (p *Persister) CreateDeviceCodeSession(ctx context.Context, signature string, requester fosite.Requester) (err error) { ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateDeviceCodeSession") defer otelx.End(span, &err) return p.createSession(ctx, signature, requester, sqlTableDeviceCode, requester.GetSession().GetExpiresAt(fosite.DeviceCode).UTC()) } -// UpdateDeviceCodeSession updates a device code session by requestID +// UpdateDeviceCodeSessionByRequestID updates a device code session by requestID func (p *Persister) UpdateDeviceCodeSessionByRequestID(ctx context.Context, requestID string, requester fosite.Requester) (err error) { ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.UpdateDeviceCodeSessionByRequestID") defer otelx.End(span, &err) @@ -785,6 +786,7 @@ func (p *Persister) UpdateDeviceCodeSessionByRequestID(ctx context.Context, requ ) } +// GetDeviceCodeSession returns a device code session from the database func (p *Persister) GetDeviceCodeSession(ctx context.Context, signature string, session fosite.Session) (_ fosite.Requester, err error) { ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.GetDeviceCodeSession") defer otelx.End(span, &err) @@ -808,18 +810,21 @@ func (p *Persister) InvalidateDeviceCodeSession(ctx context.Context, signature s ) } +// CreateUserCodeSession creates a new user code session and stores it in the database func (p *Persister) CreateUserCodeSession(ctx context.Context, signature string, requester fosite.Requester) (err error) { ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateUserCodeSession") defer otelx.End(span, &err) return p.createSession(ctx, signature, requester, sqlTableUserCode, requester.GetSession().GetExpiresAt(fosite.UserCode).UTC()) } +// GetUserCodeSession returns a user code session from the database func (p *Persister) GetUserCodeSession(ctx context.Context, signature string, session fosite.Session) (_ fosite.Requester, err error) { ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.GetUserCodeSession") defer otelx.End(span, &err) return p.findSessionBySignature(ctx, signature, session, sqlTableUserCode) } +// InvalidateUserCodeSession invalidates a user code session func (p *Persister) InvalidateUserCodeSession(ctx context.Context, signature string) (err error) { ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.InvalidateUserCodeSession") defer otelx.End(span, &err) diff --git a/spec/swagger.json b/spec/swagger.json index 14758f6e806..ac8a18d390d 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -2319,6 +2319,45 @@ } } }, + "deviceAuthorization": { + "description": "OAuth 2.0 Device Authorization endpoint", + "type": "object", + "title": "OAuth2 Device Flow", + "properties": { + "device_code": { + "description": "The device verification code.", + "type": "string", + "example": "ory_dc_smldfksmdfkl.mslkmlkmlk" + }, + "expires_in": { + "description": "The lifetime in seconds of the \"device_code\" and \"user_code\".", + "type": "integer", + "format": "int64", + "example": 16830 + }, + "interval": { + "description": "The minimum amount of time in seconds that the client\nSHOULD wait between polling requests to the token endpoint. If no\nvalue is provided, clients MUST use 5 as the default.", + "type": "integer", + "format": "int64", + "example": 5 + }, + "user_code": { + "description": "The end-user verification code.", + "type": "string", + "example": "AAAAAA" + }, + "verification_uri": { + "description": "The end-user verification URI on the authorization\nserver. The URI should be short and easy to remember as end users\nwill be asked to manually type it into their user agent.", + "type": "string", + "example": "https://auth.ory.sh/tv" + }, + "verification_uri_complete": { + "description": "A verification URI that includes the \"user_code\" (or\nother information with the same function as the \"user_code\"),\nwhich is designed for non-textual transmission.", + "type": "string", + "example": "https://auth.ory.sh/tv?user_code=AAAAAA" + } + } + }, "errorOAuth2": { "description": "Error", "type": "object", From dba944fd566650688aee0852e95432e1182d1366 Mon Sep 17 00:00:00 2001 From: Nikos Date: Wed, 28 Feb 2024 16:06:05 +0200 Subject: [PATCH 08/52] refactor: move logic to updateSessionWithRequest method --- oauth2/handler.go | 151 ++++++++++++++++++++++++---------------------- 1 file changed, 79 insertions(+), 72 deletions(-) diff --git a/oauth2/handler.go b/oauth2/handler.go index 00d9e16fd35..f0fabc23b54 100644 --- a/oauth2/handler.go +++ b/oauth2/handler.go @@ -19,6 +19,7 @@ import ( "github.com/pborman/uuid" + "github.com/ory/hydra/v2/flow" "github.com/ory/hydra/v2/x/events" "github.com/ory/x/httprouterx" "github.com/ory/x/josex" @@ -1163,7 +1164,7 @@ func (h *Handler) oAuth2Authorize(w http.ResponseWriter, r *http.Request, _ http return } - session, flow, err := h.r.ConsentStrategy().HandleOAuth2AuthorizationRequest(ctx, w, r, authorizeRequest) + acceptConsentSession, flow, err := h.r.ConsentStrategy().HandleOAuth2AuthorizationRequest(ctx, w, r, authorizeRequest) if errors.Is(err, consent.ErrAbortOAuth2Request) { x.LogAudit(r, nil, h.r.AuditLogger()) // do nothing @@ -1178,83 +1179,14 @@ func (h *Handler) oAuth2Authorize(w http.ResponseWriter, r *http.Request, _ http return } - for _, scope := range session.GrantedScope { - authorizeRequest.GrantScope(scope) - } - - for _, audience := range session.GrantedAudience { - authorizeRequest.GrantAudience(audience) - } - - openIDKeyID, err := h.r.OpenIDJWTStrategy().GetPublicKeyID(ctx) + session, err := h.updateSessionWithRequest(ctx, acceptConsentSession, flow, r, authorizeRequest) if err != nil { - x.LogError(r, err, h.r.Logger()) - h.writeAuthorizeError(w, r, authorizeRequest, err) - return - } - - var accessTokenKeyID string - if h.c.AccessTokenStrategy(ctx, client.AccessTokenStrategySource(authorizeRequest.GetClient())) == "jwt" { - accessTokenKeyID, err = h.r.AccessTokenJWTStrategy().GetPublicKeyID(ctx) - if err != nil { - x.LogError(r, err, h.r.Logger()) - h.writeAuthorizeError(w, r, authorizeRequest, err) - return - } - } - - obfuscatedSubject, err := h.r.ConsentStrategy().ObfuscateSubjectIdentifier(ctx, authorizeRequest.GetClient(), session.ConsentRequest.Subject, session.ConsentRequest.ForceSubjectIdentifier) - if e := &(fosite.RFC6749Error{}); errors.As(err, &e) { - x.LogAudit(r, err, h.r.AuditLogger()) - h.writeAuthorizeError(w, r, authorizeRequest, err) - return - } else if err != nil { - x.LogError(r, err, h.r.Logger()) h.writeAuthorizeError(w, r, authorizeRequest, err) return } - - authorizeRequest.SetID(session.ConsentRequestID) - claims := &jwt.IDTokenClaims{ - Subject: obfuscatedSubject, - Issuer: h.c.IssuerURL(ctx).String(), - AuthTime: time.Time(session.AuthenticatedAt), - RequestedAt: session.RequestedAt, - Extra: session.Session.IDToken, - AuthenticationContextClassReference: session.ConsentRequest.ACR, - AuthenticationMethodsReferences: session.ConsentRequest.AMR, - - // These are required for work around https://github.com/ory/fosite/issues/530 - Nonce: authorizeRequest.GetRequestForm().Get("nonce"), - Audience: []string{authorizeRequest.GetClient().GetID()}, - IssuedAt: time.Now().Truncate(time.Second).UTC(), - - // This is set by the fosite strategy - // ExpiresAt: time.Now().Add(h.IDTokenLifespan).UTC(), - } - claims.Add("sid", session.ConsentRequest.LoginSessionID) - - // done var response fosite.AuthorizeResponder if err := h.r.Persister().Transaction(ctx, func(ctx context.Context, _ *pop.Connection) (err error) { - response, err = h.r.OAuth2Provider().NewAuthorizeResponse(ctx, authorizeRequest, &Session{ - DefaultSession: &openid.DefaultSession{ - Claims: claims, - Headers: &jwt.Headers{Extra: map[string]interface{}{ - // required for lookup on jwk endpoint - "kid": openIDKeyID, - }}, - Subject: session.ConsentRequest.Subject, - }, - Extra: session.Session.AccessToken, - KID: accessTokenKeyID, - ClientID: authorizeRequest.GetClient().GetID(), - ConsentChallenge: session.ConsentRequestID, - ExcludeNotBeforeClaim: h.c.ExcludeNotBeforeClaim(ctx), - AllowedTopLevelClaims: h.c.AllowedTopLevelClaims(ctx), - MirrorTopLevelClaims: h.c.MirrorTopLevelClaims(ctx), - Flow: flow, - }) + response, err = h.r.OAuth2Provider().NewAuthorizeResponse(ctx, authorizeRequest, session) return err }); err != nil { x.LogError(r, err, h.r.Logger()) @@ -1326,6 +1258,81 @@ func (h *Handler) writeAuthorizeError(w http.ResponseWriter, r *http.Request, ar h.r.OAuth2Provider().WriteAuthorizeError(r.Context(), w, ar, err) } +// updateSessionWithRequest takes a session and a fosite.request as input and returns a new session. +// If any errors occur, they are logged. +func (h *Handler) updateSessionWithRequest(ctx context.Context, session *flow.AcceptOAuth2ConsentRequest, flow *flow.Flow, r *http.Request, request fosite.Requester) (*Session, error) { + for _, scope := range session.GrantedScope { + request.GrantScope(scope) + } + + for _, audience := range session.GrantedAudience { + request.GrantAudience(audience) + } + + openIDKeyID, err := h.r.OpenIDJWTStrategy().GetPublicKeyID(ctx) + if err != nil { + x.LogError(r, err, h.r.Logger()) + return nil, err + } + + var accessTokenKeyID string + if h.c.AccessTokenStrategy(ctx, client.AccessTokenStrategySource(flow.Client)) == "jwt" { + accessTokenKeyID, err = h.r.AccessTokenJWTStrategy().GetPublicKeyID(ctx) + if err != nil { + x.LogError(r, err, h.r.Logger()) + return nil, err + } + } + + obfuscatedSubject, err := h.r.ConsentStrategy().ObfuscateSubjectIdentifier(ctx, flow.Client, session.ConsentRequest.Subject, session.ConsentRequest.ForceSubjectIdentifier) + if e := &(fosite.RFC6749Error{}); errors.As(err, &e) { + x.LogAudit(r, err, h.r.AuditLogger()) + return nil, err + } else if err != nil { + x.LogError(r, err, h.r.Logger()) + return nil, err + } + + request.SetID(session.ConsentRequestID) + claims := &jwt.IDTokenClaims{ + Subject: obfuscatedSubject, + Issuer: h.c.IssuerURL(ctx).String(), + AuthTime: time.Time(session.AuthenticatedAt), + RequestedAt: session.RequestedAt, + Extra: session.Session.IDToken, + AuthenticationContextClassReference: session.ConsentRequest.ACR, + AuthenticationMethodsReferences: session.ConsentRequest.AMR, + + // These are required for work around https://github.com/ory/fosite/issues/530 + Nonce: request.GetRequestForm().Get("nonce"), + Audience: []string{flow.Client.GetID()}, + IssuedAt: time.Now().Truncate(time.Second).UTC(), + + // This is set by the fosite strategy + // ExpiresAt: time.Now().Add(h.IDTokenLifespan).UTC(), + } + claims.Add("sid", session.ConsentRequest.LoginSessionID) + + return &Session{ + DefaultSession: &openid.DefaultSession{ + Claims: claims, + Headers: &jwt.Headers{Extra: map[string]interface{}{ + // required for lookup on jwk endpoint + "kid": openIDKeyID, + }}, + Subject: session.ConsentRequest.Subject, + }, + Extra: session.Session.AccessToken, + KID: accessTokenKeyID, + ClientID: flow.Client.GetID(), + ConsentChallenge: session.ConsentRequestID, + ExcludeNotBeforeClaim: h.c.ExcludeNotBeforeClaim(ctx), + AllowedTopLevelClaims: h.c.AllowedTopLevelClaims(ctx), + MirrorTopLevelClaims: h.c.MirrorTopLevelClaims(ctx), + Flow: flow, + }, nil +} + func (h *Handler) logOrAudit(err error, r *http.Request) { if errors.Is(err, fosite.ErrServerError) || errors.Is(err, fosite.ErrTemporarilyUnavailable) || errors.Is(err, fosite.ErrMisconfiguration) { x.LogError(r, err, h.r.Logger()) From 29d72ff53f0154138b80169a9e1259136b5b38f7 Mon Sep 17 00:00:00 2001 From: Nikos Date: Wed, 28 Feb 2024 16:04:08 +0200 Subject: [PATCH 09/52] fix: rename device auth endpoint handler --- oauth2/handler.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/oauth2/handler.go b/oauth2/handler.go index f0fabc23b54..f58afca9c89 100644 --- a/oauth2/handler.go +++ b/oauth2/handler.go @@ -110,7 +110,7 @@ func (h *Handler) SetRoutes(admin *httprouterx.RouterAdmin, public *httprouterx. public.Handler("OPTIONS", VerifiableCredentialsPath, corsMiddleware(http.HandlerFunc(h.handleOptions))) public.Handler("POST", VerifiableCredentialsPath, corsMiddleware(http.HandlerFunc(h.createVerifiableCredential))) - public.Handler("POST", DeviceAuthPath, http.HandlerFunc(h.performOAuth2DeviceFlow)) + public.Handler("POST", DeviceAuthPath, http.HandlerFunc(h.oAuth2DeviceFlow)) admin.POST(IntrospectPath, h.introspectOAuth2Token) admin.DELETE(DeleteTokensPath, h.deleteOAuth2Token) @@ -700,6 +700,8 @@ func (h *Handler) getOidcUserInfo(w http.ResponseWriter, r *http.Request) { // # Ory's OAuth 2.0 Device Authorization API // // swagger:model deviceAuthorization +// +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions type deviceAuthorization struct { // The device verification code. // @@ -738,7 +740,7 @@ type deviceAuthorization struct { Interval int `json:"interval"` } -// swagger:route POST /oauth2/device/auth oauth performOAuth2DeviceFlow +// swagger:route POST /oauth2/device/auth oauth oAuth2DeviceFlow // // # The OAuth 2.0 Device Authorize Endpoint // @@ -755,7 +757,7 @@ type deviceAuthorization struct { // Responses: // 200: deviceAuthorization // default: errorOAuth2 -func (h *Handler) performOAuth2DeviceFlow(w http.ResponseWriter, r *http.Request) { +func (h *Handler) oAuth2DeviceFlow(w http.ResponseWriter, r *http.Request) { var ctx = r.Context() request, err := h.r.OAuth2Provider().NewDeviceRequest(ctx, r) if err != nil { From b5347b5d92956dc5597804d76fce99b6e56fd7c6 Mon Sep 17 00:00:00 2001 From: Nikos Date: Wed, 28 Feb 2024 16:09:43 +0200 Subject: [PATCH 10/52] feat: add device user verification handler --- driver/config/provider.go | 6 ++++++ fositex/config.go | 2 +- oauth2/handler.go | 39 +++++++++++++++++++++++++++++++++++- oauth2/oauth2_helper_test.go | 21 +++++++++++++++++++ 4 files changed, 66 insertions(+), 2 deletions(-) diff --git a/driver/config/provider.go b/driver/config/provider.go index b3f7c464de6..e0c756aaf7a 100644 --- a/driver/config/provider.go +++ b/driver/config/provider.go @@ -21,6 +21,7 @@ import ( "github.com/ory/x/otelx" + "github.com/ory/hydra/v2/oauth2" "github.com/ory/hydra/v2/spec" "github.com/ory/x/dbal" @@ -510,6 +511,11 @@ func (p *DefaultProvider) OAuth2DeviceAuthorisationURL(ctx context.Context) *url return p.getProvider(ctx).RequestURIF(KeyOAuth2DeviceAuthorisationURL, urlx.AppendPaths(p.PublicURL(ctx), "/oauth2/device/auth")) } +// OAuth2DeviceAuthorisationURL returns device verification endpoint. +func (p *DefaultProvider) OAuth2DeviceVerificationURL(ctx context.Context) *url.URL { + return urlx.AppendPaths(p.PublicURL(ctx), oauth2.DeviceVerificationPath) +} + func (p *DefaultProvider) JWKSURL(ctx context.Context) *url.URL { return p.getProvider(ctx).RequestURIF(KeyJWKSURL, urlx.AppendPaths(p.IssuerURL(ctx), "/.well-known/jwks.json")) } diff --git a/fositex/config.go b/fositex/config.go index f699eb5ab5e..4300077ea99 100644 --- a/fositex/config.go +++ b/fositex/config.go @@ -219,5 +219,5 @@ func (c *Config) GetTokenURLs(ctx context.Context) []string { // GetDeviceVerificationURL returns the device verification url func (c *Config) GetDeviceVerificationURL(ctx context.Context) string { - return urlx.AppendPaths(c.deps.Config().PublicURL(ctx), oauth2.DeviceAuthPath).String() + return urlx.AppendPaths(c.deps.Config().PublicURL(ctx), oauth2.DeviceVerificationPath).String() } diff --git a/oauth2/handler.go b/oauth2/handler.go index f58afca9c89..f8de92b1453 100644 --- a/oauth2/handler.go +++ b/oauth2/handler.go @@ -64,7 +64,8 @@ const ( DeleteTokensPath = "/oauth2/tokens" // #nosec G101 // Device authorization endpoint - DeviceAuthPath = "/oauth2/device/auth" + DeviceAuthPath = "/oauth2/device/auth" + DeviceVerificationPath = "/oauth2/device/verify" ) type Handler struct { @@ -111,6 +112,7 @@ func (h *Handler) SetRoutes(admin *httprouterx.RouterAdmin, public *httprouterx. public.Handler("POST", VerifiableCredentialsPath, corsMiddleware(http.HandlerFunc(h.createVerifiableCredential))) public.Handler("POST", DeviceAuthPath, http.HandlerFunc(h.oAuth2DeviceFlow)) + public.GET(DeviceVerificationPath, h.performOAuth2DeviceVerificationFlow) admin.POST(IntrospectPath, h.introspectOAuth2Token) admin.DELETE(DeleteTokensPath, h.deleteOAuth2Token) @@ -695,6 +697,41 @@ func (h *Handler) getOidcUserInfo(w http.ResponseWriter, r *http.Request) { } } +// swagger:route GET /oauth2/device/verify oauth performOAuth2DeviceVerificationFlow +// +// # OAuth 2.0 Device Verification Endpoint +// +// This is the device user verification endpoint. The user is redirected here when trying to login using the device flow. +// +// Consumes: +// - application/x-www-form-urlencoded +// +// Schemes: http, https +// +// Responses: +// 302: emptyResponse +// default: errorOAuth2 +func (h *Handler) performOAuth2DeviceVerificationFlow(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + ctx := r.Context() + + _, flow, err := h.r.ConsentStrategy().HandleOAuth2DeviceAuthorizationRequest(ctx, w, r) + if errors.Is(err, consent.ErrAbortOAuth2Request) { + x.LogAudit(r, nil, h.r.AuditLogger()) + // do nothing + return + } else if e := &(fosite.RFC6749Error{}); errors.As(err, &e) { + x.LogAudit(r, err, h.r.AuditLogger()) + h.r.Writer().WriteError(w, r, err) + return + } else if err != nil { + x.LogError(r, err, h.r.Logger()) + h.r.Writer().WriteError(w, r, err) + return + } + + http.Redirect(w, r, urlx.SetQuery(h.c.DeviceDoneURL(ctx), url.Values{"consent_verifier": {string(flow.ConsentVerifier)}}).String(), http.StatusFound) +} + // OAuth2 Device Flow // // # Ory's OAuth 2.0 Device Authorization API diff --git a/oauth2/oauth2_helper_test.go b/oauth2/oauth2_helper_test.go index 52a30e5975e..769679ec17e 100644 --- a/oauth2/oauth2_helper_test.go +++ b/oauth2/oauth2_helper_test.go @@ -46,6 +46,27 @@ func (c *consentMock) HandleOAuth2AuthorizationRequest(ctx context.Context, w ht }, nil, nil } +func (c *consentMock) HandleOAuth2DeviceAuthorizationRequest(ctx context.Context, w http.ResponseWriter, r *http.Request) (*flow.AcceptOAuth2ConsentRequest, *flow.Flow, error) { + if c.deny { + return nil, nil, fosite.ErrRequestForbidden + } + + return &flow.AcceptOAuth2ConsentRequest{ + ConsentRequest: &flow.OAuth2ConsentRequest{ + Subject: "foo", + ACR: "1", + DeviceChallenge: "12345", + }, + AuthenticatedAt: sqlxx.NullTime(c.authTime), + GrantedScope: []string{"offline", "openid", "hydra.*"}, + Session: &flow.AcceptOAuth2ConsentRequestSession{ + AccessToken: map[string]interface{}{}, + IDToken: map[string]interface{}{}, + }, + RequestedAt: c.requestTime, + }, nil, nil +} + func (c *consentMock) HandleOpenIDConnectLogout(ctx context.Context, w http.ResponseWriter, r *http.Request) (*flow.LogoutResult, error) { panic("not implemented") } From ef6ac13fa6f0d8f1e488ce3ee296747f13b71007 Mon Sep 17 00:00:00 2001 From: Nikos Date: Wed, 28 Feb 2024 16:25:32 +0200 Subject: [PATCH 11/52] fix: implement device user verification logic --- consent/manager.go | 5 + consent/strategy.go | 5 + consent/strategy_default.go | 208 +++++++++++++++++++++++++++++++++--- driver/config/provider.go | 6 -- 4 files changed, 204 insertions(+), 20 deletions(-) diff --git a/consent/manager.go b/consent/manager.go index 182064028ba..bcf4d2151df 100644 --- a/consent/manager.go +++ b/consent/manager.go @@ -61,6 +61,11 @@ type ( AcceptLogoutRequest(ctx context.Context, challenge string) (*flow.LogoutRequest, error) RejectLogoutRequest(ctx context.Context, challenge string) error VerifyAndInvalidateLogoutRequest(ctx context.Context, verifier string) (*flow.LogoutRequest, error) + + CreateDeviceUserAuthRequest(ctx context.Context, req *flow.DeviceUserAuthRequest) (*flow.Flow, error) + GetDeviceUserAuthRequest(ctx context.Context, challenge string) (*flow.DeviceUserAuthRequest, error) + HandleDeviceUserAuthRequest(ctx context.Context, f *flow.Flow, challenge string, r *flow.HandledDeviceUserAuthRequest) (*flow.DeviceUserAuthRequest, error) + VerifyAndInvalidateDeviceUserAuthRequest(ctx context.Context, verifier string) (*flow.HandledDeviceUserAuthRequest, error) } ManagerProvider interface { diff --git a/consent/strategy.go b/consent/strategy.go index 08e8788c756..0def2866e27 100644 --- a/consent/strategy.go +++ b/consent/strategy.go @@ -20,6 +20,11 @@ type Strategy interface { r *http.Request, req fosite.AuthorizeRequester, ) (*flow.AcceptOAuth2ConsentRequest, *flow.Flow, error) + HandleOAuth2DeviceAuthorizationRequest( + ctx context.Context, + w http.ResponseWriter, + r *http.Request, + ) (*flow.AcceptOAuth2ConsentRequest, *flow.Flow, error) HandleOpenIDConnectLogout(ctx context.Context, w http.ResponseWriter, r *http.Request) (*flow.LogoutResult, error) HandleHeadlessLogout(ctx context.Context, w http.ResponseWriter, r *http.Request, sid string) error ObfuscateSubjectIdentifier(ctx context.Context, cl fosite.Client, subject, forcedIdentifier string) (string, error) diff --git a/consent/strategy_default.go b/consent/strategy_default.go index ba8947a1ce5..6fb6cadedf9 100644 --- a/consent/strategy_default.go +++ b/consent/strategy_default.go @@ -40,7 +40,10 @@ import ( "github.com/ory/x/urlx" ) +type ctxKey int + const ( + DeviceVerificationPath = "/oauth2/device/verify" CookieAuthenticationSIDName = "sid" ) @@ -121,18 +124,24 @@ func (s *DefaultStrategy) authenticationSession(ctx context.Context, _ http.Resp return session, nil } -func (s *DefaultStrategy) requestAuthentication(ctx context.Context, w http.ResponseWriter, r *http.Request, ar fosite.AuthorizeRequester) (err error) { +func (s *DefaultStrategy) requestAuthentication( + ctx context.Context, + w http.ResponseWriter, + r *http.Request, + ar fosite.AuthorizeRequester, + f *flow.Flow, +) (err error) { ctx, span := trace.SpanFromContext(ctx).TracerProvider().Tracer("").Start(ctx, "DefaultStrategy.requestAuthentication") defer otelx.End(span, &err) prompt := stringsx.Splitx(ar.GetRequestForm().Get("prompt"), " ") if stringslice.Has(prompt, "login") { - return s.forwardAuthenticationRequest(ctx, w, r, ar, "", time.Time{}, nil) + return s.forwardAuthenticationRequest(ctx, w, r, ar, "", time.Time{}, nil, f) } session, err := s.authenticationSession(ctx, w, r) if errors.Is(err, ErrNoAuthenticationSessionFound) { - return s.forwardAuthenticationRequest(ctx, w, r, ar, "", time.Time{}, nil) + return s.forwardAuthenticationRequest(ctx, w, r, ar, "", time.Time{}, nil, f) } else if err != nil { return err } @@ -150,12 +159,12 @@ func (s *DefaultStrategy) requestAuthentication(ctx context.Context, w http.Resp if stringslice.Has(prompt, "none") { return errorsx.WithStack(fosite.ErrLoginRequired.WithHint("Request failed because prompt is set to 'none' and authentication time reached 'max_age'.")) } - return s.forwardAuthenticationRequest(ctx, w, r, ar, "", time.Time{}, nil) + return s.forwardAuthenticationRequest(ctx, w, r, ar, "", time.Time{}, nil, f) } idTokenHint := ar.GetRequestForm().Get("id_token_hint") if idTokenHint == "" { - return s.forwardAuthenticationRequest(ctx, w, r, ar, session.Subject, time.Time(session.AuthenticatedAt), session) + return s.forwardAuthenticationRequest(ctx, w, r, ar, session.Subject, time.Time(session.AuthenticatedAt), session, f) } hintSub, err := s.getSubjectFromIDTokenHint(r.Context(), idTokenHint) @@ -167,7 +176,7 @@ func (s *DefaultStrategy) requestAuthentication(ctx context.Context, w http.Resp return errorsx.WithStack(fosite.ErrLoginRequired.WithHint("Request failed because subject claim from id_token_hint does not match subject from authentication session.")) } - return s.forwardAuthenticationRequest(ctx, w, r, ar, session.Subject, time.Time(session.AuthenticatedAt), session) + return s.forwardAuthenticationRequest(ctx, w, r, ar, session.Subject, time.Time(session.AuthenticatedAt), session, f) } func (s *DefaultStrategy) getIDTokenHintClaims(ctx context.Context, idTokenHint string) (jwt.MapClaims, error) { @@ -194,7 +203,16 @@ func (s *DefaultStrategy) getSubjectFromIDTokenHint(ctx context.Context, idToken return sub, nil } -func (s *DefaultStrategy) forwardAuthenticationRequest(ctx context.Context, w http.ResponseWriter, r *http.Request, ar fosite.AuthorizeRequester, subject string, authenticatedAt time.Time, session *flow.LoginSession) error { +func (s *DefaultStrategy) forwardAuthenticationRequest( + ctx context.Context, + w http.ResponseWriter, + r *http.Request, + ar fosite.AuthorizeRequester, + subject string, + authenticatedAt time.Time, + session *flow.LoginSession, + f *flow.Flow, +) error { if (subject != "" && authenticatedAt.IsZero()) || (subject == "" && !authenticatedAt.IsZero()) { return errorsx.WithStack(fosite.ErrServerError.WithHint("Consent strategy returned a non-empty subject with an empty auth date, or an empty subject with a non-empty auth date.")) } @@ -216,8 +234,14 @@ func (s *DefaultStrategy) forwardAuthenticationRequest(ctx context.Context, w ht csrf := strings.Replace(uuid.New(), "-", "", -1) // Generate the request URL - iu := s.c.OAuth2AuthURL(ctx) - iu.RawQuery = r.URL.RawQuery + var requestURL string + if f != nil { + requestURL = f.RequestURL + } else { + oauth2URL := s.c.OAuth2AuthURL(ctx) + oauth2URL.RawQuery = r.URL.RawQuery + requestURL = oauth2URL.String() + } var idTokenHintClaims jwt.MapClaims if idTokenHint := ar.GetRequestForm().Get("id_token_hint"); len(idTokenHint) > 0 { @@ -245,7 +269,7 @@ func (s *DefaultStrategy) forwardAuthenticationRequest(ctx context.Context, w ht RequestedAudience: []string(ar.GetRequestedAudience()), Subject: subject, Client: cl, - RequestURL: iu.String(), + RequestURL: requestURL, AuthenticatedAt: sqlxx.NullTime(authenticatedAt), RequestedAt: time.Now().Truncate(time.Second).UTC(), SessionID: sqlxx.NullString(sessionID), @@ -259,6 +283,7 @@ func (s *DefaultStrategy) forwardAuthenticationRequest(ctx context.Context, w ht } f, err := s.r.ConsentManager().CreateLoginRequest( ctx, + f, loginRequest, ) if err != nil { @@ -1134,11 +1159,21 @@ func (s *DefaultStrategy) HandleOAuth2AuthorizationRequest( ctx, span := trace.SpanFromContext(ctx).TracerProvider().Tracer("").Start(ctx, "DefaultStrategy.HandleOAuth2AuthorizationRequest") defer otelx.End(span, &err) - loginVerifier := strings.TrimSpace(req.GetRequestForm().Get("login_verifier")) - consentVerifier := strings.TrimSpace(req.GetRequestForm().Get("consent_verifier")) + return s.handleOAuth2AuthorizationRequest(ctx, w, r, req, nil) +} + +func (s *DefaultStrategy) handleOAuth2AuthorizationRequest( + ctx context.Context, + w http.ResponseWriter, + r *http.Request, + req fosite.AuthorizeRequester, + f *flow.Flow, +) (_ *flow.AcceptOAuth2ConsentRequest, _ *flow.Flow, err error) { + loginVerifier := strings.TrimSpace(r.URL.Query().Get("login_verifier")) + consentVerifier := strings.TrimSpace(r.URL.Query().Get("consent_verifier")) if loginVerifier == "" && consentVerifier == "" { - // ok, we need to process this request and redirect to auth endpoint - return nil, nil, s.requestAuthentication(ctx, w, r, req) + // ok, we need to process this request and redirect to the original endpoint + return nil, nil, s.requestAuthentication(ctx, w, r, req, f) } else if loginVerifier != "" { f, err := s.verifyAuthentication(ctx, w, r, req, loginVerifier) if err != nil { @@ -1157,6 +1192,54 @@ func (s *DefaultStrategy) HandleOAuth2AuthorizationRequest( return consentSession, f, nil } +// HandleOAuth2DeviceAuthorizationRequest handles the device authorization flow +func (s *DefaultStrategy) HandleOAuth2DeviceAuthorizationRequest( + ctx context.Context, + w http.ResponseWriter, + r *http.Request, +) (*flow.AcceptOAuth2ConsentRequest, *flow.Flow, error) { + deviceVerifier := strings.TrimSpace(r.URL.Query().Get("device_verifier")) + loginVerifier := strings.TrimSpace(r.URL.Query().Get("login_verifier")) + consentVerifier := strings.TrimSpace(r.URL.Query().Get("consent_verifier")) + + var deviceFlow *flow.Flow + if deviceVerifier == "" && loginVerifier == "" && consentVerifier == "" { + // ok, we need to process this request and redirect to device auth endpoint + return nil, nil, s.requestDevice(ctx, w, r) + } else if deviceVerifier != "" && loginVerifier == "" && consentVerifier == "" { + var err error + deviceFlow, err = s.verifyDevice(ctx, w, r, deviceVerifier) + if err != nil { + return nil, nil, err + } + } + + // Validate client_id + clientID := r.URL.Query().Get("client_id") + if clientID == "" { + return nil, nil, errorsx.WithStack(fosite.ErrInvalidClient.WithHintf(`client_id query parameter is missing`)) + } + c, err := s.r.ClientManager().GetConcreteClient(r.Context(), clientID) + if errors.Is(err, x.ErrNotFound) { + return nil, nil, errorsx.WithStack(fosite.ErrInvalidClient.WithHintf(`Unknown client_id %s`, clientID)) + } else if err != nil { + return nil, nil, err + } + + // Fake an authorization request to instantiate the flow. + ar := fosite.NewAuthorizeRequest() + ar.Client = c + ar.Form = r.Form + if deviceFlow != nil { + ar.RequestedScope = fosite.Arguments(deviceFlow.RequestedScope) + ar.RequestedAudience = fosite.Arguments(deviceFlow.RequestedAudience) + } + + consentSession, f, err := s.handleOAuth2AuthorizationRequest(ctx, w, r, ar, deviceFlow) + + return consentSession, f, err +} + func (s *DefaultStrategy) ObfuscateSubjectIdentifier(ctx context.Context, cl fosite.Client, subject, forcedIdentifier string) (string, error) { if c, ok := cl.(*client.Client); ok && c.SubjectType == "pairwise" { algorithm, ok := s.r.SubjectIdentifierAlgorithm(ctx)[c.SubjectType] @@ -1174,3 +1257,100 @@ func (s *DefaultStrategy) ObfuscateSubjectIdentifier(ctx context.Context, cl fos } return subject, nil } + +func (s *DefaultStrategy) requestDevice(ctx context.Context, w http.ResponseWriter, r *http.Request) error { + return s.forwardDeviceRequest(ctx, w, r) +} + +func (s *DefaultStrategy) forwardDeviceRequest(ctx context.Context, w http.ResponseWriter, r *http.Request) error { + // Set up csrf/challenge/verifier values + verifier := strings.Replace(uuid.New(), "-", "", -1) + challenge := strings.Replace(uuid.New(), "-", "", -1) + csrf := strings.Replace(uuid.New(), "-", "", -1) + + // Generate the request URL + iu := s.getDeviceVerificationPath(ctx) + iu.RawQuery = r.URL.RawQuery + + f, err := s.r.ConsentManager().CreateDeviceUserAuthRequest( + r.Context(), + &flow.DeviceUserAuthRequest{ + ID: challenge, + Verifier: verifier, + CSRF: csrf, + RequestURL: iu.String(), + RequestedAt: time.Now().Truncate(time.Second).UTC(), + }, + ) + if err != nil { + return errorsx.WithStack(err) + } + + encodedFlow, err := f.ToDeviceChallenge(ctx, s.r) + if err != nil { + return err + } + store, err := s.r.CookieStore(ctx) + if err != nil { + return err + } + + CookieNameDeviceCSRF := s.r.Config().CookieNameDeviceCSRF(ctx) + if err := createCsrfSession(w, r, s.r.Config(), store, CookieNameDeviceCSRF, csrf, s.c.ConsentRequestMaxAge(ctx)); err != nil { + return errorsx.WithStack(err) + } + + query := url.Values{"device_challenge": {encodedFlow}} + if r.URL.Query().Has("user_code") { + query.Add("user_code", r.URL.Query().Get("user_code")) + } + + http.Redirect( + w, + r, + urlx.SetQuery(s.c.DeviceVerificationURL(ctx), query).String(), + http.StatusFound, + ) + + // generate the verifier + return errorsx.WithStack(ErrAbortOAuth2Request) +} + +func (s *DefaultStrategy) verifyDevice(ctx context.Context, _ http.ResponseWriter, r *http.Request, verifier string) (_ *flow.Flow, err error) { + ctx, span := trace.SpanFromContext(ctx).TracerProvider().Tracer("").Start(ctx, "DefaultStrategy.verifyAuthentication") + defer otelx.End(span, &err) + + // We decode the flow from the cookie again because VerifyAndInvalidateDeviceRequest does not return the flow + f, err := flowctx.Decode[flow.Flow](ctx, s.r.FlowCipher(), verifier, flowctx.AsDeviceVerifier) + if err != nil { + return nil, errorsx.WithStack(fosite.ErrAccessDenied.WithHint("The device verifier is invalid.")) + } + + session, err := s.r.ConsentManager().VerifyAndInvalidateDeviceUserAuthRequest(ctx, verifier) + if errors.Is(err, sqlcon.ErrNoRows) { + return nil, errorsx.WithStack(fosite.ErrAccessDenied.WithHint("The device verifier has already been used, has not been granted, or is invalid.")) + } else if err != nil { + return nil, err + } + + if session.HasError() { + session.Error.SetDefaults(flow.DeviceRequestDeniedErrorName) + return nil, errorsx.WithStack(session.Error.ToRFCError()) + } + + store, err := s.r.CookieStore(ctx) + if err != nil { + return nil, err + } + + cookieNameDeviceCSRF := s.r.Config().CookieNameDeviceCSRF(ctx) + if err := validateCsrfSession(r, s.r.Config(), store, cookieNameDeviceCSRF, session.Request.CSRF); err != nil { + return nil, err + } + + return f, nil +} + +func (s *DefaultStrategy) getDeviceVerificationPath(ctx context.Context) *url.URL { + return urlx.AppendPaths(s.c.PublicURL(ctx), DeviceVerificationPath) +} diff --git a/driver/config/provider.go b/driver/config/provider.go index e0c756aaf7a..b3f7c464de6 100644 --- a/driver/config/provider.go +++ b/driver/config/provider.go @@ -21,7 +21,6 @@ import ( "github.com/ory/x/otelx" - "github.com/ory/hydra/v2/oauth2" "github.com/ory/hydra/v2/spec" "github.com/ory/x/dbal" @@ -511,11 +510,6 @@ func (p *DefaultProvider) OAuth2DeviceAuthorisationURL(ctx context.Context) *url return p.getProvider(ctx).RequestURIF(KeyOAuth2DeviceAuthorisationURL, urlx.AppendPaths(p.PublicURL(ctx), "/oauth2/device/auth")) } -// OAuth2DeviceAuthorisationURL returns device verification endpoint. -func (p *DefaultProvider) OAuth2DeviceVerificationURL(ctx context.Context) *url.URL { - return urlx.AppendPaths(p.PublicURL(ctx), oauth2.DeviceVerificationPath) -} - func (p *DefaultProvider) JWKSURL(ctx context.Context) *url.URL { return p.getProvider(ctx).RequestURIF(KeyJWKSURL, urlx.AppendPaths(p.IssuerURL(ctx), "/.well-known/jwks.json")) } From 6257b3e71b827dd1137cdffd39f649a52e6fecb9 Mon Sep 17 00:00:00 2001 From: Nikos Date: Fri, 1 Mar 2024 14:52:43 +0200 Subject: [PATCH 12/52] feat: update flow --- .../TestOAuth2ConsentRequest_MarshalJSON.json | 2 +- flow/consent_types.go | 76 ++++--- flow/flow.go | 209 +++++++++++------- flow/flow_test.go | 88 ++++++++ internal/testhelpers/janitor_test_helper.go | 14 +- oauth2/fosite_store_helpers_test.go | 2 +- persistence/sql/persister_nid_test.go | 4 +- 7 files changed, 265 insertions(+), 130 deletions(-) diff --git a/flow/.snapshots/TestOAuth2ConsentRequest_MarshalJSON.json b/flow/.snapshots/TestOAuth2ConsentRequest_MarshalJSON.json index ec893803186..5e9d62cfb1f 100644 --- a/flow/.snapshots/TestOAuth2ConsentRequest_MarshalJSON.json +++ b/flow/.snapshots/TestOAuth2ConsentRequest_MarshalJSON.json @@ -1 +1 @@ -"{\"challenge\":\"\",\"consent_request_id\":\"\",\"requested_scope\":[],\"requested_access_token_audience\":[],\"skip\":false,\"subject\":\"\",\"oidc_context\":null,\"client\":null,\"request_url\":\"\",\"login_challenge\":\"\",\"login_session_id\":\"\",\"acr\":\"\",\"amr\":[]}" +"{\"challenge\":\"\",\"consent_request_id\":\"\",\"requested_scope\":[],\"requested_access_token_audience\":[],\"skip\":false,\"subject\":\"\",\"oidc_context\":null,\"client\":null,\"request_url\":\"\",\"login_challenge\":\"\",\"login_session_id\":\"\",\"device_challenge_id\":\"\",\"acr\":\"\",\"amr\":[]}" diff --git a/flow/consent_types.go b/flow/consent_types.go index e1d708d10e9..64f9434629c 100644 --- a/flow/consent_types.go +++ b/flow/consent_types.go @@ -548,40 +548,23 @@ type DeviceUserAuthRequest struct { // identify the session. // // required: true - ID string `json:"challenge"` - NID uuid.UUID `json:"-"` + ID string `json:"challenge"` + CSRF string `json:"-"` + Verifier string `json:"-"` + + // Client is the OAuth 2.0 Client that initiated the request. + Client *client.Client `json:"client"` + // RequestURL is the original Device Authorization URL requested. + RequestURL string `json:"request_url"` // RequestedScope contains the OAuth 2.0 Scope requested by the OAuth 2.0 Client. RequestedScope sqlxx.StringSliceJSONFormat `json:"requested_scope"` - // RequestedAudience contains the access token audience as requested by the OAuth 2.0 Client. RequestedAudience sqlxx.StringSliceJSONFormat `json:"requested_access_token_audience"` - // RequestURL is the original Device Grant URL requested. - RequestURL string `json:"request_url"` - // SessionID is the login session ID. If the user-agent reuses a login session (via cookie / remember flag) - // this ID will remain the same. If the user-agent did not have an existing authentication session (e.g. remember is false) - // this will be a new random value. This value is used as the "sid" parameter in the ID Token and in OIDC Front-/Back- - // channel logout. It's value can generally be used to associate consecutive login requests by a certain user. - SessionID sqlxx.NullString `json:"session_id"` - - // Client is the OAuth 2.0 Client that initiated the request. - // - // required: true - Client *client.Client `json:"client"` - ClientID string `json:"-"` - - // DeviceCodeSignature is the OAuth 2.0 Device Authorization Grant Device Code Signature - // - // required: true - DeviceCodeSignature sqlxx.NullString `json:"-"` - - CSRF string `json:"-"` - Verifier string `json:"-"` - - Accepted bool `json:"-"` - AcceptedAt sqlxx.NullTime `json:"handled_at"` RequestedAt time.Time `json:"-"` + HandledAt sqlxx.NullTime `json:"handled_at"` + WasHandled bool `json:"-"` } // HandledDeviceUserAuthRequest is the request payload used to accept a device user_code. @@ -590,16 +573,29 @@ type DeviceUserAuthRequest struct { type HandledDeviceUserAuthRequest struct { // ID is the identifier ("device challenge") of the device request. It is used to // identify the session. - // - // required: true - ID string `json:"challenge"` - UserCode string `json:"user_code"` - HandledAt sqlxx.NullTime `json:"handled_at"` - WasHandled bool `json:"-"` - DeviceRequest *DeviceUserAuthRequest `json:"-" faker:"-"` - Error *RequestDeniedError `json:"-"` + ID string `json:"challenge"` + + Request *DeviceUserAuthRequest `json:"-" faker:"-"` + // RequestURL is the original Device Authorization URL requested. + RequestURL string `json:"request_url"` + // RequestedScope contains the OAuth 2.0 Scope requested by the OAuth 2.0 Client. + RequestedScope sqlxx.StringSliceJSONFormat `json:"requested_scope"` + // RequestedAudience contains the access token audience as requested by the OAuth 2.0 Client. + RequestedAudience sqlxx.StringSliceJSONFormat `json:"requested_access_token_audience"` + + DeviceCodeRequestID string `json:"device_code_request_id"` + + // Client is the OAuth 2.0 Client that initiated the request. + Client *client.Client `json:"client"` + + RequestedAt time.Time `json:"-"` + + HandledAt sqlxx.NullTime `json:"handled_at"` + WasHandled bool `json:"-"` + Error *RequestDeniedError `json:"-"` } +// HasError returns whether the request has errors. func (r *HandledDeviceUserAuthRequest) HasError() bool { return r.Error.IsError() } @@ -686,6 +682,13 @@ func (r *LoginRequest) MarshalJSON() ([]byte, error) { return json.Marshal(alias) } +// Contains information on an device verification +// +// swagger:model acceptDeviceUserCodeRequest +type AcceptDeviceUserCodeRequest struct { + UserCode string `json:"user_code"` +} + // Contains information on an ongoing consent request. // // swagger:model oAuth2ConsentRequest @@ -736,6 +739,9 @@ type OAuth2ConsentRequest struct { // channel logout. It's value can generally be used to associate consecutive login requests by a certain user. LoginSessionID sqlxx.NullString `json:"login_session_id"` + // DeviceChallenge is the device challenge this consent challenge belongs to, if this flow was initiated by a device. + DeviceChallenge sqlxx.NullString `json:"device_challenge_id" faker:"-"` + // ACR represents the Authentication AuthorizationContext Class Reference value for this authentication session. You can use it // to express that, for example, a user authenticated using two factor authentication. ACR string `json:"acr"` diff --git a/flow/flow.go b/flow/flow.go index ff46088e7c9..8d8b21d3305 100644 --- a/flow/flow.go +++ b/flow/flow.go @@ -57,20 +57,19 @@ const ( FlowStateConsentUnused = int16(5) FlowStateConsentUsed = int16(6) - - // FlowStateLoginInitialized applies before the login app either + // DeviceFlowStateLoginInitialized applies before the login app either // accepts or rejects the login request. - FlowStateDeviceInitialized = int16(7) + DeviceFlowStateInitialized = int16(7) - // FlowStateDeviceUnused indicates that the login has been authenticated, but + // DeviceFlowStateUnused indicates that the login has been authenticated, but // the User Agent hasn't picked up the result yet. - FlowStateDeviceUnused = int16(8) + DeviceFlowStateUnused = int16(8) - // FlowStateDeviceUsed indicates that the User Agent is requesting consent and + // DeviceFlowStateUsed indicates that the User Agent is requesting consent and // Hydra has invalidated the login request. This is a short-lived state - // because the transition to FlowStateConsentInitialized should happen while - // handling the request that triggered the transition to FlowStateDeviceUsed. - FlowStateDeviceUsed = int16(9) + // because the transition to DeviceFlowStateConsentInitialized should happen while + // handling the request that triggered the transition to DeviceFlowStateUsed. + DeviceFlowStateUsed = int16(9) // TODO: Refactor error handling to persist error codes instead of JSON // strings. Currently we persist errors as JSON strings in the LoginError @@ -83,7 +82,7 @@ const ( // If the above is implemented, merge the LoginError and ConsentError fields // and use the following FlowStates when converting to/from // [Handled]{Login|Consent}Request: - FlowStateDeviceError = int16(127) + DeviceFlowStateError = int16(127) FlowStateLoginError = int16(128) FlowStateConsentError = int16(129) ) @@ -226,26 +225,26 @@ type Flow struct { LoginError *RequestDeniedError `db:"login_error" json:"le,omitempty"` LoginAuthenticatedAt sqlxx.NullTime `db:"login_authenticated_at" json:"la,omitempty"` + // DeviceChallengeID is the device request's challenge ID + DeviceChallengeID sqlxx.NullString `db:"device_challenge_id" json:"di,omitempty"` + // DeviceCodeRequestID is the device request's ID + DeviceCodeRequestID sqlxx.NullString `db:"device_code_request_id" json:"dr,omitempty"` + // DeviceVerifier is the device request's verifier + DeviceVerifier sqlxx.NullString `db:"device_verifier" json:"dv,omitempty"` + // DeviceVerifier is the device request's CSRF + DeviceCSRF sqlxx.NullString `db:"device_csrf" json:"dc,omitempty"` + // DeviceUserCodeAcceptedAt is the time when device user_code was accepted + DeviceUserCodeAcceptedAt sqlxx.NullTime `db:"device_user_code_accepted_at" json:"da,omitempty"` + // DeviceWasUsed set to true means that the device request was already handled + DeviceWasUsed sqlxx.NullBool `db:"device_was_used" json:"du,omitempty"` + // DeviceHandledAt contains the timestamp the device user_code verification request was handled + DeviceHandledAt sqlxx.NullTime `db:"device_handled_at" json:"dh,omitempty"` + // DeviceError contains any error that happened during the handling of the device flow + DeviceError *RequestDeniedError `db:"device_error" json:"de,omitempty"` + // ConsentRequestID is the identifier of the consent request. // The database column should be named `consent_request_id`, but is not for historical reasons. ConsentRequestID sqlxx.NullString `db:"consent_challenge_id" json:"cc,omitempty"` - - // DeviceChallengeID is the identifier ("authorization challenge") of the consent authorization request. It is used to - // identify the session. - // - // required: true - DeviceChallengeID sqlxx.NullString `db:"device_challenge_id"` - - DeviceVerifier string `db:"device_verifier"` - DeviceCSRF string `db:"device_csrf"` - - // The user_code was already handled. - // TODO(nsklikas): Is this needed? - DeviceUserCodeWasUsed bool `db:"device_user_code_was_used"` - // DeviceHandledAt contains the timestamp the device user verification request was handled. - DeviceUserCodeHandledAt sqlxx.NullTime `db:"device_user_code_handled_at"` - DeviceError *RequestDeniedError `db:"device_error"` - // ConsentSkip, if true, implies that the client has requested the same scopes from the same user previously. // If true, you must not ask the user to grant the requested scopes. You must however either allow or deny the // consent request using the usual API call. @@ -279,6 +278,104 @@ type Flow struct { SessionAccessToken sqlxx.MapStringInterface `db:"session_access_token" faker:"-" json:"sa"` } +// NewDeviceFlow return a new Flow from a DeviceUserAuthRequest. +func NewDeviceFlow(r *DeviceUserAuthRequest) *Flow { + f := &Flow{ + DeviceChallengeID: sqlxx.NullString(r.ID), + Client: r.Client, + RequestURL: r.RequestURL, + DeviceVerifier: sqlxx.NullString(r.Verifier), + DeviceCSRF: sqlxx.NullString(r.CSRF), + RequestedAt: r.RequestedAt, + RequestedScope: r.RequestedScope, + RequestedAudience: r.RequestedAudience, + DeviceWasUsed: sqlxx.NullBool{Bool: r.WasHandled, Valid: true}, + DeviceHandledAt: r.HandledAt, + State: DeviceFlowStateInitialized, + } + if r.Client != nil { + f.ClientID = r.Client.GetID() + } + return f +} + +// GetDeviceUserAuthRequest return the DeviceUserAuthRequest from a Flow. +func (f *Flow) GetDeviceUserAuthRequest() *DeviceUserAuthRequest { + return &DeviceUserAuthRequest{ + ID: f.DeviceChallengeID.String(), + Client: f.Client, + RequestURL: f.RequestURL, + Verifier: f.DeviceVerifier.String(), + CSRF: f.DeviceCSRF.String(), + RequestedAt: f.RequestedAt, + RequestedScope: f.RequestedScope, + RequestedAudience: f.RequestedAudience, + WasHandled: f.DeviceWasUsed.Bool, + HandledAt: f.DeviceHandledAt, + } +} + +// GetHandledDeviceUserAuthRequest return the HandledDeviceUserAuthRequest from a Flow. +func (f *Flow) GetHandledDeviceUserAuthRequest() *HandledDeviceUserAuthRequest { + return &HandledDeviceUserAuthRequest{ + ID: f.DeviceChallengeID.String(), + Client: f.Client, + Request: f.GetDeviceUserAuthRequest(), + DeviceCodeRequestID: f.DeviceCodeRequestID.String(), + RequestURL: f.RequestURL, + RequestedAt: f.RequestedAt, + RequestedScope: f.RequestedScope, + RequestedAudience: f.RequestedAudience, + WasHandled: f.DeviceWasUsed.Bool, + HandledAt: f.DeviceHandledAt, + Error: f.DeviceError, + } +} + +// HandleDeviceUserAuthRequest updates the flows fields from a handled request. +func (f *Flow) HandleDeviceUserAuthRequest(h *HandledDeviceUserAuthRequest) error { + if f.DeviceWasUsed.Bool { + return errors.WithStack(x.ErrConflict.WithHint("The device verifier was already used and can no longer be changed.")) + } + + if f.State != DeviceFlowStateInitialized && f.State != DeviceFlowStateUnused && f.State != DeviceFlowStateError { + return errors.Errorf("invalid flow state: expected %d/%d/%d, got %d", DeviceFlowStateInitialized, DeviceFlowStateUnused, DeviceFlowStateError, f.State) + } + + if f.DeviceChallengeID.String() != h.ID { + return errors.Errorf("flow device challenge ID %s does not match HandledDeviceUserAuthRequest ID %s", f.ID, h.ID) + } + + f.State = DeviceFlowStateUnused + if h.Error != nil { + f.State = DeviceFlowStateError + } + f.Client = h.Client + f.ClientID = h.Client.GetID() + f.DeviceCodeRequestID = sqlxx.NullString(h.DeviceCodeRequestID) + f.DeviceHandledAt = h.HandledAt + f.DeviceWasUsed = sqlxx.NullBool{Bool: h.WasHandled, Valid: true} + f.RequestedScope = h.RequestedScope + f.RequestedAudience = h.RequestedAudience + f.DeviceError = h.Error + + return nil +} + +// InvalidateDeviceRequest shifts the flow state to DeviceFlowStateUsed. This +// transition is executed upon device completion. +func (f *Flow) InvalidateDeviceRequest() error { + if f.State != DeviceFlowStateUnused && f.State != DeviceFlowStateError { + return errors.Errorf("invalid flow state: expected %d or %d, got %d", DeviceFlowStateUnused, DeviceFlowStateError, f.State) + } + if f.DeviceWasUsed.Bool { + return errors.New("device verifier has already been used") + } + f.DeviceWasUsed = sqlxx.NullBool{Bool: true, Valid: true} + f.State = DeviceFlowStateUsed + return nil +} + func NewFlow(r *LoginRequest) *Flow { return &Flow{ ID: r.ID, @@ -301,22 +398,6 @@ func NewFlow(r *LoginRequest) *Flow { } } -func NewDeviceFlow(r *DeviceUserAuthRequest) *Flow { - return &Flow{ - ID: r.ID, - RequestedScope: r.RequestedScope, - RequestedAudience: r.RequestedAudience, - Client: r.Client, - ClientID: r.ClientID, - RequestURL: r.RequestURL, - SessionID: r.SessionID, - LoginVerifier: r.Verifier, - LoginCSRF: r.CSRF, - RequestedAt: r.RequestedAt, - State: FlowStateDeviceInitialized, - } -} - func (f *Flow) HandleLoginRequest(h *HandledLoginRequest) error { if f.LoginWasUsed { return errors.WithStack(x.ErrConflict.WithHint("The login request was already used and can no longer be changed.")) @@ -419,47 +500,6 @@ func (f *Flow) InvalidateLoginRequest() error { return nil } -func (f *Flow) GetDeviceUserAuthRequest() *DeviceUserAuthRequest { - return &DeviceUserAuthRequest{ - ID: f.ID, - RequestedScope: f.RequestedScope, - RequestedAudience: f.RequestedAudience, - Client: f.Client, - ClientID: f.ClientID, - RequestURL: f.RequestURL, - SessionID: f.SessionID, - Verifier: f.LoginVerifier, - CSRF: f.LoginCSRF, - RequestedAt: f.RequestedAt, - } -} - -func (f *Flow) HandleDeviceUserAuthRequest(h *HandledDeviceUserAuthRequest) error { - if f.DeviceUserCodeWasUsed { - return errors.WithStack(x.ErrConflict.WithHint("The user_code was already used and can no longer be changed.")) - } - - if f.State != FlowStateDeviceInitialized && f.State != FlowStateDeviceUnused && f.State != FlowStateDeviceError { - return errors.Errorf("invalid flow state: expected %d/%d/%d, got %d", FlowStateDeviceInitialized, FlowStateDeviceUnused, FlowStateDeviceError, f.State) - } - - if f.ID != h.ID { - return errors.Errorf("flow device challenge ID %s does not match HandledDeviceUserAuthRequest ID %s", f.ID, h.ID) - } - - if h.Error != nil { - f.State = FlowStateDeviceError - } else { - f.State = FlowStateDeviceUnused - } - f.DeviceError = h.Error - f.DeviceUserCodeHandledAt = h.HandledAt - f.DeviceUserCodeWasUsed = h.WasHandled - f.DeviceError = h.Error - - return nil -} - func (f *Flow) HandleConsentRequest(r *AcceptOAuth2ConsentRequest) error { if time.Time(r.HandledAt).IsZero() { return errors.New("refusing to handle a consent request with null HandledAt") @@ -530,6 +570,7 @@ func (f *Flow) GetConsentRequest(challenge string) *OAuth2ConsentRequest { RequestURL: f.RequestURL, LoginChallenge: sqlxx.NullString(f.ID), LoginSessionID: f.SessionID, + DeviceChallenge: f.DeviceChallengeID, ACR: f.ACR, AMR: f.AMR, Context: f.Context, diff --git a/flow/flow_test.go b/flow/flow_test.go index 1b56336b0af..91959333438 100644 --- a/flow/flow_test.go +++ b/flow/flow_test.go @@ -92,6 +92,94 @@ func (f *Flow) setHandledConsentRequest(r AcceptOAuth2ConsentRequest) { } } +func (f *Flow) setDeviceRequest(r *DeviceUserAuthRequest) { + f.DeviceChallengeID = sqlxx.NullString(r.ID) + f.DeviceCSRF = sqlxx.NullString(r.CSRF) + f.DeviceVerifier = sqlxx.NullString(r.Verifier) + f.Client = r.Client + f.RequestURL = r.RequestURL + f.RequestedAt = r.RequestedAt + f.RequestedScope = r.RequestedScope + f.RequestedAudience = r.RequestedAudience + f.DeviceWasUsed = sqlxx.NullBool{Bool: r.WasHandled, Valid: true} + f.DeviceHandledAt = r.HandledAt +} + +func (f *Flow) setHandledDeviceRequest(r *HandledDeviceUserAuthRequest) { + f.DeviceChallengeID = sqlxx.NullString(r.ID) + f.Client = r.Client + f.RequestURL = r.RequestURL + f.RequestedAt = r.RequestedAt + f.RequestedScope = r.RequestedScope + f.RequestedAudience = r.RequestedAudience + f.DeviceError = r.Error + f.RequestedAt = r.RequestedAt + f.DeviceCodeRequestID = sqlxx.NullString(r.DeviceCodeRequestID) + f.DeviceWasUsed = sqlxx.NullBool{Bool: r.WasHandled, Valid: true} + f.DeviceHandledAt = r.HandledAt +} + +func TestFlow_GetDeviceUserAuthRequest(t *testing.T) { + t.Run("GetDeviceUserAuthRequest should set all fields on its return value", func(t *testing.T) { + f := Flow{} + expected := DeviceUserAuthRequest{} + assert.NoError(t, faker.FakeData(&expected)) + f.setDeviceRequest(&expected) + actual := f.GetDeviceUserAuthRequest() + assert.Equal(t, expected, *actual) + }) +} + +func TestFlow_GetHandledDeviceUserAuthRequest(t *testing.T) { + t.Run("GetHandledDeviceUserAuthRequest should set all fields on its return value", func(t *testing.T) { + f := Flow{} + expected := HandledDeviceUserAuthRequest{} + assert.NoError(t, faker.FakeData(&expected)) + f.setHandledDeviceRequest(&expected) + actual := f.GetHandledDeviceUserAuthRequest() + assert.NotNil(t, actual.Request) + expected.Request = nil + actual.Request = nil + assert.Equal(t, expected, *actual) + }) +} + +func TestFlow_NewDeviceFlow(t *testing.T) { + t.Run("NewDeviceFlow and GetDeviceUserAuthRequest should use all DeviceUserAuthRequest fields", func(t *testing.T) { + expected := &DeviceUserAuthRequest{} + assert.NoError(t, faker.FakeData(expected)) + actual := NewDeviceFlow(expected).GetDeviceUserAuthRequest() + assert.Equal(t, expected, actual) + }) +} + +func TestFlow_HandleDeviceUserAuthRequest(t *testing.T) { + t.Run( + "HandleDeviceUserAuthRequest should ignore RequestedAt in its argument and copy the other fields", + func(t *testing.T) { + f := Flow{} + assert.NoError(t, faker.FakeData(&f)) + f.State = DeviceFlowStateInitialized + + r := HandledDeviceUserAuthRequest{} + assert.NoError(t, faker.FakeData(&r)) + r.ID = f.DeviceChallengeID.String() + f.DeviceWasUsed = sqlxx.NullBool{Bool: false, Valid: true} + f.RequestedAudience = r.RequestedAudience + f.RequestedScope = r.RequestedScope + f.RequestURL = r.RequestURL + + assert.NoError(t, f.HandleDeviceUserAuthRequest(&r)) + + actual := f.GetHandledDeviceUserAuthRequest() + assert.NotEqual(t, r.RequestedAt, actual.RequestedAt) + r.Request = f.GetDeviceUserAuthRequest() + actual.RequestedAt = r.RequestedAt + assert.Equal(t, r, *actual) + }, + ) +} + func TestFlow_GetLoginRequest(t *testing.T) { t.Run("GetLoginRequest should set all fields on its return value", func(t *testing.T) { f := Flow{} diff --git a/internal/testhelpers/janitor_test_helper.go b/internal/testhelpers/janitor_test_helper.go index 89007a12585..e4692cb76e6 100644 --- a/internal/testhelpers/janitor_test_helper.go +++ b/internal/testhelpers/janitor_test_helper.go @@ -192,7 +192,7 @@ func (j *JanitorConsentTestHelper) LoginRejectionSetup(ctx context.Context, reg // Create login requests for _, r := range j.flushLoginRequests { require.NoError(t, cl.CreateClient(ctx, r.Client)) - f, err := cm.CreateLoginRequest(ctx, r) + f, err := cm.CreateLoginRequest(ctx, nil, r) require.NoError(t, err) f.RequestedAt = time.Now() // we won't handle expired flows @@ -246,7 +246,7 @@ func (j *JanitorConsentTestHelper) LimitSetup(ctx context.Context, reg interface // Create login requests for _, r := range j.flushLoginRequests { require.NoError(t, cl.CreateClient(ctx, r.Client)) - f, err = cm.CreateLoginRequest(ctx, r) + f, err = cm.CreateLoginRequest(ctx, nil, r) require.NoError(t, err) // Reject each request @@ -290,7 +290,7 @@ func (j *JanitorConsentTestHelper) ConsentRejectionSetup(ctx context.Context, re // Create login requests for i, loginRequest := range j.flushLoginRequests { require.NoError(t, cl.CreateClient(ctx, loginRequest.Client)) - f, err = cm.CreateLoginRequest(ctx, loginRequest) + f, err = cm.CreateLoginRequest(ctx, nil, loginRequest) require.NoError(t, err) // Create consent requests @@ -344,7 +344,7 @@ func (j *JanitorConsentTestHelper) LoginTimeoutSetup(ctx context.Context, reg in // Create login requests for i, loginRequest := range j.flushLoginRequests { require.NoError(t, cl.CreateClient(ctx, loginRequest.Client)) - f, err = cm.CreateLoginRequest(ctx, loginRequest) + f, err = cm.CreateLoginRequest(ctx, nil, loginRequest) require.NoError(t, err) if i == 0 { @@ -385,7 +385,7 @@ func (j *JanitorConsentTestHelper) ConsentTimeoutSetup(ctx context.Context, reg // Let's reset and accept all login requests to test the consent requests for i, loginRequest := range j.flushLoginRequests { require.NoError(t, cl.CreateClient(ctx, loginRequest.Client)) - f, err := cm.CreateLoginRequest(ctx, loginRequest) + f, err := cm.CreateLoginRequest(ctx, nil, loginRequest) require.NoError(t, err) f.RequestedAt = time.Now() // we won't handle expired flows challenge := x.Must(f.ToLoginChallenge(ctx, reg)) @@ -437,7 +437,7 @@ func (j *JanitorConsentTestHelper) LoginConsentNotAfterSetup(ctx context.Context ) for _, r := range j.flushLoginRequests { require.NoError(t, cl.CreateClient(ctx, r.Client)) - f, err = cm.CreateLoginRequest(ctx, r) + f, err = cm.CreateLoginRequest(ctx, nil, r) require.NoError(t, err) } @@ -469,7 +469,7 @@ func (j *JanitorConsentTestHelper) LoginConsentNotAfterValidate( t.Logf("login flush check:\nNotAfter: %s\nLoginRequest: %s\nis expired: %v\n%+v\n", notAfter.String(), consentRequestLifespan.String(), isExpired, r) - f = x.Must(reg.ConsentManager().CreateLoginRequest(ctx, r)) + f = x.Must(reg.ConsentManager().CreateLoginRequest(ctx, nil, r)) loginChallenge := x.Must(f.ToLoginChallenge(ctx, reg)) _, err = reg.ConsentManager().GetLoginRequest(ctx, loginChallenge) diff --git a/oauth2/fosite_store_helpers_test.go b/oauth2/fosite_store_helpers_test.go index d8eeffbd95f..e3309f0fdb3 100644 --- a/oauth2/fosite_store_helpers_test.go +++ b/oauth2/fosite_store_helpers_test.go @@ -126,7 +126,7 @@ func mockRequestForeignKey(t *testing.T, id string, x oauth2.InternalRegistry) { } f, err := x.ConsentManager().CreateLoginRequest( - ctx, &flow.LoginRequest{ + ctx, nil, &flow.LoginRequest{ Client: cl, OpenIDConnectContext: new(flow.OAuth2ConsentRequestOpenIDConnectContext), ID: id, diff --git a/persistence/sql/persister_nid_test.go b/persistence/sql/persister_nid_test.go index 836b3e561fe..b45142404e5 100644 --- a/persistence/sql/persister_nid_test.go +++ b/persistence/sql/persister_nid_test.go @@ -437,7 +437,7 @@ func (s *PersisterTestSuite) TestCreateLoginRequest() { lr := flow.LoginRequest{ID: "lr-id", ClientID: client.ID, RequestedAt: time.Now()} require.NoError(t, r.Persister().CreateClient(s.t1, client)) - f, err := r.ConsentManager().CreateLoginRequest(s.t1, &lr) + f, err := r.ConsentManager().CreateLoginRequest(s.t1, nil, &lr) require.NoError(t, err) require.Equal(t, s.t1NID, f.NID) }) @@ -1219,7 +1219,7 @@ func (s *PersisterTestSuite) TestGetLoginRequest() { lr := flow.LoginRequest{ID: "lr-id", ClientID: client.ID, RequestedAt: time.Now()} require.NoError(t, r.Persister().CreateClient(s.t1, client)) - f, err := r.ConsentManager().CreateLoginRequest(s.t1, &lr) + f, err := r.ConsentManager().CreateLoginRequest(s.t1, nil, &lr) require.NoError(t, err) require.Equal(t, s.t1NID, f.NID) From c22f5ab1c7bed154538c7729cfd3bb6a3ee165f2 Mon Sep 17 00:00:00 2001 From: Nikos Date: Fri, 1 Mar 2024 14:54:58 +0200 Subject: [PATCH 13/52] fix: add post device auth handler --- driver/config/provider.go | 7 +++++++ oauth2/handler.go | 36 +++++++++++++++++++++++++++++++----- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/driver/config/provider.go b/driver/config/provider.go index b3f7c464de6..5d3d9a062bb 100644 --- a/driver/config/provider.go +++ b/driver/config/provider.go @@ -87,6 +87,7 @@ const ( KeyConsentURL = "urls.consent" KeyErrorURL = "urls.error" KeyDeviceVerificationURL = "urls.device_verification" + KeyDeviceDoneURL = "urls.post_device_done" KeyPublicURL = "urls.self.public" KeyAdminURL = "urls.self.admin" KeyIssuerURL = "urls.self.issuer" @@ -448,6 +449,11 @@ func (p *DefaultProvider) DeviceVerificationURL(ctx context.Context) *url.URL { return urlRoot(p.getProvider(ctx).URIF(KeyDeviceVerificationURL, p.publicFallbackURL(ctx, "oauth2/fallbacks/device"))) } +// DeviceDoneURL returns the post device authorization URL. Defaults to "oauth2/fallbacks/device/done". +func (p *DefaultProvider) DeviceDoneURL(ctx context.Context) *url.URL { + return urlRoot(p.getProvider(ctx).RequestURIF(KeyDeviceDoneURL, p.publicFallbackURL(ctx, "oauth2/fallbacks/device/done"))) +} + func (p *DefaultProvider) PublicURL(ctx context.Context) *url.URL { return urlRoot(p.getProvider(ctx).RequestURIF(KeyPublicURL, p.IssuerURL(ctx))) } @@ -698,6 +704,7 @@ func (p *DefaultProvider) CookieNameLoginCSRF(ctx context.Context) string { return p.cookieSuffix(ctx, KeyCookieLoginCSRFName) } +// CookieNameDeviceCSRF returns the device CSRF cookie name. func (p *DefaultProvider) CookieNameDeviceCSRF(ctx context.Context) string { return p.cookieSuffix(ctx, KeyCookieDeviceCSRFName) } diff --git a/oauth2/handler.go b/oauth2/handler.go index f8de92b1453..1ceb543206f 100644 --- a/oauth2/handler.go +++ b/oauth2/handler.go @@ -10,6 +10,7 @@ import ( "fmt" "html/template" "net/http" + "net/url" "reflect" "strings" "time" @@ -47,6 +48,7 @@ const ( DefaultLoginPath = "/oauth2/fallbacks/login" DefaultConsentPath = "/oauth2/fallbacks/consent" DefaultPostLogoutPath = "/oauth2/fallbacks/logout/callback" + DefaultPostDevicePath = "/oauth2/fallbacks/device/done" DefaultLogoutPath = "/oauth2/fallbacks/logout" DefaultErrorPath = "/oauth2/fallbacks/error" TokenPath = "/oauth2/token" // #nosec G101 @@ -98,6 +100,12 @@ func (h *Handler) SetRoutes(admin *httprouterx.RouterAdmin, public *httprouterx. http.StatusOK, config.KeyLogoutRedirectURL, )) + public.GET(DefaultPostDevicePath, h.fallbackHandler( + "You successfully authenticated on your device!", + "The Default Post Device URL is not set which is why you are seeing this fallback page. Your device login request however succeeded.", + http.StatusOK, + config.KeyDeviceDoneURL, + )) public.GET(DefaultErrorPath, h.DefaultErrorHandler) public.Handler("OPTIONS", RevocationPath, corsMiddleware(http.HandlerFunc(h.handleOptions))) @@ -714,7 +722,7 @@ func (h *Handler) getOidcUserInfo(w http.ResponseWriter, r *http.Request) { func (h *Handler) performOAuth2DeviceVerificationFlow(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { ctx := r.Context() - _, flow, err := h.r.ConsentStrategy().HandleOAuth2DeviceAuthorizationRequest(ctx, w, r) + consentSession, flow, err := h.r.ConsentStrategy().HandleOAuth2DeviceAuthorizationRequest(ctx, w, r) if errors.Is(err, consent.ErrAbortOAuth2Request) { x.LogAudit(r, nil, h.r.AuditLogger()) // do nothing @@ -729,6 +737,24 @@ func (h *Handler) performOAuth2DeviceVerificationFlow(w http.ResponseWriter, r * return } + req := fosite.NewDeviceRequest() + req.Client = consentSession.ConsentRequest.Client + session, err := h.updateSessionWithRequest(ctx, consentSession, flow, r, req) + if err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + + req.SetSession(session) + // We update the device_code session with the claims that the user gave consent for, this + // marks it as ready to be used for the token endpoint + err = h.r.OAuth2Storage().UpdateDeviceCodeSessionByRequestID(ctx, flow.DeviceCodeRequestID.String(), req) + if err != nil { + x.LogError(r, err, h.r.Logger()) + h.r.Writer().WriteError(w, r, err) + return + } + http.Redirect(w, r, urlx.SetQuery(h.c.DeviceDoneURL(ctx), url.Values{"consent_verifier": {string(flow.ConsentVerifier)}}).String(), http.StatusFound) } @@ -796,6 +822,7 @@ type deviceAuthorization struct { // default: errorOAuth2 func (h *Handler) oAuth2DeviceFlow(w http.ResponseWriter, r *http.Request) { var ctx = r.Context() + request, err := h.r.OAuth2Provider().NewDeviceRequest(ctx, r) if err != nil { h.r.OAuth2Provider().WriteAccessError(ctx, w, request, err) @@ -1315,7 +1342,7 @@ func (h *Handler) updateSessionWithRequest(ctx context.Context, session *flow.Ac } var accessTokenKeyID string - if h.c.AccessTokenStrategy(ctx, client.AccessTokenStrategySource(flow.Client)) == "jwt" { + if h.c.AccessTokenStrategy(ctx, client.AccessTokenStrategySource(request.GetClient())) == "jwt" { accessTokenKeyID, err = h.r.AccessTokenJWTStrategy().GetPublicKeyID(ctx) if err != nil { x.LogError(r, err, h.r.Logger()) @@ -1323,7 +1350,7 @@ func (h *Handler) updateSessionWithRequest(ctx context.Context, session *flow.Ac } } - obfuscatedSubject, err := h.r.ConsentStrategy().ObfuscateSubjectIdentifier(ctx, flow.Client, session.ConsentRequest.Subject, session.ConsentRequest.ForceSubjectIdentifier) + obfuscatedSubject, err := h.r.ConsentStrategy().ObfuscateSubjectIdentifier(ctx, request.GetClient(), session.ConsentRequest.Subject, session.ConsentRequest.ForceSubjectIdentifier) if e := &(fosite.RFC6749Error{}); errors.As(err, &e) { x.LogAudit(r, err, h.r.AuditLogger()) return nil, err @@ -1344,7 +1371,7 @@ func (h *Handler) updateSessionWithRequest(ctx context.Context, session *flow.Ac // These are required for work around https://github.com/ory/fosite/issues/530 Nonce: request.GetRequestForm().Get("nonce"), - Audience: []string{flow.Client.GetID()}, + Audience: []string{request.GetClient().GetID()}, IssuedAt: time.Now().Truncate(time.Second).UTC(), // This is set by the fosite strategy @@ -1364,7 +1391,6 @@ func (h *Handler) updateSessionWithRequest(ctx context.Context, session *flow.Ac Extra: session.Session.AccessToken, KID: accessTokenKeyID, ClientID: flow.Client.GetID(), - ConsentChallenge: session.ConsentRequestID, ExcludeNotBeforeClaim: h.c.ExcludeNotBeforeClaim(ctx), AllowedTopLevelClaims: h.c.AllowedTopLevelClaims(ctx), MirrorTopLevelClaims: h.c.MirrorTopLevelClaims(ctx), From b96b63d08f36856c1256151f4dafdd07067b594a Mon Sep 17 00:00:00 2001 From: Nikos Date: Fri, 1 Mar 2024 14:58:08 +0200 Subject: [PATCH 14/52] feat: add consent handler for accepting a user_code --- client/registry.go | 2 + consent/handler.go | 141 +++++++++++ consent/handler_test.go | 367 +++++++++++++++++++++++++++- consent/manager.go | 2 +- consent/strategy_default.go | 16 +- persistence/sql/persister_oauth2.go | 13 +- x/events/events.go | 2 + x/fosite_storer.go | 5 + 8 files changed, 538 insertions(+), 10 deletions(-) diff --git a/client/registry.go b/client/registry.go index bfec25ace91..c23efd231db 100644 --- a/client/registry.go +++ b/client/registry.go @@ -8,6 +8,7 @@ import ( "github.com/ory/fosite" foauth2 "github.com/ory/fosite/handler/oauth2" + "github.com/ory/fosite/handler/rfc8628" enigma "github.com/ory/fosite/token/hmac" "github.com/ory/hydra/v2/jwk" "github.com/ory/hydra/v2/x" @@ -25,5 +26,6 @@ type Registry interface { OpenIDJWTStrategy() jwk.JWTSigner OAuth2HMACStrategy() foauth2.CoreStrategy OAuth2EnigmaStrategy() *enigma.HMACStrategy + RFC8628HMACStrategy() rfc8628.RFC8628CodeStrategy config.Provider } diff --git a/consent/handler.go b/consent/handler.go index b7af2d49afb..53b26611257 100644 --- a/consent/handler.go +++ b/consent/handler.go @@ -4,11 +4,13 @@ package consent import ( + "context" "encoding/json" "net/http" "net/url" "time" + "github.com/ory/hydra/v2/client" "github.com/ory/hydra/v2/flow" "github.com/ory/hydra/v2/oauth2/flowctx" "github.com/ory/hydra/v2/x/events" @@ -35,6 +37,7 @@ type Handler struct { const ( LoginPath = "/oauth2/auth/requests/login" + DevicePath = "/oauth2/auth/requests/device" ConsentPath = "/oauth2/auth/requests/consent" LogoutPath = "/oauth2/auth/requests/logout" SessionsPath = "/oauth2/auth/sessions" @@ -66,6 +69,8 @@ func (h *Handler) SetRoutes(admin *httprouterx.RouterAdmin) { admin.GET(LogoutPath, h.getOAuth2LogoutRequest) admin.PUT(LogoutPath+"/accept", h.acceptOAuth2LogoutRequest) admin.PUT(LogoutPath+"/reject", h.rejectOAuth2LogoutRequest) + + admin.PUT(DevicePath+"/accept", h.acceptUserCodeRequest) } // Revoke OAuth 2.0 Consent Session Parameters @@ -1064,3 +1069,139 @@ func (h *Handler) getOAuth2LogoutRequest(w http.ResponseWriter, r *http.Request, h.r.Writer().Write(w, r, request) } + +// Verify OAuth 2.0 User Code Request +// +// swagger:parameters acceptUserCodeRequest +type verifyUserCodeRequest struct { + // in: query + // required: true + Challenge string `json:"device_challenge"` + + // in: body + Body flow.AcceptDeviceUserCodeRequest +} + +// swagger:route PUT /admin/oauth2/auth/requests/device/accept oAuth2 acceptUserCodeRequest +// +// # Accepts a device grant user_code request +// +// Accepts a device grant user_code request +// +// Consumes: +// - application/json +// +// Produces: +// - application/json +// +// Schemes: http, https +// +// Responses: +// 200: oAuth2RedirectTo +// default: errorOAuth2 +func (h *Handler) acceptUserCodeRequest(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + ctx := r.Context() + + challenge := r.URL.Query().Get("device_challenge") + if challenge == "" { + h.r.Writer().WriteError(w, r, errorsx.WithStack(fosite.ErrInvalidRequest.WithHint(`Query parameter 'device_challenge' is not defined but should have been.`))) + return + } + + var reqBody flow.AcceptDeviceUserCodeRequest + d := json.NewDecoder(r.Body) + d.DisallowUnknownFields() + if err := d.Decode(&reqBody); err != nil { + h.r.Writer().WriteErrorCode(w, r, http.StatusBadRequest, errorsx.WithStack(err)) + return + } + + if reqBody.UserCode == "" { + h.r.Writer().WriteError(w, r, errorsx.WithStack(fosite.ErrInvalidRequest.WithHint("Field 'user_code' must not be empty."))) + return + } + + cr, err := h.r.ConsentManager().GetDeviceUserAuthRequest(ctx, challenge) + if err != nil { + h.r.Writer().WriteError(w, r, errorsx.WithStack(err)) + return + } + + f, err := h.decodeFlowWithClient(ctx, challenge, flowctx.AsDeviceChallenge) + if err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + + userCodeSignature, err := h.r.RFC8628HMACStrategy().UserCodeSignature(r.Context(), reqBody.UserCode) + if err != nil { + h.r.Writer().WriteError(w, r, errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithHint(`'user_code' signature could not be computed`))) + return + } + userCodeRequest, err := h.r.OAuth2Storage().GetUserCodeSession(r.Context(), userCodeSignature, nil) + if err != nil { + h.r.Writer().WriteError(w, r, errorsx.WithStack(fosite.ErrNotFound.WithWrap(err).WithHint(`'user_code' session not found`))) + return + } + err = h.r.RFC8628HMACStrategy().ValidateUserCode(ctx, userCodeRequest, reqBody.UserCode) + if err != nil { + h.r.Writer().WriteError(w, r, errorsx.WithStack(fosite.ErrTokenExpired.WithWrap(err).WithHint(`'user_code' has expired`))) + return + } + + p := flow.HandledDeviceUserAuthRequest{ + ID: f.DeviceChallengeID.String(), + RequestedAt: cr.RequestedAt, + HandledAt: sqlxx.NullTime(time.Now().UTC()), + Client: userCodeRequest.GetClient().(*client.Client), + DeviceCodeRequestID: userCodeRequest.GetID(), + RequestedScope: []string(userCodeRequest.GetRequestedScopes()), + RequestedAudience: []string(userCodeRequest.GetRequestedAudience()), + } + + // Append the client_id to the original RequestURL, as it is needed for the login flow + reqURL, err := url.Parse(f.RequestURL) + if err != nil { + h.r.Writer().WriteError(w, r, errorsx.WithStack(err)) + return + } + if reqURL.Query().Get("client_id") == "" { + q := reqURL.Query() + q.Add("client_id", userCodeRequest.GetClient().GetID()) + reqURL.RawQuery = q.Encode() + } + f.RequestURL = reqURL.String() + + hr, err := h.r.ConsentManager().HandleDeviceUserAuthRequest(ctx, f, challenge, &p) + if err != nil { + h.r.Writer().WriteError(w, r, errorsx.WithStack(err)) + return + } + + ru, err := url.Parse(hr.RequestURL) + if err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + + verifier, err := f.ToDeviceVerifier(ctx, h.r) + if err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + + events.Trace(ctx, events.DeviceUserCodeAccepted, events.WithClientID(userCodeRequest.GetClient().GetID())) + + h.r.Writer().Write(w, r, &flow.OAuth2RedirectTo{ + RedirectTo: urlx.SetQuery(ru, url.Values{"device_verifier": {verifier}, "client_id": {userCodeRequest.GetClient().GetID()}}).String(), + }) +} + +func (h *Handler) decodeFlowWithClient(ctx context.Context, challenge string, opts ...flowctx.CodecOption) (*flow.Flow, error) { + f, err := flowctx.Decode[flow.Flow](ctx, h.r.FlowCipher(), challenge, opts...) + if err != nil { + return nil, err + } + + return f, nil +} diff --git a/consent/handler_test.go b/consent/handler_test.go index 273c6a48fe5..98e55303507 100644 --- a/consent/handler_test.go +++ b/consent/handler_test.go @@ -17,10 +17,14 @@ import ( "github.com/stretchr/testify/require" + "github.com/ory/fosite" + "github.com/ory/fosite/handler/openid" + "github.com/ory/fosite/token/jwt" hydra "github.com/ory/hydra-client-go/v2" "github.com/ory/hydra/v2/client" . "github.com/ory/hydra/v2/consent" "github.com/ory/hydra/v2/flow" + "github.com/ory/hydra/v2/oauth2" "github.com/ory/hydra/v2/x" "github.com/ory/x/contextx" "github.com/ory/x/pointerx" @@ -104,7 +108,7 @@ func TestGetLoginRequest(t *testing.T) { if tc.exists { cl := &client.Client{ID: "client" + key} require.NoError(t, reg.ClientManager().CreateClient(context.Background(), cl)) - f, err := reg.ConsentManager().CreateLoginRequest(context.Background(), &flow.LoginRequest{ + f, err := reg.ConsentManager().CreateLoginRequest(context.Background(), nil, &flow.LoginRequest{ Client: cl, ID: challenge, RequestURL: requestURL, @@ -176,7 +180,7 @@ func TestGetConsentRequest(t *testing.T) { RequestURL: requestURL, RequestedAt: time.Now(), } - f, err := reg.ConsentManager().CreateLoginRequest(ctx, lr) + f, err := reg.ConsentManager().CreateLoginRequest(ctx, nil, lr) require.NoError(t, err) challenge, err = f.ToLoginChallenge(ctx, reg) require.NoError(t, err) @@ -243,7 +247,7 @@ func TestGetLoginRequestWithDuplicateAccept(t *testing.T) { cl := &client.Client{ID: "client"} require.NoError(t, reg.ClientManager().CreateClient(ctx, cl)) - f, err := reg.ConsentManager().CreateLoginRequest(ctx, &flow.LoginRequest{ + f, err := reg.ConsentManager().CreateLoginRequest(ctx, nil, &flow.LoginRequest{ Client: cl, ID: challenge, RequestURL: requestURL, @@ -300,3 +304,360 @@ func TestGetLoginRequestWithDuplicateAccept(t *testing.T) { require.Contains(t, result2.RedirectTo, "login_verifier") }) } + +func TestAcceptDeviceRequest(t *testing.T) { + ctx := context.Background() + challenge := "challenge" + requestURL := "https://hydra.example.com/" + oauth2.DeviceVerificationPath + + conf := testhelpers.NewConfigurationWithDefaults() + reg := testhelpers.NewRegistryMemory(t, conf, &contextx.Default{}) + + cl := &client.Client{ID: "client"} + require.NoError(t, reg.ClientManager().CreateClient(ctx, cl)) + f, err := reg.ConsentManager().CreateDeviceUserAuthRequest(ctx, &flow.DeviceUserAuthRequest{ + Client: cl, + ID: challenge, + RequestURL: requestURL, + RequestedAt: time.Now(), + }) + require.NoError(t, err) + challenge, err = f.ToDeviceChallenge(ctx, reg) + require.NoError(t, err) + + h := NewHandler(reg, conf) + r := x.NewRouterAdmin(conf.AdminURL) + h.SetRoutes(r) + ts := httptest.NewServer(r) + t.Cleanup(ts.Close) + + c := &http.Client{} + + deviceRequest := fosite.NewDeviceRequest() + deviceRequest.Client = cl + deviceRequest.SetSession( + &oauth2.Session{ + DefaultSession: &openid.DefaultSession{ + Headers: &jwt.Headers{}, + }, + BrowserFlowCompleted: false, + }, + ) + userCode, sig, err := reg.RFC8628HMACStrategy().GenerateUserCode(ctx) + require.NoError(t, err) + reg.OAuth2Storage().CreateUserCodeSession(ctx, sig, deviceRequest) + require.NoError(t, err) + + acceptUserCode := &hydra.AcceptDeviceUserCodeRequest{UserCode: &userCode} + + // marshal User to json + acceptUserCodeJson, err := json.Marshal(acceptUserCode) + require.NoError(t, err) + + // set the HTTP method, url, and request body + req, err := http.NewRequest(http.MethodPut, ts.URL+"/admin"+DevicePath+"/accept?device_challenge="+challenge, bytes.NewBuffer(acceptUserCodeJson)) + require.NoError(t, err) + + resp, err := c.Do(req) + require.NoError(t, err) + require.EqualValues(t, http.StatusOK, resp.StatusCode) + + var result flow.OAuth2RedirectTo + require.NoError(t, json.NewDecoder(resp.Body).Decode(&result)) + require.NotNil(t, result.RedirectTo) + require.Contains(t, result.RedirectTo, requestURL) + require.Contains(t, result.RedirectTo, "device_verifier") +} + +func TestAcceptDuplicateDeviceRequest(t *testing.T) { + ctx := context.Background() + challenge := "challenge" + requestURL := "https://hydra.example.com/" + oauth2.DeviceVerificationPath + + conf := testhelpers.NewConfigurationWithDefaults() + reg := testhelpers.NewRegistryMemory(t, conf, &contextx.Default{}) + + cl := &client.Client{ID: "client"} + require.NoError(t, reg.ClientManager().CreateClient(ctx, cl)) + f, err := reg.ConsentManager().CreateDeviceUserAuthRequest(ctx, &flow.DeviceUserAuthRequest{ + Client: cl, + ID: challenge, + RequestURL: requestURL, + RequestedAt: time.Now(), + }) + require.NoError(t, err) + challenge, err = f.ToDeviceChallenge(ctx, reg) + require.NoError(t, err) + + h := NewHandler(reg, conf) + r := x.NewRouterAdmin(conf.AdminURL) + h.SetRoutes(r) + ts := httptest.NewServer(r) + t.Cleanup(ts.Close) + + c := &http.Client{} + + deviceRequest := fosite.NewDeviceRequest() + deviceRequest.Client = cl + deviceRequest.SetSession( + &oauth2.Session{ + DefaultSession: &openid.DefaultSession{ + Headers: &jwt.Headers{}, + }, + BrowserFlowCompleted: false, + }, + ) + userCode, sig, err := reg.RFC8628HMACStrategy().GenerateUserCode(ctx) + require.NoError(t, err) + reg.OAuth2Storage().CreateUserCodeSession(ctx, sig, deviceRequest) + require.NoError(t, err) + + acceptUserCode := &hydra.AcceptDeviceUserCodeRequest{UserCode: &userCode} + + // marshal User to json + acceptUserCodeJson, err := json.Marshal(acceptUserCode) + require.NoError(t, err) + + // set the HTTP method, url, and request body + req, err := http.NewRequest(http.MethodPut, ts.URL+"/admin"+DevicePath+"/accept?device_challenge="+challenge, bytes.NewBuffer(acceptUserCodeJson)) + require.NoError(t, err) + + resp, err := c.Do(req) + require.NoError(t, err) + require.EqualValues(t, http.StatusOK, resp.StatusCode) + + var result flow.OAuth2RedirectTo + require.NoError(t, json.NewDecoder(resp.Body).Decode(&result)) + require.NotNil(t, result.RedirectTo) + require.Contains(t, result.RedirectTo, requestURL) + require.Contains(t, result.RedirectTo, "device_verifier") + + req2, err := http.NewRequest(http.MethodPut, ts.URL+"/admin"+DevicePath+"/accept?device_challenge="+challenge, bytes.NewBuffer(acceptUserCodeJson)) + require.NoError(t, err) + resp2, err := c.Do(req2) + require.NoError(t, err) + require.EqualValues(t, http.StatusOK, resp2.StatusCode) + + var result2 flow.OAuth2RedirectTo + require.NoError(t, json.NewDecoder(resp2.Body).Decode(&result2)) + require.NotNil(t, result2.RedirectTo) + require.Contains(t, result2.RedirectTo, requestURL) + require.Contains(t, result2.RedirectTo, "device_verifier") +} + +func TestAcceptCodeDeviceRequestFailure(t *testing.T) { + ctx := context.Background() + challenge := "challenge" + requestURL := "https://hydra.example.com/" + oauth2.DeviceVerificationPath + + conf := testhelpers.NewConfigurationWithDefaults() + reg := testhelpers.NewRegistryMemory(t, conf, &contextx.Default{}) + + cl := &client.Client{ID: "client"} + require.NoError(t, reg.ClientManager().CreateClient(ctx, cl)) + f, err := reg.ConsentManager().CreateDeviceUserAuthRequest(ctx, &flow.DeviceUserAuthRequest{ + Client: cl, + ID: challenge, + RequestURL: requestURL, + RequestedAt: time.Now(), + }) + require.NoError(t, err) + challenge, err = f.ToDeviceChallenge(ctx, reg) + require.NoError(t, err) + + h := NewHandler(reg, conf) + r := x.NewRouterAdmin(conf.AdminURL) + h.SetRoutes(r) + ts := httptest.NewServer(r) + t.Cleanup(ts.Close) + + c := &http.Client{} + + for _, tc := range []struct { + desc string + getBody func() ([]byte, error) + getURL func() string + validateResponse func(*http.Response) + }{ + { + desc: "random user_code, not persisted in the database", + getBody: func() ([]byte, error) { + deviceRequest := fosite.NewDeviceRequest() + deviceRequest.Client = cl + deviceRequest.SetSession( + &oauth2.Session{ + DefaultSession: &openid.DefaultSession{ + Headers: &jwt.Headers{}, + }, + BrowserFlowCompleted: false, + }, + ) + userCode, _, err := reg.RFC8628HMACStrategy().GenerateUserCode(ctx) + require.NoError(t, err) + return json.Marshal(&hydra.AcceptDeviceUserCodeRequest{UserCode: &userCode}) + }, + getURL: func() string { + return ts.URL + "/admin" + DevicePath + "/accept?device_challenge=" + challenge + }, + validateResponse: func(resp *http.Response) { + require.EqualValues(t, http.StatusNotFound, resp.StatusCode) + }, + }, + { + desc: "empty user_code", + getBody: func() ([]byte, error) { + deviceRequest := fosite.NewDeviceRequest() + deviceRequest.Client = cl + deviceRequest.SetSession( + &oauth2.Session{ + DefaultSession: &openid.DefaultSession{ + Headers: &jwt.Headers{}, + }, + BrowserFlowCompleted: false, + }, + ) + userCode := "" + return json.Marshal(&hydra.AcceptDeviceUserCodeRequest{UserCode: &userCode}) + }, + getURL: func() string { + return ts.URL + "/admin" + DevicePath + "/accept?device_challenge=" + challenge + }, + validateResponse: func(resp *http.Response) { + require.EqualValues(t, http.StatusBadRequest, resp.StatusCode) + }, + }, + { + desc: "empty challenge", + getBody: func() ([]byte, error) { + deviceRequest := fosite.NewDeviceRequest() + deviceRequest.Client = cl + deviceRequest.SetSession( + &oauth2.Session{ + DefaultSession: &openid.DefaultSession{ + Headers: &jwt.Headers{}, + }, + BrowserFlowCompleted: false, + }, + ) + userCode, _, err := reg.RFC8628HMACStrategy().GenerateUserCode(ctx) + require.NoError(t, err) + return json.Marshal(&hydra.AcceptDeviceUserCodeRequest{UserCode: &userCode}) + }, + getURL: func() string { + return ts.URL + "/admin" + DevicePath + "/accept" + }, + validateResponse: func(resp *http.Response) { + require.EqualValues(t, http.StatusBadRequest, resp.StatusCode) + }, + }, + { + desc: "random challenge", + getBody: func() ([]byte, error) { + deviceRequest := fosite.NewDeviceRequest() + deviceRequest.Client = cl + deviceRequest.SetSession( + &oauth2.Session{ + DefaultSession: &openid.DefaultSession{ + Headers: &jwt.Headers{}, + }, + BrowserFlowCompleted: false, + }, + ) + userCode, _, err := reg.RFC8628HMACStrategy().GenerateUserCode(ctx) + require.NoError(t, err) + return json.Marshal(&hydra.AcceptDeviceUserCodeRequest{UserCode: &userCode}) + }, + getURL: func() string { + return ts.URL + "/admin" + DevicePath + "/accept?device_challenge=abc" + }, + validateResponse: func(resp *http.Response) { + require.EqualValues(t, http.StatusNotFound, resp.StatusCode) + }, + }, + { + desc: "expired user_code", + getBody: func() ([]byte, error) { + deviceRequest := fosite.NewDeviceRequest() + deviceRequest.Client = cl + deviceRequest.SetSession( + &oauth2.Session{ + DefaultSession: &openid.DefaultSession{ + Headers: &jwt.Headers{}, + }, + BrowserFlowCompleted: false, + }, + ) + userCode, sig, err := reg.RFC8628HMACStrategy().GenerateUserCode(ctx) + require.NoError(t, err) + deviceRequest.SetSession( + &oauth2.Session{ + DefaultSession: &openid.DefaultSession{ + Headers: &jwt.Headers{}, + }, + BrowserFlowCompleted: false, + }, + ) + exp := time.Now().UTC() + deviceRequest.Session.SetExpiresAt(fosite.UserCode, exp) + err = reg.OAuth2Storage().CreateUserCodeSession(ctx, sig, deviceRequest) + require.NoError(t, err) + return json.Marshal(&hydra.AcceptDeviceUserCodeRequest{UserCode: &userCode}) + }, + getURL: func() string { + return ts.URL + "/admin" + DevicePath + "/accept?device_challenge=" + challenge + }, + validateResponse: func(resp *http.Response) { + require.EqualValues(t, http.StatusUnauthorized, resp.StatusCode) + result := &fosite.RFC6749Error{} + require.NoError(t, json.NewDecoder(resp.Body).Decode(&result)) + require.EqualValues(t, result.ErrorField, fosite.ErrTokenExpired.ErrorField) + }, + }, + { + desc: "extra fields", + getBody: func() ([]byte, error) { + deviceRequest := fosite.NewDeviceRequest() + deviceRequest.Client = cl + deviceRequest.SetSession( + &oauth2.Session{ + DefaultSession: &openid.DefaultSession{ + Headers: &jwt.Headers{}, + }, + BrowserFlowCompleted: false, + }, + ) + userCode, _, err := reg.RFC8628HMACStrategy().GenerateUserCode(ctx) + require.NoError(t, err) + ret := struct { + UserCode *string + Extra string + }{ + UserCode: &userCode, + Extra: "extra", + } + return json.Marshal(ret) + }, + getURL: func() string { + return ts.URL + "/admin" + DevicePath + "/accept?device_challenge=" + challenge + }, + validateResponse: func(resp *http.Response) { + require.EqualValues(t, http.StatusBadRequest, resp.StatusCode) + }, + }, + } { + tc := tc + t.Run("case="+tc.desc, func(t *testing.T) { + acceptUserCodeJson, err := tc.getBody() + require.NoError(t, err) + + // set the HTTP method, url, and request body + req, err := http.NewRequest(http.MethodPut, tc.getURL(), bytes.NewBuffer(acceptUserCodeJson)) + require.NoError(t, err) + + resp, err := c.Do(req) + require.NoError(t, err) + tc.validateResponse(resp) + }) + } + +} diff --git a/consent/manager.go b/consent/manager.go index bcf4d2151df..e2787d0de79 100644 --- a/consent/manager.go +++ b/consent/manager.go @@ -45,7 +45,7 @@ type ( RevokeSubjectLoginSession(ctx context.Context, user string) error ConfirmLoginSession(ctx context.Context, loginSession *flow.LoginSession) error - CreateLoginRequest(ctx context.Context, req *flow.LoginRequest) (*flow.Flow, error) + CreateLoginRequest(ctx context.Context, f *flow.Flow, req *flow.LoginRequest) (*flow.Flow, error) GetLoginRequest(ctx context.Context, challenge string) (*flow.LoginRequest, error) HandleLoginRequest(ctx context.Context, f *flow.Flow, challenge string, r *flow.HandledLoginRequest) (*flow.LoginRequest, error) VerifyAndInvalidateLoginRequest(ctx context.Context, verifier string) (*flow.HandledLoginRequest, error) diff --git a/consent/strategy_default.go b/consent/strategy_default.go index 6fb6cadedf9..3da4c50bd79 100644 --- a/consent/strategy_default.go +++ b/consent/strategy_default.go @@ -561,9 +561,13 @@ func (s *DefaultStrategy) requestConsent( // The OpenID Connect Test Tool fails if this returns `consent_required` when `prompt=none` is used. // According to the quote above, it should be ok to allow https to skip consent. // + // Device initiated flows are never allowed to skip consent, the user must always explicitly authorize the device. + // // This is tracked as issue: https://github.com/ory/hydra/issues/866 // This is also tracked as upstream issue: https://github.com/openid-certification/oidctest/issues/97 - if !(ar.GetRedirectURI().Scheme == "https" || (fosite.IsLocalhost(ar.GetRedirectURI()) && ar.GetRedirectURI().Scheme == "http")) { + if f.DeviceChallengeID != "" { + return s.forwardConsentRequest(ctx, w, r, ar, f, nil) + } else if !(ar.GetRedirectURI().Scheme == "https" || (fosite.IsLocalhost(ar.GetRedirectURI()) && ar.GetRedirectURI().Scheme == "http")) { return s.forwardConsentRequest(ctx, w, r, ar, f, nil) } } @@ -1235,7 +1239,15 @@ func (s *DefaultStrategy) HandleOAuth2DeviceAuthorizationRequest( ar.RequestedAudience = fosite.Arguments(deviceFlow.RequestedAudience) } + // TODO(nsklikas): wrap these 2 function calls in a transaction (one persists the flow and the other invalidates the user_code) consentSession, f, err := s.handleOAuth2AuthorizationRequest(ctx, w, r, ar, deviceFlow) + if err != nil { + return nil, nil, err + } + err = s.r.OAuth2Storage().UpdateAndInvalidateUserCodeSessionByRequestID(r.Context(), string(f.DeviceCodeRequestID), f.ID) + if err != nil { + return nil, nil, err + } return consentSession, f, err } @@ -1344,7 +1356,7 @@ func (s *DefaultStrategy) verifyDevice(ctx context.Context, _ http.ResponseWrite } cookieNameDeviceCSRF := s.r.Config().CookieNameDeviceCSRF(ctx) - if err := validateCsrfSession(r, s.r.Config(), store, cookieNameDeviceCSRF, session.Request.CSRF); err != nil { + if err := ValidateCsrfSession(r, s.r.Config(), store, cookieNameDeviceCSRF, session.Request.CSRF, f); err != nil { return nil, err } diff --git a/persistence/sql/persister_oauth2.go b/persistence/sql/persister_oauth2.go index a326ca78191..39223c58525 100644 --- a/persistence/sql/persister_oauth2.go +++ b/persistence/sql/persister_oauth2.go @@ -821,6 +821,9 @@ func (p *Persister) CreateUserCodeSession(ctx context.Context, signature string, func (p *Persister) GetUserCodeSession(ctx context.Context, signature string, session fosite.Session) (_ fosite.Requester, err error) { ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.GetUserCodeSession") defer otelx.End(span, &err) + if session == nil { + session = oauth2.NewSession("") + } return p.findSessionBySignature(ctx, signature, session, sqlTableUserCode) } @@ -841,18 +844,20 @@ func (p *Persister) InvalidateUserCodeSession(ctx context.Context, signature str ) } -// UpdateAndInvalidateUserCodeSession invalidates a user code session and connects it with the device flow challenge ID -func (p *Persister) UpdateAndInvalidateUserCodeSession(ctx context.Context, signature, challenge_id string) (err error) { +// UpdateAndInvalidateUserCodeSession invalidates a user code session and connects it with the device flow request ID +func (p *Persister) UpdateAndInvalidateUserCodeSessionByRequestID(ctx context.Context, request_id, challenge_id string) (err error) { ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.UpdateAndInvalidateUserCodeSession") defer otelx.End(span, &err) + // TODO(nsklikas): afaict this is supposed to return an error if no rows were updated, but this is not the actual behavior. + // We need to either fix this OR do a select -> check -> update (this would require 2 queries instead of 1). /* #nosec G201 table is static */ return sqlcon.HandleError( p.Connection(ctx). RawQuery( - fmt.Sprintf("UPDATE %s SET active=false, challenge_id=? WHERE signature=? AND nid = ?", OAuth2RequestSQL{Table: sqlTableUserCode}.TableName()), + fmt.Sprintf("UPDATE %s SET active=false, challenge_id=? WHERE request_id=? AND nid = ? AND active=true", OAuth2RequestSQL{Table: sqlTableUserCode}.TableName()), challenge_id, - signature, + request_id, p.NetworkID(ctx), ). Exec(), diff --git a/x/events/events.go b/x/events/events.go index afc8eb4ae81..065a8b1ea46 100644 --- a/x/events/events.go +++ b/x/events/events.go @@ -20,6 +20,8 @@ const ( // LoginRejected will be emitted when the login UI rejects a login request. LoginRejected semconv.Event = "OAuth2LoginRejected" + DeviceUserCodeAccepted semconv.Event = "OAuth2DeviceUserCodeAccepted" + // ConsentAccepted will be emitted when the consent UI accepts a consent request. ConsentAccepted semconv.Event = "OAuth2ConsentAccepted" diff --git a/x/fosite_storer.go b/x/fosite_storer.go index 546cfc98870..9d4833ddaa2 100644 --- a/x/fosite_storer.go +++ b/x/fosite_storer.go @@ -12,6 +12,7 @@ import ( "github.com/ory/fosite/handler/openid" "github.com/ory/fosite/handler/pkce" "github.com/ory/fosite/handler/rfc7523" + "github.com/ory/fosite/handler/rfc8628" "github.com/ory/fosite/handler/verifiable" ) @@ -22,6 +23,7 @@ type FositeStorer interface { openid.OpenIDConnectRequestStorage pkce.PKCERequestStorage rfc7523.RFC7523KeyStorage + rfc8628.RFC8628CoreStorage verifiable.NonceManager oauth2.ResourceOwnerPasswordCredentialsGrantStorage @@ -41,4 +43,7 @@ type FositeStorer interface { // DeleteOpenIDConnectSession deletes an OpenID Connect session. // This is duplicated from Ory Fosite to help against deprecation linting errors. DeleteOpenIDConnectSession(ctx context.Context, authorizeCode string) error + + UpdateDeviceCodeSessionByRequestID(ctx context.Context, requestID string, requester fosite.Requester) error + UpdateAndInvalidateUserCodeSessionByRequestID(ctx context.Context, signature, request_id string) (err error) } From 2dbdfc655cd81ae878819973f60612c64797777f Mon Sep 17 00:00:00 2001 From: Nikos Date: Thu, 7 Mar 2024 16:04:34 +0200 Subject: [PATCH 15/52] chore: add post_device_done to config schema --- driver/config/provider_test.go | 1 + internal/.hydra.yaml | 1 + spec/config.json | 8 ++++++++ 3 files changed, 10 insertions(+) diff --git a/driver/config/provider_test.go b/driver/config/provider_test.go index e6d1c7f9e16..a4385a03e7a 100644 --- a/driver/config/provider_test.go +++ b/driver/config/provider_test.go @@ -306,6 +306,7 @@ func TestViperProviderValidates(t *testing.T) { assert.Equal(t, urlx.ParseOrPanic("https://login/"), c.LoginURL(ctx)) assert.Equal(t, urlx.ParseOrPanic("https://consent/"), c.ConsentURL(ctx)) assert.Equal(t, urlx.ParseOrPanic("https://device/"), c.DeviceVerificationURL(ctx)) + assert.Equal(t, urlx.ParseOrPanic("https://device/callback"), c.DeviceDoneURL(ctx)) assert.Equal(t, urlx.ParseOrPanic("https://logout/"), c.LogoutURL(ctx)) assert.Equal(t, urlx.ParseOrPanic("https://error/"), c.ErrorURL(ctx)) assert.Equal(t, urlx.ParseOrPanic("https://post_logout/"), c.LogoutRedirectURL(ctx)) diff --git a/internal/.hydra.yaml b/internal/.hydra.yaml index 7442fe036f2..4e7cbb0143c 100644 --- a/internal/.hydra.yaml +++ b/internal/.hydra.yaml @@ -102,6 +102,7 @@ urls: logout: https://logout error: https://error device_verification: https://device + post_device_done: https://device/callback post_logout_redirect: https://post_logout strategies: diff --git a/spec/config.json b/spec/config.json index 821ace2cf76..46e9c4a549d 100644 --- a/spec/config.json +++ b/spec/config.json @@ -825,6 +825,14 @@ "/ui/device" ] }, + "post_device_done": { + "type": "string", + "description": "When a user completes an authentication flow initiated by a device, they will be redirected to this url afterwards.", + "format": "uri-reference", + "examples": [ + "https://my-app/device/post" + ] + }, "error": { "type": "string", "description": "Sets the error endpoint. The error ui will be shown when an OAuth2 error occurs that which can not be sent back to the client. Defaults to an internal fallback URL showing an error.", From 94c2f620ad76681a85cdc6a8d955a7c503db7ae4 Mon Sep 17 00:00:00 2001 From: Nikos Date: Mon, 11 Mar 2024 19:03:20 +0200 Subject: [PATCH 16/52] chore: add e2e tests --- consent/strategy_default_test.go | 59 ++++++++++++ consent/strategy_oauth_test.go | 127 ++++++++++++++++++++++++++ consent/test/manager_test_helpers.go | 129 +++++++++++++++++++++++++-- 3 files changed, 307 insertions(+), 8 deletions(-) diff --git a/consent/strategy_default_test.go b/consent/strategy_default_test.go index 628542a0e81..a65c1306f85 100644 --- a/consent/strategy_default_test.go +++ b/consent/strategy_default_test.go @@ -9,6 +9,7 @@ import ( "net/http/cookiejar" "net/http/httptest" "net/url" + "strings" "testing" "github.com/google/uuid" @@ -21,10 +22,28 @@ import ( . "github.com/ory/hydra/v2/consent" "github.com/ory/hydra/v2/driver" "github.com/ory/hydra/v2/internal/testhelpers" + "github.com/ory/hydra/v2/oauth2" "github.com/ory/x/ioutilx" "github.com/ory/x/urlx" ) +func checkAndAcceptDeviceHandler(t *testing.T, apiClient *hydra.APIClient) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + userCode := r.URL.Query().Get("user_code") + payload := hydra.AcceptDeviceUserCodeRequest{ + UserCode: &userCode, + } + + v, _, err := apiClient.OAuth2API.AcceptUserCodeRequest(context.Background()). + DeviceChallenge(r.URL.Query().Get("device_challenge")). + AcceptDeviceUserCodeRequest(payload). + Execute() + require.NoError(t, err) + require.NotEmpty(t, v.RedirectTo) + http.Redirect(w, r, v.RedirectTo, http.StatusFound) + } +} + func checkAndAcceptLoginHandler(t *testing.T, apiClient *hydra.APIClient, subject string, cb func(*testing.T, *hydra.OAuth2LoginRequest, error) hydra.AcceptOAuth2LoginRequest) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { res, _, err := apiClient.OAuth2API.GetOAuth2LoginRequest(context.Background()).LoginChallenge(r.URL.Query().Get("login_challenge")).Execute() @@ -73,6 +92,46 @@ func makeOAuth2Request(t *testing.T, reg driver.Registry, hc *http.Client, oc *c return gjson.ParseBytes(ioutilx.MustReadAll(res.Body)), res } +func makeOAuth2DeviceAuthRequest(t *testing.T, reg driver.Registry, hc *http.Client, oc *client.Client, scope string) (gjson.Result, *http.Response) { + ctx := context.Background() + if hc == nil { + hc = testhelpers.NewEmptyJarClient(t) + } + + data := url.Values{} + data.Set("scope", scope) + data.Set("client_id", oc.GetID()) + req, err := http.NewRequest( + http.MethodPost, + reg.Config().OAuth2DeviceAuthorisationURL(ctx).String(), + strings.NewReader(data.Encode()), + ) + require.NoError(t, err) + req.SetBasicAuth(oc.GetID(), oc.Secret) + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + + res, err := hc.Do(req) + require.NoError(t, err) + + defer res.Body.Close() + + return gjson.ParseBytes(ioutilx.MustReadAll(res.Body)), res +} + +func makeOAuth2DeviceVerificationRequest(t *testing.T, reg driver.Registry, hc *http.Client, oc *client.Client, values url.Values) (gjson.Result, *http.Response) { + ctx := context.Background() + if hc == nil { + hc = testhelpers.NewEmptyJarClient(t) + } + + values.Add("client_id", oc.GetID()) + res, err := hc.Get(urlx.CopyWithQuery(urlx.AppendPaths(reg.Config().PublicURL(ctx), oauth2.DeviceVerificationPath), values).String()) + require.NoError(t, err) + defer res.Body.Close() + + return gjson.ParseBytes(ioutilx.MustReadAll(res.Body)), res +} + func createClient(t *testing.T, reg driver.Registry, c *client.Client) *client.Client { secret := uuid.New().String() c.Secret = secret diff --git a/consent/strategy_oauth_test.go b/consent/strategy_oauth_test.go index 1c17d1bc814..02345589e1c 100644 --- a/consent/strategy_oauth_test.go +++ b/consent/strategy_oauth_test.go @@ -1110,6 +1110,133 @@ func TestStrategyLoginConsentNext(t *testing.T) { }) } +func TestStrategyDeviceLoginConsent(t *testing.T) { + ctx := context.Background() + reg := testhelpers.NewMockedRegistry(t, &contextx.Default{}) + reg.Config().MustSet(ctx, config.KeyAccessTokenStrategy, "opaque") + reg.Config().MustSet(ctx, config.KeyConsentRequestMaxAge, time.Hour) + reg.Config().MustSet(ctx, config.KeyConsentRequestMaxAge, time.Hour) + reg.Config().MustSet(ctx, config.KeyScopeStrategy, "exact") + reg.Config().MustSet(ctx, config.KeySubjectTypesSupported, []string{"pairwise", "public"}) + reg.Config().MustSet(ctx, config.KeySubjectIdentifierAlgorithmSalt, "76d5d2bf-747f-4592-9fbd-d2b895a54b3a") + + publicTS, adminTS := testhelpers.NewOAuth2Server(ctx, t, reg) + adminClient := hydra.NewAPIClient(hydra.NewConfiguration()) + adminClient.GetConfig().Servers = hydra.ServerConfigurations{{URL: adminTS.URL}} + + oauth2Config := func(t *testing.T, c *client.Client) *oauth2.Config { + return &oauth2.Config{ + ClientID: c.GetID(), + ClientSecret: c.Secret, + Endpoint: oauth2.Endpoint{ + DeviceAuthURL: publicTS.URL + "/oauth2/device/auth", + TokenURL: publicTS.URL + "/oauth2/token", + AuthStyle: oauth2.AuthStyleInHeader, + }, + } + } + + acceptDeviceHandler := func(t *testing.T) http.HandlerFunc { + return checkAndAcceptDeviceHandler(t, adminClient) + } + + acceptLoginHandler := func(t *testing.T, subject string, payload *hydra.AcceptOAuth2LoginRequest) http.HandlerFunc { + return checkAndAcceptLoginHandler(t, adminClient, subject, func(*testing.T, *hydra.OAuth2LoginRequest, error) hydra.AcceptOAuth2LoginRequest { + if payload == nil { + return hydra.AcceptOAuth2LoginRequest{} + } + return *payload + }) + } + + acceptConsentHandler := func(t *testing.T, payload *hydra.AcceptOAuth2ConsentRequest) http.HandlerFunc { + return checkAndAcceptConsentHandler(t, adminClient, func(*testing.T, *hydra.OAuth2ConsentRequest, error) hydra.AcceptOAuth2ConsentRequest { + if payload == nil { + return hydra.AcceptOAuth2ConsentRequest{} + } + return *payload + }) + } + + createDefaultClient := func(t *testing.T) *client.Client { + c := &client.Client{GrantTypes: []string{"urn:ietf:params:oauth:grant-type:device_code"}} + return createClient(t, reg, c) + } + t.Run("case=should pass if both login and consent are granted and check remember flows as well as various payloads", func(t *testing.T) { + subject := "aeneas-rekkas" + c := createDefaultClient(t) + testhelpers.NewDeviceLoginConsentUI(t, reg.Config(), + acceptDeviceHandler(t), + acceptLoginHandler(t, subject, &hydra.AcceptOAuth2LoginRequest{ + Remember: pointerx.Bool(true), + }), + acceptConsentHandler(t, &hydra.AcceptOAuth2ConsentRequest{ + Remember: pointerx.Bool(true), + GrantScope: []string{"openid"}, + Session: &hydra.AcceptOAuth2ConsentRequestSession{ + AccessToken: map[string]interface{}{"foo": "bar"}, + IdToken: map[string]interface{}{"bar": "baz"}, + }, + })) + + hc := testhelpers.NewEmptyJarClient(t) + + var run = func(t *testing.T) { + res, resp := makeOAuth2DeviceAuthRequest(t, reg, hc, c, "openid") + assert.EqualValues(t, http.StatusOK, resp.StatusCode) + + devResp := new(oauth2.DeviceAuthResponse) + require.NoError(t, json.Unmarshal([]byte(res.Raw), devResp)) + + resp, err := hc.Get(devResp.VerificationURIComplete) + require.NoError(t, err) + require.Contains(t, reg.Config().DeviceDoneURL(ctx).String(), resp.Request.URL.Path, "did not end up in post device URL") + + conf := oauth2Config(t, c) + _, err = conf.DeviceAccessToken(ctx, devResp) + // TODO(nsklikas): Uncomment after the token endpoint is implemented + // require.NoError(t, err) + + // claims := testhelpers.IntrospectToken(t, conf, token.AccessToken, adminTS) + // assert.Equal(t, "bar", claims.Get("ext.foo").String(), "%s", claims.Raw) + + // idClaims := testhelpers.DecodeIDToken(t, token) + // assert.Equal(t, "baz", idClaims.Get("bar").String(), "%s", idClaims.Raw) + // sid = idClaims.Get("sid").String() + // assert.NotNil(t, sid) + } + + t.Run("perform first flow", run) + + }) + t.Run("case=should fail because a device verifier was given that doesn't exist in the store", func(t *testing.T) { + testhelpers.NewDeviceLoginConsentUI(t, reg.Config(), testhelpers.HTTPServerNoExpectedCallHandler(t), testhelpers.HTTPServerNoExpectedCallHandler(t), testhelpers.HTTPServerNoExpectedCallHandler(t)) + c := createDefaultClient(t) + hc := testhelpers.NewEmptyJarClient(t) + + _, res := makeOAuth2DeviceVerificationRequest(t, reg, hc, c, url.Values{"device_verifier": {"does-not-exist"}}) + assert.EqualValues(t, http.StatusForbidden, res.StatusCode) + }) + + t.Run("case=should fail because a login verifier was given that doesn't exist in the store", func(t *testing.T) { + testhelpers.NewLoginConsentUI(t, reg.Config(), testhelpers.HTTPServerNoExpectedCallHandler(t), testhelpers.HTTPServerNoExpectedCallHandler(t)) + c := createDefaultClient(t) + hc := testhelpers.NewEmptyJarClient(t) + + _, res := makeOAuth2DeviceVerificationRequest(t, reg, hc, c, url.Values{"login_verifier": {"does-not-exist"}}) + assert.EqualValues(t, http.StatusForbidden, res.StatusCode) + }) + + t.Run("case=should fail because a consent verifier was given that doesn't exist in the store", func(t *testing.T) { + testhelpers.NewLoginConsentUI(t, reg.Config(), testhelpers.HTTPServerNoExpectedCallHandler(t), testhelpers.HTTPServerNoExpectedCallHandler(t)) + c := createDefaultClient(t) + hc := testhelpers.NewEmptyJarClient(t) + + _, res := makeOAuth2DeviceVerificationRequest(t, reg, hc, c, url.Values{"consent_verifier": {"does-not-exist"}}) + assert.EqualValues(t, http.StatusForbidden, res.StatusCode) + }) +} + func DropCookieJar(drop *regexp.Regexp) http.CookieJar { jar, _ := cookiejar.New(nil) return &dropCSRFCookieJar{ diff --git a/consent/test/manager_test_helpers.go b/consent/test/manager_test_helpers.go index 003f666f056..7b820eb3245 100644 --- a/consent/test/manager_test_helpers.go +++ b/consent/test/manager_test_helpers.go @@ -130,6 +130,40 @@ func MockLogoutRequest(key string, withClient bool, network string) (c *flow.Log } } +func MockDeviceRequest(key string, network string) (c *flow.DeviceUserAuthRequest, h *flow.HandledDeviceUserAuthRequest, f *flow.Flow) { + client := &client.Client{ID: "fk-client-" + key} + c = &flow.DeviceUserAuthRequest{ + RequestedAt: time.Now().UTC().Add(-time.Minute), + Client: client, + RequestURL: "https://request-url/path" + key, + ID: makeID("challenge", network, key), + Verifier: makeID("verifier", network, key), + CSRF: "csrf" + key, + } + + f = flow.NewDeviceFlow(c) + + var err = &flow.RequestDeniedError{ + Name: "error_name" + key, + Description: "error_description" + key, + Hint: "error_hint,omitempty" + key, + Code: 100, + Debug: "error_debug,omitempty" + key, + Valid: true, + } + + h = &flow.HandledDeviceUserAuthRequest{ + ID: makeID("challenge", network, key), + RequestedAt: time.Now().UTC().Add(-time.Minute), + Client: client, + Error: err, + Request: c, + WasHandled: false, + } + + return c, h, f +} + func MockAuthRequest(key string, authAt bool, network string) (c *flow.LoginRequest, h *flow.HandledLoginRequest, f *flow.Flow) { c = &flow.LoginRequest{ OpenIDConnectContext: &flow.OAuth2ConsentRequestOpenIDConnectContext{ @@ -267,7 +301,7 @@ func SaneMockAuthRequest(t *testing.T, m consent.Manager, ls *flow.LoginSession, ID: uuid.New().String(), Verifier: uuid.New().String(), } - _, err := m.CreateLoginRequest(context.Background(), c) + _, err := m.CreateLoginRequest(context.Background(), nil, c) require.NoError(t, err) return c } @@ -312,9 +346,9 @@ func TestHelperNID(r interface { require.Error(t, t2InvalidNID.CreateLoginSession(ctx, &testLS)) require.NoError(t, t1ValidNID.CreateLoginSession(ctx, &testLS)) - _, err := t2InvalidNID.CreateLoginRequest(ctx, &testLR) + _, err := t2InvalidNID.CreateLoginRequest(ctx, nil, &testLR) require.Error(t, err) - f, err := t1ValidNID.CreateLoginRequest(ctx, &testLR) + f, err := t1ValidNID.CreateLoginRequest(ctx, nil, &testLR) require.NoError(t, err) testLR.ID = x.Must(f.ToLoginChallenge(ctx, r)) @@ -372,7 +406,7 @@ func ManagerTests(deps Deps, m consent.Manager, clientManager client.Manager, fo RequestedAt: time.Now(), } - _, err := m.CreateLoginRequest(ctx, lr[k]) + _, err := m.CreateLoginRequest(ctx, nil, lr[k]) require.NoError(t, err) } }) @@ -459,6 +493,73 @@ func ManagerTests(deps Deps, m consent.Manager, clientManager client.Manager, fo } }) + t.Run("case=device-request", func(t *testing.T) { + challenges := make([]string, 0) + + c, h, f := MockDeviceRequest("0", network) + _ = clientManager.CreateClient(ctx, c.Client) // Ignore errors that are caused by duplication + deviceChallenge := x.Must(f.ToDeviceChallenge(ctx, deps)) + + _, err := m.GetDeviceUserAuthRequest(ctx, deviceChallenge) + require.Error(t, err) + + f, err = m.CreateDeviceUserAuthRequest(ctx, c) + require.NoError(t, err) + + deviceChallenge = x.Must(f.ToDeviceChallenge(ctx, deps)) + + got1, err := m.GetDeviceUserAuthRequest(ctx, deviceChallenge) + require.NoError(t, err) + assert.False(t, got1.WasHandled) + compareDeviceRequest(t, c, got1) + + got1, err = m.HandleDeviceUserAuthRequest(ctx, f, deviceChallenge, h) + require.NoError(t, err) + compareDeviceRequest(t, c, got1) + + for _, key := range []string{"1", "2", "3", "4", "5", "6", "7"} { + c, h, f := MockDeviceRequest(key, network) + deviceChallenge := x.Must(f.ToDeviceChallenge(ctx, deps)) + + _, err := m.GetDeviceUserAuthRequest(ctx, deviceChallenge) + require.Error(t, err) + + f, err = m.CreateDeviceUserAuthRequest(ctx, c) + require.NoError(t, err) + + deviceChallenge = x.Must(f.ToDeviceChallenge(ctx, deps)) + challenges = append(challenges, deviceChallenge) + + got1, err := m.GetDeviceUserAuthRequest(ctx, deviceChallenge) + require.NoError(t, err) + assert.False(t, got1.WasHandled) + compareDeviceRequest(t, c, got1) + + got1, err = m.HandleDeviceUserAuthRequest(ctx, f, deviceChallenge, h) + require.NoError(t, err) + compareDeviceRequest(t, c, got1) + } + + DeviceVerifier := x.Must(f.ToDeviceVerifier(ctx, deps)) + + got2, err := m.VerifyAndInvalidateDeviceUserAuthRequest(ctx, DeviceVerifier) + require.NoError(t, err) + c.WasHandled = true + compareDeviceRequest(t, c, got2.Request) + + deviceChallenge = x.Must(f.ToDeviceChallenge(ctx, deps)) + authReq, err := m.GetDeviceUserAuthRequest(ctx, deviceChallenge) + require.NoError(t, err) + c.WasHandled = false + compareDeviceRequest(t, c, authReq) + + for _, challenge := range challenges { + authReq, err := m.GetDeviceUserAuthRequest(ctx, challenge) + require.NoError(t, err) + assert.Equal(t, authReq.WasHandled, false) + } + }) + t.Run("case=auth-request", func(t *testing.T) { for _, tc := range []struct { key string @@ -480,7 +581,7 @@ func ManagerTests(deps Deps, m consent.Manager, clientManager client.Manager, fo _, err := m.GetLoginRequest(ctx, loginChallenge) require.Error(t, err) - f, err = m.CreateLoginRequest(ctx, c) + f, err = m.CreateLoginRequest(ctx, nil, c) require.NoError(t, err) loginChallenge = x.Must(f.ToLoginChallenge(ctx, deps)) @@ -749,9 +850,9 @@ func ManagerTests(deps Deps, m consent.Manager, clientManager client.Manager, fo }) t.Run("case=list-used-consent-requests", func(t *testing.T) { - f1, err := m.CreateLoginRequest(ctx, lr["rv1"]) + f1, err := m.CreateLoginRequest(ctx, nil, lr["rv1"]) require.NoError(t, err) - f2, err := m.CreateLoginRequest(ctx, lr["rv2"]) + f2, err := m.CreateLoginRequest(ctx, nil, lr["rv2"]) require.NoError(t, err) cr1, hcr1, _ := MockConsentRequest("rv1", true, 0, false, false, false, "fk-login-challenge", network) @@ -1071,7 +1172,7 @@ func ManagerTests(deps Deps, m consent.Manager, clientManager client.Manager, fo SessionID: sqlxx.NullString(s.ID), } - f, err := m.CreateLoginRequest(ctx, lr) + f, err := m.CreateLoginRequest(ctx, nil, lr) require.NoError(t, err) expected := &flow.OAuth2ConsentRequest{ ConsentRequestID: uuid.NewString(), @@ -1133,6 +1234,17 @@ func compareAuthenticationRequest(t *testing.T, a, b *flow.LoginRequest) { assert.EqualValues(t, a.SessionID, b.SessionID) } +func compareDeviceRequest(t *testing.T, a, b *flow.DeviceUserAuthRequest) { + assert.EqualValues(t, a.Client.GetID(), b.Client.GetID()) + assert.EqualValues(t, a.CSRF, b.CSRF) + assert.EqualValues(t, a.RequestURL, b.RequestURL) + assert.EqualValues(t, a.Verifier, b.Verifier) + assert.EqualValues(t, a.HandledAt, b.HandledAt) + assert.EqualValues(t, a.RequestedAudience, b.RequestedAudience) + assert.EqualValues(t, a.RequestedScope, b.RequestedScope) + assert.EqualValues(t, a.WasHandled, b.WasHandled) +} + func compareConsentRequest(t *testing.T, a, b *flow.OAuth2ConsentRequest) { assert.EqualValues(t, a.Client.GetID(), b.Client.GetID()) assert.EqualValues(t, a.ConsentRequestID, b.ConsentRequestID) @@ -1145,4 +1257,5 @@ func compareConsentRequest(t *testing.T, a, b *flow.OAuth2ConsentRequest) { assert.EqualValues(t, a.Skip, b.Skip) assert.EqualValues(t, a.LoginChallenge, b.LoginChallenge) assert.EqualValues(t, a.LoginSessionID, b.LoginSessionID) + assert.EqualValues(t, a.DeviceChallenge, b.DeviceChallenge) } From e268ba1ed32443d2b8104665b4517e77ee9d96e6 Mon Sep 17 00:00:00 2001 From: dushu Date: Sat, 23 Mar 2024 16:54:13 -0600 Subject: [PATCH 17/52] feat: token request handling for device flow --- fositex/config.go | 1 + ..._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 | 16 +- oauth2/oauth2_auth_code_test.go | 2 +- oauth2/oauth2_device_code_test.go | 166 ++++++++++++++++++ oauth2/session.go | 9 + oauth2/session_test.go | 1 + 22 files changed, 221 insertions(+), 22 deletions(-) create mode 100644 oauth2/oauth2_device_code_test.go diff --git a/fositex/config.go b/fositex/config.go index 4300077ea99..35c551c50a6 100644 --- a/fositex/config.go +++ b/fositex/config.go @@ -63,6 +63,7 @@ var defaultFactories = []Factory{ compose.RFC7523AssertionGrantFactory, compose.OIDCUserinfoVerifiableCredentialFactory, compose.RFC8628DeviceFactory, + compose.RFC8628DeviceAuthorizationTokenFactory, } func NewConfig(deps configDependencies) *Config { 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 c9ca2a2ca07..59ca3fc86e2 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 c9ca2a2ca07..59ca3fc86e2 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 c9ca2a2ca07..59ca3fc86e2 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 c9ca2a2ca07..59ca3fc86e2 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 c9ca2a2ca07..59ca3fc86e2 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 c9ca2a2ca07..59ca3fc86e2 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 03e8881ee72..341c3556d09 100644 --- a/oauth2/.snapshots/TestUnmarshalSession-v1.11.8.json +++ b/oauth2/.snapshots/TestUnmarshalSession-v1.11.8.json @@ -47,5 +47,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 03e8881ee72..341c3556d09 100644 --- a/oauth2/.snapshots/TestUnmarshalSession-v1.11.9.json +++ b/oauth2/.snapshots/TestUnmarshalSession-v1.11.9.json @@ -47,5 +47,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 4608026d74e..8f7f9a13125 100644 --- a/oauth2/fixtures/v1.11.8-session.json +++ b/oauth2/fixtures/v1.11.8-session.json @@ -44,5 +44,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 9636d07b8d6..10bd3ec8d87 100644 --- a/oauth2/fixtures/v1.11.9-session.json +++ b/oauth2/fixtures/v1.11.9-session.json @@ -44,5 +44,6 @@ "market", "zone", "login_session_id" - ] + ], + "browser_flow_completed": false } diff --git a/oauth2/handler.go b/oauth2/handler.go index 1ceb543206f..43095dc0347 100644 --- a/oauth2/handler.go +++ b/oauth2/handler.go @@ -829,11 +829,11 @@ func (h *Handler) oAuth2DeviceFlow(w http.ResponseWriter, r *http.Request) { return } - // TODO: We need to call the consent manager here to create a new loginFlow with the - // 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) @@ -1379,7 +1379,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{}{ @@ -1395,7 +1395,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.SetBrowserFlowCompleted(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 fdd4ba94121..797fd1144f8 100644 --- a/oauth2/oauth2_auth_code_test.go +++ b/oauth2/oauth2_auth_code_test.go @@ -1804,7 +1804,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 0630cb09142..cc067916a34 100644 --- a/oauth2/session.go +++ b/oauth2/session.go @@ -33,6 +33,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:"-"` } @@ -208,3 +209,11 @@ func (s *Session) GetExtraClaims() map[string]interface{} { return s.Extra } + +func (s *Session) GetBrowserFlowCompleted() bool { + return s.BrowserFlowCompleted +} + +func (s *Session) SetBrowserFlowCompleted(flag bool) { + s.BrowserFlowCompleted = flag +} diff --git a/oauth2/session_test.go b/oauth2/session_test.go index 461d753581a..a5094b4d9cd 100644 --- a/oauth2/session_test.go +++ b/oauth2/session_test.go @@ -77,6 +77,7 @@ func TestUnmarshalSession(t *testing.T) { "zone", "login_session_id", }, + BrowserFlowCompleted: false, } t.Run("v1.11.8", func(t *testing.T) { From 46cfe109cc845585641bdbda0f0da4afe89d50eb Mon Sep 17 00:00:00 2001 From: Nikos Date: Thu, 21 Mar 2024 18:05:27 +0200 Subject: [PATCH 18/52] chore: update config --- contrib/quickstart/5-min/hydra.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contrib/quickstart/5-min/hydra.yml b/contrib/quickstart/5-min/hydra.yml index 8d69cc1d243..3becd68594f 100644 --- a/contrib/quickstart/5-min/hydra.yml +++ b/contrib/quickstart/5-min/hydra.yml @@ -8,6 +8,8 @@ urls: consent: http://127.0.0.1:3000/consent login: http://127.0.0.1:3000/login logout: http://127.0.0.1:3000/logout + device_verification: http://127.0.0.1:3000/device_code + post_device_done: http://127.0.0.1:3000/device_complete secrets: system: From 079f568fa9b394a2bea4b1309fa47f86dabb5722 Mon Sep 17 00:00:00 2001 From: dushu Date: Wed, 10 Apr 2024 20:28:42 -0600 Subject: [PATCH 19/52] fix: fix the OIDC token and refresh token issue for device flow --- fositex/config.go | 1 + oauth2/handler.go | 25 +++++--- oauth2/oauth2_device_code_test.go | 91 +++++++++++++++++++++++------ persistence/sql/persister_oauth2.go | 23 ++++---- 4 files changed, 102 insertions(+), 38 deletions(-) diff --git a/fositex/config.go b/fositex/config.go index 35c551c50a6..40efcd33de3 100644 --- a/fositex/config.go +++ b/fositex/config.go @@ -64,6 +64,7 @@ var defaultFactories = []Factory{ compose.OIDCUserinfoVerifiableCredentialFactory, compose.RFC8628DeviceFactory, compose.RFC8628DeviceAuthorizationTokenFactory, + compose.OpenIDConnectDeviceFactory, } func NewConfig(deps configDependencies) *Config { diff --git a/oauth2/handler.go b/oauth2/handler.go index 43095dc0347..68ccc327b5a 100644 --- a/oauth2/handler.go +++ b/oauth2/handler.go @@ -722,16 +722,19 @@ func (h *Handler) getOidcUserInfo(w http.ResponseWriter, r *http.Request) { func (h *Handler) performOAuth2DeviceVerificationFlow(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { ctx := r.Context() - consentSession, flow, err := h.r.ConsentStrategy().HandleOAuth2DeviceAuthorizationRequest(ctx, w, r) + consentSession, f, err := h.r.ConsentStrategy().HandleOAuth2DeviceAuthorizationRequest(ctx, w, r) if errors.Is(err, consent.ErrAbortOAuth2Request) { x.LogAudit(r, nil, h.r.AuditLogger()) - // do nothing return - } else if e := &(fosite.RFC6749Error{}); errors.As(err, &e) { + } + + if e := &(fosite.RFC6749Error{}); errors.As(err, &e) { x.LogAudit(r, err, h.r.AuditLogger()) h.r.Writer().WriteError(w, r, err) return - } else if err != nil { + } + + if err != nil { x.LogError(r, err, h.r.Logger()) h.r.Writer().WriteError(w, r, err) return @@ -739,23 +742,27 @@ func (h *Handler) performOAuth2DeviceVerificationFlow(w http.ResponseWriter, r * req := fosite.NewDeviceRequest() req.Client = consentSession.ConsentRequest.Client - session, err := h.updateSessionWithRequest(ctx, consentSession, flow, r, req) + session, err := h.updateSessionWithRequest(ctx, consentSession, f, r, req) if err != nil { h.r.Writer().WriteError(w, r, err) return } req.SetSession(session) - // We update the device_code session with the claims that the user gave consent for, this - // marks it as ready to be used for the token endpoint - err = h.r.OAuth2Storage().UpdateDeviceCodeSessionByRequestID(ctx, flow.DeviceCodeRequestID.String(), req) + // Update the device code session with + // - the claims for which the user gave consent + // - the granted scopes + // - the granted audiences + // This marks it as ready to be used for the token exchange endpoint. + err = h.r.OAuth2Storage().UpdateDeviceCodeSessionByRequestID(ctx, f.DeviceCodeRequestID.String(), req) if err != nil { x.LogError(r, err, h.r.Logger()) h.r.Writer().WriteError(w, r, err) return } - http.Redirect(w, r, urlx.SetQuery(h.c.DeviceDoneURL(ctx), url.Values{"consent_verifier": {string(flow.ConsentVerifier)}}).String(), http.StatusFound) + redirectURL := urlx.SetQuery(h.c.DeviceDoneURL(ctx), url.Values{"consent_verifier": {string(f.ConsentVerifier)}}).String() + http.Redirect(w, r, redirectURL, http.StatusFound) } // OAuth2 Device Flow diff --git a/oauth2/oauth2_device_code_test.go b/oauth2/oauth2_device_code_test.go index 022b79f45f6..758b916be8a 100644 --- a/oauth2/oauth2_device_code_test.go +++ b/oauth2/oauth2_device_code_test.go @@ -9,6 +9,10 @@ import ( "testing" "time" + "github.com/pborman/uuid" + + "github.com/ory/fosite/token/jwt" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/oauth2" @@ -27,22 +31,26 @@ func TestDeviceAuthRequest(t *testing.T) { reg := internal.NewMockedRegistry(t, &contextx.Default{}) testhelpers.NewOAuth2Server(ctx, t, reg) + secret := uuid.New() c := &client.Client{ - ResponseTypes: []string{"id_token", "code", "token"}, + ID: "device-client", + Secret: secret, GrantTypes: []string{ string(fosite.GrantTypeDeviceCode), }, Scope: "hydra offline openid", Audience: []string{"https://api.ory.sh/"}, - TokenEndpointAuthMethod: "none", + TokenEndpointAuthMethod: "client_secret_post", } require.NoError(t, reg.ClientManager().CreateClient(ctx, c)) oauthClient := &oauth2.Config{ - ClientID: c.GetID(), + ClientID: c.GetID(), + ClientSecret: secret, Endpoint: oauth2.Endpoint{ DeviceAuthURL: reg.Config().OAuth2DeviceAuthorisationURL(ctx).String(), TokenURL: reg.Config().OAuth2TokenURL(ctx).String(), + AuthStyle: oauth2.AuthStyleInParams, }, Scopes: strings.Split(c.Scope, " "), } @@ -71,7 +79,7 @@ func TestDeviceAuthRequest(t *testing.T) { testCase.setUp() } - resp, err := oauthClient.DeviceAuth(context.Background()) + resp, err := oauthClient.DeviceAuth(context.Background(), []oauth2.AuthCodeOption{oauth2.SetAuthURLParam("client_secret", secret)}...) if testCase.check != nil { testCase.check(t, resp, err) @@ -89,44 +97,85 @@ func TestDeviceTokenRequest(t *testing.T) { reg := internal.NewMockedRegistry(t, &contextx.Default{}) testhelpers.NewOAuth2Server(ctx, t, reg) + secret := uuid.New() c := &client.Client{ + ID: "device-client", + Secret: secret, GrantTypes: []string{ string(fosite.GrantTypeDeviceCode), + string(fosite.GrantTypeRefreshToken), }, - Scope: "hydra offline openid", - Audience: []string{"https://api.ory.sh/"}, - TokenEndpointAuthMethod: "none", + Scope: "hydra offline openid", + Audience: []string{"https://api.ory.sh/"}, } require.NoError(t, reg.ClientManager().CreateClient(ctx, c)) oauthClient := &oauth2.Config{ - ClientID: c.GetID(), + ClientID: c.GetID(), + ClientSecret: secret, Endpoint: oauth2.Endpoint{ DeviceAuthURL: reg.Config().OAuth2DeviceAuthorisationURL(ctx).String(), TokenURL: reg.Config().OAuth2TokenURL(ctx).String(), + AuthStyle: oauth2.AuthStyleInHeader, }, 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() + setUp func(signature string) check func(t *testing.T, token *oauth2.Token, err error) cleanUp func() }{ { - description: "should pass", - setUp: func() { + description: "should pass with refresh token", + setUp: func(signature string) { + authreq := &fosite.DeviceRequest{ + Request: fosite.Request{ + Client: &fosite.DefaultClient{ + ID: c.GetID(), + GrantTypes: []string{string(fosite.GrantTypeDeviceCode)}, + }, + RequestedScope: []string{"hydra", "offline"}, + GrantedScope: []string{"hydra", "offline"}, + Session: &hydraoauth2.Session{ + DefaultSession: &openid.DefaultSession{ + Claims: &jwt.IDTokenClaims{ + Subject: "hydra", + }, + 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) + assert.NotEmpty(t, token.RefreshToken) + }, + }, + { + description: "should pass with ID token", + setUp: func(signature string) { authreq := &fosite.DeviceRequest{ Request: fosite.Request{ - Client: &fosite.DefaultClient{ID: c.GetID(), GrantTypes: []string{string(fosite.GrantTypeDeviceCode)}}, + Client: &fosite.DefaultClient{ + ID: c.GetID(), + GrantTypes: []string{string(fosite.GrantTypeDeviceCode)}, + }, + RequestedScope: []string{"hydra", "offline", "openid"}, + GrantedScope: []string{"hydra", "offline", "openid"}, Session: &hydraoauth2.Session{ DefaultSession: &openid.DefaultSession{ + Claims: &jwt.IDTokenClaims{ + Subject: "hydra", + }, ExpiresAt: map[fosite.TokenType]time.Time{ fosite.DeviceCode: time.Now().Add(time.Hour).UTC(), }, @@ -138,17 +187,23 @@ func TestDeviceTokenRequest(t *testing.T) { } require.NoError(t, reg.OAuth2Storage().CreateDeviceCodeSession(context.TODO(), signature, authreq)) + require.NoError(t, reg.OAuth2Storage().CreateOpenIDConnectSession(context.TODO(), signature, authreq)) }, check: func(t *testing.T, token *oauth2.Token, err error) { assert.NotEmpty(t, token.AccessToken) + assert.NotEmpty(t, token.RefreshToken) + assert.NotEmpty(t, token.Extra("id_token")) }, }, } for _, testCase := range testCases { t.Run("case="+testCase.description, func(t *testing.T) { + code, signature, err := reg.RFC8628HMACStrategy().GenerateDeviceCode(context.TODO()) + require.NoError(t, err) + if testCase.setUp != nil { - testCase.setUp() + testCase.setUp(signature) } var token *oauth2.Token diff --git a/persistence/sql/persister_oauth2.go b/persistence/sql/persister_oauth2.go index 39223c58525..e49ca5d82f0 100644 --- a/persistence/sql/persister_oauth2.go +++ b/persistence/sql/persister_oauth2.go @@ -770,20 +770,21 @@ func (p *Persister) UpdateDeviceCodeSessionByRequestID(ctx context.Context, requ req, err := p.sqlSchemaFromRequest(ctx, requestID, requester, sqlTableDeviceCode, requester.GetSession().GetExpiresAt(fosite.DeviceCode).UTC()) if err != nil { - return + return err } - /* #nosec G201 table is static */ - return sqlcon.HandleError( - p.Connection(ctx). - RawQuery( - fmt.Sprintf("UPDATE %s SET session_data=? WHERE request_id=? AND nid = ?", OAuth2RequestSQL{Table: sqlTableDeviceCode}.TableName()), - req.Session, - requestID, - p.NetworkID(ctx), - ). - Exec(), + stmt := fmt.Sprintf( + "UPDATE %s SET granted_scope=?, granted_audience=?, session_data=? WHERE request_id=? AND nid = ?", + OAuth2RequestSQL{Table: sqlTableDeviceCode}.TableName(), ) + + /* #nosec G201 table is static */ + err = p.Connection(ctx).RawQuery(stmt, req.GrantedScope, req.GrantedAudience, req.Session, requestID, p.NetworkID(ctx)).Exec() + if err != nil { + return sqlcon.HandleError(err) + } + + return nil } // GetDeviceCodeSession returns a device code session from the database From fd40c2399d65b479b09b1b3061a4e3f7e413bff0 Mon Sep 17 00:00:00 2001 From: dushu Date: Thu, 11 Apr 2024 21:29:41 -0600 Subject: [PATCH 20/52] fix: update OpenID Connect session after user consent --- oauth2/handler.go | 10 ++++++++++ persistence/sql/persister_oauth2.go | 24 ++++++++++++++++++++++++ x/fosite_storer.go | 2 ++ 3 files changed, 36 insertions(+) diff --git a/oauth2/handler.go b/oauth2/handler.go index 68ccc327b5a..3c00249d717 100644 --- a/oauth2/handler.go +++ b/oauth2/handler.go @@ -761,6 +761,16 @@ func (h *Handler) performOAuth2DeviceVerificationFlow(w http.ResponseWriter, r * return } + // Update the OpenID Connect session if "openid" scope is granted + if req.GetGrantedScopes().Has("openid") { + err = h.r.OAuth2Storage().UpdateOpenIDConnectSessionByRequestID(ctx, f.DeviceCodeRequestID.String(), req) + if err != nil { + x.LogError(r, err, h.r.Logger()) + h.r.Writer().WriteError(w, r, err) + return + } + } + redirectURL := urlx.SetQuery(h.c.DeviceDoneURL(ctx), url.Values{"consent_verifier": {string(f.ConsentVerifier)}}).String() http.Redirect(w, r, redirectURL, http.StatusFound) } diff --git a/persistence/sql/persister_oauth2.go b/persistence/sql/persister_oauth2.go index e49ca5d82f0..2ece9f4671f 100644 --- a/persistence/sql/persister_oauth2.go +++ b/persistence/sql/persister_oauth2.go @@ -533,6 +533,30 @@ func (p *Persister) CreateOpenIDConnectSession(ctx context.Context, signature st return p.createSession(ctx, signature, requester, sqlTableOpenID, requester.GetSession().GetExpiresAt(fosite.AuthorizeCode).UTC()) } +// UpdateOpenIDConnectSessionByRequestID updates an OpenID session by requestID +func (p *Persister) UpdateOpenIDConnectSessionByRequestID(ctx context.Context, requestID string, requester fosite.Requester) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.UpdateOpenIDConnectSessionByRequestID") + defer otelx.End(span, &err) + + req, err := p.sqlSchemaFromRequest(ctx, requestID, requester, sqlTableOpenID, requester.GetSession().GetExpiresAt(fosite.IDToken).UTC()) + if err != nil { + return err + } + + stmt := fmt.Sprintf( + "UPDATE %s SET granted_scope=?, granted_audience=?, session_data=? WHERE request_id=? AND nid = ?", + OAuth2RequestSQL{Table: sqlTableOpenID}.TableName(), + ) + + /* #nosec G201 table is static */ + err = p.Connection(ctx).RawQuery(stmt, req.GrantedScope, req.GrantedAudience, req.Session, requestID, p.NetworkID(ctx)).Exec() + if err != nil { + return sqlcon.HandleError(err) + } + + return nil +} + func (p *Persister) GetOpenIDConnectSession(ctx context.Context, signature string, requester fosite.Requester) (_ fosite.Requester, err error) { ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.GetOpenIDConnectSession") defer otelx.End(span, &err) diff --git a/x/fosite_storer.go b/x/fosite_storer.go index 9d4833ddaa2..91c9ff3e698 100644 --- a/x/fosite_storer.go +++ b/x/fosite_storer.go @@ -40,6 +40,8 @@ type FositeStorer interface { FlushInactiveRefreshTokens(ctx context.Context, notAfter time.Time, limit int, batchSize int) error + UpdateOpenIDConnectSessionByRequestID(ctx context.Context, requestID string, requester fosite.Requester) error + // DeleteOpenIDConnectSession deletes an OpenID Connect session. // This is duplicated from Ory Fosite to help against deprecation linting errors. DeleteOpenIDConnectSession(ctx context.Context, authorizeCode string) error From 23c8dce28da1f82e4da13fe06b293475b3499f42 Mon Sep 17 00:00:00 2001 From: Nikos Date: Mon, 15 Apr 2024 14:02:19 +0300 Subject: [PATCH 21/52] fix: add GetDeviceCodeSessionByRequestID method --- persistence/sql/persister_oauth2.go | 30 +++++++++++++++++++++++++++++ x/fosite_storer.go | 1 + 2 files changed, 31 insertions(+) diff --git a/persistence/sql/persister_oauth2.go b/persistence/sql/persister_oauth2.go index 2ece9f4671f..278a7d46fd5 100644 --- a/persistence/sql/persister_oauth2.go +++ b/persistence/sql/persister_oauth2.go @@ -286,6 +286,29 @@ func (p *Persister) findSessionBySignature(ctx context.Context, signature string return r.toRequest(ctx, session, p) } +func (p *Persister) findSessionByRequestID(ctx context.Context, requestID string, session fosite.Session, table tableName) (fosite.Requester, error) { + r := OAuth2RequestSQL{Table: table} + err := p.QueryWithNetwork(ctx).Where("request_id = ?", requestID).First(&r) + if errors.Is(err, sql.ErrNoRows) { + return nil, errorsx.WithStack(fosite.ErrNotFound) + } + if err != nil { + return nil, sqlcon.HandleError(err) + } + if !r.Active { + fr, err := r.toRequest(ctx, session, p) + if err != nil { + return nil, err + } + if table == sqlTableCode { + return fr, errorsx.WithStack(fosite.ErrInvalidatedAuthorizeCode) + } + return fr, errorsx.WithStack(fosite.ErrInactiveToken) + } + + return r.toRequest(ctx, session, p) +} + func (p *Persister) deleteSessionBySignature(ctx context.Context, signature string, table tableName) error { err := sqlcon.HandleError( p.QueryWithNetwork(ctx). @@ -818,6 +841,13 @@ func (p *Persister) GetDeviceCodeSession(ctx context.Context, signature string, return p.findSessionBySignature(ctx, signature, session, sqlTableDeviceCode) } +// GetDeviceCodeSessionByRequestID returns a device code session from the database +func (p *Persister) GetDeviceCodeSessionByRequestID(ctx context.Context, requestID string, session fosite.Session) (_ fosite.Requester, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.GetDeviceCodeSessionByRequestID") + defer otelx.End(span, &err) + return p.findSessionByRequestID(ctx, requestID, session, sqlTableDeviceCode) +} + // InvalidateDeviceCodeSession invalidates a device code session func (p *Persister) InvalidateDeviceCodeSession(ctx context.Context, signature string) (err error) { ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.InvalidateDeviceCodeSession") diff --git a/x/fosite_storer.go b/x/fosite_storer.go index 91c9ff3e698..d8c2d871b40 100644 --- a/x/fosite_storer.go +++ b/x/fosite_storer.go @@ -46,6 +46,7 @@ type FositeStorer interface { // This is duplicated from Ory Fosite to help against deprecation linting errors. DeleteOpenIDConnectSession(ctx context.Context, authorizeCode string) error + GetDeviceCodeSessionByRequestID(ctx context.Context, requestID string, requester fosite.Session) (fosite.Requester, error) UpdateDeviceCodeSessionByRequestID(ctx context.Context, requestID string, requester fosite.Requester) error UpdateAndInvalidateUserCodeSessionByRequestID(ctx context.Context, signature, request_id string) (err error) } From a9f1dded3c1241349b596e2016dc925ed2c46809 Mon Sep 17 00:00:00 2001 From: Nikos Date: Mon, 15 Apr 2024 14:03:47 +0300 Subject: [PATCH 22/52] fix: return client_id to post_device page --- consent/strategy_oauth_test.go | 18 +++++++++--------- oauth2/handler.go | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/consent/strategy_oauth_test.go b/consent/strategy_oauth_test.go index 02345589e1c..f91e7c7f380 100644 --- a/consent/strategy_oauth_test.go +++ b/consent/strategy_oauth_test.go @@ -1191,19 +1191,19 @@ func TestStrategyDeviceLoginConsent(t *testing.T) { resp, err := hc.Get(devResp.VerificationURIComplete) require.NoError(t, err) require.Contains(t, reg.Config().DeviceDoneURL(ctx).String(), resp.Request.URL.Path, "did not end up in post device URL") + require.Equal(t, resp.Request.URL.Query().Get("client_id"), c.ID) conf := oauth2Config(t, c) - _, err = conf.DeviceAccessToken(ctx, devResp) - // TODO(nsklikas): Uncomment after the token endpoint is implemented - // require.NoError(t, err) + token, err := conf.DeviceAccessToken(ctx, devResp) + require.NoError(t, err) - // claims := testhelpers.IntrospectToken(t, conf, token.AccessToken, adminTS) - // assert.Equal(t, "bar", claims.Get("ext.foo").String(), "%s", claims.Raw) + claims := testhelpers.IntrospectToken(t, conf, token.AccessToken, adminTS) + assert.Equal(t, "bar", claims.Get("ext.foo").String(), "%s", claims.Raw) - // idClaims := testhelpers.DecodeIDToken(t, token) - // assert.Equal(t, "baz", idClaims.Get("bar").String(), "%s", idClaims.Raw) - // sid = idClaims.Get("sid").String() - // assert.NotNil(t, sid) + idClaims := testhelpers.DecodeIDToken(t, token) + assert.Equal(t, "baz", idClaims.Get("bar").String(), "%s", idClaims.Raw) + sid := idClaims.Get("sid").String() + assert.NotNil(t, sid) } t.Run("perform first flow", run) diff --git a/oauth2/handler.go b/oauth2/handler.go index 3c00249d717..cbb72bf5f8f 100644 --- a/oauth2/handler.go +++ b/oauth2/handler.go @@ -771,7 +771,7 @@ func (h *Handler) performOAuth2DeviceVerificationFlow(w http.ResponseWriter, r * } } - redirectURL := urlx.SetQuery(h.c.DeviceDoneURL(ctx), url.Values{"consent_verifier": {string(f.ConsentVerifier)}}).String() + redirectURL := urlx.SetQuery(h.c.DeviceDoneURL(ctx), url.Values{"client_id": {f.Client.GetID()}}).String() http.Redirect(w, r, redirectURL, http.StatusFound) } From e8cc25e6d9b8bfbdd7e431c133b589e72d914f0b Mon Sep 17 00:00:00 2001 From: Nikos Date: Mon, 15 Apr 2024 14:05:46 +0300 Subject: [PATCH 23/52] fix: update existing device session Instead of updating the device session, we were over-writing it causing existing session info that were created from fosite to be lost. --- oauth2/handler.go | 83 ++++++++++++++++++++++++++++------------------- 1 file changed, 49 insertions(+), 34 deletions(-) diff --git a/oauth2/handler.go b/oauth2/handler.go index cbb72bf5f8f..86b8f71b073 100644 --- a/oauth2/handler.go +++ b/oauth2/handler.go @@ -740,13 +740,20 @@ func (h *Handler) performOAuth2DeviceVerificationFlow(w http.ResponseWriter, r * return } - req := fosite.NewDeviceRequest() - req.Client = consentSession.ConsentRequest.Client - session, err := h.updateSessionWithRequest(ctx, consentSession, f, r, req) + // TODO(nsklikas): We need to add a db transaction here + req, err := h.r.OAuth2Storage().GetDeviceCodeSessionByRequestID(ctx, f.DeviceCodeRequestID.String(), &Session{}) if err != nil { + x.LogError(r, err, h.r.Logger()) h.r.Writer().WriteError(w, r, err) return } + // TODO(nsklika): Can we refactor this so we don't have to pass in the session? + session, err := h.updateSessionWithRequest(ctx, consentSession, f, r, req, req.GetSession().(*Session)) + if err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + session.SetBrowserFlowCompleted(true) req.SetSession(session) // Update the device code session with @@ -1262,7 +1269,8 @@ func (h *Handler) oAuth2Authorize(w http.ResponseWriter, r *http.Request, _ http return } - session, err := h.updateSessionWithRequest(ctx, acceptConsentSession, flow, r, authorizeRequest) + authorizeRequest.SetID(acceptConsentSession.ConsentRequestID) + session, err := h.updateSessionWithRequest(ctx, acceptConsentSession, flow, r, authorizeRequest, nil) if err != nil { h.writeAuthorizeError(w, r, authorizeRequest, err) return @@ -1343,12 +1351,19 @@ func (h *Handler) writeAuthorizeError(w http.ResponseWriter, r *http.Request, ar // updateSessionWithRequest takes a session and a fosite.request as input and returns a new session. // If any errors occur, they are logged. -func (h *Handler) updateSessionWithRequest(ctx context.Context, session *flow.AcceptOAuth2ConsentRequest, flow *flow.Flow, r *http.Request, request fosite.Requester) (*Session, error) { - for _, scope := range session.GrantedScope { +func (h *Handler) updateSessionWithRequest( + ctx context.Context, + consent *flow.AcceptOAuth2ConsentRequest, + flow *flow.Flow, + r *http.Request, + request fosite.Requester, + session *Session, +) (*Session, error) { + for _, scope := range consent.GrantedScope { request.GrantScope(scope) } - for _, audience := range session.GrantedAudience { + for _, audience := range consent.GrantedAudience { request.GrantAudience(audience) } @@ -1367,7 +1382,7 @@ func (h *Handler) updateSessionWithRequest(ctx context.Context, session *flow.Ac } } - obfuscatedSubject, err := h.r.ConsentStrategy().ObfuscateSubjectIdentifier(ctx, request.GetClient(), session.ConsentRequest.Subject, session.ConsentRequest.ForceSubjectIdentifier) + obfuscatedSubject, err := h.r.ConsentStrategy().ObfuscateSubjectIdentifier(ctx, request.GetClient(), consent.ConsentRequest.Subject, consent.ConsentRequest.ForceSubjectIdentifier) if e := &(fosite.RFC6749Error{}); errors.As(err, &e) { x.LogAudit(r, err, h.r.AuditLogger()) return nil, err @@ -1376,15 +1391,15 @@ func (h *Handler) updateSessionWithRequest(ctx context.Context, session *flow.Ac return nil, err } - request.SetID(session.ConsentRequestID) + request.SetID(consent.ConsentRequestID) claims := &jwt.IDTokenClaims{ Subject: obfuscatedSubject, Issuer: h.c.IssuerURL(ctx).String(), - AuthTime: time.Time(session.AuthenticatedAt), - RequestedAt: session.RequestedAt, - Extra: session.Session.IDToken, - AuthenticationContextClassReference: session.ConsentRequest.ACR, - AuthenticationMethodsReferences: session.ConsentRequest.AMR, + AuthTime: time.Time(consent.AuthenticatedAt), + RequestedAt: consent.RequestedAt, + Extra: consent.Session.IDToken, + AuthenticationContextClassReference: consent.ConsentRequest.ACR, + AuthenticationMethodsReferences: consent.ConsentRequest.AMR, // These are required for work around https://github.com/ory/fosite/issues/530 Nonce: request.GetRequestForm().Get("nonce"), @@ -1394,31 +1409,31 @@ func (h *Handler) updateSessionWithRequest(ctx context.Context, session *flow.Ac // This is set by the fosite strategy // ExpiresAt: time.Now().Add(h.IDTokenLifespan).UTC(), } - claims.Add("sid", session.ConsentRequest.LoginSessionID) + claims.Add("sid", consent.ConsentRequest.LoginSessionID) - s := &Session{ - DefaultSession: &openid.DefaultSession{ - Claims: claims, - Headers: &jwt.Headers{Extra: map[string]interface{}{ - // required for lookup on jwk endpoint - "kid": openIDKeyID, - }}, - Subject: session.ConsentRequest.Subject, - }, - Extra: session.Session.AccessToken, - KID: accessTokenKeyID, - ClientID: flow.Client.GetID(), - ExcludeNotBeforeClaim: h.c.ExcludeNotBeforeClaim(ctx), - AllowedTopLevelClaims: h.c.AllowedTopLevelClaims(ctx), - MirrorTopLevelClaims: h.c.MirrorTopLevelClaims(ctx), - Flow: flow, + if session == nil { + session = &Session{} } - if _, ok := request.(*fosite.DeviceRequest); ok { - s.SetBrowserFlowCompleted(true) + if session.DefaultSession == nil { + session.DefaultSession = &openid.DefaultSession{} } + session.DefaultSession.Claims = claims + session.DefaultSession.Headers = &jwt.Headers{Extra: map[string]interface{}{ + // required for lookup on jwk endpoint + "kid": openIDKeyID, + }} + session.DefaultSession.Subject = consent.ConsentRequest.Subject + session.Extra = consent.Session.AccessToken + session.KID = accessTokenKeyID + session.ClientID = request.GetClient().GetID() + session.ConsentChallenge = consent.ConsentRequestID + session.ExcludeNotBeforeClaim = h.c.ExcludeNotBeforeClaim(ctx) + session.AllowedTopLevelClaims = h.c.AllowedTopLevelClaims(ctx) + session.MirrorTopLevelClaims = h.c.MirrorTopLevelClaims(ctx) + session.Flow = flow - return s, nil + return session, nil } func (h *Handler) logOrAudit(err error, r *http.Request) { From 5a40ef4991c3b54369bbe22ff631b2b5b80797df Mon Sep 17 00:00:00 2001 From: Nikos Date: Mon, 15 Apr 2024 14:06:49 +0300 Subject: [PATCH 24/52] fix: update tests --- oauth2/oauth2_device_code_test.go | 489 +++++++++++++++++++++++++++++- 1 file changed, 481 insertions(+), 8 deletions(-) diff --git a/oauth2/oauth2_device_code_test.go b/oauth2/oauth2_device_code_test.go index 758b916be8a..d8ed58a3bf3 100644 --- a/oauth2/oauth2_device_code_test.go +++ b/oauth2/oauth2_device_code_test.go @@ -5,6 +5,8 @@ package oauth2_test import ( "context" + "net/http" + "strconv" "strings" "testing" "time" @@ -15,29 +17,32 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" "golang.org/x/oauth2" "github.com/ory/fosite" "github.com/ory/fosite/handler/openid" + hydra "github.com/ory/hydra-client-go/v2" "github.com/ory/hydra/v2/client" - "github.com/ory/hydra/v2/internal" + "github.com/ory/hydra/v2/driver/config" "github.com/ory/hydra/v2/internal/testhelpers" hydraoauth2 "github.com/ory/hydra/v2/oauth2" + "github.com/ory/hydra/v2/x" "github.com/ory/x/contextx" + "github.com/ory/x/pointerx" + "github.com/ory/x/requirex" ) func TestDeviceAuthRequest(t *testing.T) { ctx := context.Background() - reg := internal.NewMockedRegistry(t, &contextx.Default{}) + reg := testhelpers.NewMockedRegistry(t, &contextx.Default{}) testhelpers.NewOAuth2Server(ctx, t, reg) secret := uuid.New() c := &client.Client{ - ID: "device-client", - Secret: secret, - GrantTypes: []string{ - string(fosite.GrantTypeDeviceCode), - }, + ID: "device-client", + Secret: secret, + GrantTypes: []string{"urn:ietf:params:oauth:grant-type:device_code"}, Scope: "hydra offline openid", Audience: []string{"https://api.ory.sh/"}, TokenEndpointAuthMethod: "client_secret_post", @@ -94,7 +99,7 @@ func TestDeviceAuthRequest(t *testing.T) { func TestDeviceTokenRequest(t *testing.T) { ctx := context.Background() - reg := internal.NewMockedRegistry(t, &contextx.Default{}) + reg := testhelpers.NewMockedRegistry(t, &contextx.Default{}) testhelpers.NewOAuth2Server(ctx, t, reg) secret := uuid.New() @@ -219,3 +224,471 @@ func TestDeviceTokenRequest(t *testing.T) { }) } } + +func TestDeviceCodeWithDefaultStrategy(t *testing.T) { + ctx := context.Background() + reg := testhelpers.NewMockedRegistry(t, &contextx.Default{}) + reg.Config().MustSet(ctx, config.KeyAccessTokenStrategy, "opaque") + reg.Config().MustSet(ctx, config.KeyRefreshTokenHook, "") + publicTS, adminTS := testhelpers.NewOAuth2Server(ctx, t, reg) + + publicClient := hydra.NewAPIClient(hydra.NewConfiguration()) + publicClient.GetConfig().Servers = hydra.ServerConfigurations{{URL: publicTS.URL}} + adminClient := hydra.NewAPIClient(hydra.NewConfiguration()) + adminClient.GetConfig().Servers = hydra.ServerConfigurations{{URL: adminTS.URL}} + + getDeviceCode := func(t *testing.T, conf *oauth2.Config, c *http.Client, params ...oauth2.AuthCodeOption) (*oauth2.DeviceAuthResponse, error) { + if c == nil { + c = testhelpers.NewEmptyJarClient(t) + } + + return conf.DeviceAuth(ctx, params...) + } + + acceptUserCode := func(t *testing.T, conf *oauth2.Config, c *http.Client, devResp *oauth2.DeviceAuthResponse) *http.Response { + if c == nil { + c = testhelpers.NewEmptyJarClient(t) + } + + resp, err := c.Get(devResp.VerificationURIComplete) + require.NoError(t, err) + require.Contains(t, reg.Config().DeviceDoneURL(ctx).String(), resp.Request.URL.Path, "did not end up in post device URL") + require.Equal(t, resp.Request.URL.Query().Get("client_id"), conf.ClientID) + + return resp + } + + acceptDeviceHandler := func(t *testing.T, c *client.Client) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + userCode := r.URL.Query().Get("user_code") + payload := hydra.AcceptDeviceUserCodeRequest{ + UserCode: &userCode, + } + + v, _, err := adminClient.OAuth2API.AcceptUserCodeRequest(context.Background()). + DeviceChallenge(r.URL.Query().Get("device_challenge")). + AcceptDeviceUserCodeRequest(payload). + Execute() + require.NoError(t, err) + require.NotEmpty(t, v.RedirectTo) + http.Redirect(w, r, v.RedirectTo, http.StatusFound) + } + } + + acceptLoginHandler := func(t *testing.T, c *client.Client, subject string, checkRequestPayload func(request *hydra.OAuth2LoginRequest) *hydra.AcceptOAuth2LoginRequest) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + rr, _, err := adminClient.OAuth2API.GetOAuth2LoginRequest(context.Background()).LoginChallenge(r.URL.Query().Get("login_challenge")).Execute() + require.NoError(t, err) + + assert.EqualValues(t, c.GetID(), pointerx.Deref(rr.Client.ClientId)) + assert.Empty(t, pointerx.Deref(rr.Client.ClientSecret)) + assert.EqualValues(t, c.GrantTypes, rr.Client.GrantTypes) + assert.EqualValues(t, c.LogoURI, pointerx.Deref(rr.Client.LogoUri)) + assert.EqualValues(t, r.URL.Query().Get("login_challenge"), rr.Challenge) + assert.EqualValues(t, []string{"hydra", "offline", "openid"}, rr.RequestedScope) + assert.Contains(t, rr.RequestUrl, hydraoauth2.DeviceVerificationPath) + + acceptBody := hydra.AcceptOAuth2LoginRequest{ + Subject: subject, + Remember: pointerx.Ptr(!rr.Skip), + Acr: pointerx.Ptr("1"), + Amr: []string{"pwd"}, + Context: map[string]interface{}{"context": "bar"}, + } + if checkRequestPayload != nil { + if b := checkRequestPayload(rr); b != nil { + acceptBody = *b + } + } + + v, _, err := adminClient.OAuth2API.AcceptOAuth2LoginRequest(context.Background()). + LoginChallenge(r.URL.Query().Get("login_challenge")). + AcceptOAuth2LoginRequest(acceptBody). + Execute() + require.NoError(t, err) + require.NotEmpty(t, v.RedirectTo) + http.Redirect(w, r, v.RedirectTo, http.StatusFound) + } + } + + acceptConsentHandler := func(t *testing.T, c *client.Client, subject string, checkRequestPayload func(*hydra.OAuth2ConsentRequest)) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + rr, _, err := adminClient.OAuth2API.GetOAuth2ConsentRequest(context.Background()).ConsentChallenge(r.URL.Query().Get("consent_challenge")).Execute() + require.NoError(t, err) + + assert.EqualValues(t, c.GetID(), pointerx.Deref(rr.Client.ClientId)) + assert.Empty(t, pointerx.Deref(rr.Client.ClientSecret)) + assert.EqualValues(t, c.GrantTypes, rr.Client.GrantTypes) + assert.EqualValues(t, c.LogoURI, pointerx.Deref(rr.Client.LogoUri)) + assert.EqualValues(t, subject, pointerx.Deref(rr.Subject)) + assert.EqualValues(t, []string{"hydra", "offline", "openid"}, rr.RequestedScope) + assert.EqualValues(t, r.URL.Query().Get("consent_challenge"), rr.Challenge) + assert.Contains(t, *rr.RequestUrl, hydraoauth2.DeviceVerificationPath) + if checkRequestPayload != nil { + checkRequestPayload(rr) + } + + assert.Equal(t, map[string]interface{}{"context": "bar"}, rr.Context) + v, _, err := adminClient.OAuth2API.AcceptOAuth2ConsentRequest(context.Background()). + ConsentChallenge(r.URL.Query().Get("consent_challenge")). + AcceptOAuth2ConsentRequest(hydra.AcceptOAuth2ConsentRequest{ + GrantScope: []string{"hydra", "offline", "openid"}, Remember: pointerx.Ptr(true), RememberFor: pointerx.Ptr[int64](0), + GrantAccessTokenAudience: rr.RequestedAccessTokenAudience, + Session: &hydra.AcceptOAuth2ConsentRequestSession{ + AccessToken: map[string]interface{}{"foo": "bar"}, + IdToken: map[string]interface{}{"bar": "baz"}, + }, + }). + Execute() + require.NoError(t, err) + require.NotEmpty(t, v.RedirectTo) + http.Redirect(w, r, v.RedirectTo, http.StatusFound) + } + } + + assertRefreshToken := func(t *testing.T, token *oauth2.Token, c *oauth2.Config, expectedExp time.Time) { + actualExp, err := strconv.ParseInt(testhelpers.IntrospectToken(t, c, token.RefreshToken, adminTS).Get("exp").String(), 10, 64) + require.NoError(t, err) + requirex.EqualTime(t, expectedExp, time.Unix(actualExp, 0), time.Second) + } + + assertIDToken := func(t *testing.T, token *oauth2.Token, c *oauth2.Config, expectedSubject, expectedNonce string, expectedExp time.Time) gjson.Result { + idt, ok := token.Extra("id_token").(string) + require.True(t, ok) + assert.NotEmpty(t, idt) + + body, err := x.DecodeSegment(strings.Split(idt, ".")[1]) + require.NoError(t, err) + + claims := gjson.ParseBytes(body) + assert.True(t, time.Now().After(time.Unix(claims.Get("iat").Int(), 0)), "%s", claims) + assert.True(t, time.Now().After(time.Unix(claims.Get("nbf").Int(), 0)), "%s", claims) + assert.True(t, time.Now().Before(time.Unix(claims.Get("exp").Int(), 0)), "%s", claims) + requirex.EqualTime(t, expectedExp, time.Unix(claims.Get("exp").Int(), 0), 2*time.Second) + assert.NotEmpty(t, claims.Get("jti").String(), "%s", claims) + assert.EqualValues(t, reg.Config().IssuerURL(ctx).String(), claims.Get("iss").String(), "%s", claims) + assert.NotEmpty(t, claims.Get("sid").String(), "%s", claims) + assert.Equal(t, "1", claims.Get("acr").String(), "%s", claims) + require.Len(t, claims.Get("amr").Array(), 1, "%s", claims) + assert.EqualValues(t, "pwd", claims.Get("amr").Array()[0].String(), "%s", claims) + + require.Len(t, claims.Get("aud").Array(), 1, "%s", claims) + assert.EqualValues(t, c.ClientID, claims.Get("aud").Array()[0].String(), "%s", claims) + assert.EqualValues(t, expectedSubject, claims.Get("sub").String(), "%s", claims) + assert.EqualValues(t, `baz`, claims.Get("bar").String(), "%s", claims) + + return claims + } + + introspectAccessToken := func(t *testing.T, conf *oauth2.Config, token *oauth2.Token, expectedSubject string) gjson.Result { + require.NotEmpty(t, token.AccessToken) + i := testhelpers.IntrospectToken(t, conf, token.AccessToken, adminTS) + assert.True(t, i.Get("active").Bool(), "%s", i) + assert.EqualValues(t, conf.ClientID, i.Get("client_id").String(), "%s", i) + assert.EqualValues(t, expectedSubject, i.Get("sub").String(), "%s", i) + assert.EqualValues(t, `bar`, i.Get("ext.foo").String(), "%s", i) + return i + } + + assertJWTAccessToken := func(t *testing.T, strat string, conf *oauth2.Config, token *oauth2.Token, expectedSubject string, expectedExp time.Time, scopes string) gjson.Result { + require.NotEmpty(t, token.AccessToken) + parts := strings.Split(token.AccessToken, ".") + if strat != "jwt" { + require.Len(t, parts, 2) + return gjson.Parse("null") + } + require.Len(t, parts, 3) + + body, err := x.DecodeSegment(parts[1]) + require.NoError(t, err) + + i := gjson.ParseBytes(body) + assert.NotEmpty(t, i.Get("jti").String()) + assert.EqualValues(t, conf.ClientID, i.Get("client_id").String(), "%s", i) + assert.EqualValues(t, expectedSubject, i.Get("sub").String(), "%s", i) + assert.EqualValues(t, reg.Config().IssuerURL(ctx).String(), i.Get("iss").String(), "%s", i) + assert.True(t, time.Now().After(time.Unix(i.Get("iat").Int(), 0)), "%s", i) + assert.True(t, time.Now().After(time.Unix(i.Get("nbf").Int(), 0)), "%s", i) + assert.True(t, time.Now().Before(time.Unix(i.Get("exp").Int(), 0)), "%s", i) + requirex.EqualTime(t, expectedExp, time.Unix(i.Get("exp").Int(), 0), time.Second) + assert.EqualValues(t, `bar`, i.Get("ext.foo").String(), "%s", i) + assert.EqualValues(t, scopes, i.Get("scp").Raw, "%s", i) + return i + } + + waitForRefreshTokenExpiry := func() { + time.Sleep(reg.Config().GetRefreshTokenLifespan(ctx) + time.Second) + } + + t.Run("case=checks if request fails when audience does not match", func(t *testing.T) { + testhelpers.NewLoginConsentUI(t, reg.Config(), testhelpers.HTTPServerNoExpectedCallHandler(t), testhelpers.HTTPServerNoExpectedCallHandler(t)) + _, conf := newDeviceClient(t, reg) + resp, err := getDeviceCode(t, conf, nil, oauth2.SetAuthURLParam("audience", "https://not-ory-api/")) + require.Error(t, err) + devErr := err.(*oauth2.RetrieveError) + require.Nil(t, resp) + require.Equal(t, devErr.Response.StatusCode, http.StatusBadRequest) + }) + + subject := "aeneas-rekkas" + nonce := uuid.New() + t.Run("case=perform device flow with ID token and refresh tokens", func(t *testing.T) { + run := func(t *testing.T, strategy string) { + c, conf := newDeviceClient(t, reg) + testhelpers.NewDeviceLoginConsentUI(t, reg.Config(), + acceptDeviceHandler(t, c), + acceptLoginHandler(t, c, subject, nil), + acceptConsentHandler(t, c, subject, nil), + ) + + resp, err := getDeviceCode(t, conf, nil) + require.NoError(t, err) + require.NotEmpty(t, resp.DeviceCode) + require.NotEmpty(t, resp.UserCode) + loginFlowResp := acceptUserCode(t, conf, nil, resp) + require.NotNil(t, loginFlowResp) + token, err := conf.DeviceAccessToken(context.Background(), resp) + iat := time.Now() + require.NoError(t, err) + + assert.Empty(t, token.Extra("c_nonce_draft_00"), "should not be set if not requested") + assert.Empty(t, token.Extra("c_nonce_expires_in_draft_00"), "should not be set if not requested") + introspectAccessToken(t, conf, token, subject) + assertJWTAccessToken(t, strategy, conf, token, subject, iat.Add(reg.Config().GetAccessTokenLifespan(ctx)), `["hydra","offline","openid"]`) + assertIDToken(t, token, conf, subject, nonce, iat.Add(reg.Config().GetIDTokenLifespan(ctx))) + assertRefreshToken(t, token, conf, iat.Add(reg.Config().GetRefreshTokenLifespan(ctx))) + + t.Run("followup=successfully perform refresh token flow", func(t *testing.T) { + require.NotEmpty(t, token.RefreshToken) + token.Expiry = token.Expiry.Add(-time.Hour * 24) + iat = time.Now() + refreshedToken, err := conf.TokenSource(context.Background(), token).Token() + require.NoError(t, err) + + require.NotEqual(t, token.AccessToken, refreshedToken.AccessToken) + require.NotEqual(t, token.RefreshToken, refreshedToken.RefreshToken) + require.NotEqual(t, token.Extra("id_token"), refreshedToken.Extra("id_token")) + introspectAccessToken(t, conf, refreshedToken, subject) + + t.Run("followup=refreshed tokens contain valid tokens", func(t *testing.T) { + assertJWTAccessToken(t, strategy, conf, refreshedToken, subject, iat.Add(reg.Config().GetAccessTokenLifespan(ctx)), `["hydra","offline","openid"]`) + assertIDToken(t, refreshedToken, conf, subject, nonce, iat.Add(reg.Config().GetIDTokenLifespan(ctx))) + assertRefreshToken(t, refreshedToken, conf, iat.Add(reg.Config().GetRefreshTokenLifespan(ctx))) + }) + + t.Run("followup=original access token is no longer valid", func(t *testing.T) { + i := testhelpers.IntrospectToken(t, conf, token.AccessToken, adminTS) + assert.False(t, i.Get("active").Bool(), "%s", i) + }) + + t.Run("followup=original refresh token is no longer valid", func(t *testing.T) { + _, err := conf.TokenSource(context.Background(), token).Token() + assert.Error(t, err) + }) + + t.Run("followup=but fail subsequent refresh because expiry was reached", func(t *testing.T) { + waitForRefreshTokenExpiry() + + // Force golang to refresh token + refreshedToken.Expiry = refreshedToken.Expiry.Add(-time.Hour * 24) + _, err := conf.TokenSource(context.Background(), refreshedToken).Token() + require.Error(t, err) + }) + }) + } + + t.Run("strategy=jwt", func(t *testing.T) { + reg.Config().MustSet(ctx, config.KeyAccessTokenStrategy, "jwt") + run(t, "jwt") + }) + + t.Run("strategy=opaque", func(t *testing.T) { + reg.Config().MustSet(ctx, config.KeyAccessTokenStrategy, "opaque") + run(t, "opaque") + }) + }) + t.Run("case=perform flow with audience", func(t *testing.T) { + expectAud := "https://api.ory.sh/" + c, conf := newDeviceClient(t, reg) + testhelpers.NewDeviceLoginConsentUI( + t, + reg.Config(), + acceptDeviceHandler(t, c), + acceptLoginHandler(t, c, subject, func(r *hydra.OAuth2LoginRequest) *hydra.AcceptOAuth2LoginRequest { + assert.False(t, r.Skip) + assert.EqualValues(t, []string{expectAud}, r.RequestedAccessTokenAudience) + return nil + }), + acceptConsentHandler(t, c, subject, func(r *hydra.OAuth2ConsentRequest) { + assert.False(t, *r.Skip) + assert.EqualValues(t, []string{expectAud}, r.RequestedAccessTokenAudience) + }), + ) + + resp, err := getDeviceCode(t, conf, nil, oauth2.SetAuthURLParam("audience", "https://api.ory.sh/")) + require.NoError(t, err) + require.NotEmpty(t, resp.DeviceCode) + require.NotEmpty(t, resp.UserCode) + loginFlowResp := acceptUserCode(t, conf, nil, resp) + require.NotNil(t, loginFlowResp) + + token, err := conf.DeviceAccessToken(context.Background(), resp) + require.NoError(t, err) + + claims := introspectAccessToken(t, conf, token, subject) + aud := claims.Get("aud").Array() + require.Len(t, aud, 1) + assert.EqualValues(t, aud[0].String(), expectAud) + + assertIDToken(t, token, conf, subject, nonce, time.Now().Add(reg.Config().GetIDTokenLifespan(ctx))) + }) + + t.Run("case=respects client token lifespan configuration", func(t *testing.T) { + run := func(t *testing.T, strategy string, c *client.Client, conf *oauth2.Config, expectedLifespans client.Lifespans) { + testhelpers.NewDeviceLoginConsentUI( + t, + reg.Config(), + acceptDeviceHandler(t, c), + acceptLoginHandler(t, c, subject, nil), + acceptConsentHandler(t, c, subject, nil), + ) + + resp, err := getDeviceCode(t, conf, nil) + require.NoError(t, err) + require.NotEmpty(t, resp.DeviceCode) + require.NotEmpty(t, resp.UserCode) + loginFlowResp := acceptUserCode(t, conf, nil, resp) + require.NotNil(t, loginFlowResp) + + token, err := conf.DeviceAccessToken(context.Background(), resp) + iat := time.Now() + require.NoError(t, err) + + body := introspectAccessToken(t, conf, token, subject) + requirex.EqualTime(t, iat.Add(expectedLifespans.DeviceAuthorizationGrantAccessTokenLifespan.Duration), time.Unix(body.Get("exp").Int(), 0), time.Second) + + assertJWTAccessToken(t, strategy, conf, token, subject, iat.Add(expectedLifespans.DeviceAuthorizationGrantAccessTokenLifespan.Duration), `["hydra","offline","openid"]`) + assertIDToken(t, token, conf, subject, nonce, iat.Add(expectedLifespans.DeviceAuthorizationGrantIDTokenLifespan.Duration)) + assertRefreshToken(t, token, conf, iat.Add(expectedLifespans.DeviceAuthorizationGrantRefreshTokenLifespan.Duration)) + + t.Run("followup=successfully perform refresh token flow", func(t *testing.T) { + require.NotEmpty(t, token.RefreshToken) + token.Expiry = token.Expiry.Add(-time.Hour * 24) + refreshedToken, err := conf.TokenSource(context.Background(), token).Token() + iat = time.Now() + require.NoError(t, err) + assertRefreshToken(t, refreshedToken, conf, iat.Add(expectedLifespans.RefreshTokenGrantRefreshTokenLifespan.Duration)) + assertJWTAccessToken(t, strategy, conf, refreshedToken, subject, iat.Add(expectedLifespans.RefreshTokenGrantAccessTokenLifespan.Duration), `["hydra","offline","openid"]`) + assertIDToken(t, refreshedToken, conf, subject, nonce, iat.Add(expectedLifespans.RefreshTokenGrantIDTokenLifespan.Duration)) + + require.NotEqual(t, token.AccessToken, refreshedToken.AccessToken) + require.NotEqual(t, token.RefreshToken, refreshedToken.RefreshToken) + require.NotEqual(t, token.Extra("id_token"), refreshedToken.Extra("id_token")) + + body := introspectAccessToken(t, conf, refreshedToken, subject) + requirex.EqualTime(t, iat.Add(expectedLifespans.RefreshTokenGrantAccessTokenLifespan.Duration), time.Unix(body.Get("exp").Int(), 0), time.Second) + + t.Run("followup=original access token is no longer valid", func(t *testing.T) { + i := testhelpers.IntrospectToken(t, conf, token.AccessToken, adminTS) + assert.False(t, i.Get("active").Bool(), "%s", i) + }) + + t.Run("followup=original refresh token is no longer valid", func(t *testing.T) { + _, err := conf.TokenSource(context.Background(), token).Token() + assert.Error(t, err) + }) + }) + } + + t.Run("case=custom-lifespans-active-jwt", func(t *testing.T) { + c, conf := newDeviceClient(t, reg) + ls := testhelpers.TestLifespans + ls.DeviceAuthorizationGrantAccessTokenLifespan = x.NullDuration{Valid: true, Duration: 6 * time.Second} + testhelpers.UpdateClientTokenLifespans( + t, + &oauth2.Config{ClientID: c.GetID(), ClientSecret: conf.ClientSecret}, + c.GetID(), + ls, adminTS, + ) + reg.Config().MustSet(ctx, config.KeyAccessTokenStrategy, "jwt") + run(t, "jwt", c, conf, ls) + }) + + t.Run("case=custom-lifespans-active-opaque", func(t *testing.T) { + c, conf := newDeviceClient(t, reg) + ls := testhelpers.TestLifespans + ls.DeviceAuthorizationGrantAccessTokenLifespan = x.NullDuration{Valid: true, Duration: 6 * time.Second} + testhelpers.UpdateClientTokenLifespans( + t, + &oauth2.Config{ClientID: c.GetID(), ClientSecret: conf.ClientSecret}, + c.GetID(), + ls, adminTS, + ) + reg.Config().MustSet(ctx, config.KeyAccessTokenStrategy, "opaque") + run(t, "opaque", c, conf, ls) + }) + + t.Run("case=custom-lifespans-unset", func(t *testing.T) { + c, conf := newDeviceClient(t, reg) + testhelpers.UpdateClientTokenLifespans(t, &oauth2.Config{ClientID: c.GetID(), ClientSecret: conf.ClientSecret}, c.GetID(), testhelpers.TestLifespans, adminTS) + testhelpers.UpdateClientTokenLifespans(t, &oauth2.Config{ClientID: c.GetID(), ClientSecret: conf.ClientSecret}, c.GetID(), client.Lifespans{}, adminTS) + reg.Config().MustSet(ctx, config.KeyAccessTokenStrategy, "opaque") + + //goland:noinspection GoDeprecation + expectedLifespans := client.Lifespans{ + AuthorizationCodeGrantAccessTokenLifespan: x.NullDuration{Valid: true, Duration: reg.Config().GetAccessTokenLifespan(ctx)}, + AuthorizationCodeGrantIDTokenLifespan: x.NullDuration{Valid: true, Duration: reg.Config().GetIDTokenLifespan(ctx)}, + AuthorizationCodeGrantRefreshTokenLifespan: x.NullDuration{Valid: true, Duration: reg.Config().GetRefreshTokenLifespan(ctx)}, + ClientCredentialsGrantAccessTokenLifespan: x.NullDuration{Valid: true, Duration: reg.Config().GetAccessTokenLifespan(ctx)}, + ImplicitGrantAccessTokenLifespan: x.NullDuration{Valid: true, Duration: reg.Config().GetAccessTokenLifespan(ctx)}, + ImplicitGrantIDTokenLifespan: x.NullDuration{Valid: true, Duration: reg.Config().GetIDTokenLifespan(ctx)}, + JwtBearerGrantAccessTokenLifespan: x.NullDuration{Valid: true, Duration: reg.Config().GetAccessTokenLifespan(ctx)}, + PasswordGrantAccessTokenLifespan: x.NullDuration{Valid: true, Duration: reg.Config().GetAccessTokenLifespan(ctx)}, + PasswordGrantRefreshTokenLifespan: x.NullDuration{Valid: true, Duration: reg.Config().GetRefreshTokenLifespan(ctx)}, + RefreshTokenGrantIDTokenLifespan: x.NullDuration{Valid: true, Duration: reg.Config().GetIDTokenLifespan(ctx)}, + RefreshTokenGrantAccessTokenLifespan: x.NullDuration{Valid: true, Duration: reg.Config().GetAccessTokenLifespan(ctx)}, + RefreshTokenGrantRefreshTokenLifespan: x.NullDuration{Valid: true, Duration: reg.Config().GetRefreshTokenLifespan(ctx)}, + DeviceAuthorizationGrantIDTokenLifespan: x.NullDuration{Valid: true, Duration: reg.Config().GetIDTokenLifespan(ctx)}, + DeviceAuthorizationGrantAccessTokenLifespan: x.NullDuration{Valid: true, Duration: reg.Config().GetAccessTokenLifespan(ctx)}, + DeviceAuthorizationGrantRefreshTokenLifespan: x.NullDuration{Valid: true, Duration: reg.Config().GetRefreshTokenLifespan(ctx)}, + } + run(t, "opaque", c, conf, expectedLifespans) + }) + }) +} + +func newDeviceClient( + t *testing.T, + reg interface { + config.Provider + client.Registry + }, + opts ...func(*client.Client), +) (*client.Client, *oauth2.Config) { + ctx := context.Background() + c := &client.Client{ + GrantTypes: []string{ + "refresh_token", + "urn:ietf:params:oauth:grant-type:device_code", + }, + Scope: "hydra offline openid", + Audience: []string{"https://api.ory.sh/"}, + TokenEndpointAuthMethod: "none", + } + + // apply options + for _, o := range opts { + o(c) + } + + require.NoError(t, reg.ClientManager().CreateClient(ctx, c)) + return c, &oauth2.Config{ + ClientID: c.GetID(), + Endpoint: oauth2.Endpoint{ + DeviceAuthURL: reg.Config().OAuth2DeviceAuthorisationURL(ctx).String(), + TokenURL: reg.Config().OAuth2TokenURL(ctx).String(), + AuthStyle: oauth2.AuthStyleInHeader, + }, + Scopes: strings.Split(c.Scope, " "), + } +} From b558b493eef660c57cc0f57f02db874304861cf7 Mon Sep 17 00:00:00 2001 From: Nikos Date: Tue, 23 Apr 2024 11:05:09 +0300 Subject: [PATCH 25/52] fix: add device auth endpoint in discovery metadata --- .../TestHandlerWellKnown-hsm_enabled=false.json | 4 +++- .../TestHandlerWellKnown-hsm_enabled=true.json | 4 +++- oauth2/handler.go | 13 ++++++++++--- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/oauth2/.snapshots/TestHandlerWellKnown-hsm_enabled=false.json b/oauth2/.snapshots/TestHandlerWellKnown-hsm_enabled=false.json index 5bc92ec79a5..177300163a0 100644 --- a/oauth2/.snapshots/TestHandlerWellKnown-hsm_enabled=false.json +++ b/oauth2/.snapshots/TestHandlerWellKnown-hsm_enabled=false.json @@ -35,6 +35,7 @@ ] } ], + "device_authorization_endpoint": "http://hydra.localhost/oauth2/device/auth", "end_session_endpoint": "http://hydra.localhost/oauth2/sessions/logout", "frontchannel_logout_session_supported": true, "frontchannel_logout_supported": true, @@ -42,7 +43,8 @@ "authorization_code", "implicit", "client_credentials", - "refresh_token" + "refresh_token", + "urn:ietf:params:oauth:grant-type:device_code" ], "id_token_signed_response_alg": [ "RS256" diff --git a/oauth2/.snapshots/TestHandlerWellKnown-hsm_enabled=true.json b/oauth2/.snapshots/TestHandlerWellKnown-hsm_enabled=true.json index 5bc92ec79a5..177300163a0 100644 --- a/oauth2/.snapshots/TestHandlerWellKnown-hsm_enabled=true.json +++ b/oauth2/.snapshots/TestHandlerWellKnown-hsm_enabled=true.json @@ -35,6 +35,7 @@ ] } ], + "device_authorization_endpoint": "http://hydra.localhost/oauth2/device/auth", "end_session_endpoint": "http://hydra.localhost/oauth2/sessions/logout", "frontchannel_logout_session_supported": true, "frontchannel_logout_supported": true, @@ -42,7 +43,8 @@ "authorization_code", "implicit", "client_credentials", - "refresh_token" + "refresh_token", + "urn:ietf:params:oauth:grant-type:device_code" ], "id_token_signed_response_alg": [ "RS256" diff --git a/oauth2/handler.go b/oauth2/handler.go index 86b8f71b073..433ad897891 100644 --- a/oauth2/handler.go +++ b/oauth2/handler.go @@ -262,6 +262,12 @@ type oidcConfiguration struct { // example: https://playground.ory.sh/ory-hydra/public/oauth2/auth AuthURL string `json:"authorization_endpoint"` + // OAuth 2.0 Device Authorization Endpoint URL + // + // required: true + // example: https://playground.ory.sh/ory-hydra/public/oauth2/device/oauth + DeviceAuthorizationURL string `json:"device_authorization_endpoint"` + // OpenID Connect Dynamic Client Registration Endpoint URL // // example: https://playground.ory.sh/ory-hydra/admin/client @@ -499,6 +505,7 @@ func (h *Handler) discoverOidcConfiguration(w http.ResponseWriter, r *http.Reque h.r.Writer().Write(w, r, &oidcConfiguration{ Issuer: h.c.IssuerURL(ctx).String(), AuthURL: h.c.OAuth2AuthURL(ctx).String(), + DeviceAuthorizationURL: h.c.OAuth2DeviceAuthorisationURL(ctx).String(), TokenURL: h.c.OAuth2TokenURL(ctx).String(), JWKsURI: h.c.JWKSURL(ctx).String(), RevocationEndpoint: urlx.AppendPaths(h.c.IssuerURL(ctx), RevocationPath).String(), @@ -512,7 +519,7 @@ func (h *Handler) discoverOidcConfiguration(w http.ResponseWriter, r *http.Reque IDTokenSigningAlgValuesSupported: []string{key.Algorithm}, IDTokenSignedResponseAlg: []string{key.Algorithm}, UserinfoSignedResponseAlg: []string{key.Algorithm}, - GrantTypesSupported: []string{"authorization_code", "implicit", "client_credentials", "refresh_token"}, + GrantTypesSupported: []string{"authorization_code", "implicit", "client_credentials", "refresh_token", "urn:ietf:params:oauth:grant-type:device_code"}, ResponseModesSupported: []string{"query", "fragment", "form_post"}, UserinfoSigningAlgValuesSupported: []string{"none", key.Algorithm}, RequestParameterSupported: true, @@ -705,7 +712,7 @@ func (h *Handler) getOidcUserInfo(w http.ResponseWriter, r *http.Request) { } } -// swagger:route GET /oauth2/device/verify oauth performOAuth2DeviceVerificationFlow +// swagger:route GET /oauth2/device/verify oAuth2 performOAuth2DeviceVerificationFlow // // # OAuth 2.0 Device Verification Endpoint // @@ -827,7 +834,7 @@ type deviceAuthorization struct { Interval int `json:"interval"` } -// swagger:route POST /oauth2/device/auth oauth oAuth2DeviceFlow +// swagger:route POST /oauth2/device/auth oAuth2 oAuth2DeviceFlow // // # The OAuth 2.0 Device Authorize Endpoint // From 989d9ec4f456691d39cae627464cec4c81fb5bb0 Mon Sep 17 00:00:00 2001 From: Nikos Date: Thu, 25 Apr 2024 17:58:00 +0300 Subject: [PATCH 26/52] fix: make device grant lifetimes configurable --- ...ion=basic_dynamic_client_registration.json | 5 +++- ...-description=basic_admin_registration.json | 5 +++- ...case=10-description=empty_ID_succeeds.json | 5 +++- ...nsent_succeeds_for_admin_registration.json | 5 +++- ...case=12-description=empty_ID_succeeds.json | 5 +++- ...-case=2-description=empty_ID_succeeds.json | 5 +++- ...nts-case=4-description=non-uuid_works.json | 5 +++- ...ption=setting_client_id_as_uuid_works.json | 5 +++- ...onsent_suceeds_for_admin_registration.json | 5 +++- ...-case=8-description=empty_ID_succeeds.json | 5 +++- ...nsent_succeeds_for_admin_registration.json | 5 +++- ...onsent_suceeds_for_admin_registration.json | 5 +++- ...-case=9-description=empty_ID_succeeds.json | 5 +++- ...tching_existing_client-endpoint=admin.json | 5 +++- ..._existing_client-endpoint=selfservice.json | 5 +++- ...ate_the_lifespans_of_an_OAuth2_client.json | 5 +++- ...dating_existing_client-endpoint=admin.json | 5 +++- ...-endpoint=dynamic_client_registration.json | 5 +++- ...gistration_tokens-case=0-dynamic=true.json | 5 +++- ...istration_tokens-case=1-dynamic=false.json | 5 +++- ...istration_tokens-case=2-dynamic=false.json | 5 +++- client/client.go | 23 ++++++++++++++++ internal/testhelpers/lifespans.go | 27 ++++++++++--------- 23 files changed, 122 insertions(+), 33 deletions(-) diff --git a/client/.snapshots/TestHandler-common-case=create_clients-case=0-description=basic_dynamic_client_registration.json b/client/.snapshots/TestHandler-common-case=create_clients-case=0-description=basic_dynamic_client_registration.json index 15cff8f77d2..a9ac8197dff 100644 --- a/client/.snapshots/TestHandler-common-case=create_clients-case=0-description=basic_dynamic_client_registration.json +++ b/client/.snapshots/TestHandler-common-case=create_clients-case=0-description=basic_dynamic_client_registration.json @@ -31,5 +31,8 @@ "jwt_bearer_grant_access_token_lifespan": null, "refresh_token_grant_id_token_lifespan": null, "refresh_token_grant_access_token_lifespan": null, - "refresh_token_grant_refresh_token_lifespan": null + "refresh_token_grant_refresh_token_lifespan": null, + "device_authorization_grant_id_token_lifespan": null, + "device_authorization_grant_access_token_lifespan": null, + "device_authorization_grant_refresh_token_lifespan": null } diff --git a/client/.snapshots/TestHandler-common-case=create_clients-case=1-description=basic_admin_registration.json b/client/.snapshots/TestHandler-common-case=create_clients-case=1-description=basic_admin_registration.json index 7956cbdb0bb..75972d053bb 100644 --- a/client/.snapshots/TestHandler-common-case=create_clients-case=1-description=basic_admin_registration.json +++ b/client/.snapshots/TestHandler-common-case=create_clients-case=1-description=basic_admin_registration.json @@ -34,5 +34,8 @@ "jwt_bearer_grant_access_token_lifespan": null, "refresh_token_grant_id_token_lifespan": null, "refresh_token_grant_access_token_lifespan": null, - "refresh_token_grant_refresh_token_lifespan": null + "refresh_token_grant_refresh_token_lifespan": null, + "device_authorization_grant_id_token_lifespan": null, + "device_authorization_grant_access_token_lifespan": null, + "device_authorization_grant_refresh_token_lifespan": null } diff --git a/client/.snapshots/TestHandler-common-case=create_clients-case=10-description=empty_ID_succeeds.json b/client/.snapshots/TestHandler-common-case=create_clients-case=10-description=empty_ID_succeeds.json index bf89ac9fbb8..19b5e5afae5 100644 --- a/client/.snapshots/TestHandler-common-case=create_clients-case=10-description=empty_ID_succeeds.json +++ b/client/.snapshots/TestHandler-common-case=create_clients-case=10-description=empty_ID_succeeds.json @@ -31,5 +31,8 @@ "jwt_bearer_grant_access_token_lifespan": null, "refresh_token_grant_id_token_lifespan": null, "refresh_token_grant_access_token_lifespan": null, - "refresh_token_grant_refresh_token_lifespan": null + "refresh_token_grant_refresh_token_lifespan": null, + "device_authorization_grant_id_token_lifespan": null, + "device_authorization_grant_access_token_lifespan": null, + "device_authorization_grant_refresh_token_lifespan": null } diff --git a/client/.snapshots/TestHandler-common-case=create_clients-case=10-description=setting_skip_logout_consent_succeeds_for_admin_registration.json b/client/.snapshots/TestHandler-common-case=create_clients-case=10-description=setting_skip_logout_consent_succeeds_for_admin_registration.json index 80b03c03c1e..16fb5b31144 100644 --- a/client/.snapshots/TestHandler-common-case=create_clients-case=10-description=setting_skip_logout_consent_succeeds_for_admin_registration.json +++ b/client/.snapshots/TestHandler-common-case=create_clients-case=10-description=setting_skip_logout_consent_succeeds_for_admin_registration.json @@ -32,5 +32,8 @@ "jwt_bearer_grant_access_token_lifespan": null, "refresh_token_grant_id_token_lifespan": null, "refresh_token_grant_access_token_lifespan": null, - "refresh_token_grant_refresh_token_lifespan": null + "refresh_token_grant_refresh_token_lifespan": null, + "device_authorization_grant_id_token_lifespan": null, + "device_authorization_grant_access_token_lifespan": null, + "device_authorization_grant_refresh_token_lifespan": null } diff --git a/client/.snapshots/TestHandler-common-case=create_clients-case=12-description=empty_ID_succeeds.json b/client/.snapshots/TestHandler-common-case=create_clients-case=12-description=empty_ID_succeeds.json index 51c70ec465c..69682c03242 100644 --- a/client/.snapshots/TestHandler-common-case=create_clients-case=12-description=empty_ID_succeeds.json +++ b/client/.snapshots/TestHandler-common-case=create_clients-case=12-description=empty_ID_succeeds.json @@ -32,5 +32,8 @@ "jwt_bearer_grant_access_token_lifespan": null, "refresh_token_grant_id_token_lifespan": null, "refresh_token_grant_access_token_lifespan": null, - "refresh_token_grant_refresh_token_lifespan": null + "refresh_token_grant_refresh_token_lifespan": null, + "device_authorization_grant_id_token_lifespan": null, + "device_authorization_grant_access_token_lifespan": null, + "device_authorization_grant_refresh_token_lifespan": null } diff --git a/client/.snapshots/TestHandler-common-case=create_clients-case=2-description=empty_ID_succeeds.json b/client/.snapshots/TestHandler-common-case=create_clients-case=2-description=empty_ID_succeeds.json index c21aa5b3710..e23aa7bed82 100644 --- a/client/.snapshots/TestHandler-common-case=create_clients-case=2-description=empty_ID_succeeds.json +++ b/client/.snapshots/TestHandler-common-case=create_clients-case=2-description=empty_ID_succeeds.json @@ -30,5 +30,8 @@ "jwt_bearer_grant_access_token_lifespan": null, "refresh_token_grant_id_token_lifespan": null, "refresh_token_grant_access_token_lifespan": null, - "refresh_token_grant_refresh_token_lifespan": null + "refresh_token_grant_refresh_token_lifespan": null, + "device_authorization_grant_id_token_lifespan": null, + "device_authorization_grant_access_token_lifespan": null, + "device_authorization_grant_refresh_token_lifespan": null } diff --git a/client/.snapshots/TestHandler-common-case=create_clients-case=4-description=non-uuid_works.json b/client/.snapshots/TestHandler-common-case=create_clients-case=4-description=non-uuid_works.json index f2b7a739e55..25e7e615220 100644 --- a/client/.snapshots/TestHandler-common-case=create_clients-case=4-description=non-uuid_works.json +++ b/client/.snapshots/TestHandler-common-case=create_clients-case=4-description=non-uuid_works.json @@ -34,5 +34,8 @@ "jwt_bearer_grant_access_token_lifespan": null, "refresh_token_grant_id_token_lifespan": null, "refresh_token_grant_access_token_lifespan": null, - "refresh_token_grant_refresh_token_lifespan": null + "refresh_token_grant_refresh_token_lifespan": null, + "device_authorization_grant_id_token_lifespan": null, + "device_authorization_grant_access_token_lifespan": null, + "device_authorization_grant_refresh_token_lifespan": null } diff --git a/client/.snapshots/TestHandler-common-case=create_clients-case=5-description=setting_client_id_as_uuid_works.json b/client/.snapshots/TestHandler-common-case=create_clients-case=5-description=setting_client_id_as_uuid_works.json index 8726a5b41a0..e88c1c9d9be 100644 --- a/client/.snapshots/TestHandler-common-case=create_clients-case=5-description=setting_client_id_as_uuid_works.json +++ b/client/.snapshots/TestHandler-common-case=create_clients-case=5-description=setting_client_id_as_uuid_works.json @@ -34,5 +34,8 @@ "jwt_bearer_grant_access_token_lifespan": null, "refresh_token_grant_id_token_lifespan": null, "refresh_token_grant_access_token_lifespan": null, - "refresh_token_grant_refresh_token_lifespan": null + "refresh_token_grant_refresh_token_lifespan": null, + "device_authorization_grant_id_token_lifespan": null, + "device_authorization_grant_access_token_lifespan": null, + "device_authorization_grant_refresh_token_lifespan": null } diff --git a/client/.snapshots/TestHandler-common-case=create_clients-case=7-description=setting_skip_consent_suceeds_for_admin_registration.json b/client/.snapshots/TestHandler-common-case=create_clients-case=7-description=setting_skip_consent_suceeds_for_admin_registration.json index 96fa08bab16..91e85c55a58 100644 --- a/client/.snapshots/TestHandler-common-case=create_clients-case=7-description=setting_skip_consent_suceeds_for_admin_registration.json +++ b/client/.snapshots/TestHandler-common-case=create_clients-case=7-description=setting_skip_consent_suceeds_for_admin_registration.json @@ -31,5 +31,8 @@ "jwt_bearer_grant_access_token_lifespan": null, "refresh_token_grant_id_token_lifespan": null, "refresh_token_grant_access_token_lifespan": null, - "refresh_token_grant_refresh_token_lifespan": null + "refresh_token_grant_refresh_token_lifespan": null, + "device_authorization_grant_id_token_lifespan": null, + "device_authorization_grant_access_token_lifespan": null, + "device_authorization_grant_refresh_token_lifespan": null } diff --git a/client/.snapshots/TestHandler-common-case=create_clients-case=8-description=empty_ID_succeeds.json b/client/.snapshots/TestHandler-common-case=create_clients-case=8-description=empty_ID_succeeds.json index c21aa5b3710..e23aa7bed82 100644 --- a/client/.snapshots/TestHandler-common-case=create_clients-case=8-description=empty_ID_succeeds.json +++ b/client/.snapshots/TestHandler-common-case=create_clients-case=8-description=empty_ID_succeeds.json @@ -30,5 +30,8 @@ "jwt_bearer_grant_access_token_lifespan": null, "refresh_token_grant_id_token_lifespan": null, "refresh_token_grant_access_token_lifespan": null, - "refresh_token_grant_refresh_token_lifespan": null + "refresh_token_grant_refresh_token_lifespan": null, + "device_authorization_grant_id_token_lifespan": null, + "device_authorization_grant_access_token_lifespan": null, + "device_authorization_grant_refresh_token_lifespan": null } diff --git a/client/.snapshots/TestHandler-common-case=create_clients-case=8-description=setting_skip_consent_succeeds_for_admin_registration.json b/client/.snapshots/TestHandler-common-case=create_clients-case=8-description=setting_skip_consent_succeeds_for_admin_registration.json index 08bfd968627..1191ae414eb 100644 --- a/client/.snapshots/TestHandler-common-case=create_clients-case=8-description=setting_skip_consent_succeeds_for_admin_registration.json +++ b/client/.snapshots/TestHandler-common-case=create_clients-case=8-description=setting_skip_consent_succeeds_for_admin_registration.json @@ -32,5 +32,8 @@ "jwt_bearer_grant_access_token_lifespan": null, "refresh_token_grant_id_token_lifespan": null, "refresh_token_grant_access_token_lifespan": null, - "refresh_token_grant_refresh_token_lifespan": null + "refresh_token_grant_refresh_token_lifespan": null, + "device_authorization_grant_id_token_lifespan": null, + "device_authorization_grant_access_token_lifespan": null, + "device_authorization_grant_refresh_token_lifespan": null } diff --git a/client/.snapshots/TestHandler-common-case=create_clients-case=8-description=setting_skip_consent_suceeds_for_admin_registration.json b/client/.snapshots/TestHandler-common-case=create_clients-case=8-description=setting_skip_consent_suceeds_for_admin_registration.json index 96fa08bab16..91e85c55a58 100644 --- a/client/.snapshots/TestHandler-common-case=create_clients-case=8-description=setting_skip_consent_suceeds_for_admin_registration.json +++ b/client/.snapshots/TestHandler-common-case=create_clients-case=8-description=setting_skip_consent_suceeds_for_admin_registration.json @@ -31,5 +31,8 @@ "jwt_bearer_grant_access_token_lifespan": null, "refresh_token_grant_id_token_lifespan": null, "refresh_token_grant_access_token_lifespan": null, - "refresh_token_grant_refresh_token_lifespan": null + "refresh_token_grant_refresh_token_lifespan": null, + "device_authorization_grant_id_token_lifespan": null, + "device_authorization_grant_access_token_lifespan": null, + "device_authorization_grant_refresh_token_lifespan": null } diff --git a/client/.snapshots/TestHandler-common-case=create_clients-case=9-description=empty_ID_succeeds.json b/client/.snapshots/TestHandler-common-case=create_clients-case=9-description=empty_ID_succeeds.json index bf89ac9fbb8..19b5e5afae5 100644 --- a/client/.snapshots/TestHandler-common-case=create_clients-case=9-description=empty_ID_succeeds.json +++ b/client/.snapshots/TestHandler-common-case=create_clients-case=9-description=empty_ID_succeeds.json @@ -31,5 +31,8 @@ "jwt_bearer_grant_access_token_lifespan": null, "refresh_token_grant_id_token_lifespan": null, "refresh_token_grant_access_token_lifespan": null, - "refresh_token_grant_refresh_token_lifespan": null + "refresh_token_grant_refresh_token_lifespan": null, + "device_authorization_grant_id_token_lifespan": null, + "device_authorization_grant_access_token_lifespan": null, + "device_authorization_grant_refresh_token_lifespan": null } diff --git a/client/.snapshots/TestHandler-common-case=fetching_existing_client-endpoint=admin.json b/client/.snapshots/TestHandler-common-case=fetching_existing_client-endpoint=admin.json index 7ac99ae55c2..9fc694022cd 100644 --- a/client/.snapshots/TestHandler-common-case=fetching_existing_client-endpoint=admin.json +++ b/client/.snapshots/TestHandler-common-case=fetching_existing_client-endpoint=admin.json @@ -32,7 +32,10 @@ "jwt_bearer_grant_access_token_lifespan": null, "refresh_token_grant_id_token_lifespan": null, "refresh_token_grant_access_token_lifespan": null, - "refresh_token_grant_refresh_token_lifespan": null + "refresh_token_grant_refresh_token_lifespan": null, + "device_authorization_grant_id_token_lifespan": null, + "device_authorization_grant_access_token_lifespan": null, + "device_authorization_grant_refresh_token_lifespan": null }, "status": 200 } diff --git a/client/.snapshots/TestHandler-common-case=fetching_existing_client-endpoint=selfservice.json b/client/.snapshots/TestHandler-common-case=fetching_existing_client-endpoint=selfservice.json index 6f80f123353..d6544830e52 100644 --- a/client/.snapshots/TestHandler-common-case=fetching_existing_client-endpoint=selfservice.json +++ b/client/.snapshots/TestHandler-common-case=fetching_existing_client-endpoint=selfservice.json @@ -31,7 +31,10 @@ "jwt_bearer_grant_access_token_lifespan": null, "refresh_token_grant_id_token_lifespan": null, "refresh_token_grant_access_token_lifespan": null, - "refresh_token_grant_refresh_token_lifespan": null + "refresh_token_grant_refresh_token_lifespan": null, + "device_authorization_grant_id_token_lifespan": null, + "device_authorization_grant_access_token_lifespan": null, + "device_authorization_grant_refresh_token_lifespan": null }, "status": 200 } diff --git a/client/.snapshots/TestHandler-common-case=update_the_lifespans_of_an_OAuth2_client.json b/client/.snapshots/TestHandler-common-case=update_the_lifespans_of_an_OAuth2_client.json index 4472241967d..aca2c7bbca9 100644 --- a/client/.snapshots/TestHandler-common-case=update_the_lifespans_of_an_OAuth2_client.json +++ b/client/.snapshots/TestHandler-common-case=update_the_lifespans_of_an_OAuth2_client.json @@ -32,7 +32,10 @@ "jwt_bearer_grant_access_token_lifespan": "37h0m0s", "refresh_token_grant_id_token_lifespan": "40h0m0s", "refresh_token_grant_access_token_lifespan": "41h0m0s", - "refresh_token_grant_refresh_token_lifespan": "42h0m0s" + "refresh_token_grant_refresh_token_lifespan": "42h0m0s", + "device_authorization_grant_id_token_lifespan": "45h0m0s", + "device_authorization_grant_access_token_lifespan": "46h0m0s", + "device_authorization_grant_refresh_token_lifespan": "47h0m0s" }, "status": 200 } diff --git a/client/.snapshots/TestHandler-common-case=updating_existing_client-endpoint=admin.json b/client/.snapshots/TestHandler-common-case=updating_existing_client-endpoint=admin.json index 12b431ec4b2..4953cd54220 100644 --- a/client/.snapshots/TestHandler-common-case=updating_existing_client-endpoint=admin.json +++ b/client/.snapshots/TestHandler-common-case=updating_existing_client-endpoint=admin.json @@ -34,7 +34,10 @@ "jwt_bearer_grant_access_token_lifespan": null, "refresh_token_grant_id_token_lifespan": null, "refresh_token_grant_access_token_lifespan": null, - "refresh_token_grant_refresh_token_lifespan": null + "refresh_token_grant_refresh_token_lifespan": null, + "device_authorization_grant_id_token_lifespan": null, + "device_authorization_grant_access_token_lifespan": null, + "device_authorization_grant_refresh_token_lifespan": null }, "status": 200 } diff --git a/client/.snapshots/TestHandler-common-case=updating_existing_client-endpoint=dynamic_client_registration.json b/client/.snapshots/TestHandler-common-case=updating_existing_client-endpoint=dynamic_client_registration.json index 24b0eecfeb7..5727960363b 100644 --- a/client/.snapshots/TestHandler-common-case=updating_existing_client-endpoint=dynamic_client_registration.json +++ b/client/.snapshots/TestHandler-common-case=updating_existing_client-endpoint=dynamic_client_registration.json @@ -33,7 +33,10 @@ "jwt_bearer_grant_access_token_lifespan": null, "refresh_token_grant_id_token_lifespan": null, "refresh_token_grant_access_token_lifespan": null, - "refresh_token_grant_refresh_token_lifespan": null + "refresh_token_grant_refresh_token_lifespan": null, + "device_authorization_grant_id_token_lifespan": null, + "device_authorization_grant_access_token_lifespan": null, + "device_authorization_grant_refresh_token_lifespan": null }, "status": 200 } diff --git a/client/.snapshots/TestHandler-create_client_registration_tokens-case=0-dynamic=true.json b/client/.snapshots/TestHandler-create_client_registration_tokens-case=0-dynamic=true.json index 281b21ecdbf..b161bf055fa 100644 --- a/client/.snapshots/TestHandler-create_client_registration_tokens-case=0-dynamic=true.json +++ b/client/.snapshots/TestHandler-create_client_registration_tokens-case=0-dynamic=true.json @@ -27,5 +27,8 @@ "jwt_bearer_grant_access_token_lifespan": null, "refresh_token_grant_id_token_lifespan": null, "refresh_token_grant_access_token_lifespan": null, - "refresh_token_grant_refresh_token_lifespan": null + "refresh_token_grant_refresh_token_lifespan": null, + "device_authorization_grant_id_token_lifespan": null, + "device_authorization_grant_access_token_lifespan": null, + "device_authorization_grant_refresh_token_lifespan": null } diff --git a/client/.snapshots/TestHandler-create_client_registration_tokens-case=1-dynamic=false.json b/client/.snapshots/TestHandler-create_client_registration_tokens-case=1-dynamic=false.json index 281b21ecdbf..b161bf055fa 100644 --- a/client/.snapshots/TestHandler-create_client_registration_tokens-case=1-dynamic=false.json +++ b/client/.snapshots/TestHandler-create_client_registration_tokens-case=1-dynamic=false.json @@ -27,5 +27,8 @@ "jwt_bearer_grant_access_token_lifespan": null, "refresh_token_grant_id_token_lifespan": null, "refresh_token_grant_access_token_lifespan": null, - "refresh_token_grant_refresh_token_lifespan": null + "refresh_token_grant_refresh_token_lifespan": null, + "device_authorization_grant_id_token_lifespan": null, + "device_authorization_grant_access_token_lifespan": null, + "device_authorization_grant_refresh_token_lifespan": null } diff --git a/client/.snapshots/TestHandler-create_client_registration_tokens-case=2-dynamic=false.json b/client/.snapshots/TestHandler-create_client_registration_tokens-case=2-dynamic=false.json index 0718f2d2227..aa0b8b3ae78 100644 --- a/client/.snapshots/TestHandler-create_client_registration_tokens-case=2-dynamic=false.json +++ b/client/.snapshots/TestHandler-create_client_registration_tokens-case=2-dynamic=false.json @@ -28,5 +28,8 @@ "jwt_bearer_grant_access_token_lifespan": null, "refresh_token_grant_id_token_lifespan": null, "refresh_token_grant_access_token_lifespan": null, - "refresh_token_grant_refresh_token_lifespan": null + "refresh_token_grant_refresh_token_lifespan": null, + "device_authorization_grant_id_token_lifespan": null, + "device_authorization_grant_access_token_lifespan": null, + "device_authorization_grant_refresh_token_lifespan": null } diff --git a/client/client.go b/client/client.go index 1d78e7add34..c027d505192 100644 --- a/client/client.go +++ b/client/client.go @@ -380,6 +380,21 @@ type Lifespans struct { // // The lifespan of a refresh token issued by the OAuth2 2.0 Refresh Token Grant for this OAuth 2.0 Client. RefreshTokenGrantRefreshTokenLifespan x.NullDuration `json:"refresh_token_grant_refresh_token_lifespan,omitempty" db:"refresh_token_grant_refresh_token_lifespan"` + + // OAuth2 2.0 Device Authorization Grant ID Token Lifespan + // + // The lifespan of an ID token issued by the OAuth2 2.0 Device Authorization Grant for this OAuth 2.0 Client. + DeviceAuthorizationGrantIDTokenLifespan x.NullDuration `json:"device_authorization_grant_id_token_lifespan,omitempty" db:"device_authorization_grant_id_token_lifespan"` + + // OAuth2 2.0 Device Authorization Grant Access Token Lifespan + // + // The lifespan of an access token issued by the OAuth2 2.0 Device Authorization Grant for this OAuth 2.0 Client. + DeviceAuthorizationGrantAccessTokenLifespan x.NullDuration `json:"device_authorization_grant_access_token_lifespan,omitempty" db:"device_authorization_grant_access_token_lifespan"` + + // OAuth2 2.0 Device Authorization Grant Device Authorization Lifespan + // + // The lifespan of a Device Authorization issued by the OAuth2 2.0 Device Authorization Grant for this OAuth 2.0 Client. + DeviceAuthorizationGrantRefreshTokenLifespan x.NullDuration `json:"device_authorization_grant_refresh_token_lifespan,omitempty" db:"device_authorization_grant_refresh_token_lifespan"` } func (Client) TableName() string { @@ -550,6 +565,14 @@ func (c *Client) GetEffectiveLifespan(gt fosite.GrantType, tt fosite.TokenType, } else if tt == fosite.RefreshToken && c.RefreshTokenGrantRefreshTokenLifespan.Valid { cl = &c.RefreshTokenGrantRefreshTokenLifespan.Duration } + } else if gt == fosite.GrantTypeDeviceCode { + if tt == fosite.AccessToken && c.DeviceAuthorizationGrantAccessTokenLifespan.Valid { + cl = &c.DeviceAuthorizationGrantAccessTokenLifespan.Duration + } else if tt == fosite.IDToken && c.DeviceAuthorizationGrantIDTokenLifespan.Valid { + cl = &c.DeviceAuthorizationGrantIDTokenLifespan.Duration + } else if tt == fosite.RefreshToken && c.DeviceAuthorizationGrantRefreshTokenLifespan.Valid { + cl = &c.DeviceAuthorizationGrantRefreshTokenLifespan.Duration + } } if cl == nil { diff --git a/internal/testhelpers/lifespans.go b/internal/testhelpers/lifespans.go index 86477c90b09..e2ba8a218c4 100644 --- a/internal/testhelpers/lifespans.go +++ b/internal/testhelpers/lifespans.go @@ -11,16 +11,19 @@ import ( ) var TestLifespans = client.Lifespans{ - AuthorizationCodeGrantAccessTokenLifespan: x.NullDuration{Duration: 31 * time.Hour, Valid: true}, - AuthorizationCodeGrantIDTokenLifespan: x.NullDuration{Duration: 32 * time.Hour, Valid: true}, - AuthorizationCodeGrantRefreshTokenLifespan: x.NullDuration{Duration: 33 * time.Hour, Valid: true}, - ClientCredentialsGrantAccessTokenLifespan: x.NullDuration{Duration: 34 * time.Hour, Valid: true}, - ImplicitGrantAccessTokenLifespan: x.NullDuration{Duration: 35 * time.Hour, Valid: true}, - ImplicitGrantIDTokenLifespan: x.NullDuration{Duration: 36 * time.Hour, Valid: true}, - JwtBearerGrantAccessTokenLifespan: x.NullDuration{Duration: 37 * time.Hour, Valid: true}, - PasswordGrantAccessTokenLifespan: x.NullDuration{Duration: 38 * time.Hour, Valid: true}, - PasswordGrantRefreshTokenLifespan: x.NullDuration{Duration: 39 * time.Hour, Valid: true}, - RefreshTokenGrantIDTokenLifespan: x.NullDuration{Duration: 40 * time.Hour, Valid: true}, - RefreshTokenGrantAccessTokenLifespan: x.NullDuration{Duration: 41 * time.Hour, Valid: true}, - RefreshTokenGrantRefreshTokenLifespan: x.NullDuration{Duration: 42 * time.Hour, Valid: true}, + AuthorizationCodeGrantAccessTokenLifespan: x.NullDuration{Duration: 31 * time.Hour, Valid: true}, + AuthorizationCodeGrantIDTokenLifespan: x.NullDuration{Duration: 32 * time.Hour, Valid: true}, + AuthorizationCodeGrantRefreshTokenLifespan: x.NullDuration{Duration: 33 * time.Hour, Valid: true}, + ClientCredentialsGrantAccessTokenLifespan: x.NullDuration{Duration: 34 * time.Hour, Valid: true}, + ImplicitGrantAccessTokenLifespan: x.NullDuration{Duration: 35 * time.Hour, Valid: true}, + ImplicitGrantIDTokenLifespan: x.NullDuration{Duration: 36 * time.Hour, Valid: true}, + JwtBearerGrantAccessTokenLifespan: x.NullDuration{Duration: 37 * time.Hour, Valid: true}, + PasswordGrantAccessTokenLifespan: x.NullDuration{Duration: 38 * time.Hour, Valid: true}, + PasswordGrantRefreshTokenLifespan: x.NullDuration{Duration: 39 * time.Hour, Valid: true}, + RefreshTokenGrantIDTokenLifespan: x.NullDuration{Duration: 40 * time.Hour, Valid: true}, + RefreshTokenGrantAccessTokenLifespan: x.NullDuration{Duration: 41 * time.Hour, Valid: true}, + RefreshTokenGrantRefreshTokenLifespan: x.NullDuration{Duration: 42 * time.Hour, Valid: true}, + DeviceAuthorizationGrantIDTokenLifespan: x.NullDuration{Duration: 45 * time.Hour, Valid: true}, + DeviceAuthorizationGrantAccessTokenLifespan: x.NullDuration{Duration: 46 * time.Hour, Valid: true}, + DeviceAuthorizationGrantRefreshTokenLifespan: x.NullDuration{Duration: 47 * time.Hour, Valid: true}, } From 3881ef23029dbe6fae3c484620e3e197ffa7279f Mon Sep 17 00:00:00 2001 From: Nikos Date: Mon, 29 Apr 2024 16:53:30 +0300 Subject: [PATCH 27/52] test: update sql fixtures --- .../migratest/fixtures/hydra_client/client-0001.json | 12 ++++++++++++ .../migratest/fixtures/hydra_client/client-0002.json | 12 ++++++++++++ .../migratest/fixtures/hydra_client/client-0003.json | 12 ++++++++++++ .../migratest/fixtures/hydra_client/client-0004.json | 12 ++++++++++++ .../migratest/fixtures/hydra_client/client-0005.json | 12 ++++++++++++ .../migratest/fixtures/hydra_client/client-0006.json | 12 ++++++++++++ .../migratest/fixtures/hydra_client/client-0007.json | 12 ++++++++++++ .../migratest/fixtures/hydra_client/client-0008.json | 12 ++++++++++++ .../migratest/fixtures/hydra_client/client-0009.json | 12 ++++++++++++ .../migratest/fixtures/hydra_client/client-0010.json | 12 ++++++++++++ .../migratest/fixtures/hydra_client/client-0011.json | 12 ++++++++++++ .../migratest/fixtures/hydra_client/client-0012.json | 12 ++++++++++++ .../migratest/fixtures/hydra_client/client-0013.json | 12 ++++++++++++ .../migratest/fixtures/hydra_client/client-0014.json | 12 ++++++++++++ .../migratest/fixtures/hydra_client/client-0015.json | 12 ++++++++++++ .../migratest/fixtures/hydra_client/client-20.json | 12 ++++++++++++ .../migratest/fixtures/hydra_client/client-2005.json | 12 ++++++++++++ .../migratest/fixtures/hydra_client/client-21.json | 12 ++++++++++++ 18 files changed, 216 insertions(+) diff --git a/persistence/sql/migratest/fixtures/hydra_client/client-0001.json b/persistence/sql/migratest/fixtures/hydra_client/client-0001.json index eb65327c43f..92a6eb6b00f 100644 --- a/persistence/sql/migratest/fixtures/hydra_client/client-0001.json +++ b/persistence/sql/migratest/fixtures/hydra_client/client-0001.json @@ -36,6 +36,18 @@ "Duration": 0, "Valid": false }, + "DeviceAuthorizationGrantAccessTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantIDTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantRefreshTokenLifespan": { + "Duration": 0, + "Valid": false + }, "ImplicitGrantAccessTokenLifespan": { "Duration": 0, "Valid": false diff --git a/persistence/sql/migratest/fixtures/hydra_client/client-0002.json b/persistence/sql/migratest/fixtures/hydra_client/client-0002.json index d58301981be..1cb9ff6e769 100644 --- a/persistence/sql/migratest/fixtures/hydra_client/client-0002.json +++ b/persistence/sql/migratest/fixtures/hydra_client/client-0002.json @@ -36,6 +36,18 @@ "Duration": 0, "Valid": false }, + "DeviceAuthorizationGrantAccessTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantIDTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantRefreshTokenLifespan": { + "Duration": 0, + "Valid": false + }, "ImplicitGrantAccessTokenLifespan": { "Duration": 0, "Valid": false diff --git a/persistence/sql/migratest/fixtures/hydra_client/client-0003.json b/persistence/sql/migratest/fixtures/hydra_client/client-0003.json index b0a9c4116b0..b2d8a612220 100644 --- a/persistence/sql/migratest/fixtures/hydra_client/client-0003.json +++ b/persistence/sql/migratest/fixtures/hydra_client/client-0003.json @@ -36,6 +36,18 @@ "Duration": 0, "Valid": false }, + "DeviceAuthorizationGrantAccessTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantIDTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantRefreshTokenLifespan": { + "Duration": 0, + "Valid": false + }, "ImplicitGrantAccessTokenLifespan": { "Duration": 0, "Valid": false diff --git a/persistence/sql/migratest/fixtures/hydra_client/client-0004.json b/persistence/sql/migratest/fixtures/hydra_client/client-0004.json index ad8ddb8fa58..10e001ac97e 100644 --- a/persistence/sql/migratest/fixtures/hydra_client/client-0004.json +++ b/persistence/sql/migratest/fixtures/hydra_client/client-0004.json @@ -36,6 +36,18 @@ "Duration": 0, "Valid": false }, + "DeviceAuthorizationGrantAccessTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantIDTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantRefreshTokenLifespan": { + "Duration": 0, + "Valid": false + }, "ImplicitGrantAccessTokenLifespan": { "Duration": 0, "Valid": false diff --git a/persistence/sql/migratest/fixtures/hydra_client/client-0005.json b/persistence/sql/migratest/fixtures/hydra_client/client-0005.json index 295a11833a1..c51c01b13e7 100644 --- a/persistence/sql/migratest/fixtures/hydra_client/client-0005.json +++ b/persistence/sql/migratest/fixtures/hydra_client/client-0005.json @@ -36,6 +36,18 @@ "Duration": 0, "Valid": false }, + "DeviceAuthorizationGrantAccessTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantIDTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantRefreshTokenLifespan": { + "Duration": 0, + "Valid": false + }, "ImplicitGrantAccessTokenLifespan": { "Duration": 0, "Valid": false diff --git a/persistence/sql/migratest/fixtures/hydra_client/client-0006.json b/persistence/sql/migratest/fixtures/hydra_client/client-0006.json index 9864869db0a..f87065ee097 100644 --- a/persistence/sql/migratest/fixtures/hydra_client/client-0006.json +++ b/persistence/sql/migratest/fixtures/hydra_client/client-0006.json @@ -36,6 +36,18 @@ "Duration": 0, "Valid": false }, + "DeviceAuthorizationGrantAccessTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantIDTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantRefreshTokenLifespan": { + "Duration": 0, + "Valid": false + }, "ImplicitGrantAccessTokenLifespan": { "Duration": 0, "Valid": false diff --git a/persistence/sql/migratest/fixtures/hydra_client/client-0007.json b/persistence/sql/migratest/fixtures/hydra_client/client-0007.json index 8186c89de28..6bf27b0d29b 100644 --- a/persistence/sql/migratest/fixtures/hydra_client/client-0007.json +++ b/persistence/sql/migratest/fixtures/hydra_client/client-0007.json @@ -36,6 +36,18 @@ "Duration": 0, "Valid": false }, + "DeviceAuthorizationGrantAccessTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantIDTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantRefreshTokenLifespan": { + "Duration": 0, + "Valid": false + }, "ImplicitGrantAccessTokenLifespan": { "Duration": 0, "Valid": false diff --git a/persistence/sql/migratest/fixtures/hydra_client/client-0008.json b/persistence/sql/migratest/fixtures/hydra_client/client-0008.json index 84bf09f3572..51cbcaf1c58 100644 --- a/persistence/sql/migratest/fixtures/hydra_client/client-0008.json +++ b/persistence/sql/migratest/fixtures/hydra_client/client-0008.json @@ -38,6 +38,18 @@ "Duration": 0, "Valid": false }, + "DeviceAuthorizationGrantAccessTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantIDTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantRefreshTokenLifespan": { + "Duration": 0, + "Valid": false + }, "ImplicitGrantAccessTokenLifespan": { "Duration": 0, "Valid": false diff --git a/persistence/sql/migratest/fixtures/hydra_client/client-0009.json b/persistence/sql/migratest/fixtures/hydra_client/client-0009.json index afae63b8668..ffe308afe0a 100644 --- a/persistence/sql/migratest/fixtures/hydra_client/client-0009.json +++ b/persistence/sql/migratest/fixtures/hydra_client/client-0009.json @@ -38,6 +38,18 @@ "Duration": 0, "Valid": false }, + "DeviceAuthorizationGrantAccessTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantIDTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantRefreshTokenLifespan": { + "Duration": 0, + "Valid": false + }, "ImplicitGrantAccessTokenLifespan": { "Duration": 0, "Valid": false diff --git a/persistence/sql/migratest/fixtures/hydra_client/client-0010.json b/persistence/sql/migratest/fixtures/hydra_client/client-0010.json index 5385cde2a58..573049c6c96 100644 --- a/persistence/sql/migratest/fixtures/hydra_client/client-0010.json +++ b/persistence/sql/migratest/fixtures/hydra_client/client-0010.json @@ -38,6 +38,18 @@ "Duration": 0, "Valid": false }, + "DeviceAuthorizationGrantAccessTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantIDTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantRefreshTokenLifespan": { + "Duration": 0, + "Valid": false + }, "ImplicitGrantAccessTokenLifespan": { "Duration": 0, "Valid": false diff --git a/persistence/sql/migratest/fixtures/hydra_client/client-0011.json b/persistence/sql/migratest/fixtures/hydra_client/client-0011.json index 7e3e68023fc..a49000472ae 100644 --- a/persistence/sql/migratest/fixtures/hydra_client/client-0011.json +++ b/persistence/sql/migratest/fixtures/hydra_client/client-0011.json @@ -40,6 +40,18 @@ "Duration": 0, "Valid": false }, + "DeviceAuthorizationGrantAccessTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantIDTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantRefreshTokenLifespan": { + "Duration": 0, + "Valid": false + }, "ImplicitGrantAccessTokenLifespan": { "Duration": 0, "Valid": false diff --git a/persistence/sql/migratest/fixtures/hydra_client/client-0012.json b/persistence/sql/migratest/fixtures/hydra_client/client-0012.json index cd61f01cbbe..1877d4b298f 100644 --- a/persistence/sql/migratest/fixtures/hydra_client/client-0012.json +++ b/persistence/sql/migratest/fixtures/hydra_client/client-0012.json @@ -40,6 +40,18 @@ "Duration": 0, "Valid": false }, + "DeviceAuthorizationGrantAccessTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantIDTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantRefreshTokenLifespan": { + "Duration": 0, + "Valid": false + }, "ImplicitGrantAccessTokenLifespan": { "Duration": 0, "Valid": false diff --git a/persistence/sql/migratest/fixtures/hydra_client/client-0013.json b/persistence/sql/migratest/fixtures/hydra_client/client-0013.json index 7eaff4d8137..fb67f9202b9 100644 --- a/persistence/sql/migratest/fixtures/hydra_client/client-0013.json +++ b/persistence/sql/migratest/fixtures/hydra_client/client-0013.json @@ -40,6 +40,18 @@ "Duration": 0, "Valid": false }, + "DeviceAuthorizationGrantAccessTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantIDTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantRefreshTokenLifespan": { + "Duration": 0, + "Valid": false + }, "ImplicitGrantAccessTokenLifespan": { "Duration": 0, "Valid": false diff --git a/persistence/sql/migratest/fixtures/hydra_client/client-0014.json b/persistence/sql/migratest/fixtures/hydra_client/client-0014.json index 7571ef23533..1bc2ef1ea63 100644 --- a/persistence/sql/migratest/fixtures/hydra_client/client-0014.json +++ b/persistence/sql/migratest/fixtures/hydra_client/client-0014.json @@ -40,6 +40,18 @@ "Duration": 0, "Valid": false }, + "DeviceAuthorizationGrantAccessTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantIDTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantRefreshTokenLifespan": { + "Duration": 0, + "Valid": false + }, "ImplicitGrantAccessTokenLifespan": { "Duration": 0, "Valid": false diff --git a/persistence/sql/migratest/fixtures/hydra_client/client-0015.json b/persistence/sql/migratest/fixtures/hydra_client/client-0015.json index ab4ee61170d..42b12e6b492 100644 --- a/persistence/sql/migratest/fixtures/hydra_client/client-0015.json +++ b/persistence/sql/migratest/fixtures/hydra_client/client-0015.json @@ -40,6 +40,18 @@ "Duration": 154000000000, "Valid": true }, + "DeviceAuthorizationGrantAccessTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantIDTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantRefreshTokenLifespan": { + "Duration": 0, + "Valid": false + }, "ImplicitGrantAccessTokenLifespan": { "Duration": 155000000000, "Valid": true diff --git a/persistence/sql/migratest/fixtures/hydra_client/client-20.json b/persistence/sql/migratest/fixtures/hydra_client/client-20.json index 63339ce7da9..fbc35aedfc4 100644 --- a/persistence/sql/migratest/fixtures/hydra_client/client-20.json +++ b/persistence/sql/migratest/fixtures/hydra_client/client-20.json @@ -40,6 +40,18 @@ "Duration": 0, "Valid": false }, + "DeviceAuthorizationGrantAccessTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantIDTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantRefreshTokenLifespan": { + "Duration": 0, + "Valid": false + }, "ImplicitGrantAccessTokenLifespan": { "Duration": 0, "Valid": false diff --git a/persistence/sql/migratest/fixtures/hydra_client/client-2005.json b/persistence/sql/migratest/fixtures/hydra_client/client-2005.json index 140d8a42021..40470238a5b 100644 --- a/persistence/sql/migratest/fixtures/hydra_client/client-2005.json +++ b/persistence/sql/migratest/fixtures/hydra_client/client-2005.json @@ -40,6 +40,18 @@ "Duration": 0, "Valid": false }, + "DeviceAuthorizationGrantAccessTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantIDTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantRefreshTokenLifespan": { + "Duration": 0, + "Valid": false + }, "ImplicitGrantAccessTokenLifespan": { "Duration": 0, "Valid": false diff --git a/persistence/sql/migratest/fixtures/hydra_client/client-21.json b/persistence/sql/migratest/fixtures/hydra_client/client-21.json index 6bc3911af94..7b3e67c7770 100644 --- a/persistence/sql/migratest/fixtures/hydra_client/client-21.json +++ b/persistence/sql/migratest/fixtures/hydra_client/client-21.json @@ -44,6 +44,18 @@ "Duration": 0, "Valid": false }, + "DeviceAuthorizationGrantAccessTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantIDTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantRefreshTokenLifespan": { + "Duration": 0, + "Valid": false + }, "ImplicitGrantAccessTokenLifespan": { "Duration": 0, "Valid": false From 3bdc5bc6a7efc04028532c61d3a29998ed769f42 Mon Sep 17 00:00:00 2001 From: Nikos Date: Wed, 22 May 2024 14:35:33 +0300 Subject: [PATCH 28/52] fix: perform device flow from CLI --- cmd/cmd_perform_device_flow.go | 108 +++++++++++++++++++++++++++++++++ cmd/root.go | 1 + 2 files changed, 109 insertions(+) create mode 100644 cmd/cmd_perform_device_flow.go diff --git a/cmd/cmd_perform_device_flow.go b/cmd/cmd_perform_device_flow.go new file mode 100644 index 00000000000..74e9a33a4b3 --- /dev/null +++ b/cmd/cmd_perform_device_flow.go @@ -0,0 +1,108 @@ +// Copyright © 2022 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package cmd + +import ( + "context" + "fmt" + "os" + "strings" + + "github.com/ory/hydra/v2/cmd/cliclient" + + "github.com/spf13/cobra" + "golang.org/x/oauth2" + + "github.com/ory/x/cmdx" + "github.com/ory/x/flagx" + "github.com/ory/x/urlx" +) + +func NewPerformDeviceCodeCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "device-code", + Example: "{{ .CommandPath }} --client-id ... --client-secret ...", + Short: "An exemplary OAuth 2.0 Client performing the OAuth 2.0 Device Code Flow", + Long: `Performs the device code flow. Useful for getting an access token and an ID token in machines without a browser. + The client that will be used MUST support the "client_secret_post" token-endpoint-auth-method + `, + RunE: func(cmd *cobra.Command, args []string) error { + client, endpoint, err := cliclient.NewClient(cmd) + if err != nil { + return err + } + + endpoint = cliclient.GetOAuth2URLOverride(cmd, endpoint) + + ctx := context.WithValue(cmd.Context(), oauth2.HTTPClient, client) + scopes := flagx.MustGetStringSlice(cmd, "scope") + deviceAuthUrl := flagx.MustGetString(cmd, "device-auth-url") + tokenUrl := flagx.MustGetString(cmd, "token-url") + audience := flagx.MustGetStringSlice(cmd, "audience") + + clientID := flagx.MustGetString(cmd, "client-id") + if clientID == "" { + _, _ = fmt.Fprint(cmd.OutOrStdout(), cmd.UsageString()) + _, _ = fmt.Fprintln(cmd.OutOrStdout(), "Please provide a Client ID using --client-id flag, or OAUTH2_CLIENT_ID environment variable.") + return cmdx.FailSilently(cmd) + } + + clientSecret := flagx.MustGetString(cmd, "client-secret") + + if deviceAuthUrl == "" { + deviceAuthUrl = urlx.AppendPaths(endpoint, "/oauth2/device/auth").String() + } + + if tokenUrl == "" { + tokenUrl = urlx.AppendPaths(endpoint, "/oauth2/token").String() + } + + conf := oauth2.Config{ + ClientID: clientID, + ClientSecret: clientSecret, + Endpoint: oauth2.Endpoint{ + DeviceAuthURL: deviceAuthUrl, + TokenURL: tokenUrl, + }, + Scopes: scopes, + } + + deviceAuthResponse, err := conf.DeviceAuth( + ctx, + oauth2.SetAuthURLParam("audience", strings.Join(audience, "+")), + oauth2.SetAuthURLParam("client_secret", clientSecret), + ) + if err != nil { + cmdx.Fatalf("Failed to perform the device authorization request", err.Error()) + } + + fmt.Fprintln( + cmd.OutOrStdout(), + "To login please go to:\n\t", + deviceAuthResponse.VerificationURIComplete, + ) + + token, err := conf.DeviceAccessToken(ctx, deviceAuthResponse) + if err != nil { + cmdx.Fatalf("Failed to perform the device token request: %e", err.Error()) + } + + fmt.Println("Successfully signed in!") + + cmdx.PrintRow(cmd, outputOAuth2Token(*token)) + return nil + }, + } + + cmd.Flags().StringSlice("scope", []string{"offline", "openid"}, "Request OAuth2 scope") + + cmd.Flags().String("client-id", os.Getenv("OAUTH2_CLIENT_ID"), "Use the provided OAuth 2.0 Client ID, defaults to environment variable OAUTH2_CLIENT_ID") + cmd.Flags().String("client-secret", os.Getenv("OAUTH2_CLIENT_SECRET"), "Use the provided OAuth 2.0 Client Secret, defaults to environment variable OAUTH2_CLIENT_SECRET") + + cmd.Flags().StringSlice("audience", []string{}, "Request a specific OAuth 2.0 Access Token Audience") + cmd.Flags().String("device-auth-url", "", "Usually it is enough to specify the `endpoint` flag, but if you want to force the device authorization url, use this flag") + cmd.Flags().String("token-url", "", "Usually it is enough to specify the `endpoint` flag, but if you want to force the token url, use this flag") + + return cmd +} diff --git a/cmd/root.go b/cmd/root.go index 20832d40546..12055fb0b85 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -64,6 +64,7 @@ func RegisterCommandRecursive(parent *cobra.Command, slOpts []servicelocatorx.Op performCmd.AddCommand( NewPerformClientCredentialsCmd(), NewPerformAuthorizationCodeCmd(), + NewPerformDeviceCodeCmd(), ) revokeCmd := NewRevokeCmd() From 34a9750fce5113fd0f12de1720469530086b5fff Mon Sep 17 00:00:00 2001 From: Nikos Date: Tue, 30 Jul 2024 14:56:56 +0300 Subject: [PATCH 29/52] fix: wrap db calls in transaction --- consent/manager.go | 3 +++ consent/strategy_default.go | 54 +++++++++++++++++++++---------------- oauth2/handler.go | 2 -- 3 files changed, 34 insertions(+), 25 deletions(-) diff --git a/consent/manager.go b/consent/manager.go index e2787d0de79..8396550b2bb 100644 --- a/consent/manager.go +++ b/consent/manager.go @@ -6,6 +6,7 @@ package consent import ( "context" + "github.com/gobuffalo/pop/v6" "github.com/gofrs/uuid" "github.com/ory/hydra/v2/client" @@ -66,6 +67,8 @@ type ( GetDeviceUserAuthRequest(ctx context.Context, challenge string) (*flow.DeviceUserAuthRequest, error) HandleDeviceUserAuthRequest(ctx context.Context, f *flow.Flow, challenge string, r *flow.HandledDeviceUserAuthRequest) (*flow.DeviceUserAuthRequest, error) VerifyAndInvalidateDeviceUserAuthRequest(ctx context.Context, verifier string) (*flow.HandledDeviceUserAuthRequest, error) + + Transaction(context.Context, func(ctx context.Context, c *pop.Connection) error) error } ManagerProvider interface { diff --git a/consent/strategy_default.go b/consent/strategy_default.go index 3da4c50bd79..c72329d8987 100644 --- a/consent/strategy_default.go +++ b/consent/strategy_default.go @@ -14,6 +14,7 @@ import ( "strings" "time" + "github.com/gobuffalo/pop/v6" "github.com/gorilla/sessions" "github.com/hashicorp/go-retryablehttp" "github.com/pborman/uuid" @@ -40,8 +41,6 @@ import ( "github.com/ory/x/urlx" ) -type ctxKey int - const ( DeviceVerificationPath = "/oauth2/device/verify" CookieAuthenticationSIDName = "sid" @@ -1163,21 +1162,11 @@ func (s *DefaultStrategy) HandleOAuth2AuthorizationRequest( ctx, span := trace.SpanFromContext(ctx).TracerProvider().Tracer("").Start(ctx, "DefaultStrategy.HandleOAuth2AuthorizationRequest") defer otelx.End(span, &err) - return s.handleOAuth2AuthorizationRequest(ctx, w, r, req, nil) -} - -func (s *DefaultStrategy) handleOAuth2AuthorizationRequest( - ctx context.Context, - w http.ResponseWriter, - r *http.Request, - req fosite.AuthorizeRequester, - f *flow.Flow, -) (_ *flow.AcceptOAuth2ConsentRequest, _ *flow.Flow, err error) { loginVerifier := strings.TrimSpace(r.URL.Query().Get("login_verifier")) consentVerifier := strings.TrimSpace(r.URL.Query().Get("consent_verifier")) if loginVerifier == "" && consentVerifier == "" { // ok, we need to process this request and redirect to the original endpoint - return nil, nil, s.requestAuthentication(ctx, w, r, req, f) + return nil, nil, s.requestAuthentication(ctx, w, r, req, nil) } else if loginVerifier != "" { f, err := s.verifyAuthentication(ctx, w, r, req, loginVerifier) if err != nil { @@ -1201,7 +1190,10 @@ func (s *DefaultStrategy) HandleOAuth2DeviceAuthorizationRequest( ctx context.Context, w http.ResponseWriter, r *http.Request, -) (*flow.AcceptOAuth2ConsentRequest, *flow.Flow, error) { +) (_ *flow.AcceptOAuth2ConsentRequest, _ *flow.Flow, err error) { + ctx, span := trace.SpanFromContext(ctx).TracerProvider().Tracer("").Start(ctx, "DefaultStrategy.HandleOAuth2DeviceAuthorizationRequest") + defer otelx.End(span, &err) + deviceVerifier := strings.TrimSpace(r.URL.Query().Get("device_verifier")) loginVerifier := strings.TrimSpace(r.URL.Query().Get("login_verifier")) consentVerifier := strings.TrimSpace(r.URL.Query().Get("consent_verifier")) @@ -1239,16 +1231,32 @@ func (s *DefaultStrategy) HandleOAuth2DeviceAuthorizationRequest( ar.RequestedAudience = fosite.Arguments(deviceFlow.RequestedAudience) } - // TODO(nsklikas): wrap these 2 function calls in a transaction (one persists the flow and the other invalidates the user_code) - consentSession, f, err := s.handleOAuth2AuthorizationRequest(ctx, w, r, ar, deviceFlow) - if err != nil { - return nil, nil, err - } - err = s.r.OAuth2Storage().UpdateAndInvalidateUserCodeSessionByRequestID(r.Context(), string(f.DeviceCodeRequestID), f.ID) - if err != nil { - return nil, nil, err + if loginVerifier == "" && consentVerifier == "" { + // ok, we need to process this request and redirect to the authentication endpoint + return nil, nil, s.requestAuthentication(ctx, w, r, ar, deviceFlow) + } else if loginVerifier != "" { + f, err := s.verifyAuthentication(ctx, w, r, ar, loginVerifier) + if err != nil { + return nil, nil, err + } + + // ok, we need to process this request and redirect to consent endpoint + return nil, f, s.requestConsent(ctx, w, r, ar, f) } + var consentSession *flow.AcceptOAuth2ConsentRequest + var f *flow.Flow + + err = s.r.ConsentManager().Transaction(ctx, func(ctx context.Context, c *pop.Connection) error { + consentSession, f, err = s.verifyConsent(ctx, w, r, consentVerifier) + if err != nil { + return err + } + err = s.r.OAuth2Storage().UpdateAndInvalidateUserCodeSessionByRequestID(ctx, string(f.DeviceCodeRequestID), f.ID) + + return err + }) + return consentSession, f, err } @@ -1329,7 +1337,7 @@ func (s *DefaultStrategy) forwardDeviceRequest(ctx context.Context, w http.Respo } func (s *DefaultStrategy) verifyDevice(ctx context.Context, _ http.ResponseWriter, r *http.Request, verifier string) (_ *flow.Flow, err error) { - ctx, span := trace.SpanFromContext(ctx).TracerProvider().Tracer("").Start(ctx, "DefaultStrategy.verifyAuthentication") + ctx, span := trace.SpanFromContext(ctx).TracerProvider().Tracer("").Start(ctx, "DefaultStrategy.verifyDevice") defer otelx.End(span, &err) // We decode the flow from the cookie again because VerifyAndInvalidateDeviceRequest does not return the flow diff --git a/oauth2/handler.go b/oauth2/handler.go index 433ad897891..0e87cfe5d1f 100644 --- a/oauth2/handler.go +++ b/oauth2/handler.go @@ -747,14 +747,12 @@ func (h *Handler) performOAuth2DeviceVerificationFlow(w http.ResponseWriter, r * return } - // TODO(nsklikas): We need to add a db transaction here req, err := h.r.OAuth2Storage().GetDeviceCodeSessionByRequestID(ctx, f.DeviceCodeRequestID.String(), &Session{}) if err != nil { x.LogError(r, err, h.r.Logger()) h.r.Writer().WriteError(w, r, err) return } - // TODO(nsklika): Can we refactor this so we don't have to pass in the session? session, err := h.updateSessionWithRequest(ctx, consentSession, f, r, req, req.GetSession().(*Session)) if err != nil { h.r.Writer().WriteError(w, r, err) From 1656830c1c5fd0bf8686cc620211f831be515b34 Mon Sep 17 00:00:00 2001 From: Nikos Date: Wed, 25 Sep 2024 14:56:40 +0300 Subject: [PATCH 30/52] chore: fix license --- internal/mock/config_cookie.go | 3 +++ jwk/registry_mock_test.go | 4 ++++ oauth2/oauth2_provider_mock_test.go | 4 ++++ 3 files changed, 11 insertions(+) diff --git a/internal/mock/config_cookie.go b/internal/mock/config_cookie.go index d6898a7b8d8..b326baec6b4 100644 --- a/internal/mock/config_cookie.go +++ b/internal/mock/config_cookie.go @@ -1,3 +1,6 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + // Code generated by MockGen. DO NOT EDIT. // Source: github.com/ory/hydra/v2/x (interfaces: CookieConfigProvider) diff --git a/jwk/registry_mock_test.go b/jwk/registry_mock_test.go index 68de41ca30b..f9624dc2b75 100644 --- a/jwk/registry_mock_test.go +++ b/jwk/registry_mock_test.go @@ -1,3 +1,6 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + // Code generated by MockGen. DO NOT EDIT. // Source: jwk/registry.go @@ -8,6 +11,7 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" + herodot "github.com/ory/herodot" aead "github.com/ory/hydra/v2/aead" config "github.com/ory/hydra/v2/driver/config" diff --git a/oauth2/oauth2_provider_mock_test.go b/oauth2/oauth2_provider_mock_test.go index 8149e206b28..7dd35e6a157 100644 --- a/oauth2/oauth2_provider_mock_test.go +++ b/oauth2/oauth2_provider_mock_test.go @@ -1,3 +1,6 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + // Code generated by MockGen. DO NOT EDIT. // Source: github.com/ory/fosite (interfaces: OAuth2Provider) @@ -10,6 +13,7 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" + fosite "github.com/ory/fosite" ) From 4669117fb60e1b1e4f81ed6a60ec15db9871a99f Mon Sep 17 00:00:00 2001 From: Nikos Date: Tue, 24 Sep 2024 13:40:48 +0300 Subject: [PATCH 31/52] chore: update sdk --- internal/httpclient/.openapi-generator/FILES | 8 + internal/httpclient/README.md | 7 + internal/httpclient/api/openapi.yaml | 538 +++++++++++++----- internal/httpclient/api_o_auth2.go | 345 +++++++++++ .../docs/AcceptDeviceUserCodeRequest.md | 56 ++ .../httpclient/docs/DeviceAuthorization.md | 186 ++++++ .../httpclient/docs/DeviceUserAuthRequest.md | 181 ++++++ internal/httpclient/docs/OAuth2API.md | 193 +++++++ internal/httpclient/docs/OAuth2Client.md | 78 +++ .../docs/OAuth2ClientTokenLifespans.md | 78 +++ .../httpclient/docs/OAuth2ConsentRequest.md | 26 + internal/httpclient/docs/OidcConfiguration.md | 23 +- .../httpclient/docs/VerifyUserCodeRequest.md | 212 +++++++ .../model_accept_device_user_code_request.go | 125 ++++ .../httpclient/model_device_authorization.go | 311 ++++++++++ .../model_device_user_auth_request.go | 340 +++++++++++ internal/httpclient/model_o_auth2_client.go | 111 ++++ .../model_o_auth2_client_token_lifespans.go | 111 ++++ .../model_o_auth2_consent_request.go | 37 ++ .../httpclient/model_oidc_configuration.go | 31 +- .../model_verify_user_code_request.go | 344 +++++++++++ internal/testhelpers/oauth2.go | 11 + spec/api.json | 243 ++++++++ spec/swagger.json | 205 ++++++- 24 files changed, 3657 insertions(+), 143 deletions(-) create mode 100644 internal/httpclient/docs/AcceptDeviceUserCodeRequest.md create mode 100644 internal/httpclient/docs/DeviceAuthorization.md create mode 100644 internal/httpclient/docs/DeviceUserAuthRequest.md create mode 100644 internal/httpclient/docs/VerifyUserCodeRequest.md create mode 100644 internal/httpclient/model_accept_device_user_code_request.go create mode 100644 internal/httpclient/model_device_authorization.go create mode 100644 internal/httpclient/model_device_user_auth_request.go create mode 100644 internal/httpclient/model_verify_user_code_request.go diff --git a/internal/httpclient/.openapi-generator/FILES b/internal/httpclient/.openapi-generator/FILES index d0e465ce1c3..395d900b30e 100644 --- a/internal/httpclient/.openapi-generator/FILES +++ b/internal/httpclient/.openapi-generator/FILES @@ -10,12 +10,15 @@ api_oidc.go api_wellknown.go client.go configuration.go +docs/AcceptDeviceUserCodeRequest.md docs/AcceptOAuth2ConsentRequest.md docs/AcceptOAuth2ConsentRequestSession.md docs/AcceptOAuth2LoginRequest.md docs/CreateJsonWebKeySet.md docs/CreateVerifiableCredentialRequestBody.md docs/CredentialSupportedDraft00.md +docs/DeviceAuthorization.md +docs/DeviceUserAuthRequest.md docs/ErrorOAuth2.md docs/GenericError.md docs/GetVersion200Response.md @@ -57,17 +60,21 @@ docs/TrustedOAuth2JwtGrantJsonWebKey.md docs/VerifiableCredentialPrimingResponse.md docs/VerifiableCredentialProof.md docs/VerifiableCredentialResponse.md +docs/VerifyUserCodeRequest.md docs/Version.md docs/WellknownAPI.md git_push.sh go.mod go.sum +model_accept_device_user_code_request.go model_accept_o_auth2_consent_request.go model_accept_o_auth2_consent_request_session.go model_accept_o_auth2_login_request.go model_create_json_web_key_set.go model_create_verifiable_credential_request_body.go model_credential_supported_draft00.go +model_device_authorization.go +model_device_user_auth_request.go model_error_o_auth2.go model_generic_error.go model_get_version_200_response.go @@ -105,6 +112,7 @@ model_trusted_o_auth2_jwt_grant_json_web_key.go model_verifiable_credential_priming_response.go model_verifiable_credential_proof.go model_verifiable_credential_response.go +model_verify_user_code_request.go model_version.go response.go utils.go diff --git a/internal/httpclient/README.md b/internal/httpclient/README.md index a2f17fd7fe5..f5ccc0e780c 100644 --- a/internal/httpclient/README.md +++ b/internal/httpclient/README.md @@ -92,6 +92,7 @@ Class | Method | HTTP request | Description *OAuth2API* | [**AcceptOAuth2ConsentRequest**](docs/OAuth2API.md#acceptoauth2consentrequest) | **Put** /admin/oauth2/auth/requests/consent/accept | Accept OAuth 2.0 Consent Request *OAuth2API* | [**AcceptOAuth2LoginRequest**](docs/OAuth2API.md#acceptoauth2loginrequest) | **Put** /admin/oauth2/auth/requests/login/accept | Accept OAuth 2.0 Login Request *OAuth2API* | [**AcceptOAuth2LogoutRequest**](docs/OAuth2API.md#acceptoauth2logoutrequest) | **Put** /admin/oauth2/auth/requests/logout/accept | Accept OAuth 2.0 Session Logout Request +*OAuth2API* | [**AcceptUserCodeRequest**](docs/OAuth2API.md#acceptusercoderequest) | **Put** /admin/oauth2/auth/requests/device/accept | Accepts a device grant user_code request *OAuth2API* | [**CreateOAuth2Client**](docs/OAuth2API.md#createoauth2client) | **Post** /admin/clients | Create OAuth 2.0 Client *OAuth2API* | [**DeleteOAuth2Client**](docs/OAuth2API.md#deleteoauth2client) | **Delete** /admin/clients/{id} | Delete OAuth 2.0 Client *OAuth2API* | [**DeleteOAuth2Token**](docs/OAuth2API.md#deleteoauth2token) | **Delete** /admin/oauth2/tokens | Delete OAuth 2.0 Access Tokens from specific OAuth 2.0 Client @@ -106,8 +107,10 @@ Class | Method | HTTP request | Description *OAuth2API* | [**ListOAuth2ConsentSessions**](docs/OAuth2API.md#listoauth2consentsessions) | **Get** /admin/oauth2/auth/sessions/consent | List OAuth 2.0 Consent Sessions of a Subject *OAuth2API* | [**ListTrustedOAuth2JwtGrantIssuers**](docs/OAuth2API.md#listtrustedoauth2jwtgrantissuers) | **Get** /admin/trust/grants/jwt-bearer/issuers | List Trusted OAuth2 JWT Bearer Grant Type Issuers *OAuth2API* | [**OAuth2Authorize**](docs/OAuth2API.md#oauth2authorize) | **Get** /oauth2/auth | OAuth 2.0 Authorize Endpoint +*OAuth2API* | [**OAuth2DeviceFlow**](docs/OAuth2API.md#oauth2deviceflow) | **Post** /oauth2/device/auth | The OAuth 2.0 Device Authorize Endpoint *OAuth2API* | [**Oauth2TokenExchange**](docs/OAuth2API.md#oauth2tokenexchange) | **Post** /oauth2/token | The OAuth 2.0 Token Endpoint *OAuth2API* | [**PatchOAuth2Client**](docs/OAuth2API.md#patchoauth2client) | **Patch** /admin/clients/{id} | Patch OAuth 2.0 Client +*OAuth2API* | [**PerformOAuth2DeviceVerificationFlow**](docs/OAuth2API.md#performoauth2deviceverificationflow) | **Get** /oauth2/device/verify | OAuth 2.0 Device Verification Endpoint *OAuth2API* | [**RejectOAuth2ConsentRequest**](docs/OAuth2API.md#rejectoauth2consentrequest) | **Put** /admin/oauth2/auth/requests/consent/reject | Reject OAuth 2.0 Consent Request *OAuth2API* | [**RejectOAuth2LoginRequest**](docs/OAuth2API.md#rejectoauth2loginrequest) | **Put** /admin/oauth2/auth/requests/login/reject | Reject OAuth 2.0 Login Request *OAuth2API* | [**RejectOAuth2LogoutRequest**](docs/OAuth2API.md#rejectoauth2logoutrequest) | **Put** /admin/oauth2/auth/requests/logout/reject | Reject OAuth 2.0 Session Logout Request @@ -130,12 +133,15 @@ Class | Method | HTTP request | Description ## Documentation For Models + - [AcceptDeviceUserCodeRequest](docs/AcceptDeviceUserCodeRequest.md) - [AcceptOAuth2ConsentRequest](docs/AcceptOAuth2ConsentRequest.md) - [AcceptOAuth2ConsentRequestSession](docs/AcceptOAuth2ConsentRequestSession.md) - [AcceptOAuth2LoginRequest](docs/AcceptOAuth2LoginRequest.md) - [CreateJsonWebKeySet](docs/CreateJsonWebKeySet.md) - [CreateVerifiableCredentialRequestBody](docs/CreateVerifiableCredentialRequestBody.md) - [CredentialSupportedDraft00](docs/CredentialSupportedDraft00.md) + - [DeviceAuthorization](docs/DeviceAuthorization.md) + - [DeviceUserAuthRequest](docs/DeviceUserAuthRequest.md) - [ErrorOAuth2](docs/ErrorOAuth2.md) - [GenericError](docs/GenericError.md) - [GetVersion200Response](docs/GetVersion200Response.md) @@ -173,6 +179,7 @@ Class | Method | HTTP request | Description - [VerifiableCredentialPrimingResponse](docs/VerifiableCredentialPrimingResponse.md) - [VerifiableCredentialProof](docs/VerifiableCredentialProof.md) - [VerifiableCredentialResponse](docs/VerifiableCredentialResponse.md) + - [VerifyUserCodeRequest](docs/VerifyUserCodeRequest.md) - [Version](docs/Version.md) diff --git a/internal/httpclient/api/openapi.yaml b/internal/httpclient/api/openapi.yaml index c94ecdf9f1b..36def440519 100644 --- a/internal/httpclient/api/openapi.yaml +++ b/internal/httpclient/api/openapi.yaml @@ -787,6 +787,40 @@ paths: summary: Reject OAuth 2.0 Consent Request tags: - oAuth2 + /admin/oauth2/auth/requests/device/accept: + put: + description: Accepts a device grant user_code request + operationId: acceptUserCodeRequest + parameters: + - explode: true + in: query + name: device_challenge + required: true + schema: + type: string + style: form + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/acceptDeviceUserCodeRequest' + x-originalParamName: Body + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/oAuth2RedirectTo' + description: oAuth2RedirectTo + default: + content: + application/json: + schema: + $ref: '#/components/schemas/errorOAuth2' + description: errorOAuth2 + summary: Accepts a device grant user_code request + tags: + - oAuth2 /admin/oauth2/auth/requests/login: get: description: |- @@ -1512,6 +1546,49 @@ paths: summary: OAuth 2.0 Authorize Endpoint tags: - oAuth2 + /oauth2/device/auth: + post: + description: |- + This endpoint is not documented here because you should never use your own implementation to perform OAuth2 flows. + OAuth2 is a very popular protocol and a library for your programming language will exists. + + To learn more about this flow please refer to the specification: https://tools.ietf.org/html/rfc8628 + operationId: oAuth2DeviceFlow + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/deviceAuthorization' + description: deviceAuthorization + default: + content: + application/json: + schema: + $ref: '#/components/schemas/errorOAuth2' + description: errorOAuth2 + summary: The OAuth 2.0 Device Authorize Endpoint + tags: + - oAuth2 + /oauth2/device/verify: + get: + description: This is the device user verification endpoint. The user is redirected + here when trying to login using the device flow. + operationId: performOAuth2DeviceVerificationFlow + responses: + "302": + description: |- + Empty responses are sent when, for example, resources are deleted. The HTTP status code for empty responses is + typically 204. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/errorOAuth2' + description: errorOAuth2 + summary: OAuth 2.0 Device Verification Endpoint + tags: + - oAuth2 /oauth2/register: post: description: |- @@ -1879,6 +1956,38 @@ components: a verifiable credential. type: object DefaultError: {} + DeviceUserAuthRequest: + properties: + challenge: + description: |- + ID is the identifier ("device challenge") of the device grant request. It is used to + identify the session. + type: string + client: + $ref: '#/components/schemas/oAuth2Client' + handled_at: + format: date-time + title: NullTime implements sql.NullTime functionality. + type: string + request_url: + description: RequestURL is the original Device Authorization URL requested. + type: string + requested_access_token_audience: + items: + type: string + title: "StringSliceJSONFormat represents []string{} which is encoded to/from\ + \ JSON for SQL storage." + type: array + requested_scope: + items: + type: string + title: "StringSliceJSONFormat represents []string{} which is encoded to/from\ + \ JSON for SQL storage." + type: array + required: + - challenge + title: Contains information on an ongoing device grant request. + type: object JSONRawMessage: title: "JSONRawMessage represents a json.RawMessage that works well with JSON,\ \ SQL, and Swagger." @@ -1942,6 +2051,12 @@ components: type: string title: VerifiableCredentialProof contains the proof of a verifiable credential. type: object + acceptDeviceUserCodeRequest: + description: Contains information on an device verification + properties: + user_code: + type: string + type: object acceptOAuth2ConsentRequest: properties: context: @@ -2138,6 +2253,53 @@ components: type: array title: Verifiable Credentials Metadata (Draft 00) type: object + deviceAuthorization: + description: '# Ory''s OAuth 2.0 Device Authorization API' + example: + user_code: AAAAAA + device_code: ory_dc_smldfksmdfkl.mslkmlkmlk + interval: 5 + verification_uri_complete: https://auth.ory.sh/tv?user_code=AAAAAA + verification_uri: https://auth.ory.sh/tv + expires_in: 16830 + properties: + device_code: + description: The device verification code. + example: ory_dc_smldfksmdfkl.mslkmlkmlk + type: string + expires_in: + description: The lifetime in seconds of the "device_code" and "user_code". + example: 16830 + format: int64 + type: integer + interval: + description: |- + The minimum amount of time in seconds that the client + SHOULD wait between polling requests to the token endpoint. If no + value is provided, clients MUST use 5 as the default. + example: 5 + format: int64 + type: integer + user_code: + description: The end-user verification code. + example: AAAAAA + type: string + verification_uri: + description: |- + The end-user verification URI on the authorization + server. The URI should be short and easy to remember as end users + will be asked to manually type it into their user agent. + example: https://auth.ory.sh/tv + type: string + verification_uri_complete: + description: |- + A verification URI that includes the "user_code" (or + other information with the same function as the "user_code"), + which is designed for non-textual transmission. + example: https://auth.ory.sh/tv?user_code=AAAAAA + type: string + title: OAuth2 Device Flow + type: object errorOAuth2: description: Error example: @@ -2568,47 +2730,28 @@ components: generated for applications which want to consume your OAuth 2.0 or OpenID Connect capabilities. example: metadata: "" - token_endpoint_auth_signing_alg: token_endpoint_auth_signing_alg - client_uri: client_uri - jwt_bearer_grant_access_token_lifespan: jwt_bearer_grant_access_token_lifespan - jwks: "" logo_uri: logo_uri - created_at: 2000-01-23T04:56:07.000+00:00 - registration_client_uri: registration_client_uri allowed_cors_origins: - allowed_cors_origins - allowed_cors_origins refresh_token_grant_access_token_lifespan: refresh_token_grant_access_token_lifespan - registration_access_token: registration_access_token client_id: client_id - token_endpoint_auth_method: client_secret_basic - userinfo_signed_response_alg: userinfo_signed_response_alg - authorization_code_grant_id_token_lifespan: authorization_code_grant_id_token_lifespan authorization_code_grant_refresh_token_lifespan: authorization_code_grant_refresh_token_lifespan client_credentials_grant_access_token_lifespan: client_credentials_grant_access_token_lifespan - updated_at: 2000-01-23T04:56:07.000+00:00 - scope: scope1 scope-2 scope.3 scope:4 request_uris: - request_uris - request_uris client_secret: client_secret backchannel_logout_session_required: true backchannel_logout_uri: backchannel_logout_uri - client_name: client_name - policy_uri: policy_uri - owner: owner - skip_consent: true audience: - audience - audience - authorization_code_grant_access_token_lifespan: authorization_code_grant_access_token_lifespan post_logout_redirect_uris: - post_logout_redirect_uris - post_logout_redirect_uris - grant_types: - - grant_types - - grant_types - subject_type: subject_type + device_authorization_grant_id_token_lifespan: device_authorization_grant_id_token_lifespan + device_authorization_grant_access_token_lifespan: device_authorization_grant_access_token_lifespan refresh_token_grant_refresh_token_lifespan: refresh_token_grant_refresh_token_lifespan redirect_uris: - redirect_uris @@ -2616,21 +2759,43 @@ components: sector_identifier_uri: sector_identifier_uri frontchannel_logout_session_required: true frontchannel_logout_uri: frontchannel_logout_uri - skip_logout_consent: true refresh_token_grant_id_token_lifespan: refresh_token_grant_id_token_lifespan + access_token_strategy: access_token_strategy + request_object_signing_alg: request_object_signing_alg + tos_uri: tos_uri + response_types: + - response_types + - response_types + token_endpoint_auth_signing_alg: token_endpoint_auth_signing_alg + client_uri: client_uri + jwt_bearer_grant_access_token_lifespan: jwt_bearer_grant_access_token_lifespan + jwks: "" + created_at: 2000-01-23T04:56:07.000+00:00 + registration_client_uri: registration_client_uri + registration_access_token: registration_access_token + token_endpoint_auth_method: client_secret_basic + userinfo_signed_response_alg: userinfo_signed_response_alg + authorization_code_grant_id_token_lifespan: authorization_code_grant_id_token_lifespan + updated_at: 2000-01-23T04:56:07.000+00:00 + scope: scope1 scope-2 scope.3 scope:4 + device_authorization_grant_refresh_token_lifespan: device_authorization_grant_refresh_token_lifespan + client_name: client_name + policy_uri: policy_uri + owner: owner + skip_consent: true + authorization_code_grant_access_token_lifespan: authorization_code_grant_access_token_lifespan + grant_types: + - grant_types + - grant_types + subject_type: subject_type + skip_logout_consent: true implicit_grant_id_token_lifespan: implicit_grant_id_token_lifespan client_secret_expires_at: 0 implicit_grant_access_token_lifespan: implicit_grant_access_token_lifespan - access_token_strategy: access_token_strategy jwks_uri: jwks_uri - request_object_signing_alg: request_object_signing_alg - tos_uri: tos_uri contacts: - contacts - contacts - response_types: - - response_types - - response_types properties: access_token_strategy: description: |- @@ -2738,6 +2903,24 @@ components: CreatedAt returns the timestamp of the client's creation. format: date-time type: string + device_authorization_grant_access_token_lifespan: + description: "Specify a time duration in milliseconds, seconds, minutes,\ + \ hours." + pattern: "^([0-9]+(ns|us|ms|s|m|h))*$" + title: Time duration + type: string + device_authorization_grant_id_token_lifespan: + description: "Specify a time duration in milliseconds, seconds, minutes,\ + \ hours." + pattern: "^([0-9]+(ns|us|ms|s|m|h))*$" + title: Time duration + type: string + device_authorization_grant_refresh_token_lifespan: + description: "Specify a time duration in milliseconds, seconds, minutes,\ + \ hours." + pattern: "^([0-9]+(ns|us|ms|s|m|h))*$" + title: Time duration + type: string frontchannel_logout_session_required: description: |- OpenID Connect Front-Channel Logout Session Required @@ -2992,6 +3175,24 @@ components: pattern: "^([0-9]+(ns|us|ms|s|m|h))*$" title: Time duration type: string + device_authorization_grant_access_token_lifespan: + description: "Specify a time duration in milliseconds, seconds, minutes,\ + \ hours." + pattern: "^([0-9]+(ns|us|ms|s|m|h))*$" + title: Time duration + type: string + device_authorization_grant_id_token_lifespan: + description: "Specify a time duration in milliseconds, seconds, minutes,\ + \ hours." + pattern: "^([0-9]+(ns|us|ms|s|m|h))*$" + title: Time duration + type: string + device_authorization_grant_refresh_token_lifespan: + description: "Specify a time duration in milliseconds, seconds, minutes,\ + \ hours." + pattern: "^([0-9]+(ns|us|ms|s|m|h))*$" + title: Time duration + type: string implicit_grant_access_token_lifespan: description: "Specify a time duration in milliseconds, seconds, minutes,\ \ hours." @@ -3052,6 +3253,7 @@ components: - acr_values - acr_values display: display + device_challenge_id: device_challenge_id skip: true request_url: request_url acr: acr @@ -3059,47 +3261,28 @@ components: challenge: challenge client: metadata: "" - token_endpoint_auth_signing_alg: token_endpoint_auth_signing_alg - client_uri: client_uri - jwt_bearer_grant_access_token_lifespan: jwt_bearer_grant_access_token_lifespan - jwks: "" logo_uri: logo_uri - created_at: 2000-01-23T04:56:07.000+00:00 - registration_client_uri: registration_client_uri allowed_cors_origins: - allowed_cors_origins - allowed_cors_origins refresh_token_grant_access_token_lifespan: refresh_token_grant_access_token_lifespan - registration_access_token: registration_access_token client_id: client_id - token_endpoint_auth_method: client_secret_basic - userinfo_signed_response_alg: userinfo_signed_response_alg - authorization_code_grant_id_token_lifespan: authorization_code_grant_id_token_lifespan authorization_code_grant_refresh_token_lifespan: authorization_code_grant_refresh_token_lifespan client_credentials_grant_access_token_lifespan: client_credentials_grant_access_token_lifespan - updated_at: 2000-01-23T04:56:07.000+00:00 - scope: scope1 scope-2 scope.3 scope:4 request_uris: - request_uris - request_uris client_secret: client_secret backchannel_logout_session_required: true backchannel_logout_uri: backchannel_logout_uri - client_name: client_name - policy_uri: policy_uri - owner: owner - skip_consent: true audience: - audience - audience - authorization_code_grant_access_token_lifespan: authorization_code_grant_access_token_lifespan post_logout_redirect_uris: - post_logout_redirect_uris - post_logout_redirect_uris - grant_types: - - grant_types - - grant_types - subject_type: subject_type + device_authorization_grant_id_token_lifespan: device_authorization_grant_id_token_lifespan + device_authorization_grant_access_token_lifespan: device_authorization_grant_access_token_lifespan refresh_token_grant_refresh_token_lifespan: refresh_token_grant_refresh_token_lifespan redirect_uris: - redirect_uris @@ -3107,21 +3290,43 @@ components: sector_identifier_uri: sector_identifier_uri frontchannel_logout_session_required: true frontchannel_logout_uri: frontchannel_logout_uri - skip_logout_consent: true refresh_token_grant_id_token_lifespan: refresh_token_grant_id_token_lifespan + access_token_strategy: access_token_strategy + request_object_signing_alg: request_object_signing_alg + tos_uri: tos_uri + response_types: + - response_types + - response_types + token_endpoint_auth_signing_alg: token_endpoint_auth_signing_alg + client_uri: client_uri + jwt_bearer_grant_access_token_lifespan: jwt_bearer_grant_access_token_lifespan + jwks: "" + created_at: 2000-01-23T04:56:07.000+00:00 + registration_client_uri: registration_client_uri + registration_access_token: registration_access_token + token_endpoint_auth_method: client_secret_basic + userinfo_signed_response_alg: userinfo_signed_response_alg + authorization_code_grant_id_token_lifespan: authorization_code_grant_id_token_lifespan + updated_at: 2000-01-23T04:56:07.000+00:00 + scope: scope1 scope-2 scope.3 scope:4 + device_authorization_grant_refresh_token_lifespan: device_authorization_grant_refresh_token_lifespan + client_name: client_name + policy_uri: policy_uri + owner: owner + skip_consent: true + authorization_code_grant_access_token_lifespan: authorization_code_grant_access_token_lifespan + grant_types: + - grant_types + - grant_types + subject_type: subject_type + skip_logout_consent: true implicit_grant_id_token_lifespan: implicit_grant_id_token_lifespan client_secret_expires_at: 0 implicit_grant_access_token_lifespan: implicit_grant_access_token_lifespan - access_token_strategy: access_token_strategy jwks_uri: jwks_uri - request_object_signing_alg: request_object_signing_alg - tos_uri: tos_uri contacts: - contacts - contacts - response_types: - - response_types - - response_types login_session_id: login_session_id requested_scope: - requested_scope @@ -3149,6 +3354,10 @@ components: context: title: "JSONRawMessage represents a json.RawMessage that works well with\ \ JSON, SQL, and Swagger." + device_challenge_id: + description: "DeviceChallenge is the device challenge this consent challenge\ + \ belongs to, if this flow was initiated by a device." + type: string login_challenge: description: |- LoginChallenge is the login challenge this consent challenge belongs to. It can be used to associate @@ -3285,6 +3494,7 @@ components: - acr_values - acr_values display: display + device_challenge_id: device_challenge_id skip: true request_url: request_url acr: acr @@ -3292,47 +3502,28 @@ components: challenge: challenge client: metadata: "" - token_endpoint_auth_signing_alg: token_endpoint_auth_signing_alg - client_uri: client_uri - jwt_bearer_grant_access_token_lifespan: jwt_bearer_grant_access_token_lifespan - jwks: "" logo_uri: logo_uri - created_at: 2000-01-23T04:56:07.000+00:00 - registration_client_uri: registration_client_uri allowed_cors_origins: - allowed_cors_origins - allowed_cors_origins refresh_token_grant_access_token_lifespan: refresh_token_grant_access_token_lifespan - registration_access_token: registration_access_token client_id: client_id - token_endpoint_auth_method: client_secret_basic - userinfo_signed_response_alg: userinfo_signed_response_alg - authorization_code_grant_id_token_lifespan: authorization_code_grant_id_token_lifespan authorization_code_grant_refresh_token_lifespan: authorization_code_grant_refresh_token_lifespan client_credentials_grant_access_token_lifespan: client_credentials_grant_access_token_lifespan - updated_at: 2000-01-23T04:56:07.000+00:00 - scope: scope1 scope-2 scope.3 scope:4 request_uris: - request_uris - request_uris client_secret: client_secret backchannel_logout_session_required: true backchannel_logout_uri: backchannel_logout_uri - client_name: client_name - policy_uri: policy_uri - owner: owner - skip_consent: true audience: - audience - audience - authorization_code_grant_access_token_lifespan: authorization_code_grant_access_token_lifespan post_logout_redirect_uris: - post_logout_redirect_uris - post_logout_redirect_uris - grant_types: - - grant_types - - grant_types - subject_type: subject_type + device_authorization_grant_id_token_lifespan: device_authorization_grant_id_token_lifespan + device_authorization_grant_access_token_lifespan: device_authorization_grant_access_token_lifespan refresh_token_grant_refresh_token_lifespan: refresh_token_grant_refresh_token_lifespan redirect_uris: - redirect_uris @@ -3340,21 +3531,43 @@ components: sector_identifier_uri: sector_identifier_uri frontchannel_logout_session_required: true frontchannel_logout_uri: frontchannel_logout_uri - skip_logout_consent: true refresh_token_grant_id_token_lifespan: refresh_token_grant_id_token_lifespan + access_token_strategy: access_token_strategy + request_object_signing_alg: request_object_signing_alg + tos_uri: tos_uri + response_types: + - response_types + - response_types + token_endpoint_auth_signing_alg: token_endpoint_auth_signing_alg + client_uri: client_uri + jwt_bearer_grant_access_token_lifespan: jwt_bearer_grant_access_token_lifespan + jwks: "" + created_at: 2000-01-23T04:56:07.000+00:00 + registration_client_uri: registration_client_uri + registration_access_token: registration_access_token + token_endpoint_auth_method: client_secret_basic + userinfo_signed_response_alg: userinfo_signed_response_alg + authorization_code_grant_id_token_lifespan: authorization_code_grant_id_token_lifespan + updated_at: 2000-01-23T04:56:07.000+00:00 + scope: scope1 scope-2 scope.3 scope:4 + device_authorization_grant_refresh_token_lifespan: device_authorization_grant_refresh_token_lifespan + client_name: client_name + policy_uri: policy_uri + owner: owner + skip_consent: true + authorization_code_grant_access_token_lifespan: authorization_code_grant_access_token_lifespan + grant_types: + - grant_types + - grant_types + subject_type: subject_type + skip_logout_consent: true implicit_grant_id_token_lifespan: implicit_grant_id_token_lifespan client_secret_expires_at: 0 implicit_grant_access_token_lifespan: implicit_grant_access_token_lifespan - access_token_strategy: access_token_strategy jwks_uri: jwks_uri - request_object_signing_alg: request_object_signing_alg - tos_uri: tos_uri contacts: - contacts - contacts - response_types: - - response_types - - response_types login_session_id: login_session_id requested_scope: - requested_scope @@ -3449,47 +3662,28 @@ components: challenge: challenge client: metadata: "" - token_endpoint_auth_signing_alg: token_endpoint_auth_signing_alg - client_uri: client_uri - jwt_bearer_grant_access_token_lifespan: jwt_bearer_grant_access_token_lifespan - jwks: "" logo_uri: logo_uri - created_at: 2000-01-23T04:56:07.000+00:00 - registration_client_uri: registration_client_uri allowed_cors_origins: - allowed_cors_origins - allowed_cors_origins refresh_token_grant_access_token_lifespan: refresh_token_grant_access_token_lifespan - registration_access_token: registration_access_token client_id: client_id - token_endpoint_auth_method: client_secret_basic - userinfo_signed_response_alg: userinfo_signed_response_alg - authorization_code_grant_id_token_lifespan: authorization_code_grant_id_token_lifespan authorization_code_grant_refresh_token_lifespan: authorization_code_grant_refresh_token_lifespan client_credentials_grant_access_token_lifespan: client_credentials_grant_access_token_lifespan - updated_at: 2000-01-23T04:56:07.000+00:00 - scope: scope1 scope-2 scope.3 scope:4 request_uris: - request_uris - request_uris client_secret: client_secret backchannel_logout_session_required: true backchannel_logout_uri: backchannel_logout_uri - client_name: client_name - policy_uri: policy_uri - owner: owner - skip_consent: true audience: - audience - audience - authorization_code_grant_access_token_lifespan: authorization_code_grant_access_token_lifespan post_logout_redirect_uris: - post_logout_redirect_uris - post_logout_redirect_uris - grant_types: - - grant_types - - grant_types - subject_type: subject_type + device_authorization_grant_id_token_lifespan: device_authorization_grant_id_token_lifespan + device_authorization_grant_access_token_lifespan: device_authorization_grant_access_token_lifespan refresh_token_grant_refresh_token_lifespan: refresh_token_grant_refresh_token_lifespan redirect_uris: - redirect_uris @@ -3497,21 +3691,43 @@ components: sector_identifier_uri: sector_identifier_uri frontchannel_logout_session_required: true frontchannel_logout_uri: frontchannel_logout_uri - skip_logout_consent: true refresh_token_grant_id_token_lifespan: refresh_token_grant_id_token_lifespan + access_token_strategy: access_token_strategy + request_object_signing_alg: request_object_signing_alg + tos_uri: tos_uri + response_types: + - response_types + - response_types + token_endpoint_auth_signing_alg: token_endpoint_auth_signing_alg + client_uri: client_uri + jwt_bearer_grant_access_token_lifespan: jwt_bearer_grant_access_token_lifespan + jwks: "" + created_at: 2000-01-23T04:56:07.000+00:00 + registration_client_uri: registration_client_uri + registration_access_token: registration_access_token + token_endpoint_auth_method: client_secret_basic + userinfo_signed_response_alg: userinfo_signed_response_alg + authorization_code_grant_id_token_lifespan: authorization_code_grant_id_token_lifespan + updated_at: 2000-01-23T04:56:07.000+00:00 + scope: scope1 scope-2 scope.3 scope:4 + device_authorization_grant_refresh_token_lifespan: device_authorization_grant_refresh_token_lifespan + client_name: client_name + policy_uri: policy_uri + owner: owner + skip_consent: true + authorization_code_grant_access_token_lifespan: authorization_code_grant_access_token_lifespan + grant_types: + - grant_types + - grant_types + subject_type: subject_type + skip_logout_consent: true implicit_grant_id_token_lifespan: implicit_grant_id_token_lifespan client_secret_expires_at: 0 implicit_grant_access_token_lifespan: implicit_grant_access_token_lifespan - access_token_strategy: access_token_strategy jwks_uri: jwks_uri - request_object_signing_alg: request_object_signing_alg - tos_uri: tos_uri contacts: - contacts - contacts - response_types: - - response_types - - response_types session_id: session_id skip: true request_url: request_url @@ -3579,47 +3795,28 @@ components: challenge: challenge client: metadata: "" - token_endpoint_auth_signing_alg: token_endpoint_auth_signing_alg - client_uri: client_uri - jwt_bearer_grant_access_token_lifespan: jwt_bearer_grant_access_token_lifespan - jwks: "" logo_uri: logo_uri - created_at: 2000-01-23T04:56:07.000+00:00 - registration_client_uri: registration_client_uri allowed_cors_origins: - allowed_cors_origins - allowed_cors_origins refresh_token_grant_access_token_lifespan: refresh_token_grant_access_token_lifespan - registration_access_token: registration_access_token client_id: client_id - token_endpoint_auth_method: client_secret_basic - userinfo_signed_response_alg: userinfo_signed_response_alg - authorization_code_grant_id_token_lifespan: authorization_code_grant_id_token_lifespan authorization_code_grant_refresh_token_lifespan: authorization_code_grant_refresh_token_lifespan client_credentials_grant_access_token_lifespan: client_credentials_grant_access_token_lifespan - updated_at: 2000-01-23T04:56:07.000+00:00 - scope: scope1 scope-2 scope.3 scope:4 request_uris: - request_uris - request_uris client_secret: client_secret backchannel_logout_session_required: true backchannel_logout_uri: backchannel_logout_uri - client_name: client_name - policy_uri: policy_uri - owner: owner - skip_consent: true audience: - audience - audience - authorization_code_grant_access_token_lifespan: authorization_code_grant_access_token_lifespan post_logout_redirect_uris: - post_logout_redirect_uris - post_logout_redirect_uris - grant_types: - - grant_types - - grant_types - subject_type: subject_type + device_authorization_grant_id_token_lifespan: device_authorization_grant_id_token_lifespan + device_authorization_grant_access_token_lifespan: device_authorization_grant_access_token_lifespan refresh_token_grant_refresh_token_lifespan: refresh_token_grant_refresh_token_lifespan redirect_uris: - redirect_uris @@ -3627,21 +3824,43 @@ components: sector_identifier_uri: sector_identifier_uri frontchannel_logout_session_required: true frontchannel_logout_uri: frontchannel_logout_uri - skip_logout_consent: true refresh_token_grant_id_token_lifespan: refresh_token_grant_id_token_lifespan + access_token_strategy: access_token_strategy + request_object_signing_alg: request_object_signing_alg + tos_uri: tos_uri + response_types: + - response_types + - response_types + token_endpoint_auth_signing_alg: token_endpoint_auth_signing_alg + client_uri: client_uri + jwt_bearer_grant_access_token_lifespan: jwt_bearer_grant_access_token_lifespan + jwks: "" + created_at: 2000-01-23T04:56:07.000+00:00 + registration_client_uri: registration_client_uri + registration_access_token: registration_access_token + token_endpoint_auth_method: client_secret_basic + userinfo_signed_response_alg: userinfo_signed_response_alg + authorization_code_grant_id_token_lifespan: authorization_code_grant_id_token_lifespan + updated_at: 2000-01-23T04:56:07.000+00:00 + scope: scope1 scope-2 scope.3 scope:4 + device_authorization_grant_refresh_token_lifespan: device_authorization_grant_refresh_token_lifespan + client_name: client_name + policy_uri: policy_uri + owner: owner + skip_consent: true + authorization_code_grant_access_token_lifespan: authorization_code_grant_access_token_lifespan + grant_types: + - grant_types + - grant_types + subject_type: subject_type + skip_logout_consent: true implicit_grant_id_token_lifespan: implicit_grant_id_token_lifespan client_secret_expires_at: 0 implicit_grant_access_token_lifespan: implicit_grant_access_token_lifespan - access_token_strategy: access_token_strategy jwks_uri: jwks_uri - request_object_signing_alg: request_object_signing_alg - tos_uri: tos_uri contacts: - contacts - contacts - response_types: - - response_types - - response_types rp_initiated: true request_url: request_url requested_at: 2000-01-23T04:56:07.000+00:00 @@ -3741,6 +3960,7 @@ components: - userinfo_signed_response_alg - userinfo_signed_response_alg authorization_endpoint: https://playground.ory.sh/ory-hydra/public/oauth2/auth + device_authorization_endpoint: https://playground.ory.sh/ory-hydra/public/oauth2/device/oauth claims_supported: - claims_supported - claims_supported @@ -3863,6 +4083,10 @@ components: items: $ref: '#/components/schemas/credentialSupportedDraft00' type: array + device_authorization_endpoint: + description: OAuth 2.0 Device Authorization Endpoint URL + example: https://playground.ory.sh/ory-hydra/public/oauth2/device/oauth + type: string end_session_endpoint: description: |- OpenID Connect End-Session Endpoint @@ -4042,6 +4266,7 @@ components: type: array required: - authorization_endpoint + - device_authorization_endpoint - id_token_signed_response_alg - id_token_signing_alg_values_supported - issuer @@ -4509,6 +4734,39 @@ components: type: string title: VerifiableCredentialResponse contains the verifiable credential. type: object + verifyUserCodeRequest: + properties: + challenge: + description: |- + ID is the identifier ("device challenge") of the device request. It is used to + identify the session. + type: string + client: + $ref: '#/components/schemas/oAuth2Client' + device_code_request_id: + type: string + handled_at: + format: date-time + title: NullTime implements sql.NullTime functionality. + type: string + request_url: + description: RequestURL is the original Device Authorization URL requested. + type: string + requested_access_token_audience: + items: + type: string + title: "StringSliceJSONFormat represents []string{} which is encoded to/from\ + \ JSON for SQL storage." + type: array + requested_scope: + items: + type: string + title: "StringSliceJSONFormat represents []string{} which is encoded to/from\ + \ JSON for SQL storage." + type: array + title: HandledDeviceUserAuthRequest is the request payload used to accept a + device user_code. + type: object version: properties: version: diff --git a/internal/httpclient/api_o_auth2.go b/internal/httpclient/api_o_auth2.go index 867f4a2af49..e25afb3871c 100644 --- a/internal/httpclient/api_o_auth2.go +++ b/internal/httpclient/api_o_auth2.go @@ -423,6 +423,132 @@ func (a *OAuth2APIService) AcceptOAuth2LogoutRequestExecute(r ApiAcceptOAuth2Log return localVarReturnValue, localVarHTTPResponse, nil } +type ApiAcceptUserCodeRequestRequest struct { + ctx context.Context + ApiService *OAuth2APIService + deviceChallenge *string + acceptDeviceUserCodeRequest *AcceptDeviceUserCodeRequest +} + +func (r ApiAcceptUserCodeRequestRequest) DeviceChallenge(deviceChallenge string) ApiAcceptUserCodeRequestRequest { + r.deviceChallenge = &deviceChallenge + return r +} + +func (r ApiAcceptUserCodeRequestRequest) AcceptDeviceUserCodeRequest(acceptDeviceUserCodeRequest AcceptDeviceUserCodeRequest) ApiAcceptUserCodeRequestRequest { + r.acceptDeviceUserCodeRequest = &acceptDeviceUserCodeRequest + return r +} + +func (r ApiAcceptUserCodeRequestRequest) Execute() (*OAuth2RedirectTo, *http.Response, error) { + return r.ApiService.AcceptUserCodeRequestExecute(r) +} + +/* +AcceptUserCodeRequest Accepts a device grant user_code request + +Accepts a device grant user_code request + + @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + @return ApiAcceptUserCodeRequestRequest +*/ +func (a *OAuth2APIService) AcceptUserCodeRequest(ctx context.Context) ApiAcceptUserCodeRequestRequest { + return ApiAcceptUserCodeRequestRequest{ + ApiService: a, + ctx: ctx, + } +} + +// Execute executes the request +// +// @return OAuth2RedirectTo +func (a *OAuth2APIService) AcceptUserCodeRequestExecute(r ApiAcceptUserCodeRequestRequest) (*OAuth2RedirectTo, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodPut + localVarPostBody interface{} + formFiles []formFile + localVarReturnValue *OAuth2RedirectTo + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "OAuth2APIService.AcceptUserCodeRequest") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/admin/oauth2/auth/requests/device/accept" + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + if r.deviceChallenge == nil { + return localVarReturnValue, nil, reportError("deviceChallenge is required and must be specified") + } + + parameterAddToHeaderOrQuery(localVarQueryParams, "device_challenge", r.deviceChallenge, "") + // to determine the Content-Type header + localVarHTTPContentTypes := []string{"application/json"} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + // body params + localVarPostBody = r.acceptDeviceUserCodeRequest + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + var v ErrorOAuth2 + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} + type ApiCreateOAuth2ClientRequest struct { ctx context.Context ApiService *OAuth2APIService @@ -2196,6 +2322,117 @@ func (a *OAuth2APIService) OAuth2AuthorizeExecute(r ApiOAuth2AuthorizeRequest) ( return localVarReturnValue, localVarHTTPResponse, nil } +type ApiOAuth2DeviceFlowRequest struct { + ctx context.Context + ApiService *OAuth2APIService +} + +func (r ApiOAuth2DeviceFlowRequest) Execute() (*DeviceAuthorization, *http.Response, error) { + return r.ApiService.OAuth2DeviceFlowExecute(r) +} + +/* +OAuth2DeviceFlow The OAuth 2.0 Device Authorize Endpoint + +This endpoint is not documented here because you should never use your own implementation to perform OAuth2 flows. +OAuth2 is a very popular protocol and a library for your programming language will exists. + +To learn more about this flow please refer to the specification: https://tools.ietf.org/html/rfc8628 + + @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + @return ApiOAuth2DeviceFlowRequest +*/ +func (a *OAuth2APIService) OAuth2DeviceFlow(ctx context.Context) ApiOAuth2DeviceFlowRequest { + return ApiOAuth2DeviceFlowRequest{ + ApiService: a, + ctx: ctx, + } +} + +// Execute executes the request +// +// @return DeviceAuthorization +func (a *OAuth2APIService) OAuth2DeviceFlowExecute(r ApiOAuth2DeviceFlowRequest) (*DeviceAuthorization, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodPost + localVarPostBody interface{} + formFiles []formFile + localVarReturnValue *DeviceAuthorization + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "OAuth2APIService.OAuth2DeviceFlow") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/oauth2/device/auth" + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + var v ErrorOAuth2 + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} + type ApiOauth2TokenExchangeRequest struct { ctx context.Context ApiService *OAuth2APIService @@ -2494,6 +2731,114 @@ func (a *OAuth2APIService) PatchOAuth2ClientExecute(r ApiPatchOAuth2ClientReques return localVarReturnValue, localVarHTTPResponse, nil } +type ApiPerformOAuth2DeviceVerificationFlowRequest struct { + ctx context.Context + ApiService *OAuth2APIService +} + +func (r ApiPerformOAuth2DeviceVerificationFlowRequest) Execute() (*ErrorOAuth2, *http.Response, error) { + return r.ApiService.PerformOAuth2DeviceVerificationFlowExecute(r) +} + +/* +PerformOAuth2DeviceVerificationFlow OAuth 2.0 Device Verification Endpoint + +This is the device user verification endpoint. The user is redirected here when trying to login using the device flow. + + @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + @return ApiPerformOAuth2DeviceVerificationFlowRequest +*/ +func (a *OAuth2APIService) PerformOAuth2DeviceVerificationFlow(ctx context.Context) ApiPerformOAuth2DeviceVerificationFlowRequest { + return ApiPerformOAuth2DeviceVerificationFlowRequest{ + ApiService: a, + ctx: ctx, + } +} + +// Execute executes the request +// +// @return ErrorOAuth2 +func (a *OAuth2APIService) PerformOAuth2DeviceVerificationFlowExecute(r ApiPerformOAuth2DeviceVerificationFlowRequest) (*ErrorOAuth2, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodGet + localVarPostBody interface{} + formFiles []formFile + localVarReturnValue *ErrorOAuth2 + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "OAuth2APIService.PerformOAuth2DeviceVerificationFlow") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/oauth2/device/verify" + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + var v ErrorOAuth2 + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} + type ApiRejectOAuth2ConsentRequestRequest struct { ctx context.Context ApiService *OAuth2APIService diff --git a/internal/httpclient/docs/AcceptDeviceUserCodeRequest.md b/internal/httpclient/docs/AcceptDeviceUserCodeRequest.md new file mode 100644 index 00000000000..2f892922a77 --- /dev/null +++ b/internal/httpclient/docs/AcceptDeviceUserCodeRequest.md @@ -0,0 +1,56 @@ +# AcceptDeviceUserCodeRequest + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**UserCode** | Pointer to **string** | | [optional] + +## Methods + +### NewAcceptDeviceUserCodeRequest + +`func NewAcceptDeviceUserCodeRequest() *AcceptDeviceUserCodeRequest` + +NewAcceptDeviceUserCodeRequest instantiates a new AcceptDeviceUserCodeRequest object +This constructor will assign default values to properties that have it defined, +and makes sure properties required by API are set, but the set of arguments +will change when the set of required properties is changed + +### NewAcceptDeviceUserCodeRequestWithDefaults + +`func NewAcceptDeviceUserCodeRequestWithDefaults() *AcceptDeviceUserCodeRequest` + +NewAcceptDeviceUserCodeRequestWithDefaults instantiates a new AcceptDeviceUserCodeRequest object +This constructor will only assign default values to properties that have it defined, +but it doesn't guarantee that properties required by API are set + +### GetUserCode + +`func (o *AcceptDeviceUserCodeRequest) GetUserCode() string` + +GetUserCode returns the UserCode field if non-nil, zero value otherwise. + +### GetUserCodeOk + +`func (o *AcceptDeviceUserCodeRequest) GetUserCodeOk() (*string, bool)` + +GetUserCodeOk returns a tuple with the UserCode field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetUserCode + +`func (o *AcceptDeviceUserCodeRequest) SetUserCode(v string)` + +SetUserCode sets UserCode field to given value. + +### HasUserCode + +`func (o *AcceptDeviceUserCodeRequest) HasUserCode() bool` + +HasUserCode returns a boolean if a field has been set. + + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/internal/httpclient/docs/DeviceAuthorization.md b/internal/httpclient/docs/DeviceAuthorization.md new file mode 100644 index 00000000000..4ba933a4b24 --- /dev/null +++ b/internal/httpclient/docs/DeviceAuthorization.md @@ -0,0 +1,186 @@ +# DeviceAuthorization + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**DeviceCode** | Pointer to **string** | The device verification code. | [optional] +**ExpiresIn** | Pointer to **int64** | The lifetime in seconds of the \"device_code\" and \"user_code\". | [optional] +**Interval** | Pointer to **int64** | The minimum amount of time in seconds that the client SHOULD wait between polling requests to the token endpoint. If no value is provided, clients MUST use 5 as the default. | [optional] +**UserCode** | Pointer to **string** | The end-user verification code. | [optional] +**VerificationUri** | Pointer to **string** | The end-user verification URI on the authorization server. The URI should be short and easy to remember as end users will be asked to manually type it into their user agent. | [optional] +**VerificationUriComplete** | Pointer to **string** | A verification URI that includes the \"user_code\" (or other information with the same function as the \"user_code\"), which is designed for non-textual transmission. | [optional] + +## Methods + +### NewDeviceAuthorization + +`func NewDeviceAuthorization() *DeviceAuthorization` + +NewDeviceAuthorization instantiates a new DeviceAuthorization object +This constructor will assign default values to properties that have it defined, +and makes sure properties required by API are set, but the set of arguments +will change when the set of required properties is changed + +### NewDeviceAuthorizationWithDefaults + +`func NewDeviceAuthorizationWithDefaults() *DeviceAuthorization` + +NewDeviceAuthorizationWithDefaults instantiates a new DeviceAuthorization object +This constructor will only assign default values to properties that have it defined, +but it doesn't guarantee that properties required by API are set + +### GetDeviceCode + +`func (o *DeviceAuthorization) GetDeviceCode() string` + +GetDeviceCode returns the DeviceCode field if non-nil, zero value otherwise. + +### GetDeviceCodeOk + +`func (o *DeviceAuthorization) GetDeviceCodeOk() (*string, bool)` + +GetDeviceCodeOk returns a tuple with the DeviceCode field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetDeviceCode + +`func (o *DeviceAuthorization) SetDeviceCode(v string)` + +SetDeviceCode sets DeviceCode field to given value. + +### HasDeviceCode + +`func (o *DeviceAuthorization) HasDeviceCode() bool` + +HasDeviceCode returns a boolean if a field has been set. + +### GetExpiresIn + +`func (o *DeviceAuthorization) GetExpiresIn() int64` + +GetExpiresIn returns the ExpiresIn field if non-nil, zero value otherwise. + +### GetExpiresInOk + +`func (o *DeviceAuthorization) GetExpiresInOk() (*int64, bool)` + +GetExpiresInOk returns a tuple with the ExpiresIn field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetExpiresIn + +`func (o *DeviceAuthorization) SetExpiresIn(v int64)` + +SetExpiresIn sets ExpiresIn field to given value. + +### HasExpiresIn + +`func (o *DeviceAuthorization) HasExpiresIn() bool` + +HasExpiresIn returns a boolean if a field has been set. + +### GetInterval + +`func (o *DeviceAuthorization) GetInterval() int64` + +GetInterval returns the Interval field if non-nil, zero value otherwise. + +### GetIntervalOk + +`func (o *DeviceAuthorization) GetIntervalOk() (*int64, bool)` + +GetIntervalOk returns a tuple with the Interval field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetInterval + +`func (o *DeviceAuthorization) SetInterval(v int64)` + +SetInterval sets Interval field to given value. + +### HasInterval + +`func (o *DeviceAuthorization) HasInterval() bool` + +HasInterval returns a boolean if a field has been set. + +### GetUserCode + +`func (o *DeviceAuthorization) GetUserCode() string` + +GetUserCode returns the UserCode field if non-nil, zero value otherwise. + +### GetUserCodeOk + +`func (o *DeviceAuthorization) GetUserCodeOk() (*string, bool)` + +GetUserCodeOk returns a tuple with the UserCode field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetUserCode + +`func (o *DeviceAuthorization) SetUserCode(v string)` + +SetUserCode sets UserCode field to given value. + +### HasUserCode + +`func (o *DeviceAuthorization) HasUserCode() bool` + +HasUserCode returns a boolean if a field has been set. + +### GetVerificationUri + +`func (o *DeviceAuthorization) GetVerificationUri() string` + +GetVerificationUri returns the VerificationUri field if non-nil, zero value otherwise. + +### GetVerificationUriOk + +`func (o *DeviceAuthorization) GetVerificationUriOk() (*string, bool)` + +GetVerificationUriOk returns a tuple with the VerificationUri field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetVerificationUri + +`func (o *DeviceAuthorization) SetVerificationUri(v string)` + +SetVerificationUri sets VerificationUri field to given value. + +### HasVerificationUri + +`func (o *DeviceAuthorization) HasVerificationUri() bool` + +HasVerificationUri returns a boolean if a field has been set. + +### GetVerificationUriComplete + +`func (o *DeviceAuthorization) GetVerificationUriComplete() string` + +GetVerificationUriComplete returns the VerificationUriComplete field if non-nil, zero value otherwise. + +### GetVerificationUriCompleteOk + +`func (o *DeviceAuthorization) GetVerificationUriCompleteOk() (*string, bool)` + +GetVerificationUriCompleteOk returns a tuple with the VerificationUriComplete field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetVerificationUriComplete + +`func (o *DeviceAuthorization) SetVerificationUriComplete(v string)` + +SetVerificationUriComplete sets VerificationUriComplete field to given value. + +### HasVerificationUriComplete + +`func (o *DeviceAuthorization) HasVerificationUriComplete() bool` + +HasVerificationUriComplete returns a boolean if a field has been set. + + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/internal/httpclient/docs/DeviceUserAuthRequest.md b/internal/httpclient/docs/DeviceUserAuthRequest.md new file mode 100644 index 00000000000..ae99e6223ff --- /dev/null +++ b/internal/httpclient/docs/DeviceUserAuthRequest.md @@ -0,0 +1,181 @@ +# DeviceUserAuthRequest + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**Challenge** | **string** | ID is the identifier (\"device challenge\") of the device grant request. It is used to identify the session. | +**Client** | Pointer to [**OAuth2Client**](OAuth2Client.md) | | [optional] +**HandledAt** | Pointer to **time.Time** | | [optional] +**RequestUrl** | Pointer to **string** | RequestURL is the original Device Authorization URL requested. | [optional] +**RequestedAccessTokenAudience** | Pointer to **[]string** | | [optional] +**RequestedScope** | Pointer to **[]string** | | [optional] + +## Methods + +### NewDeviceUserAuthRequest + +`func NewDeviceUserAuthRequest(challenge string, ) *DeviceUserAuthRequest` + +NewDeviceUserAuthRequest instantiates a new DeviceUserAuthRequest object +This constructor will assign default values to properties that have it defined, +and makes sure properties required by API are set, but the set of arguments +will change when the set of required properties is changed + +### NewDeviceUserAuthRequestWithDefaults + +`func NewDeviceUserAuthRequestWithDefaults() *DeviceUserAuthRequest` + +NewDeviceUserAuthRequestWithDefaults instantiates a new DeviceUserAuthRequest object +This constructor will only assign default values to properties that have it defined, +but it doesn't guarantee that properties required by API are set + +### GetChallenge + +`func (o *DeviceUserAuthRequest) GetChallenge() string` + +GetChallenge returns the Challenge field if non-nil, zero value otherwise. + +### GetChallengeOk + +`func (o *DeviceUserAuthRequest) GetChallengeOk() (*string, bool)` + +GetChallengeOk returns a tuple with the Challenge field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetChallenge + +`func (o *DeviceUserAuthRequest) SetChallenge(v string)` + +SetChallenge sets Challenge field to given value. + + +### GetClient + +`func (o *DeviceUserAuthRequest) GetClient() OAuth2Client` + +GetClient returns the Client field if non-nil, zero value otherwise. + +### GetClientOk + +`func (o *DeviceUserAuthRequest) GetClientOk() (*OAuth2Client, bool)` + +GetClientOk returns a tuple with the Client field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetClient + +`func (o *DeviceUserAuthRequest) SetClient(v OAuth2Client)` + +SetClient sets Client field to given value. + +### HasClient + +`func (o *DeviceUserAuthRequest) HasClient() bool` + +HasClient returns a boolean if a field has been set. + +### GetHandledAt + +`func (o *DeviceUserAuthRequest) GetHandledAt() time.Time` + +GetHandledAt returns the HandledAt field if non-nil, zero value otherwise. + +### GetHandledAtOk + +`func (o *DeviceUserAuthRequest) GetHandledAtOk() (*time.Time, bool)` + +GetHandledAtOk returns a tuple with the HandledAt field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetHandledAt + +`func (o *DeviceUserAuthRequest) SetHandledAt(v time.Time)` + +SetHandledAt sets HandledAt field to given value. + +### HasHandledAt + +`func (o *DeviceUserAuthRequest) HasHandledAt() bool` + +HasHandledAt returns a boolean if a field has been set. + +### GetRequestUrl + +`func (o *DeviceUserAuthRequest) GetRequestUrl() string` + +GetRequestUrl returns the RequestUrl field if non-nil, zero value otherwise. + +### GetRequestUrlOk + +`func (o *DeviceUserAuthRequest) GetRequestUrlOk() (*string, bool)` + +GetRequestUrlOk returns a tuple with the RequestUrl field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetRequestUrl + +`func (o *DeviceUserAuthRequest) SetRequestUrl(v string)` + +SetRequestUrl sets RequestUrl field to given value. + +### HasRequestUrl + +`func (o *DeviceUserAuthRequest) HasRequestUrl() bool` + +HasRequestUrl returns a boolean if a field has been set. + +### GetRequestedAccessTokenAudience + +`func (o *DeviceUserAuthRequest) GetRequestedAccessTokenAudience() []string` + +GetRequestedAccessTokenAudience returns the RequestedAccessTokenAudience field if non-nil, zero value otherwise. + +### GetRequestedAccessTokenAudienceOk + +`func (o *DeviceUserAuthRequest) GetRequestedAccessTokenAudienceOk() (*[]string, bool)` + +GetRequestedAccessTokenAudienceOk returns a tuple with the RequestedAccessTokenAudience field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetRequestedAccessTokenAudience + +`func (o *DeviceUserAuthRequest) SetRequestedAccessTokenAudience(v []string)` + +SetRequestedAccessTokenAudience sets RequestedAccessTokenAudience field to given value. + +### HasRequestedAccessTokenAudience + +`func (o *DeviceUserAuthRequest) HasRequestedAccessTokenAudience() bool` + +HasRequestedAccessTokenAudience returns a boolean if a field has been set. + +### GetRequestedScope + +`func (o *DeviceUserAuthRequest) GetRequestedScope() []string` + +GetRequestedScope returns the RequestedScope field if non-nil, zero value otherwise. + +### GetRequestedScopeOk + +`func (o *DeviceUserAuthRequest) GetRequestedScopeOk() (*[]string, bool)` + +GetRequestedScopeOk returns a tuple with the RequestedScope field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetRequestedScope + +`func (o *DeviceUserAuthRequest) SetRequestedScope(v []string)` + +SetRequestedScope sets RequestedScope field to given value. + +### HasRequestedScope + +`func (o *DeviceUserAuthRequest) HasRequestedScope() bool` + +HasRequestedScope returns a boolean if a field has been set. + + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/internal/httpclient/docs/OAuth2API.md b/internal/httpclient/docs/OAuth2API.md index a9a8f672ccb..be2d8c97467 100644 --- a/internal/httpclient/docs/OAuth2API.md +++ b/internal/httpclient/docs/OAuth2API.md @@ -7,6 +7,7 @@ Method | HTTP request | Description [**AcceptOAuth2ConsentRequest**](OAuth2API.md#AcceptOAuth2ConsentRequest) | **Put** /admin/oauth2/auth/requests/consent/accept | Accept OAuth 2.0 Consent Request [**AcceptOAuth2LoginRequest**](OAuth2API.md#AcceptOAuth2LoginRequest) | **Put** /admin/oauth2/auth/requests/login/accept | Accept OAuth 2.0 Login Request [**AcceptOAuth2LogoutRequest**](OAuth2API.md#AcceptOAuth2LogoutRequest) | **Put** /admin/oauth2/auth/requests/logout/accept | Accept OAuth 2.0 Session Logout Request +[**AcceptUserCodeRequest**](OAuth2API.md#AcceptUserCodeRequest) | **Put** /admin/oauth2/auth/requests/device/accept | Accepts a device grant user_code request [**CreateOAuth2Client**](OAuth2API.md#CreateOAuth2Client) | **Post** /admin/clients | Create OAuth 2.0 Client [**DeleteOAuth2Client**](OAuth2API.md#DeleteOAuth2Client) | **Delete** /admin/clients/{id} | Delete OAuth 2.0 Client [**DeleteOAuth2Token**](OAuth2API.md#DeleteOAuth2Token) | **Delete** /admin/oauth2/tokens | Delete OAuth 2.0 Access Tokens from specific OAuth 2.0 Client @@ -21,8 +22,10 @@ Method | HTTP request | Description [**ListOAuth2ConsentSessions**](OAuth2API.md#ListOAuth2ConsentSessions) | **Get** /admin/oauth2/auth/sessions/consent | List OAuth 2.0 Consent Sessions of a Subject [**ListTrustedOAuth2JwtGrantIssuers**](OAuth2API.md#ListTrustedOAuth2JwtGrantIssuers) | **Get** /admin/trust/grants/jwt-bearer/issuers | List Trusted OAuth2 JWT Bearer Grant Type Issuers [**OAuth2Authorize**](OAuth2API.md#OAuth2Authorize) | **Get** /oauth2/auth | OAuth 2.0 Authorize Endpoint +[**OAuth2DeviceFlow**](OAuth2API.md#OAuth2DeviceFlow) | **Post** /oauth2/device/auth | The OAuth 2.0 Device Authorize Endpoint [**Oauth2TokenExchange**](OAuth2API.md#Oauth2TokenExchange) | **Post** /oauth2/token | The OAuth 2.0 Token Endpoint [**PatchOAuth2Client**](OAuth2API.md#PatchOAuth2Client) | **Patch** /admin/clients/{id} | Patch OAuth 2.0 Client +[**PerformOAuth2DeviceVerificationFlow**](OAuth2API.md#PerformOAuth2DeviceVerificationFlow) | **Get** /oauth2/device/verify | OAuth 2.0 Device Verification Endpoint [**RejectOAuth2ConsentRequest**](OAuth2API.md#RejectOAuth2ConsentRequest) | **Put** /admin/oauth2/auth/requests/consent/reject | Reject OAuth 2.0 Consent Request [**RejectOAuth2LoginRequest**](OAuth2API.md#RejectOAuth2LoginRequest) | **Put** /admin/oauth2/auth/requests/login/reject | Reject OAuth 2.0 Login Request [**RejectOAuth2LogoutRequest**](OAuth2API.md#RejectOAuth2LogoutRequest) | **Put** /admin/oauth2/auth/requests/logout/reject | Reject OAuth 2.0 Session Logout Request @@ -237,6 +240,74 @@ No authorization required [[Back to README]](../README.md) +## AcceptUserCodeRequest + +> OAuth2RedirectTo AcceptUserCodeRequest(ctx).DeviceChallenge(deviceChallenge).AcceptDeviceUserCodeRequest(acceptDeviceUserCodeRequest).Execute() + +Accepts a device grant user_code request + + + +### Example + +```go +package main + +import ( + "context" + "fmt" + "os" + openapiclient "github.com/ory/hydra-client-go/v2" +) + +func main() { + deviceChallenge := "deviceChallenge_example" // string | + acceptDeviceUserCodeRequest := *openapiclient.NewAcceptDeviceUserCodeRequest() // AcceptDeviceUserCodeRequest | (optional) + + configuration := openapiclient.NewConfiguration() + apiClient := openapiclient.NewAPIClient(configuration) + resp, r, err := apiClient.OAuth2API.AcceptUserCodeRequest(context.Background()).DeviceChallenge(deviceChallenge).AcceptDeviceUserCodeRequest(acceptDeviceUserCodeRequest).Execute() + if err != nil { + fmt.Fprintf(os.Stderr, "Error when calling `OAuth2API.AcceptUserCodeRequest``: %v\n", err) + fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r) + } + // response from `AcceptUserCodeRequest`: OAuth2RedirectTo + fmt.Fprintf(os.Stdout, "Response from `OAuth2API.AcceptUserCodeRequest`: %v\n", resp) +} +``` + +### Path Parameters + + + +### Other Parameters + +Other parameters are passed through a pointer to a apiAcceptUserCodeRequestRequest struct via the builder pattern + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **deviceChallenge** | **string** | | + **acceptDeviceUserCodeRequest** | [**AcceptDeviceUserCodeRequest**](AcceptDeviceUserCodeRequest.md) | | + +### Return type + +[**OAuth2RedirectTo**](OAuth2RedirectTo.md) + +### Authorization + +No authorization required + +### HTTP request headers + +- **Content-Type**: application/json +- **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) +[[Back to Model list]](../README.md#documentation-for-models) +[[Back to README]](../README.md) + + ## CreateOAuth2Client > OAuth2Client CreateOAuth2Client(ctx).OAuth2Client(oAuth2Client).Execute() @@ -1184,6 +1255,67 @@ No authorization required [[Back to README]](../README.md) +## OAuth2DeviceFlow + +> DeviceAuthorization OAuth2DeviceFlow(ctx).Execute() + +The OAuth 2.0 Device Authorize Endpoint + + + +### Example + +```go +package main + +import ( + "context" + "fmt" + "os" + openapiclient "github.com/ory/hydra-client-go/v2" +) + +func main() { + + configuration := openapiclient.NewConfiguration() + apiClient := openapiclient.NewAPIClient(configuration) + resp, r, err := apiClient.OAuth2API.OAuth2DeviceFlow(context.Background()).Execute() + if err != nil { + fmt.Fprintf(os.Stderr, "Error when calling `OAuth2API.OAuth2DeviceFlow``: %v\n", err) + fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r) + } + // response from `OAuth2DeviceFlow`: DeviceAuthorization + fmt.Fprintf(os.Stdout, "Response from `OAuth2API.OAuth2DeviceFlow`: %v\n", resp) +} +``` + +### Path Parameters + +This endpoint does not need any parameter. + +### Other Parameters + +Other parameters are passed through a pointer to a apiOAuth2DeviceFlowRequest struct via the builder pattern + + +### Return type + +[**DeviceAuthorization**](DeviceAuthorization.md) + +### Authorization + +No authorization required + +### HTTP request headers + +- **Content-Type**: Not defined +- **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) +[[Back to Model list]](../README.md#documentation-for-models) +[[Back to README]](../README.md) + + ## Oauth2TokenExchange > OAuth2TokenExchange Oauth2TokenExchange(ctx).GrantType(grantType).ClientId(clientId).Code(code).RedirectUri(redirectUri).RefreshToken(refreshToken).Execute() @@ -1330,6 +1462,67 @@ No authorization required [[Back to README]](../README.md) +## PerformOAuth2DeviceVerificationFlow + +> ErrorOAuth2 PerformOAuth2DeviceVerificationFlow(ctx).Execute() + +OAuth 2.0 Device Verification Endpoint + + + +### Example + +```go +package main + +import ( + "context" + "fmt" + "os" + openapiclient "github.com/ory/hydra-client-go/v2" +) + +func main() { + + configuration := openapiclient.NewConfiguration() + apiClient := openapiclient.NewAPIClient(configuration) + resp, r, err := apiClient.OAuth2API.PerformOAuth2DeviceVerificationFlow(context.Background()).Execute() + if err != nil { + fmt.Fprintf(os.Stderr, "Error when calling `OAuth2API.PerformOAuth2DeviceVerificationFlow``: %v\n", err) + fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r) + } + // response from `PerformOAuth2DeviceVerificationFlow`: ErrorOAuth2 + fmt.Fprintf(os.Stdout, "Response from `OAuth2API.PerformOAuth2DeviceVerificationFlow`: %v\n", resp) +} +``` + +### Path Parameters + +This endpoint does not need any parameter. + +### Other Parameters + +Other parameters are passed through a pointer to a apiPerformOAuth2DeviceVerificationFlowRequest struct via the builder pattern + + +### Return type + +[**ErrorOAuth2**](ErrorOAuth2.md) + +### Authorization + +No authorization required + +### HTTP request headers + +- **Content-Type**: Not defined +- **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) +[[Back to Model list]](../README.md#documentation-for-models) +[[Back to README]](../README.md) + + ## RejectOAuth2ConsentRequest > OAuth2RedirectTo RejectOAuth2ConsentRequest(ctx).ConsentChallenge(consentChallenge).RejectOAuth2Request(rejectOAuth2Request).Execute() diff --git a/internal/httpclient/docs/OAuth2Client.md b/internal/httpclient/docs/OAuth2Client.md index 9638bd221b3..7c62a2daf50 100644 --- a/internal/httpclient/docs/OAuth2Client.md +++ b/internal/httpclient/docs/OAuth2Client.md @@ -20,6 +20,9 @@ Name | Type | Description | Notes **ClientUri** | Pointer to **string** | OAuth 2.0 Client URI ClientURI is a URL string of a web page providing information about the client. If present, the server SHOULD display this URL to the end-user in a clickable fashion. | [optional] **Contacts** | Pointer to **[]string** | | [optional] **CreatedAt** | Pointer to **time.Time** | OAuth 2.0 Client Creation Date CreatedAt returns the timestamp of the client's creation. | [optional] +**DeviceAuthorizationGrantAccessTokenLifespan** | Pointer to **string** | Specify a time duration in milliseconds, seconds, minutes, hours. | [optional] +**DeviceAuthorizationGrantIdTokenLifespan** | Pointer to **string** | Specify a time duration in milliseconds, seconds, minutes, hours. | [optional] +**DeviceAuthorizationGrantRefreshTokenLifespan** | Pointer to **string** | Specify a time duration in milliseconds, seconds, minutes, hours. | [optional] **FrontchannelLogoutSessionRequired** | Pointer to **bool** | OpenID Connect Front-Channel Logout Session Required Boolean value specifying whether the RP requires that iss (issuer) and sid (session ID) query parameters be included to identify the RP session with the OP when the frontchannel_logout_uri is used. If omitted, the default value is false. | [optional] **FrontchannelLogoutUri** | Pointer to **string** | OpenID Connect Front-Channel Logout URI RP URL that will cause the RP to log itself out when rendered in an iframe by the OP. An iss (issuer) query parameter and a sid (session ID) query parameter MAY be included by the OP to enable the RP to validate the request and to determine which of the potentially multiple sessions is to be logged out; if either is included, both MUST be. | [optional] **GrantTypes** | Pointer to **[]string** | | [optional] @@ -472,6 +475,81 @@ SetCreatedAt sets CreatedAt field to given value. HasCreatedAt returns a boolean if a field has been set. +### GetDeviceAuthorizationGrantAccessTokenLifespan + +`func (o *OAuth2Client) GetDeviceAuthorizationGrantAccessTokenLifespan() string` + +GetDeviceAuthorizationGrantAccessTokenLifespan returns the DeviceAuthorizationGrantAccessTokenLifespan field if non-nil, zero value otherwise. + +### GetDeviceAuthorizationGrantAccessTokenLifespanOk + +`func (o *OAuth2Client) GetDeviceAuthorizationGrantAccessTokenLifespanOk() (*string, bool)` + +GetDeviceAuthorizationGrantAccessTokenLifespanOk returns a tuple with the DeviceAuthorizationGrantAccessTokenLifespan field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetDeviceAuthorizationGrantAccessTokenLifespan + +`func (o *OAuth2Client) SetDeviceAuthorizationGrantAccessTokenLifespan(v string)` + +SetDeviceAuthorizationGrantAccessTokenLifespan sets DeviceAuthorizationGrantAccessTokenLifespan field to given value. + +### HasDeviceAuthorizationGrantAccessTokenLifespan + +`func (o *OAuth2Client) HasDeviceAuthorizationGrantAccessTokenLifespan() bool` + +HasDeviceAuthorizationGrantAccessTokenLifespan returns a boolean if a field has been set. + +### GetDeviceAuthorizationGrantIdTokenLifespan + +`func (o *OAuth2Client) GetDeviceAuthorizationGrantIdTokenLifespan() string` + +GetDeviceAuthorizationGrantIdTokenLifespan returns the DeviceAuthorizationGrantIdTokenLifespan field if non-nil, zero value otherwise. + +### GetDeviceAuthorizationGrantIdTokenLifespanOk + +`func (o *OAuth2Client) GetDeviceAuthorizationGrantIdTokenLifespanOk() (*string, bool)` + +GetDeviceAuthorizationGrantIdTokenLifespanOk returns a tuple with the DeviceAuthorizationGrantIdTokenLifespan field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetDeviceAuthorizationGrantIdTokenLifespan + +`func (o *OAuth2Client) SetDeviceAuthorizationGrantIdTokenLifespan(v string)` + +SetDeviceAuthorizationGrantIdTokenLifespan sets DeviceAuthorizationGrantIdTokenLifespan field to given value. + +### HasDeviceAuthorizationGrantIdTokenLifespan + +`func (o *OAuth2Client) HasDeviceAuthorizationGrantIdTokenLifespan() bool` + +HasDeviceAuthorizationGrantIdTokenLifespan returns a boolean if a field has been set. + +### GetDeviceAuthorizationGrantRefreshTokenLifespan + +`func (o *OAuth2Client) GetDeviceAuthorizationGrantRefreshTokenLifespan() string` + +GetDeviceAuthorizationGrantRefreshTokenLifespan returns the DeviceAuthorizationGrantRefreshTokenLifespan field if non-nil, zero value otherwise. + +### GetDeviceAuthorizationGrantRefreshTokenLifespanOk + +`func (o *OAuth2Client) GetDeviceAuthorizationGrantRefreshTokenLifespanOk() (*string, bool)` + +GetDeviceAuthorizationGrantRefreshTokenLifespanOk returns a tuple with the DeviceAuthorizationGrantRefreshTokenLifespan field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetDeviceAuthorizationGrantRefreshTokenLifespan + +`func (o *OAuth2Client) SetDeviceAuthorizationGrantRefreshTokenLifespan(v string)` + +SetDeviceAuthorizationGrantRefreshTokenLifespan sets DeviceAuthorizationGrantRefreshTokenLifespan field to given value. + +### HasDeviceAuthorizationGrantRefreshTokenLifespan + +`func (o *OAuth2Client) HasDeviceAuthorizationGrantRefreshTokenLifespan() bool` + +HasDeviceAuthorizationGrantRefreshTokenLifespan returns a boolean if a field has been set. + ### GetFrontchannelLogoutSessionRequired `func (o *OAuth2Client) GetFrontchannelLogoutSessionRequired() bool` diff --git a/internal/httpclient/docs/OAuth2ClientTokenLifespans.md b/internal/httpclient/docs/OAuth2ClientTokenLifespans.md index cda6ca600ca..b38aef35d74 100644 --- a/internal/httpclient/docs/OAuth2ClientTokenLifespans.md +++ b/internal/httpclient/docs/OAuth2ClientTokenLifespans.md @@ -8,6 +8,9 @@ Name | Type | Description | Notes **AuthorizationCodeGrantIdTokenLifespan** | Pointer to **string** | Specify a time duration in milliseconds, seconds, minutes, hours. | [optional] **AuthorizationCodeGrantRefreshTokenLifespan** | Pointer to **string** | Specify a time duration in milliseconds, seconds, minutes, hours. | [optional] **ClientCredentialsGrantAccessTokenLifespan** | Pointer to **string** | Specify a time duration in milliseconds, seconds, minutes, hours. | [optional] +**DeviceAuthorizationGrantAccessTokenLifespan** | Pointer to **string** | Specify a time duration in milliseconds, seconds, minutes, hours. | [optional] +**DeviceAuthorizationGrantIdTokenLifespan** | Pointer to **string** | Specify a time duration in milliseconds, seconds, minutes, hours. | [optional] +**DeviceAuthorizationGrantRefreshTokenLifespan** | Pointer to **string** | Specify a time duration in milliseconds, seconds, minutes, hours. | [optional] **ImplicitGrantAccessTokenLifespan** | Pointer to **string** | Specify a time duration in milliseconds, seconds, minutes, hours. | [optional] **ImplicitGrantIdTokenLifespan** | Pointer to **string** | Specify a time duration in milliseconds, seconds, minutes, hours. | [optional] **JwtBearerGrantAccessTokenLifespan** | Pointer to **string** | Specify a time duration in milliseconds, seconds, minutes, hours. | [optional] @@ -134,6 +137,81 @@ SetClientCredentialsGrantAccessTokenLifespan sets ClientCredentialsGrantAccessTo HasClientCredentialsGrantAccessTokenLifespan returns a boolean if a field has been set. +### GetDeviceAuthorizationGrantAccessTokenLifespan + +`func (o *OAuth2ClientTokenLifespans) GetDeviceAuthorizationGrantAccessTokenLifespan() string` + +GetDeviceAuthorizationGrantAccessTokenLifespan returns the DeviceAuthorizationGrantAccessTokenLifespan field if non-nil, zero value otherwise. + +### GetDeviceAuthorizationGrantAccessTokenLifespanOk + +`func (o *OAuth2ClientTokenLifespans) GetDeviceAuthorizationGrantAccessTokenLifespanOk() (*string, bool)` + +GetDeviceAuthorizationGrantAccessTokenLifespanOk returns a tuple with the DeviceAuthorizationGrantAccessTokenLifespan field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetDeviceAuthorizationGrantAccessTokenLifespan + +`func (o *OAuth2ClientTokenLifespans) SetDeviceAuthorizationGrantAccessTokenLifespan(v string)` + +SetDeviceAuthorizationGrantAccessTokenLifespan sets DeviceAuthorizationGrantAccessTokenLifespan field to given value. + +### HasDeviceAuthorizationGrantAccessTokenLifespan + +`func (o *OAuth2ClientTokenLifespans) HasDeviceAuthorizationGrantAccessTokenLifespan() bool` + +HasDeviceAuthorizationGrantAccessTokenLifespan returns a boolean if a field has been set. + +### GetDeviceAuthorizationGrantIdTokenLifespan + +`func (o *OAuth2ClientTokenLifespans) GetDeviceAuthorizationGrantIdTokenLifespan() string` + +GetDeviceAuthorizationGrantIdTokenLifespan returns the DeviceAuthorizationGrantIdTokenLifespan field if non-nil, zero value otherwise. + +### GetDeviceAuthorizationGrantIdTokenLifespanOk + +`func (o *OAuth2ClientTokenLifespans) GetDeviceAuthorizationGrantIdTokenLifespanOk() (*string, bool)` + +GetDeviceAuthorizationGrantIdTokenLifespanOk returns a tuple with the DeviceAuthorizationGrantIdTokenLifespan field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetDeviceAuthorizationGrantIdTokenLifespan + +`func (o *OAuth2ClientTokenLifespans) SetDeviceAuthorizationGrantIdTokenLifespan(v string)` + +SetDeviceAuthorizationGrantIdTokenLifespan sets DeviceAuthorizationGrantIdTokenLifespan field to given value. + +### HasDeviceAuthorizationGrantIdTokenLifespan + +`func (o *OAuth2ClientTokenLifespans) HasDeviceAuthorizationGrantIdTokenLifespan() bool` + +HasDeviceAuthorizationGrantIdTokenLifespan returns a boolean if a field has been set. + +### GetDeviceAuthorizationGrantRefreshTokenLifespan + +`func (o *OAuth2ClientTokenLifespans) GetDeviceAuthorizationGrantRefreshTokenLifespan() string` + +GetDeviceAuthorizationGrantRefreshTokenLifespan returns the DeviceAuthorizationGrantRefreshTokenLifespan field if non-nil, zero value otherwise. + +### GetDeviceAuthorizationGrantRefreshTokenLifespanOk + +`func (o *OAuth2ClientTokenLifespans) GetDeviceAuthorizationGrantRefreshTokenLifespanOk() (*string, bool)` + +GetDeviceAuthorizationGrantRefreshTokenLifespanOk returns a tuple with the DeviceAuthorizationGrantRefreshTokenLifespan field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetDeviceAuthorizationGrantRefreshTokenLifespan + +`func (o *OAuth2ClientTokenLifespans) SetDeviceAuthorizationGrantRefreshTokenLifespan(v string)` + +SetDeviceAuthorizationGrantRefreshTokenLifespan sets DeviceAuthorizationGrantRefreshTokenLifespan field to given value. + +### HasDeviceAuthorizationGrantRefreshTokenLifespan + +`func (o *OAuth2ClientTokenLifespans) HasDeviceAuthorizationGrantRefreshTokenLifespan() bool` + +HasDeviceAuthorizationGrantRefreshTokenLifespan returns a boolean if a field has been set. + ### GetImplicitGrantAccessTokenLifespan `func (o *OAuth2ClientTokenLifespans) GetImplicitGrantAccessTokenLifespan() string` diff --git a/internal/httpclient/docs/OAuth2ConsentRequest.md b/internal/httpclient/docs/OAuth2ConsentRequest.md index 1197410e106..c067d5f4806 100644 --- a/internal/httpclient/docs/OAuth2ConsentRequest.md +++ b/internal/httpclient/docs/OAuth2ConsentRequest.md @@ -10,6 +10,7 @@ Name | Type | Description | Notes **Client** | Pointer to [**OAuth2Client**](OAuth2Client.md) | | [optional] **ConsentRequestId** | Pointer to **string** | ConsentRequestID is the ID of the consent request. | [optional] **Context** | Pointer to **interface{}** | | [optional] +**DeviceChallengeId** | Pointer to **string** | DeviceChallenge is the device challenge this consent challenge belongs to, if this flow was initiated by a device. | [optional] **LoginChallenge** | Pointer to **string** | LoginChallenge is the login challenge this consent challenge belongs to. It can be used to associate a login and consent request in the login & consent app. | [optional] **LoginSessionId** | Pointer to **string** | LoginSessionID is the login session ID. If the user-agent reuses a login session (via cookie / remember flag) this ID will remain the same. If the user-agent did not have an existing authentication session (e.g. remember is false) this will be a new random value. This value is used as the \"sid\" parameter in the ID Token and in OIDC Front-/Back- channel logout. It's value can generally be used to associate consecutive login requests by a certain user. | [optional] **OidcContext** | Pointer to [**OAuth2ConsentRequestOpenIDConnectContext**](OAuth2ConsentRequestOpenIDConnectContext.md) | | [optional] @@ -193,6 +194,31 @@ HasContext returns a boolean if a field has been set. `func (o *OAuth2ConsentRequest) UnsetContext()` UnsetContext ensures that no value is present for Context, not even an explicit nil +### GetDeviceChallengeId + +`func (o *OAuth2ConsentRequest) GetDeviceChallengeId() string` + +GetDeviceChallengeId returns the DeviceChallengeId field if non-nil, zero value otherwise. + +### GetDeviceChallengeIdOk + +`func (o *OAuth2ConsentRequest) GetDeviceChallengeIdOk() (*string, bool)` + +GetDeviceChallengeIdOk returns a tuple with the DeviceChallengeId field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetDeviceChallengeId + +`func (o *OAuth2ConsentRequest) SetDeviceChallengeId(v string)` + +SetDeviceChallengeId sets DeviceChallengeId field to given value. + +### HasDeviceChallengeId + +`func (o *OAuth2ConsentRequest) HasDeviceChallengeId() bool` + +HasDeviceChallengeId returns a boolean if a field has been set. + ### GetLoginChallenge `func (o *OAuth2ConsentRequest) GetLoginChallenge() string` diff --git a/internal/httpclient/docs/OidcConfiguration.md b/internal/httpclient/docs/OidcConfiguration.md index 1b20c7d8733..27f0134440c 100644 --- a/internal/httpclient/docs/OidcConfiguration.md +++ b/internal/httpclient/docs/OidcConfiguration.md @@ -12,6 +12,7 @@ Name | Type | Description | Notes **CodeChallengeMethodsSupported** | Pointer to **[]string** | OAuth 2.0 PKCE Supported Code Challenge Methods JSON array containing a list of Proof Key for Code Exchange (PKCE) [RFC7636] code challenge methods supported by this authorization server. | [optional] **CredentialsEndpointDraft00** | Pointer to **string** | OpenID Connect Verifiable Credentials Endpoint Contains the URL of the Verifiable Credentials Endpoint. | [optional] **CredentialsSupportedDraft00** | Pointer to [**[]CredentialSupportedDraft00**](CredentialSupportedDraft00.md) | OpenID Connect Verifiable Credentials Supported JSON array containing a list of the Verifiable Credentials supported by this authorization server. | [optional] +**DeviceAuthorizationEndpoint** | **string** | OAuth 2.0 Device Authorization Endpoint URL | **EndSessionEndpoint** | Pointer to **string** | OpenID Connect End-Session Endpoint URL at the OP to which an RP can perform a redirect to request that the End-User be logged out at the OP. | [optional] **FrontchannelLogoutSessionSupported** | Pointer to **bool** | OpenID Connect Front-Channel Logout Session Required Boolean value specifying whether the OP can pass iss (issuer) and sid (session ID) query parameters to identify the RP session with the OP when the frontchannel_logout_uri is used. If supported, the sid Claim is also included in ID Tokens issued by the OP. | [optional] **FrontchannelLogoutSupported** | Pointer to **bool** | OpenID Connect Front-Channel Logout Supported Boolean value specifying whether the OP supports HTTP-based logout, with true indicating support. | [optional] @@ -40,7 +41,7 @@ Name | Type | Description | Notes ### NewOidcConfiguration -`func NewOidcConfiguration(authorizationEndpoint string, idTokenSignedResponseAlg []string, idTokenSigningAlgValuesSupported []string, issuer string, jwksUri string, responseTypesSupported []string, subjectTypesSupported []string, tokenEndpoint string, userinfoSignedResponseAlg []string, ) *OidcConfiguration` +`func NewOidcConfiguration(authorizationEndpoint string, deviceAuthorizationEndpoint string, idTokenSignedResponseAlg []string, idTokenSigningAlgValuesSupported []string, issuer string, jwksUri string, responseTypesSupported []string, subjectTypesSupported []string, tokenEndpoint string, userinfoSignedResponseAlg []string, ) *OidcConfiguration` NewOidcConfiguration instantiates a new OidcConfiguration object This constructor will assign default values to properties that have it defined, @@ -250,6 +251,26 @@ SetCredentialsSupportedDraft00 sets CredentialsSupportedDraft00 field to given v HasCredentialsSupportedDraft00 returns a boolean if a field has been set. +### GetDeviceAuthorizationEndpoint + +`func (o *OidcConfiguration) GetDeviceAuthorizationEndpoint() string` + +GetDeviceAuthorizationEndpoint returns the DeviceAuthorizationEndpoint field if non-nil, zero value otherwise. + +### GetDeviceAuthorizationEndpointOk + +`func (o *OidcConfiguration) GetDeviceAuthorizationEndpointOk() (*string, bool)` + +GetDeviceAuthorizationEndpointOk returns a tuple with the DeviceAuthorizationEndpoint field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetDeviceAuthorizationEndpoint + +`func (o *OidcConfiguration) SetDeviceAuthorizationEndpoint(v string)` + +SetDeviceAuthorizationEndpoint sets DeviceAuthorizationEndpoint field to given value. + + ### GetEndSessionEndpoint `func (o *OidcConfiguration) GetEndSessionEndpoint() string` diff --git a/internal/httpclient/docs/VerifyUserCodeRequest.md b/internal/httpclient/docs/VerifyUserCodeRequest.md new file mode 100644 index 00000000000..09a2270ab44 --- /dev/null +++ b/internal/httpclient/docs/VerifyUserCodeRequest.md @@ -0,0 +1,212 @@ +# VerifyUserCodeRequest + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**Challenge** | Pointer to **string** | ID is the identifier (\"device challenge\") of the device request. It is used to identify the session. | [optional] +**Client** | Pointer to [**OAuth2Client**](OAuth2Client.md) | | [optional] +**DeviceCodeRequestId** | Pointer to **string** | | [optional] +**HandledAt** | Pointer to **time.Time** | | [optional] +**RequestUrl** | Pointer to **string** | RequestURL is the original Device Authorization URL requested. | [optional] +**RequestedAccessTokenAudience** | Pointer to **[]string** | | [optional] +**RequestedScope** | Pointer to **[]string** | | [optional] + +## Methods + +### NewVerifyUserCodeRequest + +`func NewVerifyUserCodeRequest() *VerifyUserCodeRequest` + +NewVerifyUserCodeRequest instantiates a new VerifyUserCodeRequest object +This constructor will assign default values to properties that have it defined, +and makes sure properties required by API are set, but the set of arguments +will change when the set of required properties is changed + +### NewVerifyUserCodeRequestWithDefaults + +`func NewVerifyUserCodeRequestWithDefaults() *VerifyUserCodeRequest` + +NewVerifyUserCodeRequestWithDefaults instantiates a new VerifyUserCodeRequest object +This constructor will only assign default values to properties that have it defined, +but it doesn't guarantee that properties required by API are set + +### GetChallenge + +`func (o *VerifyUserCodeRequest) GetChallenge() string` + +GetChallenge returns the Challenge field if non-nil, zero value otherwise. + +### GetChallengeOk + +`func (o *VerifyUserCodeRequest) GetChallengeOk() (*string, bool)` + +GetChallengeOk returns a tuple with the Challenge field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetChallenge + +`func (o *VerifyUserCodeRequest) SetChallenge(v string)` + +SetChallenge sets Challenge field to given value. + +### HasChallenge + +`func (o *VerifyUserCodeRequest) HasChallenge() bool` + +HasChallenge returns a boolean if a field has been set. + +### GetClient + +`func (o *VerifyUserCodeRequest) GetClient() OAuth2Client` + +GetClient returns the Client field if non-nil, zero value otherwise. + +### GetClientOk + +`func (o *VerifyUserCodeRequest) GetClientOk() (*OAuth2Client, bool)` + +GetClientOk returns a tuple with the Client field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetClient + +`func (o *VerifyUserCodeRequest) SetClient(v OAuth2Client)` + +SetClient sets Client field to given value. + +### HasClient + +`func (o *VerifyUserCodeRequest) HasClient() bool` + +HasClient returns a boolean if a field has been set. + +### GetDeviceCodeRequestId + +`func (o *VerifyUserCodeRequest) GetDeviceCodeRequestId() string` + +GetDeviceCodeRequestId returns the DeviceCodeRequestId field if non-nil, zero value otherwise. + +### GetDeviceCodeRequestIdOk + +`func (o *VerifyUserCodeRequest) GetDeviceCodeRequestIdOk() (*string, bool)` + +GetDeviceCodeRequestIdOk returns a tuple with the DeviceCodeRequestId field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetDeviceCodeRequestId + +`func (o *VerifyUserCodeRequest) SetDeviceCodeRequestId(v string)` + +SetDeviceCodeRequestId sets DeviceCodeRequestId field to given value. + +### HasDeviceCodeRequestId + +`func (o *VerifyUserCodeRequest) HasDeviceCodeRequestId() bool` + +HasDeviceCodeRequestId returns a boolean if a field has been set. + +### GetHandledAt + +`func (o *VerifyUserCodeRequest) GetHandledAt() time.Time` + +GetHandledAt returns the HandledAt field if non-nil, zero value otherwise. + +### GetHandledAtOk + +`func (o *VerifyUserCodeRequest) GetHandledAtOk() (*time.Time, bool)` + +GetHandledAtOk returns a tuple with the HandledAt field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetHandledAt + +`func (o *VerifyUserCodeRequest) SetHandledAt(v time.Time)` + +SetHandledAt sets HandledAt field to given value. + +### HasHandledAt + +`func (o *VerifyUserCodeRequest) HasHandledAt() bool` + +HasHandledAt returns a boolean if a field has been set. + +### GetRequestUrl + +`func (o *VerifyUserCodeRequest) GetRequestUrl() string` + +GetRequestUrl returns the RequestUrl field if non-nil, zero value otherwise. + +### GetRequestUrlOk + +`func (o *VerifyUserCodeRequest) GetRequestUrlOk() (*string, bool)` + +GetRequestUrlOk returns a tuple with the RequestUrl field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetRequestUrl + +`func (o *VerifyUserCodeRequest) SetRequestUrl(v string)` + +SetRequestUrl sets RequestUrl field to given value. + +### HasRequestUrl + +`func (o *VerifyUserCodeRequest) HasRequestUrl() bool` + +HasRequestUrl returns a boolean if a field has been set. + +### GetRequestedAccessTokenAudience + +`func (o *VerifyUserCodeRequest) GetRequestedAccessTokenAudience() []string` + +GetRequestedAccessTokenAudience returns the RequestedAccessTokenAudience field if non-nil, zero value otherwise. + +### GetRequestedAccessTokenAudienceOk + +`func (o *VerifyUserCodeRequest) GetRequestedAccessTokenAudienceOk() (*[]string, bool)` + +GetRequestedAccessTokenAudienceOk returns a tuple with the RequestedAccessTokenAudience field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetRequestedAccessTokenAudience + +`func (o *VerifyUserCodeRequest) SetRequestedAccessTokenAudience(v []string)` + +SetRequestedAccessTokenAudience sets RequestedAccessTokenAudience field to given value. + +### HasRequestedAccessTokenAudience + +`func (o *VerifyUserCodeRequest) HasRequestedAccessTokenAudience() bool` + +HasRequestedAccessTokenAudience returns a boolean if a field has been set. + +### GetRequestedScope + +`func (o *VerifyUserCodeRequest) GetRequestedScope() []string` + +GetRequestedScope returns the RequestedScope field if non-nil, zero value otherwise. + +### GetRequestedScopeOk + +`func (o *VerifyUserCodeRequest) GetRequestedScopeOk() (*[]string, bool)` + +GetRequestedScopeOk returns a tuple with the RequestedScope field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetRequestedScope + +`func (o *VerifyUserCodeRequest) SetRequestedScope(v []string)` + +SetRequestedScope sets RequestedScope field to given value. + +### HasRequestedScope + +`func (o *VerifyUserCodeRequest) HasRequestedScope() bool` + +HasRequestedScope returns a boolean if a field has been set. + + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/internal/httpclient/model_accept_device_user_code_request.go b/internal/httpclient/model_accept_device_user_code_request.go new file mode 100644 index 00000000000..c34d1cd5045 --- /dev/null +++ b/internal/httpclient/model_accept_device_user_code_request.go @@ -0,0 +1,125 @@ +/* +Ory Hydra API + +Documentation for all of Ory Hydra's APIs. + +API version: +Contact: hi@ory.sh +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package openapi + +import ( + "encoding/json" +) + +// checks if the AcceptDeviceUserCodeRequest type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &AcceptDeviceUserCodeRequest{} + +// AcceptDeviceUserCodeRequest Contains information on an device verification +type AcceptDeviceUserCodeRequest struct { + UserCode *string `json:"user_code,omitempty"` +} + +// NewAcceptDeviceUserCodeRequest instantiates a new AcceptDeviceUserCodeRequest object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewAcceptDeviceUserCodeRequest() *AcceptDeviceUserCodeRequest { + this := AcceptDeviceUserCodeRequest{} + return &this +} + +// NewAcceptDeviceUserCodeRequestWithDefaults instantiates a new AcceptDeviceUserCodeRequest object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewAcceptDeviceUserCodeRequestWithDefaults() *AcceptDeviceUserCodeRequest { + this := AcceptDeviceUserCodeRequest{} + return &this +} + +// GetUserCode returns the UserCode field value if set, zero value otherwise. +func (o *AcceptDeviceUserCodeRequest) GetUserCode() string { + if o == nil || IsNil(o.UserCode) { + var ret string + return ret + } + return *o.UserCode +} + +// GetUserCodeOk returns a tuple with the UserCode field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *AcceptDeviceUserCodeRequest) GetUserCodeOk() (*string, bool) { + if o == nil || IsNil(o.UserCode) { + return nil, false + } + return o.UserCode, true +} + +// HasUserCode returns a boolean if a field has been set. +func (o *AcceptDeviceUserCodeRequest) HasUserCode() bool { + if o != nil && !IsNil(o.UserCode) { + return true + } + + return false +} + +// SetUserCode gets a reference to the given string and assigns it to the UserCode field. +func (o *AcceptDeviceUserCodeRequest) SetUserCode(v string) { + o.UserCode = &v +} + +func (o AcceptDeviceUserCodeRequest) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o AcceptDeviceUserCodeRequest) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.UserCode) { + toSerialize["user_code"] = o.UserCode + } + return toSerialize, nil +} + +type NullableAcceptDeviceUserCodeRequest struct { + value *AcceptDeviceUserCodeRequest + isSet bool +} + +func (v NullableAcceptDeviceUserCodeRequest) Get() *AcceptDeviceUserCodeRequest { + return v.value +} + +func (v *NullableAcceptDeviceUserCodeRequest) Set(val *AcceptDeviceUserCodeRequest) { + v.value = val + v.isSet = true +} + +func (v NullableAcceptDeviceUserCodeRequest) IsSet() bool { + return v.isSet +} + +func (v *NullableAcceptDeviceUserCodeRequest) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableAcceptDeviceUserCodeRequest(val *AcceptDeviceUserCodeRequest) *NullableAcceptDeviceUserCodeRequest { + return &NullableAcceptDeviceUserCodeRequest{value: val, isSet: true} +} + +func (v NullableAcceptDeviceUserCodeRequest) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableAcceptDeviceUserCodeRequest) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/model_device_authorization.go b/internal/httpclient/model_device_authorization.go new file mode 100644 index 00000000000..975972a8532 --- /dev/null +++ b/internal/httpclient/model_device_authorization.go @@ -0,0 +1,311 @@ +/* +Ory Hydra API + +Documentation for all of Ory Hydra's APIs. + +API version: +Contact: hi@ory.sh +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package openapi + +import ( + "encoding/json" +) + +// checks if the DeviceAuthorization type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &DeviceAuthorization{} + +// DeviceAuthorization # Ory's OAuth 2.0 Device Authorization API +type DeviceAuthorization struct { + // The device verification code. + DeviceCode *string `json:"device_code,omitempty"` + // The lifetime in seconds of the \"device_code\" and \"user_code\". + ExpiresIn *int64 `json:"expires_in,omitempty"` + // The minimum amount of time in seconds that the client SHOULD wait between polling requests to the token endpoint. If no value is provided, clients MUST use 5 as the default. + Interval *int64 `json:"interval,omitempty"` + // The end-user verification code. + UserCode *string `json:"user_code,omitempty"` + // The end-user verification URI on the authorization server. The URI should be short and easy to remember as end users will be asked to manually type it into their user agent. + VerificationUri *string `json:"verification_uri,omitempty"` + // A verification URI that includes the \"user_code\" (or other information with the same function as the \"user_code\"), which is designed for non-textual transmission. + VerificationUriComplete *string `json:"verification_uri_complete,omitempty"` +} + +// NewDeviceAuthorization instantiates a new DeviceAuthorization object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewDeviceAuthorization() *DeviceAuthorization { + this := DeviceAuthorization{} + return &this +} + +// NewDeviceAuthorizationWithDefaults instantiates a new DeviceAuthorization object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewDeviceAuthorizationWithDefaults() *DeviceAuthorization { + this := DeviceAuthorization{} + return &this +} + +// GetDeviceCode returns the DeviceCode field value if set, zero value otherwise. +func (o *DeviceAuthorization) GetDeviceCode() string { + if o == nil || IsNil(o.DeviceCode) { + var ret string + return ret + } + return *o.DeviceCode +} + +// GetDeviceCodeOk returns a tuple with the DeviceCode field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthorization) GetDeviceCodeOk() (*string, bool) { + if o == nil || IsNil(o.DeviceCode) { + return nil, false + } + return o.DeviceCode, true +} + +// HasDeviceCode returns a boolean if a field has been set. +func (o *DeviceAuthorization) HasDeviceCode() bool { + if o != nil && !IsNil(o.DeviceCode) { + return true + } + + return false +} + +// SetDeviceCode gets a reference to the given string and assigns it to the DeviceCode field. +func (o *DeviceAuthorization) SetDeviceCode(v string) { + o.DeviceCode = &v +} + +// GetExpiresIn returns the ExpiresIn field value if set, zero value otherwise. +func (o *DeviceAuthorization) GetExpiresIn() int64 { + if o == nil || IsNil(o.ExpiresIn) { + var ret int64 + return ret + } + return *o.ExpiresIn +} + +// GetExpiresInOk returns a tuple with the ExpiresIn field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthorization) GetExpiresInOk() (*int64, bool) { + if o == nil || IsNil(o.ExpiresIn) { + return nil, false + } + return o.ExpiresIn, true +} + +// HasExpiresIn returns a boolean if a field has been set. +func (o *DeviceAuthorization) HasExpiresIn() bool { + if o != nil && !IsNil(o.ExpiresIn) { + return true + } + + return false +} + +// SetExpiresIn gets a reference to the given int64 and assigns it to the ExpiresIn field. +func (o *DeviceAuthorization) SetExpiresIn(v int64) { + o.ExpiresIn = &v +} + +// GetInterval returns the Interval field value if set, zero value otherwise. +func (o *DeviceAuthorization) GetInterval() int64 { + if o == nil || IsNil(o.Interval) { + var ret int64 + return ret + } + return *o.Interval +} + +// GetIntervalOk returns a tuple with the Interval field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthorization) GetIntervalOk() (*int64, bool) { + if o == nil || IsNil(o.Interval) { + return nil, false + } + return o.Interval, true +} + +// HasInterval returns a boolean if a field has been set. +func (o *DeviceAuthorization) HasInterval() bool { + if o != nil && !IsNil(o.Interval) { + return true + } + + return false +} + +// SetInterval gets a reference to the given int64 and assigns it to the Interval field. +func (o *DeviceAuthorization) SetInterval(v int64) { + o.Interval = &v +} + +// GetUserCode returns the UserCode field value if set, zero value otherwise. +func (o *DeviceAuthorization) GetUserCode() string { + if o == nil || IsNil(o.UserCode) { + var ret string + return ret + } + return *o.UserCode +} + +// GetUserCodeOk returns a tuple with the UserCode field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthorization) GetUserCodeOk() (*string, bool) { + if o == nil || IsNil(o.UserCode) { + return nil, false + } + return o.UserCode, true +} + +// HasUserCode returns a boolean if a field has been set. +func (o *DeviceAuthorization) HasUserCode() bool { + if o != nil && !IsNil(o.UserCode) { + return true + } + + return false +} + +// SetUserCode gets a reference to the given string and assigns it to the UserCode field. +func (o *DeviceAuthorization) SetUserCode(v string) { + o.UserCode = &v +} + +// GetVerificationUri returns the VerificationUri field value if set, zero value otherwise. +func (o *DeviceAuthorization) GetVerificationUri() string { + if o == nil || IsNil(o.VerificationUri) { + var ret string + return ret + } + return *o.VerificationUri +} + +// GetVerificationUriOk returns a tuple with the VerificationUri field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthorization) GetVerificationUriOk() (*string, bool) { + if o == nil || IsNil(o.VerificationUri) { + return nil, false + } + return o.VerificationUri, true +} + +// HasVerificationUri returns a boolean if a field has been set. +func (o *DeviceAuthorization) HasVerificationUri() bool { + if o != nil && !IsNil(o.VerificationUri) { + return true + } + + return false +} + +// SetVerificationUri gets a reference to the given string and assigns it to the VerificationUri field. +func (o *DeviceAuthorization) SetVerificationUri(v string) { + o.VerificationUri = &v +} + +// GetVerificationUriComplete returns the VerificationUriComplete field value if set, zero value otherwise. +func (o *DeviceAuthorization) GetVerificationUriComplete() string { + if o == nil || IsNil(o.VerificationUriComplete) { + var ret string + return ret + } + return *o.VerificationUriComplete +} + +// GetVerificationUriCompleteOk returns a tuple with the VerificationUriComplete field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthorization) GetVerificationUriCompleteOk() (*string, bool) { + if o == nil || IsNil(o.VerificationUriComplete) { + return nil, false + } + return o.VerificationUriComplete, true +} + +// HasVerificationUriComplete returns a boolean if a field has been set. +func (o *DeviceAuthorization) HasVerificationUriComplete() bool { + if o != nil && !IsNil(o.VerificationUriComplete) { + return true + } + + return false +} + +// SetVerificationUriComplete gets a reference to the given string and assigns it to the VerificationUriComplete field. +func (o *DeviceAuthorization) SetVerificationUriComplete(v string) { + o.VerificationUriComplete = &v +} + +func (o DeviceAuthorization) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o DeviceAuthorization) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.DeviceCode) { + toSerialize["device_code"] = o.DeviceCode + } + if !IsNil(o.ExpiresIn) { + toSerialize["expires_in"] = o.ExpiresIn + } + if !IsNil(o.Interval) { + toSerialize["interval"] = o.Interval + } + if !IsNil(o.UserCode) { + toSerialize["user_code"] = o.UserCode + } + if !IsNil(o.VerificationUri) { + toSerialize["verification_uri"] = o.VerificationUri + } + if !IsNil(o.VerificationUriComplete) { + toSerialize["verification_uri_complete"] = o.VerificationUriComplete + } + return toSerialize, nil +} + +type NullableDeviceAuthorization struct { + value *DeviceAuthorization + isSet bool +} + +func (v NullableDeviceAuthorization) Get() *DeviceAuthorization { + return v.value +} + +func (v *NullableDeviceAuthorization) Set(val *DeviceAuthorization) { + v.value = val + v.isSet = true +} + +func (v NullableDeviceAuthorization) IsSet() bool { + return v.isSet +} + +func (v *NullableDeviceAuthorization) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableDeviceAuthorization(val *DeviceAuthorization) *NullableDeviceAuthorization { + return &NullableDeviceAuthorization{value: val, isSet: true} +} + +func (v NullableDeviceAuthorization) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableDeviceAuthorization) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/model_device_user_auth_request.go b/internal/httpclient/model_device_user_auth_request.go new file mode 100644 index 00000000000..a101144b4a1 --- /dev/null +++ b/internal/httpclient/model_device_user_auth_request.go @@ -0,0 +1,340 @@ +/* +Ory Hydra API + +Documentation for all of Ory Hydra's APIs. + +API version: +Contact: hi@ory.sh +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package openapi + +import ( + "bytes" + "encoding/json" + "fmt" + "time" +) + +// checks if the DeviceUserAuthRequest type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &DeviceUserAuthRequest{} + +// DeviceUserAuthRequest struct for DeviceUserAuthRequest +type DeviceUserAuthRequest struct { + // ID is the identifier (\"device challenge\") of the device grant request. It is used to identify the session. + Challenge string `json:"challenge"` + Client *OAuth2Client `json:"client,omitempty"` + HandledAt *time.Time `json:"handled_at,omitempty"` + // RequestURL is the original Device Authorization URL requested. + RequestUrl *string `json:"request_url,omitempty"` + RequestedAccessTokenAudience []string `json:"requested_access_token_audience,omitempty"` + RequestedScope []string `json:"requested_scope,omitempty"` +} + +type _DeviceUserAuthRequest DeviceUserAuthRequest + +// NewDeviceUserAuthRequest instantiates a new DeviceUserAuthRequest object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewDeviceUserAuthRequest(challenge string) *DeviceUserAuthRequest { + this := DeviceUserAuthRequest{} + this.Challenge = challenge + return &this +} + +// NewDeviceUserAuthRequestWithDefaults instantiates a new DeviceUserAuthRequest object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewDeviceUserAuthRequestWithDefaults() *DeviceUserAuthRequest { + this := DeviceUserAuthRequest{} + return &this +} + +// GetChallenge returns the Challenge field value +func (o *DeviceUserAuthRequest) GetChallenge() string { + if o == nil { + var ret string + return ret + } + + return o.Challenge +} + +// GetChallengeOk returns a tuple with the Challenge field value +// and a boolean to check if the value has been set. +func (o *DeviceUserAuthRequest) GetChallengeOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Challenge, true +} + +// SetChallenge sets field value +func (o *DeviceUserAuthRequest) SetChallenge(v string) { + o.Challenge = v +} + +// GetClient returns the Client field value if set, zero value otherwise. +func (o *DeviceUserAuthRequest) GetClient() OAuth2Client { + if o == nil || IsNil(o.Client) { + var ret OAuth2Client + return ret + } + return *o.Client +} + +// GetClientOk returns a tuple with the Client field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceUserAuthRequest) GetClientOk() (*OAuth2Client, bool) { + if o == nil || IsNil(o.Client) { + return nil, false + } + return o.Client, true +} + +// HasClient returns a boolean if a field has been set. +func (o *DeviceUserAuthRequest) HasClient() bool { + if o != nil && !IsNil(o.Client) { + return true + } + + return false +} + +// SetClient gets a reference to the given OAuth2Client and assigns it to the Client field. +func (o *DeviceUserAuthRequest) SetClient(v OAuth2Client) { + o.Client = &v +} + +// GetHandledAt returns the HandledAt field value if set, zero value otherwise. +func (o *DeviceUserAuthRequest) GetHandledAt() time.Time { + if o == nil || IsNil(o.HandledAt) { + var ret time.Time + return ret + } + return *o.HandledAt +} + +// GetHandledAtOk returns a tuple with the HandledAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceUserAuthRequest) GetHandledAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.HandledAt) { + return nil, false + } + return o.HandledAt, true +} + +// HasHandledAt returns a boolean if a field has been set. +func (o *DeviceUserAuthRequest) HasHandledAt() bool { + if o != nil && !IsNil(o.HandledAt) { + return true + } + + return false +} + +// SetHandledAt gets a reference to the given time.Time and assigns it to the HandledAt field. +func (o *DeviceUserAuthRequest) SetHandledAt(v time.Time) { + o.HandledAt = &v +} + +// GetRequestUrl returns the RequestUrl field value if set, zero value otherwise. +func (o *DeviceUserAuthRequest) GetRequestUrl() string { + if o == nil || IsNil(o.RequestUrl) { + var ret string + return ret + } + return *o.RequestUrl +} + +// GetRequestUrlOk returns a tuple with the RequestUrl field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceUserAuthRequest) GetRequestUrlOk() (*string, bool) { + if o == nil || IsNil(o.RequestUrl) { + return nil, false + } + return o.RequestUrl, true +} + +// HasRequestUrl returns a boolean if a field has been set. +func (o *DeviceUserAuthRequest) HasRequestUrl() bool { + if o != nil && !IsNil(o.RequestUrl) { + return true + } + + return false +} + +// SetRequestUrl gets a reference to the given string and assigns it to the RequestUrl field. +func (o *DeviceUserAuthRequest) SetRequestUrl(v string) { + o.RequestUrl = &v +} + +// GetRequestedAccessTokenAudience returns the RequestedAccessTokenAudience field value if set, zero value otherwise. +func (o *DeviceUserAuthRequest) GetRequestedAccessTokenAudience() []string { + if o == nil || IsNil(o.RequestedAccessTokenAudience) { + var ret []string + return ret + } + return o.RequestedAccessTokenAudience +} + +// GetRequestedAccessTokenAudienceOk returns a tuple with the RequestedAccessTokenAudience field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceUserAuthRequest) GetRequestedAccessTokenAudienceOk() ([]string, bool) { + if o == nil || IsNil(o.RequestedAccessTokenAudience) { + return nil, false + } + return o.RequestedAccessTokenAudience, true +} + +// HasRequestedAccessTokenAudience returns a boolean if a field has been set. +func (o *DeviceUserAuthRequest) HasRequestedAccessTokenAudience() bool { + if o != nil && !IsNil(o.RequestedAccessTokenAudience) { + return true + } + + return false +} + +// SetRequestedAccessTokenAudience gets a reference to the given []string and assigns it to the RequestedAccessTokenAudience field. +func (o *DeviceUserAuthRequest) SetRequestedAccessTokenAudience(v []string) { + o.RequestedAccessTokenAudience = v +} + +// GetRequestedScope returns the RequestedScope field value if set, zero value otherwise. +func (o *DeviceUserAuthRequest) GetRequestedScope() []string { + if o == nil || IsNil(o.RequestedScope) { + var ret []string + return ret + } + return o.RequestedScope +} + +// GetRequestedScopeOk returns a tuple with the RequestedScope field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceUserAuthRequest) GetRequestedScopeOk() ([]string, bool) { + if o == nil || IsNil(o.RequestedScope) { + return nil, false + } + return o.RequestedScope, true +} + +// HasRequestedScope returns a boolean if a field has been set. +func (o *DeviceUserAuthRequest) HasRequestedScope() bool { + if o != nil && !IsNil(o.RequestedScope) { + return true + } + + return false +} + +// SetRequestedScope gets a reference to the given []string and assigns it to the RequestedScope field. +func (o *DeviceUserAuthRequest) SetRequestedScope(v []string) { + o.RequestedScope = v +} + +func (o DeviceUserAuthRequest) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o DeviceUserAuthRequest) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["challenge"] = o.Challenge + if !IsNil(o.Client) { + toSerialize["client"] = o.Client + } + if !IsNil(o.HandledAt) { + toSerialize["handled_at"] = o.HandledAt + } + if !IsNil(o.RequestUrl) { + toSerialize["request_url"] = o.RequestUrl + } + if !IsNil(o.RequestedAccessTokenAudience) { + toSerialize["requested_access_token_audience"] = o.RequestedAccessTokenAudience + } + if !IsNil(o.RequestedScope) { + toSerialize["requested_scope"] = o.RequestedScope + } + return toSerialize, nil +} + +func (o *DeviceUserAuthRequest) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "challenge", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varDeviceUserAuthRequest := _DeviceUserAuthRequest{} + + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.DisallowUnknownFields() + err = decoder.Decode(&varDeviceUserAuthRequest) + + if err != nil { + return err + } + + *o = DeviceUserAuthRequest(varDeviceUserAuthRequest) + + return err +} + +type NullableDeviceUserAuthRequest struct { + value *DeviceUserAuthRequest + isSet bool +} + +func (v NullableDeviceUserAuthRequest) Get() *DeviceUserAuthRequest { + return v.value +} + +func (v *NullableDeviceUserAuthRequest) Set(val *DeviceUserAuthRequest) { + v.value = val + v.isSet = true +} + +func (v NullableDeviceUserAuthRequest) IsSet() bool { + return v.isSet +} + +func (v *NullableDeviceUserAuthRequest) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableDeviceUserAuthRequest(val *DeviceUserAuthRequest) *NullableDeviceUserAuthRequest { + return &NullableDeviceUserAuthRequest{value: val, isSet: true} +} + +func (v NullableDeviceUserAuthRequest) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableDeviceUserAuthRequest) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/model_o_auth2_client.go b/internal/httpclient/model_o_auth2_client.go index 0be981e6017..ddf0d733545 100644 --- a/internal/httpclient/model_o_auth2_client.go +++ b/internal/httpclient/model_o_auth2_client.go @@ -50,6 +50,12 @@ type OAuth2Client struct { Contacts []string `json:"contacts,omitempty"` // OAuth 2.0 Client Creation Date CreatedAt returns the timestamp of the client's creation. CreatedAt *time.Time `json:"created_at,omitempty"` + // Specify a time duration in milliseconds, seconds, minutes, hours. + DeviceAuthorizationGrantAccessTokenLifespan *string `json:"device_authorization_grant_access_token_lifespan,omitempty"` + // Specify a time duration in milliseconds, seconds, minutes, hours. + DeviceAuthorizationGrantIdTokenLifespan *string `json:"device_authorization_grant_id_token_lifespan,omitempty"` + // Specify a time duration in milliseconds, seconds, minutes, hours. + DeviceAuthorizationGrantRefreshTokenLifespan *string `json:"device_authorization_grant_refresh_token_lifespan,omitempty"` // OpenID Connect Front-Channel Logout Session Required Boolean value specifying whether the RP requires that iss (issuer) and sid (session ID) query parameters be included to identify the RP session with the OP when the frontchannel_logout_uri is used. If omitted, the default value is false. FrontchannelLogoutSessionRequired *bool `json:"frontchannel_logout_session_required,omitempty"` // OpenID Connect Front-Channel Logout URI RP URL that will cause the RP to log itself out when rendered in an iframe by the OP. An iss (issuer) query parameter and a sid (session ID) query parameter MAY be included by the OP to enable the RP to validate the request and to determine which of the potentially multiple sessions is to be logged out; if either is included, both MUST be. @@ -643,6 +649,102 @@ func (o *OAuth2Client) SetCreatedAt(v time.Time) { o.CreatedAt = &v } +// GetDeviceAuthorizationGrantAccessTokenLifespan returns the DeviceAuthorizationGrantAccessTokenLifespan field value if set, zero value otherwise. +func (o *OAuth2Client) GetDeviceAuthorizationGrantAccessTokenLifespan() string { + if o == nil || IsNil(o.DeviceAuthorizationGrantAccessTokenLifespan) { + var ret string + return ret + } + return *o.DeviceAuthorizationGrantAccessTokenLifespan +} + +// GetDeviceAuthorizationGrantAccessTokenLifespanOk returns a tuple with the DeviceAuthorizationGrantAccessTokenLifespan field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetDeviceAuthorizationGrantAccessTokenLifespanOk() (*string, bool) { + if o == nil || IsNil(o.DeviceAuthorizationGrantAccessTokenLifespan) { + return nil, false + } + return o.DeviceAuthorizationGrantAccessTokenLifespan, true +} + +// HasDeviceAuthorizationGrantAccessTokenLifespan returns a boolean if a field has been set. +func (o *OAuth2Client) HasDeviceAuthorizationGrantAccessTokenLifespan() bool { + if o != nil && !IsNil(o.DeviceAuthorizationGrantAccessTokenLifespan) { + return true + } + + return false +} + +// SetDeviceAuthorizationGrantAccessTokenLifespan gets a reference to the given string and assigns it to the DeviceAuthorizationGrantAccessTokenLifespan field. +func (o *OAuth2Client) SetDeviceAuthorizationGrantAccessTokenLifespan(v string) { + o.DeviceAuthorizationGrantAccessTokenLifespan = &v +} + +// GetDeviceAuthorizationGrantIdTokenLifespan returns the DeviceAuthorizationGrantIdTokenLifespan field value if set, zero value otherwise. +func (o *OAuth2Client) GetDeviceAuthorizationGrantIdTokenLifespan() string { + if o == nil || IsNil(o.DeviceAuthorizationGrantIdTokenLifespan) { + var ret string + return ret + } + return *o.DeviceAuthorizationGrantIdTokenLifespan +} + +// GetDeviceAuthorizationGrantIdTokenLifespanOk returns a tuple with the DeviceAuthorizationGrantIdTokenLifespan field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetDeviceAuthorizationGrantIdTokenLifespanOk() (*string, bool) { + if o == nil || IsNil(o.DeviceAuthorizationGrantIdTokenLifespan) { + return nil, false + } + return o.DeviceAuthorizationGrantIdTokenLifespan, true +} + +// HasDeviceAuthorizationGrantIdTokenLifespan returns a boolean if a field has been set. +func (o *OAuth2Client) HasDeviceAuthorizationGrantIdTokenLifespan() bool { + if o != nil && !IsNil(o.DeviceAuthorizationGrantIdTokenLifespan) { + return true + } + + return false +} + +// SetDeviceAuthorizationGrantIdTokenLifespan gets a reference to the given string and assigns it to the DeviceAuthorizationGrantIdTokenLifespan field. +func (o *OAuth2Client) SetDeviceAuthorizationGrantIdTokenLifespan(v string) { + o.DeviceAuthorizationGrantIdTokenLifespan = &v +} + +// GetDeviceAuthorizationGrantRefreshTokenLifespan returns the DeviceAuthorizationGrantRefreshTokenLifespan field value if set, zero value otherwise. +func (o *OAuth2Client) GetDeviceAuthorizationGrantRefreshTokenLifespan() string { + if o == nil || IsNil(o.DeviceAuthorizationGrantRefreshTokenLifespan) { + var ret string + return ret + } + return *o.DeviceAuthorizationGrantRefreshTokenLifespan +} + +// GetDeviceAuthorizationGrantRefreshTokenLifespanOk returns a tuple with the DeviceAuthorizationGrantRefreshTokenLifespan field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetDeviceAuthorizationGrantRefreshTokenLifespanOk() (*string, bool) { + if o == nil || IsNil(o.DeviceAuthorizationGrantRefreshTokenLifespan) { + return nil, false + } + return o.DeviceAuthorizationGrantRefreshTokenLifespan, true +} + +// HasDeviceAuthorizationGrantRefreshTokenLifespan returns a boolean if a field has been set. +func (o *OAuth2Client) HasDeviceAuthorizationGrantRefreshTokenLifespan() bool { + if o != nil && !IsNil(o.DeviceAuthorizationGrantRefreshTokenLifespan) { + return true + } + + return false +} + +// SetDeviceAuthorizationGrantRefreshTokenLifespan gets a reference to the given string and assigns it to the DeviceAuthorizationGrantRefreshTokenLifespan field. +func (o *OAuth2Client) SetDeviceAuthorizationGrantRefreshTokenLifespan(v string) { + o.DeviceAuthorizationGrantRefreshTokenLifespan = &v +} + // GetFrontchannelLogoutSessionRequired returns the FrontchannelLogoutSessionRequired field value if set, zero value otherwise. func (o *OAuth2Client) GetFrontchannelLogoutSessionRequired() bool { if o == nil || IsNil(o.FrontchannelLogoutSessionRequired) { @@ -1727,6 +1829,15 @@ func (o OAuth2Client) ToMap() (map[string]interface{}, error) { if !IsNil(o.CreatedAt) { toSerialize["created_at"] = o.CreatedAt } + if !IsNil(o.DeviceAuthorizationGrantAccessTokenLifespan) { + toSerialize["device_authorization_grant_access_token_lifespan"] = o.DeviceAuthorizationGrantAccessTokenLifespan + } + if !IsNil(o.DeviceAuthorizationGrantIdTokenLifespan) { + toSerialize["device_authorization_grant_id_token_lifespan"] = o.DeviceAuthorizationGrantIdTokenLifespan + } + if !IsNil(o.DeviceAuthorizationGrantRefreshTokenLifespan) { + toSerialize["device_authorization_grant_refresh_token_lifespan"] = o.DeviceAuthorizationGrantRefreshTokenLifespan + } if !IsNil(o.FrontchannelLogoutSessionRequired) { toSerialize["frontchannel_logout_session_required"] = o.FrontchannelLogoutSessionRequired } diff --git a/internal/httpclient/model_o_auth2_client_token_lifespans.go b/internal/httpclient/model_o_auth2_client_token_lifespans.go index 2ed10b8508c..16e925f679c 100644 --- a/internal/httpclient/model_o_auth2_client_token_lifespans.go +++ b/internal/httpclient/model_o_auth2_client_token_lifespans.go @@ -29,6 +29,12 @@ type OAuth2ClientTokenLifespans struct { // Specify a time duration in milliseconds, seconds, minutes, hours. ClientCredentialsGrantAccessTokenLifespan *string `json:"client_credentials_grant_access_token_lifespan,omitempty"` // Specify a time duration in milliseconds, seconds, minutes, hours. + DeviceAuthorizationGrantAccessTokenLifespan *string `json:"device_authorization_grant_access_token_lifespan,omitempty"` + // Specify a time duration in milliseconds, seconds, minutes, hours. + DeviceAuthorizationGrantIdTokenLifespan *string `json:"device_authorization_grant_id_token_lifespan,omitempty"` + // Specify a time duration in milliseconds, seconds, minutes, hours. + DeviceAuthorizationGrantRefreshTokenLifespan *string `json:"device_authorization_grant_refresh_token_lifespan,omitempty"` + // Specify a time duration in milliseconds, seconds, minutes, hours. ImplicitGrantAccessTokenLifespan *string `json:"implicit_grant_access_token_lifespan,omitempty"` // Specify a time duration in milliseconds, seconds, minutes, hours. ImplicitGrantIdTokenLifespan *string `json:"implicit_grant_id_token_lifespan,omitempty"` @@ -187,6 +193,102 @@ func (o *OAuth2ClientTokenLifespans) SetClientCredentialsGrantAccessTokenLifespa o.ClientCredentialsGrantAccessTokenLifespan = &v } +// GetDeviceAuthorizationGrantAccessTokenLifespan returns the DeviceAuthorizationGrantAccessTokenLifespan field value if set, zero value otherwise. +func (o *OAuth2ClientTokenLifespans) GetDeviceAuthorizationGrantAccessTokenLifespan() string { + if o == nil || IsNil(o.DeviceAuthorizationGrantAccessTokenLifespan) { + var ret string + return ret + } + return *o.DeviceAuthorizationGrantAccessTokenLifespan +} + +// GetDeviceAuthorizationGrantAccessTokenLifespanOk returns a tuple with the DeviceAuthorizationGrantAccessTokenLifespan field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2ClientTokenLifespans) GetDeviceAuthorizationGrantAccessTokenLifespanOk() (*string, bool) { + if o == nil || IsNil(o.DeviceAuthorizationGrantAccessTokenLifespan) { + return nil, false + } + return o.DeviceAuthorizationGrantAccessTokenLifespan, true +} + +// HasDeviceAuthorizationGrantAccessTokenLifespan returns a boolean if a field has been set. +func (o *OAuth2ClientTokenLifespans) HasDeviceAuthorizationGrantAccessTokenLifespan() bool { + if o != nil && !IsNil(o.DeviceAuthorizationGrantAccessTokenLifespan) { + return true + } + + return false +} + +// SetDeviceAuthorizationGrantAccessTokenLifespan gets a reference to the given string and assigns it to the DeviceAuthorizationGrantAccessTokenLifespan field. +func (o *OAuth2ClientTokenLifespans) SetDeviceAuthorizationGrantAccessTokenLifespan(v string) { + o.DeviceAuthorizationGrantAccessTokenLifespan = &v +} + +// GetDeviceAuthorizationGrantIdTokenLifespan returns the DeviceAuthorizationGrantIdTokenLifespan field value if set, zero value otherwise. +func (o *OAuth2ClientTokenLifespans) GetDeviceAuthorizationGrantIdTokenLifespan() string { + if o == nil || IsNil(o.DeviceAuthorizationGrantIdTokenLifespan) { + var ret string + return ret + } + return *o.DeviceAuthorizationGrantIdTokenLifespan +} + +// GetDeviceAuthorizationGrantIdTokenLifespanOk returns a tuple with the DeviceAuthorizationGrantIdTokenLifespan field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2ClientTokenLifespans) GetDeviceAuthorizationGrantIdTokenLifespanOk() (*string, bool) { + if o == nil || IsNil(o.DeviceAuthorizationGrantIdTokenLifespan) { + return nil, false + } + return o.DeviceAuthorizationGrantIdTokenLifespan, true +} + +// HasDeviceAuthorizationGrantIdTokenLifespan returns a boolean if a field has been set. +func (o *OAuth2ClientTokenLifespans) HasDeviceAuthorizationGrantIdTokenLifespan() bool { + if o != nil && !IsNil(o.DeviceAuthorizationGrantIdTokenLifespan) { + return true + } + + return false +} + +// SetDeviceAuthorizationGrantIdTokenLifespan gets a reference to the given string and assigns it to the DeviceAuthorizationGrantIdTokenLifespan field. +func (o *OAuth2ClientTokenLifespans) SetDeviceAuthorizationGrantIdTokenLifespan(v string) { + o.DeviceAuthorizationGrantIdTokenLifespan = &v +} + +// GetDeviceAuthorizationGrantRefreshTokenLifespan returns the DeviceAuthorizationGrantRefreshTokenLifespan field value if set, zero value otherwise. +func (o *OAuth2ClientTokenLifespans) GetDeviceAuthorizationGrantRefreshTokenLifespan() string { + if o == nil || IsNil(o.DeviceAuthorizationGrantRefreshTokenLifespan) { + var ret string + return ret + } + return *o.DeviceAuthorizationGrantRefreshTokenLifespan +} + +// GetDeviceAuthorizationGrantRefreshTokenLifespanOk returns a tuple with the DeviceAuthorizationGrantRefreshTokenLifespan field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2ClientTokenLifespans) GetDeviceAuthorizationGrantRefreshTokenLifespanOk() (*string, bool) { + if o == nil || IsNil(o.DeviceAuthorizationGrantRefreshTokenLifespan) { + return nil, false + } + return o.DeviceAuthorizationGrantRefreshTokenLifespan, true +} + +// HasDeviceAuthorizationGrantRefreshTokenLifespan returns a boolean if a field has been set. +func (o *OAuth2ClientTokenLifespans) HasDeviceAuthorizationGrantRefreshTokenLifespan() bool { + if o != nil && !IsNil(o.DeviceAuthorizationGrantRefreshTokenLifespan) { + return true + } + + return false +} + +// SetDeviceAuthorizationGrantRefreshTokenLifespan gets a reference to the given string and assigns it to the DeviceAuthorizationGrantRefreshTokenLifespan field. +func (o *OAuth2ClientTokenLifespans) SetDeviceAuthorizationGrantRefreshTokenLifespan(v string) { + o.DeviceAuthorizationGrantRefreshTokenLifespan = &v +} + // GetImplicitGrantAccessTokenLifespan returns the ImplicitGrantAccessTokenLifespan field value if set, zero value otherwise. func (o *OAuth2ClientTokenLifespans) GetImplicitGrantAccessTokenLifespan() string { if o == nil || IsNil(o.ImplicitGrantAccessTokenLifespan) { @@ -401,6 +503,15 @@ func (o OAuth2ClientTokenLifespans) ToMap() (map[string]interface{}, error) { if !IsNil(o.ClientCredentialsGrantAccessTokenLifespan) { toSerialize["client_credentials_grant_access_token_lifespan"] = o.ClientCredentialsGrantAccessTokenLifespan } + if !IsNil(o.DeviceAuthorizationGrantAccessTokenLifespan) { + toSerialize["device_authorization_grant_access_token_lifespan"] = o.DeviceAuthorizationGrantAccessTokenLifespan + } + if !IsNil(o.DeviceAuthorizationGrantIdTokenLifespan) { + toSerialize["device_authorization_grant_id_token_lifespan"] = o.DeviceAuthorizationGrantIdTokenLifespan + } + if !IsNil(o.DeviceAuthorizationGrantRefreshTokenLifespan) { + toSerialize["device_authorization_grant_refresh_token_lifespan"] = o.DeviceAuthorizationGrantRefreshTokenLifespan + } if !IsNil(o.ImplicitGrantAccessTokenLifespan) { toSerialize["implicit_grant_access_token_lifespan"] = o.ImplicitGrantAccessTokenLifespan } diff --git a/internal/httpclient/model_o_auth2_consent_request.go b/internal/httpclient/model_o_auth2_consent_request.go index 552c5c1e50f..8ff08a8c15e 100644 --- a/internal/httpclient/model_o_auth2_consent_request.go +++ b/internal/httpclient/model_o_auth2_consent_request.go @@ -31,6 +31,8 @@ type OAuth2ConsentRequest struct { // ConsentRequestID is the ID of the consent request. ConsentRequestId *string `json:"consent_request_id,omitempty"` Context interface{} `json:"context,omitempty"` + // DeviceChallenge is the device challenge this consent challenge belongs to, if this flow was initiated by a device. + DeviceChallengeId *string `json:"device_challenge_id,omitempty"` // LoginChallenge is the login challenge this consent challenge belongs to. It can be used to associate a login and consent request in the login & consent app. LoginChallenge *string `json:"login_challenge,omitempty"` // LoginSessionID is the login session ID. If the user-agent reuses a login session (via cookie / remember flag) this ID will remain the same. If the user-agent did not have an existing authentication session (e.g. remember is false) this will be a new random value. This value is used as the \"sid\" parameter in the ID Token and in OIDC Front-/Back- channel logout. It's value can generally be used to associate consecutive login requests by a certain user. @@ -251,6 +253,38 @@ func (o *OAuth2ConsentRequest) SetContext(v interface{}) { o.Context = v } +// GetDeviceChallengeId returns the DeviceChallengeId field value if set, zero value otherwise. +func (o *OAuth2ConsentRequest) GetDeviceChallengeId() string { + if o == nil || IsNil(o.DeviceChallengeId) { + var ret string + return ret + } + return *o.DeviceChallengeId +} + +// GetDeviceChallengeIdOk returns a tuple with the DeviceChallengeId field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2ConsentRequest) GetDeviceChallengeIdOk() (*string, bool) { + if o == nil || IsNil(o.DeviceChallengeId) { + return nil, false + } + return o.DeviceChallengeId, true +} + +// HasDeviceChallengeId returns a boolean if a field has been set. +func (o *OAuth2ConsentRequest) HasDeviceChallengeId() bool { + if o != nil && !IsNil(o.DeviceChallengeId) { + return true + } + + return false +} + +// SetDeviceChallengeId gets a reference to the given string and assigns it to the DeviceChallengeId field. +func (o *OAuth2ConsentRequest) SetDeviceChallengeId(v string) { + o.DeviceChallengeId = &v +} + // GetLoginChallenge returns the LoginChallenge field value if set, zero value otherwise. func (o *OAuth2ConsentRequest) GetLoginChallenge() string { if o == nil || IsNil(o.LoginChallenge) { @@ -533,6 +567,9 @@ func (o OAuth2ConsentRequest) ToMap() (map[string]interface{}, error) { if o.Context != nil { toSerialize["context"] = o.Context } + if !IsNil(o.DeviceChallengeId) { + toSerialize["device_challenge_id"] = o.DeviceChallengeId + } if !IsNil(o.LoginChallenge) { toSerialize["login_challenge"] = o.LoginChallenge } diff --git a/internal/httpclient/model_oidc_configuration.go b/internal/httpclient/model_oidc_configuration.go index 240e40b307f..465fa997f3f 100644 --- a/internal/httpclient/model_oidc_configuration.go +++ b/internal/httpclient/model_oidc_configuration.go @@ -38,6 +38,8 @@ type OidcConfiguration struct { CredentialsEndpointDraft00 *string `json:"credentials_endpoint_draft_00,omitempty"` // OpenID Connect Verifiable Credentials Supported JSON array containing a list of the Verifiable Credentials supported by this authorization server. CredentialsSupportedDraft00 []CredentialSupportedDraft00 `json:"credentials_supported_draft_00,omitempty"` + // OAuth 2.0 Device Authorization Endpoint URL + DeviceAuthorizationEndpoint string `json:"device_authorization_endpoint"` // OpenID Connect End-Session Endpoint URL at the OP to which an RP can perform a redirect to request that the End-User be logged out at the OP. EndSessionEndpoint *string `json:"end_session_endpoint,omitempty"` // OpenID Connect Front-Channel Logout Session Required Boolean value specifying whether the OP can pass iss (issuer) and sid (session ID) query parameters to identify the RP session with the OP when the frontchannel_logout_uri is used. If supported, the sid Claim is also included in ID Tokens issued by the OP. @@ -92,9 +94,10 @@ type _OidcConfiguration OidcConfiguration // This constructor will assign default values to properties that have it defined, // and makes sure properties required by API are set, but the set of arguments // will change when the set of required properties is changed -func NewOidcConfiguration(authorizationEndpoint string, idTokenSignedResponseAlg []string, idTokenSigningAlgValuesSupported []string, issuer string, jwksUri string, responseTypesSupported []string, subjectTypesSupported []string, tokenEndpoint string, userinfoSignedResponseAlg []string) *OidcConfiguration { +func NewOidcConfiguration(authorizationEndpoint string, deviceAuthorizationEndpoint string, idTokenSignedResponseAlg []string, idTokenSigningAlgValuesSupported []string, issuer string, jwksUri string, responseTypesSupported []string, subjectTypesSupported []string, tokenEndpoint string, userinfoSignedResponseAlg []string) *OidcConfiguration { this := OidcConfiguration{} this.AuthorizationEndpoint = authorizationEndpoint + this.DeviceAuthorizationEndpoint = deviceAuthorizationEndpoint this.IdTokenSignedResponseAlg = idTokenSignedResponseAlg this.IdTokenSigningAlgValuesSupported = idTokenSigningAlgValuesSupported this.Issuer = issuer @@ -362,6 +365,30 @@ func (o *OidcConfiguration) SetCredentialsSupportedDraft00(v []CredentialSupport o.CredentialsSupportedDraft00 = v } +// GetDeviceAuthorizationEndpoint returns the DeviceAuthorizationEndpoint field value +func (o *OidcConfiguration) GetDeviceAuthorizationEndpoint() string { + if o == nil { + var ret string + return ret + } + + return o.DeviceAuthorizationEndpoint +} + +// GetDeviceAuthorizationEndpointOk returns a tuple with the DeviceAuthorizationEndpoint field value +// and a boolean to check if the value has been set. +func (o *OidcConfiguration) GetDeviceAuthorizationEndpointOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.DeviceAuthorizationEndpoint, true +} + +// SetDeviceAuthorizationEndpoint sets field value +func (o *OidcConfiguration) SetDeviceAuthorizationEndpoint(v string) { + o.DeviceAuthorizationEndpoint = v +} + // GetEndSessionEndpoint returns the EndSessionEndpoint field value if set, zero value otherwise. func (o *OidcConfiguration) GetEndSessionEndpoint() string { if o == nil || IsNil(o.EndSessionEndpoint) { @@ -1066,6 +1093,7 @@ func (o OidcConfiguration) ToMap() (map[string]interface{}, error) { if !IsNil(o.CredentialsSupportedDraft00) { toSerialize["credentials_supported_draft_00"] = o.CredentialsSupportedDraft00 } + toSerialize["device_authorization_endpoint"] = o.DeviceAuthorizationEndpoint if !IsNil(o.EndSessionEndpoint) { toSerialize["end_session_endpoint"] = o.EndSessionEndpoint } @@ -1128,6 +1156,7 @@ func (o *OidcConfiguration) UnmarshalJSON(data []byte) (err error) { // that every required field exists as a key in the generic map. requiredProperties := []string{ "authorization_endpoint", + "device_authorization_endpoint", "id_token_signed_response_alg", "id_token_signing_alg_values_supported", "issuer", diff --git a/internal/httpclient/model_verify_user_code_request.go b/internal/httpclient/model_verify_user_code_request.go new file mode 100644 index 00000000000..692694e9040 --- /dev/null +++ b/internal/httpclient/model_verify_user_code_request.go @@ -0,0 +1,344 @@ +/* +Ory Hydra API + +Documentation for all of Ory Hydra's APIs. + +API version: +Contact: hi@ory.sh +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package openapi + +import ( + "encoding/json" + "time" +) + +// checks if the VerifyUserCodeRequest type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &VerifyUserCodeRequest{} + +// VerifyUserCodeRequest struct for VerifyUserCodeRequest +type VerifyUserCodeRequest struct { + // ID is the identifier (\"device challenge\") of the device request. It is used to identify the session. + Challenge *string `json:"challenge,omitempty"` + Client *OAuth2Client `json:"client,omitempty"` + DeviceCodeRequestId *string `json:"device_code_request_id,omitempty"` + HandledAt *time.Time `json:"handled_at,omitempty"` + // RequestURL is the original Device Authorization URL requested. + RequestUrl *string `json:"request_url,omitempty"` + RequestedAccessTokenAudience []string `json:"requested_access_token_audience,omitempty"` + RequestedScope []string `json:"requested_scope,omitempty"` +} + +// NewVerifyUserCodeRequest instantiates a new VerifyUserCodeRequest object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewVerifyUserCodeRequest() *VerifyUserCodeRequest { + this := VerifyUserCodeRequest{} + return &this +} + +// NewVerifyUserCodeRequestWithDefaults instantiates a new VerifyUserCodeRequest object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewVerifyUserCodeRequestWithDefaults() *VerifyUserCodeRequest { + this := VerifyUserCodeRequest{} + return &this +} + +// GetChallenge returns the Challenge field value if set, zero value otherwise. +func (o *VerifyUserCodeRequest) GetChallenge() string { + if o == nil || IsNil(o.Challenge) { + var ret string + return ret + } + return *o.Challenge +} + +// GetChallengeOk returns a tuple with the Challenge field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *VerifyUserCodeRequest) GetChallengeOk() (*string, bool) { + if o == nil || IsNil(o.Challenge) { + return nil, false + } + return o.Challenge, true +} + +// HasChallenge returns a boolean if a field has been set. +func (o *VerifyUserCodeRequest) HasChallenge() bool { + if o != nil && !IsNil(o.Challenge) { + return true + } + + return false +} + +// SetChallenge gets a reference to the given string and assigns it to the Challenge field. +func (o *VerifyUserCodeRequest) SetChallenge(v string) { + o.Challenge = &v +} + +// GetClient returns the Client field value if set, zero value otherwise. +func (o *VerifyUserCodeRequest) GetClient() OAuth2Client { + if o == nil || IsNil(o.Client) { + var ret OAuth2Client + return ret + } + return *o.Client +} + +// GetClientOk returns a tuple with the Client field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *VerifyUserCodeRequest) GetClientOk() (*OAuth2Client, bool) { + if o == nil || IsNil(o.Client) { + return nil, false + } + return o.Client, true +} + +// HasClient returns a boolean if a field has been set. +func (o *VerifyUserCodeRequest) HasClient() bool { + if o != nil && !IsNil(o.Client) { + return true + } + + return false +} + +// SetClient gets a reference to the given OAuth2Client and assigns it to the Client field. +func (o *VerifyUserCodeRequest) SetClient(v OAuth2Client) { + o.Client = &v +} + +// GetDeviceCodeRequestId returns the DeviceCodeRequestId field value if set, zero value otherwise. +func (o *VerifyUserCodeRequest) GetDeviceCodeRequestId() string { + if o == nil || IsNil(o.DeviceCodeRequestId) { + var ret string + return ret + } + return *o.DeviceCodeRequestId +} + +// GetDeviceCodeRequestIdOk returns a tuple with the DeviceCodeRequestId field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *VerifyUserCodeRequest) GetDeviceCodeRequestIdOk() (*string, bool) { + if o == nil || IsNil(o.DeviceCodeRequestId) { + return nil, false + } + return o.DeviceCodeRequestId, true +} + +// HasDeviceCodeRequestId returns a boolean if a field has been set. +func (o *VerifyUserCodeRequest) HasDeviceCodeRequestId() bool { + if o != nil && !IsNil(o.DeviceCodeRequestId) { + return true + } + + return false +} + +// SetDeviceCodeRequestId gets a reference to the given string and assigns it to the DeviceCodeRequestId field. +func (o *VerifyUserCodeRequest) SetDeviceCodeRequestId(v string) { + o.DeviceCodeRequestId = &v +} + +// GetHandledAt returns the HandledAt field value if set, zero value otherwise. +func (o *VerifyUserCodeRequest) GetHandledAt() time.Time { + if o == nil || IsNil(o.HandledAt) { + var ret time.Time + return ret + } + return *o.HandledAt +} + +// GetHandledAtOk returns a tuple with the HandledAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *VerifyUserCodeRequest) GetHandledAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.HandledAt) { + return nil, false + } + return o.HandledAt, true +} + +// HasHandledAt returns a boolean if a field has been set. +func (o *VerifyUserCodeRequest) HasHandledAt() bool { + if o != nil && !IsNil(o.HandledAt) { + return true + } + + return false +} + +// SetHandledAt gets a reference to the given time.Time and assigns it to the HandledAt field. +func (o *VerifyUserCodeRequest) SetHandledAt(v time.Time) { + o.HandledAt = &v +} + +// GetRequestUrl returns the RequestUrl field value if set, zero value otherwise. +func (o *VerifyUserCodeRequest) GetRequestUrl() string { + if o == nil || IsNil(o.RequestUrl) { + var ret string + return ret + } + return *o.RequestUrl +} + +// GetRequestUrlOk returns a tuple with the RequestUrl field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *VerifyUserCodeRequest) GetRequestUrlOk() (*string, bool) { + if o == nil || IsNil(o.RequestUrl) { + return nil, false + } + return o.RequestUrl, true +} + +// HasRequestUrl returns a boolean if a field has been set. +func (o *VerifyUserCodeRequest) HasRequestUrl() bool { + if o != nil && !IsNil(o.RequestUrl) { + return true + } + + return false +} + +// SetRequestUrl gets a reference to the given string and assigns it to the RequestUrl field. +func (o *VerifyUserCodeRequest) SetRequestUrl(v string) { + o.RequestUrl = &v +} + +// GetRequestedAccessTokenAudience returns the RequestedAccessTokenAudience field value if set, zero value otherwise. +func (o *VerifyUserCodeRequest) GetRequestedAccessTokenAudience() []string { + if o == nil || IsNil(o.RequestedAccessTokenAudience) { + var ret []string + return ret + } + return o.RequestedAccessTokenAudience +} + +// GetRequestedAccessTokenAudienceOk returns a tuple with the RequestedAccessTokenAudience field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *VerifyUserCodeRequest) GetRequestedAccessTokenAudienceOk() ([]string, bool) { + if o == nil || IsNil(o.RequestedAccessTokenAudience) { + return nil, false + } + return o.RequestedAccessTokenAudience, true +} + +// HasRequestedAccessTokenAudience returns a boolean if a field has been set. +func (o *VerifyUserCodeRequest) HasRequestedAccessTokenAudience() bool { + if o != nil && !IsNil(o.RequestedAccessTokenAudience) { + return true + } + + return false +} + +// SetRequestedAccessTokenAudience gets a reference to the given []string and assigns it to the RequestedAccessTokenAudience field. +func (o *VerifyUserCodeRequest) SetRequestedAccessTokenAudience(v []string) { + o.RequestedAccessTokenAudience = v +} + +// GetRequestedScope returns the RequestedScope field value if set, zero value otherwise. +func (o *VerifyUserCodeRequest) GetRequestedScope() []string { + if o == nil || IsNil(o.RequestedScope) { + var ret []string + return ret + } + return o.RequestedScope +} + +// GetRequestedScopeOk returns a tuple with the RequestedScope field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *VerifyUserCodeRequest) GetRequestedScopeOk() ([]string, bool) { + if o == nil || IsNil(o.RequestedScope) { + return nil, false + } + return o.RequestedScope, true +} + +// HasRequestedScope returns a boolean if a field has been set. +func (o *VerifyUserCodeRequest) HasRequestedScope() bool { + if o != nil && !IsNil(o.RequestedScope) { + return true + } + + return false +} + +// SetRequestedScope gets a reference to the given []string and assigns it to the RequestedScope field. +func (o *VerifyUserCodeRequest) SetRequestedScope(v []string) { + o.RequestedScope = v +} + +func (o VerifyUserCodeRequest) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o VerifyUserCodeRequest) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Challenge) { + toSerialize["challenge"] = o.Challenge + } + if !IsNil(o.Client) { + toSerialize["client"] = o.Client + } + if !IsNil(o.DeviceCodeRequestId) { + toSerialize["device_code_request_id"] = o.DeviceCodeRequestId + } + if !IsNil(o.HandledAt) { + toSerialize["handled_at"] = o.HandledAt + } + if !IsNil(o.RequestUrl) { + toSerialize["request_url"] = o.RequestUrl + } + if !IsNil(o.RequestedAccessTokenAudience) { + toSerialize["requested_access_token_audience"] = o.RequestedAccessTokenAudience + } + if !IsNil(o.RequestedScope) { + toSerialize["requested_scope"] = o.RequestedScope + } + return toSerialize, nil +} + +type NullableVerifyUserCodeRequest struct { + value *VerifyUserCodeRequest + isSet bool +} + +func (v NullableVerifyUserCodeRequest) Get() *VerifyUserCodeRequest { + return v.value +} + +func (v *NullableVerifyUserCodeRequest) Set(val *VerifyUserCodeRequest) { + v.value = val + v.isSet = true +} + +func (v NullableVerifyUserCodeRequest) IsSet() bool { + return v.isSet +} + +func (v *NullableVerifyUserCodeRequest) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableVerifyUserCodeRequest(val *VerifyUserCodeRequest) *NullableVerifyUserCodeRequest { + return &NullableVerifyUserCodeRequest{value: val, isSet: true} +} + +func (v NullableVerifyUserCodeRequest) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableVerifyUserCodeRequest) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/testhelpers/oauth2.go b/internal/testhelpers/oauth2.go index 4a7b5bc696e..bdee8493cb5 100644 --- a/internal/testhelpers/oauth2.go +++ b/internal/testhelpers/oauth2.go @@ -182,6 +182,17 @@ func NewLoginConsentUI(t testing.TB, c *config.DefaultProvider, login, consent h c.MustSet(context.Background(), config.KeyConsentURL, ct.URL) } +func NewDeviceLoginConsentUI(t testing.TB, c *config.DefaultProvider, device, login, consent http.HandlerFunc) { + if device == nil { + device = HTTPServerNotImplementedHandler + } + dt := httptest.NewServer(device) + t.Cleanup(dt.Close) + c.MustSet(context.Background(), config.KeyDeviceVerificationURL, dt.URL) + + NewLoginConsentUI(t, c, login, consent) +} + func NewCallbackURL(t testing.TB, prefix string, h http.HandlerFunc) string { if h == nil { h = HTTPServerNotImplementedHandler diff --git a/spec/api.json b/spec/api.json index c1ae0ecf1dd..772573fdb61 100644 --- a/spec/api.json +++ b/spec/api.json @@ -68,6 +68,35 @@ "type": "object" }, "DefaultError": {}, + "DeviceUserAuthRequest": { + "properties": { + "challenge": { + "description": "ID is the identifier (\"device challenge\") of the device grant request. It is used to\nidentify the session.", + "type": "string" + }, + "client": { + "$ref": "#/components/schemas/oAuth2Client" + }, + "handled_at": { + "$ref": "#/components/schemas/nullTime" + }, + "request_url": { + "description": "RequestURL is the original Device Authorization URL requested.", + "type": "string" + }, + "requested_access_token_audience": { + "$ref": "#/components/schemas/StringSliceJSONFormat" + }, + "requested_scope": { + "$ref": "#/components/schemas/StringSliceJSONFormat" + } + }, + "required": [ + "challenge" + ], + "title": "Contains information on an ongoing device grant request.", + "type": "object" + }, "JSONRawMessage": { "title": "JSONRawMessage represents a json.RawMessage that works well with JSON, SQL, and Swagger." }, @@ -148,6 +177,15 @@ "title": "VerifiableCredentialProof contains the proof of a verifiable credential.", "type": "object" }, + "acceptDeviceUserCodeRequest": { + "description": "Contains information on an device verification", + "properties": { + "user_code": { + "type": "string" + } + }, + "type": "object" + }, "acceptOAuth2ConsentRequest": { "properties": { "context": { @@ -289,6 +327,45 @@ "title": "Verifiable Credentials Metadata (Draft 00)", "type": "object" }, + "deviceAuthorization": { + "description": "# Ory's OAuth 2.0 Device Authorization API", + "properties": { + "device_code": { + "description": "The device verification code.", + "example": "ory_dc_smldfksmdfkl.mslkmlkmlk", + "type": "string" + }, + "expires_in": { + "description": "The lifetime in seconds of the \"device_code\" and \"user_code\".", + "example": 16830, + "format": "int64", + "type": "integer" + }, + "interval": { + "description": "The minimum amount of time in seconds that the client\nSHOULD wait between polling requests to the token endpoint. If no\nvalue is provided, clients MUST use 5 as the default.", + "example": 5, + "format": "int64", + "type": "integer" + }, + "user_code": { + "description": "The end-user verification code.", + "example": "AAAAAA", + "type": "string" + }, + "verification_uri": { + "description": "The end-user verification URI on the authorization\nserver. The URI should be short and easy to remember as end users\nwill be asked to manually type it into their user agent.", + "example": "https://auth.ory.sh/tv", + "type": "string" + }, + "verification_uri_complete": { + "description": "A verification URI that includes the \"user_code\" (or\nother information with the same function as the \"user_code\"),\nwhich is designed for non-textual transmission.", + "example": "https://auth.ory.sh/tv?user_code=AAAAAA", + "type": "string" + } + }, + "title": "OAuth2 Device Flow", + "type": "object" + }, "errorOAuth2": { "description": "Error", "properties": { @@ -670,6 +747,15 @@ "format": "date-time", "type": "string" }, + "device_authorization_grant_access_token_lifespan": { + "$ref": "#/components/schemas/NullDuration" + }, + "device_authorization_grant_id_token_lifespan": { + "$ref": "#/components/schemas/NullDuration" + }, + "device_authorization_grant_refresh_token_lifespan": { + "$ref": "#/components/schemas/NullDuration" + }, "frontchannel_logout_session_required": { "description": "OpenID Connect Front-Channel Logout Session Required\n\nBoolean value specifying whether the RP requires that iss (issuer) and sid (session ID) query parameters be\nincluded to identify the RP session with the OP when the frontchannel_logout_uri is used.\nIf omitted, the default value is false.", "type": "boolean" @@ -807,6 +893,15 @@ "client_credentials_grant_access_token_lifespan": { "$ref": "#/components/schemas/NullDuration" }, + "device_authorization_grant_access_token_lifespan": { + "$ref": "#/components/schemas/NullDuration" + }, + "device_authorization_grant_id_token_lifespan": { + "$ref": "#/components/schemas/NullDuration" + }, + "device_authorization_grant_refresh_token_lifespan": { + "$ref": "#/components/schemas/NullDuration" + }, "implicit_grant_access_token_lifespan": { "$ref": "#/components/schemas/NullDuration" }, @@ -852,6 +947,10 @@ "context": { "$ref": "#/components/schemas/JSONRawMessage" }, + "device_challenge_id": { + "description": "DeviceChallenge is the device challenge this consent challenge belongs to, if this flow was initiated by a device.", + "type": "string" + }, "login_challenge": { "description": "LoginChallenge is the login challenge this consent challenge belongs to. It can be used to associate\na login and consent request in the login \u0026 consent app.", "type": "string" @@ -1161,6 +1260,11 @@ }, "type": "array" }, + "device_authorization_endpoint": { + "description": "OAuth 2.0 Device Authorization Endpoint URL", + "example": "https://playground.ory.sh/ory-hydra/public/oauth2/device/oauth", + "type": "string" + }, "end_session_endpoint": { "description": "OpenID Connect End-Session Endpoint\n\nURL at the OP to which an RP can perform a redirect to request that the End-User be logged out at the OP.", "type": "string" @@ -1294,6 +1398,7 @@ "required": [ "issuer", "authorization_endpoint", + "device_authorization_endpoint", "token_endpoint", "jwks_uri", "subject_types_supported", @@ -1679,6 +1784,35 @@ "title": "VerifiableCredentialResponse contains the verifiable credential.", "type": "object" }, + "verifyUserCodeRequest": { + "properties": { + "challenge": { + "description": "ID is the identifier (\"device challenge\") of the device request. It is used to\nidentify the session.", + "type": "string" + }, + "client": { + "$ref": "#/components/schemas/oAuth2Client" + }, + "device_code_request_id": { + "type": "string" + }, + "handled_at": { + "$ref": "#/components/schemas/nullTime" + }, + "request_url": { + "description": "RequestURL is the original Device Authorization URL requested.", + "type": "string" + }, + "requested_access_token_audience": { + "$ref": "#/components/schemas/StringSliceJSONFormat" + }, + "requested_scope": { + "$ref": "#/components/schemas/StringSliceJSONFormat" + } + }, + "title": "HandledDeviceUserAuthRequest is the request payload used to accept a device user_code.", + "type": "object" + }, "version": { "properties": { "version": { @@ -2605,6 +2739,58 @@ ] } }, + "/admin/oauth2/auth/requests/device/accept": { + "put": { + "description": "Accepts a device grant user_code request", + "operationId": "acceptUserCodeRequest", + "parameters": [ + { + "in": "query", + "name": "device_challenge", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/acceptDeviceUserCodeRequest" + } + } + }, + "x-originalParamName": "Body" + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/oAuth2RedirectTo" + } + } + }, + "description": "oAuth2RedirectTo" + }, + "default": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorOAuth2" + } + } + }, + "description": "errorOAuth2" + } + }, + "summary": "Accepts a device grant user_code request", + "tags": [ + "oAuth2" + ] + } + }, "/admin/oauth2/auth/requests/login": { "get": { "description": "When an authorization code, hybrid, or implicit OAuth 2.0 Flow is initiated, Ory asks the login provider\nto authenticate the subject and then tell the Ory OAuth2 Service about it.\n\nPer default, the login provider is Ory itself. You may use a different login provider which needs to be a web-app\nyou write and host, and it must be able to authenticate (\"show the subject a login screen\")\na subject (in OAuth2 the proper name for subject is \"resource owner\").\n\nThe authentication challenge is appended to the login provider URL to which the subject's user-agent (browser) is redirected to. The login\nprovider uses that challenge to fetch information on the OAuth2 request and then accept or reject the requested authentication process.", @@ -3493,6 +3679,63 @@ ] } }, + "/oauth2/device/auth": { + "post": { + "description": "This endpoint is not documented here because you should never use your own implementation to perform OAuth2 flows.\nOAuth2 is a very popular protocol and a library for your programming language will exists.\n\nTo learn more about this flow please refer to the specification: https://tools.ietf.org/html/rfc8628", + "operationId": "oAuth2DeviceFlow", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/deviceAuthorization" + } + } + }, + "description": "deviceAuthorization" + }, + "default": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorOAuth2" + } + } + }, + "description": "errorOAuth2" + } + }, + "summary": "The OAuth 2.0 Device Authorize Endpoint", + "tags": [ + "oAuth2" + ] + } + }, + "/oauth2/device/verify": { + "get": { + "description": "This is the device user verification endpoint. The user is redirected here when trying to login using the device flow.", + "operationId": "performOAuth2DeviceVerificationFlow", + "responses": { + "302": { + "$ref": "#/components/responses/emptyResponse" + }, + "default": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorOAuth2" + } + } + }, + "description": "errorOAuth2" + } + }, + "summary": "OAuth 2.0 Device Verification Endpoint", + "tags": [ + "oAuth2" + ] + } + }, "/oauth2/register": { "post": { "description": "This endpoint behaves like the administrative counterpart (`createOAuth2Client`) but is capable of facing the\npublic internet directly and can be used in self-service. It implements the OpenID Connect\nDynamic Client Registration Protocol. This feature needs to be enabled in the configuration. This endpoint\nis disabled by default. It can be enabled by an administrator.\n\nPlease note that using this endpoint you are not able to choose the `client_secret` nor the `client_id` as those\nvalues will be server generated when specifying `token_endpoint_auth_method` as `client_secret_basic` or\n`client_secret_post`.\n\nThe `client_secret` will be returned in the response and you will not be able to retrieve it later on.\nWrite the secret down and keep it somewhere safe.", diff --git a/spec/swagger.json b/spec/swagger.json index ac8a18d390d..0db4dac74e7 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -891,6 +891,55 @@ } } }, + "/admin/oauth2/auth/requests/device/accept": { + "put": { + "description": "Accepts a device grant user_code request", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "schemes": [ + "http", + "https" + ], + "tags": [ + "oAuth2" + ], + "summary": "Accepts a device grant user_code request", + "operationId": "acceptUserCodeRequest", + "parameters": [ + { + "type": "string", + "name": "device_challenge", + "in": "query", + "required": true + }, + { + "name": "Body", + "in": "body", + "schema": { + "$ref": "#/definitions/acceptDeviceUserCodeRequest" + } + } + ], + "responses": { + "200": { + "description": "oAuth2RedirectTo", + "schema": { + "$ref": "#/definitions/oAuth2RedirectTo" + } + }, + "default": { + "description": "errorOAuth2", + "schema": { + "$ref": "#/definitions/errorOAuth2" + } + } + } + } + }, "/admin/oauth2/auth/requests/login": { "get": { "description": "When an authorization code, hybrid, or implicit OAuth 2.0 Flow is initiated, Ory asks the login provider\nto authenticate the subject and then tell the Ory OAuth2 Service about it.\n\nPer default, the login provider is Ory itself. You may use a different login provider which needs to be a web-app\nyou write and host, and it must be able to authenticate (\"show the subject a login screen\")\na subject (in OAuth2 the proper name for subject is \"resource owner\").\n\nThe authentication challenge is appended to the login provider URL to which the subject's user-agent (browser) is redirected to. The login\nprovider uses that challenge to fetch information on the OAuth2 request and then accept or reject the requested authentication process.", @@ -1718,6 +1767,65 @@ } } }, + "/oauth2/device/auth": { + "post": { + "description": "This endpoint is not documented here because you should never use your own implementation to perform OAuth2 flows.\nOAuth2 is a very popular protocol and a library for your programming language will exists.\n\nTo learn more about this flow please refer to the specification: https://tools.ietf.org/html/rfc8628", + "consumes": [ + "application/x-www-form-urlencoded" + ], + "schemes": [ + "http", + "https" + ], + "tags": [ + "oAuth2" + ], + "summary": "The OAuth 2.0 Device Authorize Endpoint", + "operationId": "oAuth2DeviceFlow", + "responses": { + "200": { + "description": "deviceAuthorization", + "schema": { + "$ref": "#/definitions/deviceAuthorization" + } + }, + "default": { + "description": "errorOAuth2", + "schema": { + "$ref": "#/definitions/errorOAuth2" + } + } + } + } + }, + "/oauth2/device/verify": { + "get": { + "description": "This is the device user verification endpoint. The user is redirected here when trying to login using the device flow.", + "consumes": [ + "application/x-www-form-urlencoded" + ], + "schemes": [ + "http", + "https" + ], + "tags": [ + "oAuth2" + ], + "summary": "OAuth 2.0 Device Verification Endpoint", + "operationId": "performOAuth2DeviceVerificationFlow", + "responses": { + "302": { + "$ref": "#/responses/emptyResponse" + }, + "default": { + "description": "errorOAuth2", + "schema": { + "$ref": "#/definitions/errorOAuth2" + } + } + } + } + }, "/oauth2/register": { "post": { "description": "This endpoint behaves like the administrative counterpart (`createOAuth2Client`) but is capable of facing the\npublic internet directly and can be used in self-service. It implements the OpenID Connect\nDynamic Client Registration Protocol. This feature needs to be enabled in the configuration. This endpoint\nis disabled by default. It can be enabled by an administrator.\n\nPlease note that using this endpoint you are not able to choose the `client_secret` nor the `client_id` as those\nvalues will be server generated when specifying `token_endpoint_auth_method` as `client_secret_basic` or\n`client_secret_post`.\n\nThe `client_secret` will be returned in the response and you will not be able to retrieve it later on.\nWrite the secret down and keep it somewhere safe.", @@ -2124,6 +2232,35 @@ } }, "DefaultError": {}, + "DeviceUserAuthRequest": { + "type": "object", + "title": "Contains information on an ongoing device grant request.", + "required": [ + "challenge" + ], + "properties": { + "challenge": { + "description": "ID is the identifier (\"device challenge\") of the device grant request. It is used to\nidentify the session.", + "type": "string" + }, + "client": { + "$ref": "#/definitions/oAuth2Client" + }, + "handled_at": { + "$ref": "#/definitions/nullTime" + }, + "request_url": { + "description": "RequestURL is the original Device Authorization URL requested.", + "type": "string" + }, + "requested_access_token_audience": { + "$ref": "#/definitions/StringSliceJSONFormat" + }, + "requested_scope": { + "$ref": "#/definitions/StringSliceJSONFormat" + } + } + }, "JSONRawMessage": { "type": "object", "title": "JSONRawMessage represents a json.RawMessage that works well with JSON, SQL, and Swagger." @@ -2174,6 +2311,15 @@ } } }, + "acceptDeviceUserCodeRequest": { + "description": "Contains information on an device verification", + "type": "object", + "properties": { + "user_code": { + "type": "string" + } + } + }, "acceptOAuth2ConsentRequest": { "type": "object", "title": "The request payload used to accept a consent request.", @@ -2320,7 +2466,7 @@ } }, "deviceAuthorization": { - "description": "OAuth 2.0 Device Authorization endpoint", + "description": "# Ory's OAuth 2.0 Device Authorization API", "type": "object", "title": "OAuth2 Device Flow", "properties": { @@ -2734,6 +2880,15 @@ "type": "string", "format": "date-time" }, + "device_authorization_grant_access_token_lifespan": { + "$ref": "#/definitions/NullDuration" + }, + "device_authorization_grant_id_token_lifespan": { + "$ref": "#/definitions/NullDuration" + }, + "device_authorization_grant_refresh_token_lifespan": { + "$ref": "#/definitions/NullDuration" + }, "frontchannel_logout_session_required": { "description": "OpenID Connect Front-Channel Logout Session Required\n\nBoolean value specifying whether the RP requires that iss (issuer) and sid (session ID) query parameters be\nincluded to identify the RP session with the OP when the frontchannel_logout_uri is used.\nIf omitted, the default value is false.", "type": "boolean" @@ -2871,6 +3026,15 @@ "client_credentials_grant_access_token_lifespan": { "$ref": "#/definitions/NullDuration" }, + "device_authorization_grant_access_token_lifespan": { + "$ref": "#/definitions/NullDuration" + }, + "device_authorization_grant_id_token_lifespan": { + "$ref": "#/definitions/NullDuration" + }, + "device_authorization_grant_refresh_token_lifespan": { + "$ref": "#/definitions/NullDuration" + }, "implicit_grant_access_token_lifespan": { "$ref": "#/definitions/NullDuration" }, @@ -2919,6 +3083,10 @@ "context": { "$ref": "#/definitions/JSONRawMessage" }, + "device_challenge_id": { + "description": "DeviceChallenge is the device challenge this consent challenge belongs to, if this flow was initiated by a device.", + "type": "string" + }, "login_challenge": { "description": "LoginChallenge is the login challenge this consent challenge belongs to. It can be used to associate\na login and consent request in the login \u0026 consent app.", "type": "string" @@ -3160,6 +3328,7 @@ "required": [ "issuer", "authorization_endpoint", + "device_authorization_endpoint", "token_endpoint", "jwks_uri", "subject_types_supported", @@ -3211,6 +3380,11 @@ "$ref": "#/definitions/credentialSupportedDraft00" } }, + "device_authorization_endpoint": { + "description": "OAuth 2.0 Device Authorization Endpoint URL", + "type": "string", + "example": "https://playground.ory.sh/ory-hydra/public/oauth2/device/oauth" + }, "end_session_endpoint": { "description": "OpenID Connect End-Session Endpoint\n\nURL at the OP to which an RP can perform a redirect to request that the End-User be logged out at the OP.", "type": "string" @@ -3716,6 +3890,35 @@ } } }, + "verifyUserCodeRequest": { + "type": "object", + "title": "HandledDeviceUserAuthRequest is the request payload used to accept a device user_code.", + "properties": { + "challenge": { + "description": "ID is the identifier (\"device challenge\") of the device request. It is used to\nidentify the session.", + "type": "string" + }, + "client": { + "$ref": "#/definitions/oAuth2Client" + }, + "device_code_request_id": { + "type": "string" + }, + "handled_at": { + "$ref": "#/definitions/nullTime" + }, + "request_url": { + "description": "RequestURL is the original Device Authorization URL requested.", + "type": "string" + }, + "requested_access_token_audience": { + "$ref": "#/definitions/StringSliceJSONFormat" + }, + "requested_scope": { + "$ref": "#/definitions/StringSliceJSONFormat" + } + } + }, "version": { "type": "object", "properties": { From fe9e20ed9534f32cbdfcd04a8672cd1ae0eeba25 Mon Sep 17 00:00:00 2001 From: Nikos Date: Thu, 26 Sep 2024 13:25:27 +0300 Subject: [PATCH 32/52] fix: duplicate user_code update --- persistence/sql/persister_oauth2.go | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/persistence/sql/persister_oauth2.go b/persistence/sql/persister_oauth2.go index 278a7d46fd5..edc289f1670 100644 --- a/persistence/sql/persister_oauth2.go +++ b/persistence/sql/persister_oauth2.go @@ -904,17 +904,15 @@ func (p *Persister) UpdateAndInvalidateUserCodeSessionByRequestID(ctx context.Co ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.UpdateAndInvalidateUserCodeSession") defer otelx.End(span, &err) - // TODO(nsklikas): afaict this is supposed to return an error if no rows were updated, but this is not the actual behavior. - // We need to either fix this OR do a select -> check -> update (this would require 2 queries instead of 1). - /* #nosec G201 table is static */ - return sqlcon.HandleError( - p.Connection(ctx). - RawQuery( - fmt.Sprintf("UPDATE %s SET active=false, challenge_id=? WHERE request_id=? AND nid = ? AND active=true", OAuth2RequestSQL{Table: sqlTableUserCode}.TableName()), - challenge_id, - request_id, - p.NetworkID(ctx), - ). - Exec(), - ) + if count, err := p.Connection(ctx).RawQuery( + fmt.Sprintf("UPDATE %s SET active=false, challenge_id=? WHERE request_id=? AND nid = ? AND active=true", OAuth2RequestSQL{Table: sqlTableUserCode}.TableName()), + challenge_id, + request_id, + p.NetworkID(ctx), + ).ExecWithCount(); count == 0 && err == nil { + return errorsx.WithStack(x.ErrNotFound) + } else if err != nil { + return sqlcon.HandleError(err) + } + return nil } From 2b0efc78a3894e510c17a28a2cf15c3a663c4257 Mon Sep 17 00:00:00 2001 From: Nikos Date: Fri, 15 Nov 2024 12:54:50 +0200 Subject: [PATCH 33/52] refactor: merge user and device code tables --- ...9000001000000_device_flow.cockroach.up.sql | 72 +++++++------------ .../20241609000001000000_device_flow.down.sql | 14 ++-- ...41609000001000000_device_flow.mysql.up.sql | 72 +++++++------------ ...09000001000000_device_flow.postgres.up.sql | 72 +++++++------------ .../20241609000001000000_device_flow.up.sql | 56 +++++---------- ...9000001000000_device_flow.cockroach.up.sql | 68 ------------------ .../20241609000001000000_device_flow.down.sql | 25 ------- ...41609000001000000_device_flow.mysql.up.sql | 68 ------------------ ...09000001000000_device_flow.postgres.up.sql | 66 ----------------- .../20241609000001000000_device_flow.up.sql | 58 --------------- 10 files changed, 96 insertions(+), 475 deletions(-) delete mode 100644 persistence/sql/src/YYYYMMDD000001_device_flow/20241609000001000000_device_flow.cockroach.up.sql delete mode 100644 persistence/sql/src/YYYYMMDD000001_device_flow/20241609000001000000_device_flow.down.sql delete mode 100644 persistence/sql/src/YYYYMMDD000001_device_flow/20241609000001000000_device_flow.mysql.up.sql delete mode 100644 persistence/sql/src/YYYYMMDD000001_device_flow/20241609000001000000_device_flow.postgres.up.sql delete mode 100644 persistence/sql/src/YYYYMMDD000001_device_flow/20241609000001000000_device_flow.up.sql diff --git a/persistence/sql/migrations/20241609000001000000_device_flow.cockroach.up.sql b/persistence/sql/migrations/20241609000001000000_device_flow.cockroach.up.sql index 69d926ba289..5f2d1b3a53c 100644 --- a/persistence/sql/migrations/20241609000001000000_device_flow.cockroach.up.sql +++ b/persistence/sql/migrations/20241609000001000000_device_flow.cockroach.up.sql @@ -1,58 +1,34 @@ -- Migration generated by the command below; DO NOT EDIT. -- hydra:generate hydra migrate gen -CREATE TABLE IF NOT EXISTS hydra_oauth2_device_code +CREATE TABLE IF NOT EXISTS hydra_oauth2_device_auth_codes ( - signature VARCHAR(255) NOT NULL PRIMARY KEY, - request_id VARCHAR(40) NOT NULL DEFAULT '', - requested_at TIMESTAMP NOT NULL DEFAULT NOW(), - client_id VARCHAR(255) NOT NULL DEFAULT '', - scope TEXT NOT NULL, - granted_scope TEXT NOT NULL, - form_data TEXT NOT NULL, - session_data TEXT NOT NULL, - subject VARCHAR(255) NOT NULL DEFAULT '', - active BOOL NOT NULL DEFAULT true, - requested_audience TEXT NOT NULL, - granted_audience TEXT NOT NULL, - challenge_id VARCHAR(40) NULL, - expires_at TIMESTAMP NULL, - nid UUID NOT NULL, + device_code_signature VARCHAR(255) NOT NULL, + user_code_signature VARCHAR(255) NOT NULL, + request_id VARCHAR(40) NOT NULL, + requested_at TIMESTAMP NOT NULL DEFAULT NOW(), + client_id VARCHAR(255) NOT NULL, + scope TEXT NOT NULL, + granted_scope TEXT NOT NULL, + form_data TEXT NOT NULL, + session_data TEXT NOT NULL, + subject VARCHAR(255) NOT NULL DEFAULT '', + device_code_active BOOL NOT NULL DEFAULT true, + user_code_state SMALLINT NOT NULL DEFAULT 0, + requested_audience TEXT NULL DEFAULT '', + granted_audience TEXT NULL DEFAULT '', + challenge_id VARCHAR(40) NULL, + expires_at TIMESTAMP NULL, + nid UUID NULL, FOREIGN KEY (client_id, nid) REFERENCES hydra_client(id, nid) ON DELETE CASCADE, - FOREIGN KEY (nid) REFERENCES networks(id) ON UPDATE RESTRICT ON DELETE CASCADE + FOREIGN KEY (nid) REFERENCES networks(id) ON UPDATE RESTRICT ON DELETE CASCADE, + PRIMARY KEY (device_code_signature, nid) ); -CREATE INDEX hydra_oauth2_device_code_request_id_idx ON hydra_oauth2_device_code (request_id, nid); -CREATE INDEX hydra_oauth2_device_code_client_id_idx ON hydra_oauth2_device_code (client_id, nid); -CREATE INDEX hydra_oauth2_device_code_challenge_id_idx ON hydra_oauth2_device_code (challenge_id); -CREATE INDEX hydra_oauth2_device_code_expires_at_idx ON hydra_oauth2_device_code (expires_at); - -CREATE TABLE IF NOT EXISTS hydra_oauth2_user_code -( - signature VARCHAR(255) NOT NULL PRIMARY KEY, - request_id VARCHAR(40) NOT NULL DEFAULT '', - requested_at TIMESTAMP NOT NULL DEFAULT NOW(), - client_id VARCHAR(255) NOT NULL DEFAULT '', - scope TEXT NOT NULL, - granted_scope TEXT NOT NULL, - form_data TEXT NOT NULL, - session_data TEXT NOT NULL, - subject VARCHAR(255) NOT NULL DEFAULT '', - active BOOL NOT NULL DEFAULT true, - requested_audience TEXT NOT NULL, - granted_audience TEXT NOT NULL, - challenge_id VARCHAR(40) NULL, - expires_at TIMESTAMP NULL, - nid UUID NOT NULL, - - FOREIGN KEY (client_id, nid) REFERENCES hydra_client(id, nid) ON DELETE CASCADE, - FOREIGN KEY (nid) REFERENCES networks(id) ON UPDATE RESTRICT ON DELETE CASCADE -); - -CREATE INDEX hydra_oauth2_user_code_request_id_idx ON hydra_oauth2_user_code (request_id, nid); -CREATE INDEX hydra_oauth2_user_code_client_id_idx ON hydra_oauth2_user_code (client_id, nid); -CREATE INDEX hydra_oauth2_user_code_challenge_id_idx ON hydra_oauth2_user_code (challenge_id); -CREATE INDEX hydra_oauth2_user_code_expires_at_idx ON hydra_oauth2_device_code (expires_at); +CREATE INDEX hydra_oauth2_device_auth_codes_request_id_idx ON hydra_oauth2_device_auth_codes (request_id, nid); +CREATE INDEX hydra_oauth2_device_auth_codes_client_id_idx ON hydra_oauth2_device_auth_codes (client_id, nid); +CREATE INDEX hydra_oauth2_device_auth_codes_challenge_id_idx ON hydra_oauth2_device_auth_codes (challenge_id); +CREATE UNIQUE INDEX hydra_oauth2_device_auth_codes_user_code_signature_idx ON hydra_oauth2_device_auth_codes (user_code_signature, nid); ALTER TABLE hydra_oauth2_flow ADD COLUMN device_challenge_id VARCHAR(255) NULL; ALTER TABLE hydra_oauth2_flow ADD COLUMN device_code_request_id VARCHAR(255) NULL; diff --git a/persistence/sql/migrations/20241609000001000000_device_flow.down.sql b/persistence/sql/migrations/20241609000001000000_device_flow.down.sql index 54ceefff83b..74f893a46cd 100644 --- a/persistence/sql/migrations/20241609000001000000_device_flow.down.sql +++ b/persistence/sql/migrations/20241609000001000000_device_flow.down.sql @@ -1,16 +1,10 @@ -- Migration generated by the command below; DO NOT EDIT. -- hydra:generate hydra migrate gen -ALTER TABLE hydra_oauth2_device_code DROP FOREIGN KEY IF EXISTS hydra_oauth2_device_code_challenge_id_fk; -ALTER TABLE hydra_oauth2_device_code DROP FOREIGN KEY IF EXISTS hydra_oauth2_device_code_client_id_fk; -ALTER TABLE hydra_oauth2_device_code DROP FOREIGN KEY IF EXISTS hydra_oauth2_device_code_nid_fk_idx; +ALTER TABLE hydra_oauth2_device_auth_codes DROP FOREIGN KEY IF EXISTS hydra_oauth2_device_auth_codes_challenge_id_fk; +ALTER TABLE hydra_oauth2_device_auth_codes DROP FOREIGN KEY IF EXISTS hydra_oauth2_device_auth_codes_client_id_fk; +ALTER TABLE hydra_oauth2_device_auth_codes DROP FOREIGN KEY IF EXISTS hydra_oauth2_device_auth_codes_nid_fk_idx; -DROP TABLE IF EXISTS hydra_oauth2_device_code; - -ALTER TABLE hydra_oauth2_user_code DROP FOREIGN KEY IF EXISTS hydra_oauth2_user_code_challenge_id_fk; -ALTER TABLE hydra_oauth2_user_code DROP FOREIGN KEY IF EXISTS hydra_oauth2_user_code_client_id_fk; -ALTER TABLE hydra_oauth2_user_code DROP FOREIGN KEY IF EXISTS hydra_oauth2_user_code_nid_fk_idx; - -DROP TABLE IF EXISTS hydra_oauth2_user_code; +DROP TABLE IF EXISTS hydra_oauth2_device_auth_codes; ALTER TABLE hydra_oauth2_flow DROP COLUMN IF EXISTS device_challenge_id; ALTER TABLE hydra_oauth2_flow DROP COLUMN IF EXISTS device_code_request_id; diff --git a/persistence/sql/migrations/20241609000001000000_device_flow.mysql.up.sql b/persistence/sql/migrations/20241609000001000000_device_flow.mysql.up.sql index c0b6446a0ec..1343058ff3a 100644 --- a/persistence/sql/migrations/20241609000001000000_device_flow.mysql.up.sql +++ b/persistence/sql/migrations/20241609000001000000_device_flow.mysql.up.sql @@ -1,58 +1,34 @@ -- Migration generated by the command below; DO NOT EDIT. -- hydra:generate hydra migrate gen -CREATE TABLE IF NOT EXISTS hydra_oauth2_device_code +CREATE TABLE IF NOT EXISTS hydra_oauth2_device_auth_codes ( - signature VARCHAR(255) NOT NULL PRIMARY KEY, - request_id VARCHAR(40) NOT NULL DEFAULT '', - requested_at TIMESTAMP NOT NULL DEFAULT NOW(), - client_id VARCHAR(255) NOT NULL DEFAULT '', - scope TEXT NOT NULL, - granted_scope TEXT NOT NULL, - form_data TEXT NOT NULL, - session_data TEXT NOT NULL, - subject VARCHAR(255) NOT NULL DEFAULT '', - active BOOL NOT NULL DEFAULT true, - requested_audience TEXT NOT NULL, - granted_audience TEXT NOT NULL, - challenge_id VARCHAR(40) NULL, - expires_at TIMESTAMP NULL, - nid CHAR(36) NOT NULL, + device_code_signature VARCHAR(255) NOT NULL, + user_code_signature VARCHAR(255) NOT NULL, + request_id VARCHAR(40) NOT NULL DEFAULT '', + requested_at TIMESTAMP NOT NULL DEFAULT NOW(), + client_id VARCHAR(255) NOT NULL, + scope TEXT NOT NULL, + granted_scope TEXT NOT NULL, + form_data TEXT NOT NULL, + session_data TEXT NOT NULL, + subject VARCHAR(255) NOT NULL DEFAULT '', + device_code_active BOOL NOT NULL DEFAULT true, + user_code_state SMALLINT NOT NULL DEFAULT 0, + requested_audience TEXT NOT NULL, + granted_audience TEXT NOT NULL, + challenge_id VARCHAR(40) NULL, + expires_at TIMESTAMP NULL, + nid CHAR(36) NOT NULL, FOREIGN KEY (client_id, nid) REFERENCES hydra_client(id, nid) ON DELETE CASCADE, - FOREIGN KEY (nid) REFERENCES networks(id) ON UPDATE RESTRICT ON DELETE CASCADE + FOREIGN KEY (nid) REFERENCES networks(id) ON UPDATE RESTRICT ON DELETE CASCADE, + PRIMARY KEY (device_code_signature, nid) ); -CREATE INDEX hydra_oauth2_device_code_request_id_idx ON hydra_oauth2_device_code (request_id, nid); -CREATE INDEX hydra_oauth2_device_code_client_id_idx ON hydra_oauth2_device_code (client_id, nid); -CREATE INDEX hydra_oauth2_device_code_challenge_id_idx ON hydra_oauth2_device_code (challenge_id); -CREATE INDEX hydra_oauth2_device_code_expires_at_idx ON hydra_oauth2_device_code (expires_at); - -CREATE TABLE IF NOT EXISTS hydra_oauth2_user_code -( - signature VARCHAR(255) NOT NULL PRIMARY KEY, - request_id VARCHAR(40) NOT NULL DEFAULT '', - requested_at TIMESTAMP NOT NULL DEFAULT NOW(), - client_id VARCHAR(255) NOT NULL DEFAULT '', - scope TEXT NOT NULL, - granted_scope TEXT NOT NULL, - form_data TEXT NOT NULL, - session_data TEXT NOT NULL, - subject VARCHAR(255) NOT NULL DEFAULT '', - active BOOL NOT NULL DEFAULT true, - requested_audience TEXT NOT NULL, - granted_audience TEXT NOT NULL, - challenge_id VARCHAR(40) NULL, - expires_at TIMESTAMP NULL, - nid CHAR(36) NOT NULL, - - FOREIGN KEY (client_id, nid) REFERENCES hydra_client(id, nid) ON DELETE CASCADE, - FOREIGN KEY (nid) REFERENCES networks(id) ON UPDATE RESTRICT ON DELETE CASCADE -); - -CREATE INDEX hydra_oauth2_user_code_request_id_idx ON hydra_oauth2_user_code (request_id, nid); -CREATE INDEX hydra_oauth2_user_code_client_id_idx ON hydra_oauth2_user_code (client_id, nid); -CREATE INDEX hydra_oauth2_user_code_challenge_id_idx ON hydra_oauth2_user_code (challenge_id); -CREATE INDEX hydra_oauth2_user_code_expires_at_idx ON hydra_oauth2_device_code (expires_at); +CREATE INDEX hydra_oauth2_device_auth_codes_request_id_idx ON hydra_oauth2_device_auth_codes (request_id, nid); +CREATE INDEX hydra_oauth2_device_auth_codes_client_id_idx ON hydra_oauth2_device_auth_codes (client_id, nid); +CREATE INDEX hydra_oauth2_device_auth_codes_challenge_id_idx ON hydra_oauth2_device_auth_codes (challenge_id); +CREATE UNIQUE INDEX hydra_oauth2_device_auth_codes_user_code_signature_idx ON hydra_oauth2_device_auth_codes (user_code_signature, nid); ALTER TABLE hydra_oauth2_flow ADD COLUMN device_challenge_id VARCHAR(255) NULL; ALTER TABLE hydra_oauth2_flow ADD COLUMN device_code_request_id VARCHAR(255) NULL; diff --git a/persistence/sql/migrations/20241609000001000000_device_flow.postgres.up.sql b/persistence/sql/migrations/20241609000001000000_device_flow.postgres.up.sql index 29bc24f69e3..8e3a76794f1 100644 --- a/persistence/sql/migrations/20241609000001000000_device_flow.postgres.up.sql +++ b/persistence/sql/migrations/20241609000001000000_device_flow.postgres.up.sql @@ -1,56 +1,34 @@ -- Migration generated by the command below; DO NOT EDIT. -- hydra:generate hydra migrate gen -CREATE TABLE IF NOT EXISTS hydra_oauth2_device_code ( - signature VARCHAR(255) NOT NULL PRIMARY KEY, - request_id VARCHAR(40) NOT NULL, - requested_at TIMESTAMP NOT NULL DEFAULT NOW(), - client_id VARCHAR(255) NOT NULL, - scope TEXT NOT NULL, - granted_scope TEXT NOT NULL, - form_data TEXT NOT NULL, - session_data TEXT NOT NULL, - subject VARCHAR(255) NOT NULL DEFAULT '', - active BOOL NOT NULL DEFAULT true, - requested_audience TEXT NULL DEFAULT '', - granted_audience TEXT NULL DEFAULT '', - challenge_id VARCHAR(40) NULL, - expires_at TIMESTAMP NULL, - nid UUID NULL, +CREATE TABLE IF NOT EXISTS hydra_oauth2_device_auth_codes +( + device_code_signature VARCHAR(255) NOT NULL, + user_code_signature VARCHAR(255) NOT NULL, + request_id VARCHAR(40) NOT NULL, + requested_at TIMESTAMP NOT NULL DEFAULT NOW(), + client_id VARCHAR(255) NOT NULL, + scope TEXT NOT NULL, + granted_scope TEXT NOT NULL, + form_data TEXT NOT NULL, + session_data TEXT NOT NULL, + subject VARCHAR(255) NOT NULL DEFAULT '', + device_code_active BOOL NOT NULL DEFAULT true, + user_code_state SMALLINT NOT NULL DEFAULT 0, + requested_audience TEXT NULL DEFAULT '', + granted_audience TEXT NULL DEFAULT '', + challenge_id VARCHAR(40) NULL, + expires_at TIMESTAMP NULL, + nid UUID NULL, FOREIGN KEY (client_id, nid) REFERENCES hydra_client(id, nid) ON DELETE CASCADE, - FOREIGN KEY (nid) REFERENCES networks(id) ON UPDATE RESTRICT ON DELETE CASCADE + FOREIGN KEY (nid) REFERENCES networks(id) ON UPDATE RESTRICT ON DELETE CASCADE, + PRIMARY KEY (device_code_signature, nid) ); -CREATE INDEX hydra_oauth2_device_code_request_id_idx ON hydra_oauth2_device_code (request_id, nid); -CREATE INDEX hydra_oauth2_device_code_client_id_idx ON hydra_oauth2_device_code (client_id, nid); -CREATE INDEX hydra_oauth2_device_code_challenge_id_idx ON hydra_oauth2_device_code (challenge_id); -CREATE INDEX hydra_oauth2_device_code_expires_at_idx ON hydra_oauth2_device_code (expires_at); - -CREATE TABLE IF NOT EXISTS hydra_oauth2_user_code ( - signature VARCHAR(255) NOT NULL PRIMARY KEY, - request_id VARCHAR(40) NOT NULL, - requested_at TIMESTAMP NOT NULL DEFAULT NOW(), - client_id VARCHAR(255) NOT NULL, - scope TEXT NOT NULL, - granted_scope TEXT NOT NULL, - form_data TEXT NOT NULL, - session_data TEXT NOT NULL, - subject VARCHAR(255) NOT NULL DEFAULT '', - active BOOL NOT NULL DEFAULT true, - requested_audience TEXT NULL DEFAULT '', - granted_audience TEXT NULL DEFAULT '', - challenge_id VARCHAR(40) NULL, - expires_at TIMESTAMP NULL, - nid UUID NULL, - - FOREIGN KEY (client_id, nid) REFERENCES hydra_client(id, nid) ON DELETE CASCADE, - FOREIGN KEY (nid) REFERENCES networks(id) ON UPDATE RESTRICT ON DELETE CASCADE -); - -CREATE INDEX hydra_oauth2_user_code_request_id_idx ON hydra_oauth2_user_code (request_id, nid); -CREATE INDEX hydra_oauth2_user_code_client_id_idx ON hydra_oauth2_user_code (client_id, nid); -CREATE INDEX hydra_oauth2_user_code_challenge_id_idx ON hydra_oauth2_user_code (challenge_id); -CREATE INDEX hydra_oauth2_user_code_expires_at_idx ON hydra_oauth2_device_code (expires_at); +CREATE INDEX hydra_oauth2_device_auth_codes_request_id_idx ON hydra_oauth2_device_auth_codes (request_id, nid); +CREATE INDEX hydra_oauth2_device_auth_codes_client_id_idx ON hydra_oauth2_device_auth_codes (client_id, nid); +CREATE INDEX hydra_oauth2_device_auth_codes_challenge_id_idx ON hydra_oauth2_device_auth_codes (challenge_id); +CREATE UNIQUE INDEX hydra_oauth2_device_auth_codes_user_code_signature_idx ON hydra_oauth2_device_auth_codes (user_code_signature, nid); ALTER TABLE hydra_oauth2_flow ADD COLUMN device_challenge_id VARCHAR(255) NULL; ALTER TABLE hydra_oauth2_flow ADD COLUMN device_code_request_id VARCHAR(255) NULL; diff --git a/persistence/sql/migrations/20241609000001000000_device_flow.up.sql b/persistence/sql/migrations/20241609000001000000_device_flow.up.sql index ceb69a1078b..47b8f54061e 100644 --- a/persistence/sql/migrations/20241609000001000000_device_flow.up.sql +++ b/persistence/sql/migrations/20241609000001000000_device_flow.up.sql @@ -1,48 +1,30 @@ -- Migration generated by the command below; DO NOT EDIT. -- hydra:generate hydra migrate gen -CREATE TABLE IF NOT EXISTS hydra_oauth2_device_code +CREATE TABLE IF NOT EXISTS hydra_oauth2_device_auth_codes ( - signature VARCHAR(255) NOT NULL PRIMARY KEY, - request_id VARCHAR(40) NOT NULL, - requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + device_code_signature VARCHAR(255) NOT NULL PRIMARY KEY, + user_code_signature VARCHAR(255) NOT NULL, + request_id VARCHAR(40) NOT NULL, + requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, client_id VARCHAR(255) NOT NULL, - scope TEXT NOT NULL, - granted_scope TEXT NOT NULL, - form_data TEXT NOT NULL, - session_data TEXT NOT NULL, + scope TEXT NOT NULL, + granted_scope TEXT NOT NULL, + form_data TEXT NOT NULL, + session_data TEXT NOT NULL, subject VARCHAR(255) NOT NULL DEFAULT '', - active BOOL NOT NULL DEFAULT true, - requested_audience TEXT NULL DEFAULT '', - granted_audience TEXT NULL DEFAULT '', - challenge_id VARCHAR(40) NULL, + device_code_active BOOL NOT NULL DEFAULT true, + user_code_state SMALLINT NOT NULL DEFAULT 0, + requested_audience TEXT NULL DEFAULT '', + granted_audience TEXT NULL DEFAULT '', + challenge_id VARCHAR(40) NULL, expires_at TIMESTAMP NULL, - nid UUID NULL + nid UUID NULL ); -CREATE INDEX hydra_oauth2_device_code_request_id_idx ON hydra_oauth2_device_code (request_id, nid); -CREATE INDEX hydra_oauth2_device_code_client_id_idx ON hydra_oauth2_device_code (client_id, nid); -CREATE INDEX hydra_oauth2_device_code_challenge_id_idx ON hydra_oauth2_device_code (challenge_id); -CREATE TABLE IF NOT EXISTS hydra_oauth2_user_code -( - signature VARCHAR(255) NOT NULL PRIMARY KEY, - request_id VARCHAR(40) NOT NULL, - requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - client_id VARCHAR(255) NOT NULL, - scope TEXT NOT NULL, - granted_scope TEXT NOT NULL, - form_data TEXT NOT NULL, - session_data TEXT NOT NULL, - subject VARCHAR(255) NOT NULL DEFAULT '', - active BOOL NOT NULL DEFAULT true, - requested_audience TEXT NULL DEFAULT '', - granted_audience TEXT NULL DEFAULT '', - challenge_id VARCHAR(40) NULL, - expires_at TIMESTAMP NULL, - nid UUID NULL -); -CREATE INDEX hydra_oauth2_user_code_request_id_idx ON hydra_oauth2_user_code (request_id, nid); -CREATE INDEX hydra_oauth2_user_code_client_id_idx ON hydra_oauth2_user_code (client_id, nid); -CREATE INDEX hydra_oauth2_user_code_challenge_id_idx ON hydra_oauth2_user_code (challenge_id); +CREATE INDEX hydra_oauth2_device_auth_codes_request_id_idx ON hydra_oauth2_device_auth_codes (request_id, nid); +CREATE INDEX hydra_oauth2_device_auth_codes_client_id_idx ON hydra_oauth2_device_auth_codes (client_id, nid); +CREATE INDEX hydra_oauth2_device_auth_codes_challenge_id_idx ON hydra_oauth2_device_auth_codes (challenge_id); +CREATE UNIQUE INDEX hydra_oauth2_device_auth_codes_user_code_signature_idx ON hydra_oauth2_device_auth_codes (user_code_signature, nid); ALTER TABLE hydra_oauth2_flow ADD COLUMN device_challenge_id VARCHAR(255) NULL; ALTER TABLE hydra_oauth2_flow ADD COLUMN device_code_request_id VARCHAR(255) NULL; diff --git a/persistence/sql/src/YYYYMMDD000001_device_flow/20241609000001000000_device_flow.cockroach.up.sql b/persistence/sql/src/YYYYMMDD000001_device_flow/20241609000001000000_device_flow.cockroach.up.sql deleted file mode 100644 index 59fae8ea86f..00000000000 --- a/persistence/sql/src/YYYYMMDD000001_device_flow/20241609000001000000_device_flow.cockroach.up.sql +++ /dev/null @@ -1,68 +0,0 @@ -CREATE TABLE IF NOT EXISTS hydra_oauth2_device_code -( - signature VARCHAR(255) NOT NULL PRIMARY KEY, - request_id VARCHAR(40) NOT NULL DEFAULT '', - requested_at TIMESTAMP NOT NULL DEFAULT NOW(), - client_id VARCHAR(255) NOT NULL DEFAULT '', - scope TEXT NOT NULL, - granted_scope TEXT NOT NULL, - form_data TEXT NOT NULL, - session_data TEXT NOT NULL, - subject VARCHAR(255) NOT NULL DEFAULT '', - active BOOL NOT NULL DEFAULT true, - requested_audience TEXT NOT NULL, - granted_audience TEXT NOT NULL, - challenge_id VARCHAR(40) NULL, - expires_at TIMESTAMP NULL, - nid UUID NOT NULL, - - FOREIGN KEY (client_id, nid) REFERENCES hydra_client(id, nid) ON DELETE CASCADE, - FOREIGN KEY (nid) REFERENCES networks(id) ON UPDATE RESTRICT ON DELETE CASCADE -); - -CREATE INDEX hydra_oauth2_device_code_request_id_idx ON hydra_oauth2_device_code (request_id, nid); -CREATE INDEX hydra_oauth2_device_code_client_id_idx ON hydra_oauth2_device_code (client_id, nid); -CREATE INDEX hydra_oauth2_device_code_challenge_id_idx ON hydra_oauth2_device_code (challenge_id); -CREATE INDEX hydra_oauth2_device_code_expires_at_idx ON hydra_oauth2_device_code (expires_at); - -CREATE TABLE IF NOT EXISTS hydra_oauth2_user_code -( - signature VARCHAR(255) NOT NULL PRIMARY KEY, - request_id VARCHAR(40) NOT NULL DEFAULT '', - requested_at TIMESTAMP NOT NULL DEFAULT NOW(), - client_id VARCHAR(255) NOT NULL DEFAULT '', - scope TEXT NOT NULL, - granted_scope TEXT NOT NULL, - form_data TEXT NOT NULL, - session_data TEXT NOT NULL, - subject VARCHAR(255) NOT NULL DEFAULT '', - active BOOL NOT NULL DEFAULT true, - requested_audience TEXT NOT NULL, - granted_audience TEXT NOT NULL, - challenge_id VARCHAR(40) NULL, - expires_at TIMESTAMP NULL, - nid UUID NOT NULL, - - FOREIGN KEY (client_id, nid) REFERENCES hydra_client(id, nid) ON DELETE CASCADE, - FOREIGN KEY (nid) REFERENCES networks(id) ON UPDATE RESTRICT ON DELETE CASCADE -); - -CREATE INDEX hydra_oauth2_user_code_request_id_idx ON hydra_oauth2_user_code (request_id, nid); -CREATE INDEX hydra_oauth2_user_code_client_id_idx ON hydra_oauth2_user_code (client_id, nid); -CREATE INDEX hydra_oauth2_user_code_challenge_id_idx ON hydra_oauth2_user_code (challenge_id); -CREATE INDEX hydra_oauth2_user_code_expires_at_idx ON hydra_oauth2_device_code (expires_at); - -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_challenge_id VARCHAR(255) NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_code_request_id VARCHAR(255) NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_verifier VARCHAR(40) NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_csrf VARCHAR(40) NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_user_code_accepted_at TIMESTAMP NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_was_used BOOL NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_handled_at TIMESTAMP NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_error TEXT NULL; - -CREATE INDEX hydra_oauth2_flow_device_challenge_idx ON hydra_oauth2_flow (device_challenge_id); - -ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_id_token_lifespan BIGINT NULL DEFAULT NULL; -ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_access_token_lifespan BIGINT NULL DEFAULT NULL; -ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_refresh_token_lifespan BIGINT NULL DEFAULT NULL; diff --git a/persistence/sql/src/YYYYMMDD000001_device_flow/20241609000001000000_device_flow.down.sql b/persistence/sql/src/YYYYMMDD000001_device_flow/20241609000001000000_device_flow.down.sql deleted file mode 100644 index 2547f10e767..00000000000 --- a/persistence/sql/src/YYYYMMDD000001_device_flow/20241609000001000000_device_flow.down.sql +++ /dev/null @@ -1,25 +0,0 @@ -ALTER TABLE hydra_oauth2_device_code DROP FOREIGN KEY IF EXISTS hydra_oauth2_device_code_challenge_id_fk; -ALTER TABLE hydra_oauth2_device_code DROP FOREIGN KEY IF EXISTS hydra_oauth2_device_code_client_id_fk; -ALTER TABLE hydra_oauth2_device_code DROP FOREIGN KEY IF EXISTS hydra_oauth2_device_code_nid_fk_idx; - -DROP TABLE IF EXISTS hydra_oauth2_device_code; - -ALTER TABLE hydra_oauth2_user_code DROP FOREIGN KEY IF EXISTS hydra_oauth2_user_code_challenge_id_fk; -ALTER TABLE hydra_oauth2_user_code DROP FOREIGN KEY IF EXISTS hydra_oauth2_user_code_client_id_fk; -ALTER TABLE hydra_oauth2_user_code DROP FOREIGN KEY IF EXISTS hydra_oauth2_user_code_nid_fk_idx; - -DROP TABLE IF EXISTS hydra_oauth2_user_code; - -ALTER TABLE hydra_oauth2_flow DROP COLUMN IF EXISTS device_challenge_id; -ALTER TABLE hydra_oauth2_flow DROP COLUMN IF EXISTS device_code_request_id; -ALTER TABLE hydra_oauth2_flow DROP COLUMN IF EXISTS device_verifier; -ALTER TABLE hydra_oauth2_flow DROP COLUMN IF EXISTS device_csrf; -ALTER TABLE hydra_oauth2_flow DROP COLUMN IF EXISTS device_user_code_accepted_at; -ALTER TABLE hydra_oauth2_flow DROP COLUMN IF EXISTS device_was_used; -ALTER TABLE hydra_oauth2_flow DROP COLUMN IF EXISTS device_handled_at; -ALTER TABLE hydra_oauth2_flow DROP COLUMN IF EXISTS device_error; - - -ALTER TABLE hydra_client DROP COLUMN device_authorization_grant_id_token_lifespan; -ALTER TABLE hydra_client DROP COLUMN device_authorization_grant_access_token_lifespan; -ALTER TABLE hydra_client DROP COLUMN device_authorization_grant_refresh_token_lifespan; \ No newline at end of file diff --git a/persistence/sql/src/YYYYMMDD000001_device_flow/20241609000001000000_device_flow.mysql.up.sql b/persistence/sql/src/YYYYMMDD000001_device_flow/20241609000001000000_device_flow.mysql.up.sql deleted file mode 100644 index 19892b22d87..00000000000 --- a/persistence/sql/src/YYYYMMDD000001_device_flow/20241609000001000000_device_flow.mysql.up.sql +++ /dev/null @@ -1,68 +0,0 @@ -CREATE TABLE IF NOT EXISTS hydra_oauth2_device_code -( - signature VARCHAR(255) NOT NULL PRIMARY KEY, - request_id VARCHAR(40) NOT NULL DEFAULT '', - requested_at TIMESTAMP NOT NULL DEFAULT NOW(), - client_id VARCHAR(255) NOT NULL DEFAULT '', - scope TEXT NOT NULL, - granted_scope TEXT NOT NULL, - form_data TEXT NOT NULL, - session_data TEXT NOT NULL, - subject VARCHAR(255) NOT NULL DEFAULT '', - active BOOL NOT NULL DEFAULT true, - requested_audience TEXT NOT NULL, - granted_audience TEXT NOT NULL, - challenge_id VARCHAR(40) NULL, - expires_at TIMESTAMP NULL, - nid CHAR(36) NOT NULL, - - FOREIGN KEY (client_id, nid) REFERENCES hydra_client(id, nid) ON DELETE CASCADE, - FOREIGN KEY (nid) REFERENCES networks(id) ON UPDATE RESTRICT ON DELETE CASCADE -); - -CREATE INDEX hydra_oauth2_device_code_request_id_idx ON hydra_oauth2_device_code (request_id, nid); -CREATE INDEX hydra_oauth2_device_code_client_id_idx ON hydra_oauth2_device_code (client_id, nid); -CREATE INDEX hydra_oauth2_device_code_challenge_id_idx ON hydra_oauth2_device_code (challenge_id); -CREATE INDEX hydra_oauth2_device_code_expires_at_idx ON hydra_oauth2_device_code (expires_at); - -CREATE TABLE IF NOT EXISTS hydra_oauth2_user_code -( - signature VARCHAR(255) NOT NULL PRIMARY KEY, - request_id VARCHAR(40) NOT NULL DEFAULT '', - requested_at TIMESTAMP NOT NULL DEFAULT NOW(), - client_id VARCHAR(255) NOT NULL DEFAULT '', - scope TEXT NOT NULL, - granted_scope TEXT NOT NULL, - form_data TEXT NOT NULL, - session_data TEXT NOT NULL, - subject VARCHAR(255) NOT NULL DEFAULT '', - active BOOL NOT NULL DEFAULT true, - requested_audience TEXT NOT NULL, - granted_audience TEXT NOT NULL, - challenge_id VARCHAR(40) NULL, - expires_at TIMESTAMP NULL, - nid CHAR(36) NOT NULL, - - FOREIGN KEY (client_id, nid) REFERENCES hydra_client(id, nid) ON DELETE CASCADE, - FOREIGN KEY (nid) REFERENCES networks(id) ON UPDATE RESTRICT ON DELETE CASCADE -); - -CREATE INDEX hydra_oauth2_user_code_request_id_idx ON hydra_oauth2_user_code (request_id, nid); -CREATE INDEX hydra_oauth2_user_code_client_id_idx ON hydra_oauth2_user_code (client_id, nid); -CREATE INDEX hydra_oauth2_user_code_challenge_id_idx ON hydra_oauth2_user_code (challenge_id); -CREATE INDEX hydra_oauth2_user_code_expires_at_idx ON hydra_oauth2_device_code (expires_at); - -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_challenge_id VARCHAR(255) NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_code_request_id VARCHAR(255) NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_verifier VARCHAR(40) NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_csrf VARCHAR(40) NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_user_code_accepted_at TIMESTAMP NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_was_used BOOL NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_handled_at TIMESTAMP NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_error TEXT NULL; - -CREATE INDEX hydra_oauth2_flow_device_challenge_idx ON hydra_oauth2_flow (device_challenge_id); - -ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_id_token_lifespan BIGINT NULL DEFAULT NULL; -ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_access_token_lifespan BIGINT NULL DEFAULT NULL; -ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_refresh_token_lifespan BIGINT NULL DEFAULT NULL; diff --git a/persistence/sql/src/YYYYMMDD000001_device_flow/20241609000001000000_device_flow.postgres.up.sql b/persistence/sql/src/YYYYMMDD000001_device_flow/20241609000001000000_device_flow.postgres.up.sql deleted file mode 100644 index 62b894fd6b8..00000000000 --- a/persistence/sql/src/YYYYMMDD000001_device_flow/20241609000001000000_device_flow.postgres.up.sql +++ /dev/null @@ -1,66 +0,0 @@ -CREATE TABLE IF NOT EXISTS hydra_oauth2_device_code ( - signature VARCHAR(255) NOT NULL PRIMARY KEY, - request_id VARCHAR(40) NOT NULL, - requested_at TIMESTAMP NOT NULL DEFAULT NOW(), - client_id VARCHAR(255) NOT NULL, - scope TEXT NOT NULL, - granted_scope TEXT NOT NULL, - form_data TEXT NOT NULL, - session_data TEXT NOT NULL, - subject VARCHAR(255) NOT NULL DEFAULT '', - active BOOL NOT NULL DEFAULT true, - requested_audience TEXT NULL DEFAULT '', - granted_audience TEXT NULL DEFAULT '', - challenge_id VARCHAR(40) NULL, - expires_at TIMESTAMP NULL, - nid UUID NULL, - - FOREIGN KEY (client_id, nid) REFERENCES hydra_client(id, nid) ON DELETE CASCADE, - FOREIGN KEY (nid) REFERENCES networks(id) ON UPDATE RESTRICT ON DELETE CASCADE -); - -CREATE INDEX hydra_oauth2_device_code_request_id_idx ON hydra_oauth2_device_code (request_id, nid); -CREATE INDEX hydra_oauth2_device_code_client_id_idx ON hydra_oauth2_device_code (client_id, nid); -CREATE INDEX hydra_oauth2_device_code_challenge_id_idx ON hydra_oauth2_device_code (challenge_id); -CREATE INDEX hydra_oauth2_device_code_expires_at_idx ON hydra_oauth2_device_code (expires_at); - -CREATE TABLE IF NOT EXISTS hydra_oauth2_user_code ( - signature VARCHAR(255) NOT NULL PRIMARY KEY, - request_id VARCHAR(40) NOT NULL, - requested_at TIMESTAMP NOT NULL DEFAULT NOW(), - client_id VARCHAR(255) NOT NULL, - scope TEXT NOT NULL, - granted_scope TEXT NOT NULL, - form_data TEXT NOT NULL, - session_data TEXT NOT NULL, - subject VARCHAR(255) NOT NULL DEFAULT '', - active BOOL NOT NULL DEFAULT true, - requested_audience TEXT NULL DEFAULT '', - granted_audience TEXT NULL DEFAULT '', - challenge_id VARCHAR(40) NULL, - expires_at TIMESTAMP NULL, - nid UUID NULL, - - FOREIGN KEY (client_id, nid) REFERENCES hydra_client(id, nid) ON DELETE CASCADE, - FOREIGN KEY (nid) REFERENCES networks(id) ON UPDATE RESTRICT ON DELETE CASCADE -); - -CREATE INDEX hydra_oauth2_user_code_request_id_idx ON hydra_oauth2_user_code (request_id, nid); -CREATE INDEX hydra_oauth2_user_code_client_id_idx ON hydra_oauth2_user_code (client_id, nid); -CREATE INDEX hydra_oauth2_user_code_challenge_id_idx ON hydra_oauth2_user_code (challenge_id); -CREATE INDEX hydra_oauth2_user_code_expires_at_idx ON hydra_oauth2_device_code (expires_at); - -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_challenge_id VARCHAR(255) NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_code_request_id VARCHAR(255) NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_verifier VARCHAR(40) NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_csrf VARCHAR(40) NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_user_code_accepted_at TIMESTAMP NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_was_used BOOLEAN NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_handled_at TIMESTAMP NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_error TEXT NULL; - -CREATE INDEX hydra_oauth2_flow_device_challenge_idx ON hydra_oauth2_flow (device_challenge_id); - -ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_id_token_lifespan BIGINT NULL DEFAULT NULL; -ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_access_token_lifespan BIGINT NULL DEFAULT NULL; -ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_refresh_token_lifespan BIGINT NULL DEFAULT NULL; diff --git a/persistence/sql/src/YYYYMMDD000001_device_flow/20241609000001000000_device_flow.up.sql b/persistence/sql/src/YYYYMMDD000001_device_flow/20241609000001000000_device_flow.up.sql deleted file mode 100644 index 15f7417c626..00000000000 --- a/persistence/sql/src/YYYYMMDD000001_device_flow/20241609000001000000_device_flow.up.sql +++ /dev/null @@ -1,58 +0,0 @@ -CREATE TABLE IF NOT EXISTS hydra_oauth2_device_code -( - signature VARCHAR(255) NOT NULL PRIMARY KEY, - request_id VARCHAR(40) NOT NULL, - requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - client_id VARCHAR(255) NOT NULL, - scope TEXT NOT NULL, - granted_scope TEXT NOT NULL, - form_data TEXT NOT NULL, - session_data TEXT NOT NULL, - subject VARCHAR(255) NOT NULL DEFAULT '', - active BOOL NOT NULL DEFAULT true, - requested_audience TEXT NULL DEFAULT '', - granted_audience TEXT NULL DEFAULT '', - challenge_id VARCHAR(40) NULL, - expires_at TIMESTAMP NULL, - nid UUID NULL -); -CREATE INDEX hydra_oauth2_device_code_request_id_idx ON hydra_oauth2_device_code (request_id, nid); -CREATE INDEX hydra_oauth2_device_code_client_id_idx ON hydra_oauth2_device_code (client_id, nid); -CREATE INDEX hydra_oauth2_device_code_challenge_id_idx ON hydra_oauth2_device_code (challenge_id); - -CREATE TABLE IF NOT EXISTS hydra_oauth2_user_code -( - signature VARCHAR(255) NOT NULL PRIMARY KEY, - request_id VARCHAR(40) NOT NULL, - requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - client_id VARCHAR(255) NOT NULL, - scope TEXT NOT NULL, - granted_scope TEXT NOT NULL, - form_data TEXT NOT NULL, - session_data TEXT NOT NULL, - subject VARCHAR(255) NOT NULL DEFAULT '', - active BOOL NOT NULL DEFAULT true, - requested_audience TEXT NULL DEFAULT '', - granted_audience TEXT NULL DEFAULT '', - challenge_id VARCHAR(40) NULL, - expires_at TIMESTAMP NULL, - nid UUID NULL -); -CREATE INDEX hydra_oauth2_user_code_request_id_idx ON hydra_oauth2_user_code (request_id, nid); -CREATE INDEX hydra_oauth2_user_code_client_id_idx ON hydra_oauth2_user_code (client_id, nid); -CREATE INDEX hydra_oauth2_user_code_challenge_id_idx ON hydra_oauth2_user_code (challenge_id); - -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_challenge_id VARCHAR(255) NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_code_request_id VARCHAR(255) NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_verifier VARCHAR(40) NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_csrf VARCHAR(40) NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_user_code_accepted_at TIMESTAMP NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_was_used BOOLEAN NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_handled_at TIMESTAMP NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_error TEXT NULL; - -CREATE INDEX hydra_oauth2_flow_device_challenge_idx ON hydra_oauth2_flow (device_challenge_id); - -ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_id_token_lifespan BIGINT NULL DEFAULT NULL; -ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_access_token_lifespan BIGINT NULL DEFAULT NULL; -ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_refresh_token_lifespan BIGINT NULL DEFAULT NULL; From 5842a86c93324d5042a316cc61aca76fba82cfcb Mon Sep 17 00:00:00 2001 From: Nikos Date: Mon, 18 Nov 2024 11:26:23 +0200 Subject: [PATCH 34/52] fix: create openid session when log in succeeds --- oauth2/handler.go | 8 +++++++- persistence/sql/persister_oauth2.go | 24 ------------------------ x/fosite_storer.go | 2 -- 3 files changed, 7 insertions(+), 27 deletions(-) diff --git a/oauth2/handler.go b/oauth2/handler.go index 0e87cfe5d1f..9aad6374844 100644 --- a/oauth2/handler.go +++ b/oauth2/handler.go @@ -775,7 +775,13 @@ func (h *Handler) performOAuth2DeviceVerificationFlow(w http.ResponseWriter, r * // Update the OpenID Connect session if "openid" scope is granted if req.GetGrantedScopes().Has("openid") { - err = h.r.OAuth2Storage().UpdateOpenIDConnectSessionByRequestID(ctx, f.DeviceCodeRequestID.String(), req) + err = h.r.OAuth2Storage().CreateOpenIDConnectSession(ctx, req.GetID(), req.Sanitize([]string{"grant_type", + "max_age", + "prompt", + "acr_values", + "id_token_hint", + "nonce", + })) if err != nil { x.LogError(r, err, h.r.Logger()) h.r.Writer().WriteError(w, r, err) diff --git a/persistence/sql/persister_oauth2.go b/persistence/sql/persister_oauth2.go index edc289f1670..8718c93ef2d 100644 --- a/persistence/sql/persister_oauth2.go +++ b/persistence/sql/persister_oauth2.go @@ -556,30 +556,6 @@ func (p *Persister) CreateOpenIDConnectSession(ctx context.Context, signature st return p.createSession(ctx, signature, requester, sqlTableOpenID, requester.GetSession().GetExpiresAt(fosite.AuthorizeCode).UTC()) } -// UpdateOpenIDConnectSessionByRequestID updates an OpenID session by requestID -func (p *Persister) UpdateOpenIDConnectSessionByRequestID(ctx context.Context, requestID string, requester fosite.Requester) (err error) { - ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.UpdateOpenIDConnectSessionByRequestID") - defer otelx.End(span, &err) - - req, err := p.sqlSchemaFromRequest(ctx, requestID, requester, sqlTableOpenID, requester.GetSession().GetExpiresAt(fosite.IDToken).UTC()) - if err != nil { - return err - } - - stmt := fmt.Sprintf( - "UPDATE %s SET granted_scope=?, granted_audience=?, session_data=? WHERE request_id=? AND nid = ?", - OAuth2RequestSQL{Table: sqlTableOpenID}.TableName(), - ) - - /* #nosec G201 table is static */ - err = p.Connection(ctx).RawQuery(stmt, req.GrantedScope, req.GrantedAudience, req.Session, requestID, p.NetworkID(ctx)).Exec() - if err != nil { - return sqlcon.HandleError(err) - } - - return nil -} - func (p *Persister) GetOpenIDConnectSession(ctx context.Context, signature string, requester fosite.Requester) (_ fosite.Requester, err error) { ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.GetOpenIDConnectSession") defer otelx.End(span, &err) diff --git a/x/fosite_storer.go b/x/fosite_storer.go index d8c2d871b40..8b879e11da7 100644 --- a/x/fosite_storer.go +++ b/x/fosite_storer.go @@ -40,8 +40,6 @@ type FositeStorer interface { FlushInactiveRefreshTokens(ctx context.Context, notAfter time.Time, limit int, batchSize int) error - UpdateOpenIDConnectSessionByRequestID(ctx context.Context, requestID string, requester fosite.Requester) error - // DeleteOpenIDConnectSession deletes an OpenID Connect session. // This is duplicated from Ory Fosite to help against deprecation linting errors. DeleteOpenIDConnectSession(ctx context.Context, authorizeCode string) error From 0dfd2fe98271d8b6673150b98cffb5d8a190b176 Mon Sep 17 00:00:00 2001 From: Nikos Date: Fri, 15 Nov 2024 13:03:28 +0200 Subject: [PATCH 35/52] refactor: update device session persistence logic --- consent/handler_test.go | 21 +- consent/manager.go | 3 - consent/strategy_default.go | 14 +- ..._token_hook_if_configured-hook=legacy.json | 3 +- ...esh_token_hook_if_configured-hook=new.json | 7 +- ..._token_hook_if_configured-hook=legacy.json | 3 +- ...esh_token_hook_if_configured-hook=new.json | 7 +- ..._token_hook_if_configured-hook=legacy.json | 3 +- ...esh_token_hook_if_configured-hook=new.json | 7 +- ..._token_hook_if_configured-hook=legacy.json | 3 +- ...esh_token_hook_if_configured-hook=new.json | 7 +- ..._token_hook_if_configured-hook=legacy.json | 3 +- ...esh_token_hook_if_configured-hook=new.json | 7 +- ..._token_hook_if_configured-hook=legacy.json | 3 +- ...esh_token_hook_if_configured-hook=new.json | 7 +- .../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 | 20 +- oauth2/oauth2_device_code_test.go | 17 +- oauth2/session.go | 9 - oauth2/session_test.go | 1 - persistence/sql/persister_device.go | 296 ++++++++++++++++++ persistence/sql/persister_oauth2.go | 149 +-------- x/clean_sql.go | 6 +- x/fosite_storer.go | 6 +- 27 files changed, 370 insertions(+), 244 deletions(-) create mode 100644 persistence/sql/persister_device.go diff --git a/consent/handler_test.go b/consent/handler_test.go index 98e55303507..9b07c400d08 100644 --- a/consent/handler_test.go +++ b/consent/handler_test.go @@ -340,12 +340,13 @@ func TestAcceptDeviceRequest(t *testing.T) { DefaultSession: &openid.DefaultSession{ Headers: &jwt.Headers{}, }, - BrowserFlowCompleted: false, }, ) + _, deviceCodesig, err := reg.RFC8628HMACStrategy().GenerateDeviceCode(ctx) + require.NoError(t, err) userCode, sig, err := reg.RFC8628HMACStrategy().GenerateUserCode(ctx) require.NoError(t, err) - reg.OAuth2Storage().CreateUserCodeSession(ctx, sig, deviceRequest) + reg.OAuth2Storage().CreateDeviceAuthSession(ctx, deviceCodesig, sig, deviceRequest) require.NoError(t, err) acceptUserCode := &hydra.AcceptDeviceUserCodeRequest{UserCode: &userCode} @@ -404,12 +405,13 @@ func TestAcceptDuplicateDeviceRequest(t *testing.T) { DefaultSession: &openid.DefaultSession{ Headers: &jwt.Headers{}, }, - BrowserFlowCompleted: false, }, ) + _, deviceCodesig, err := reg.RFC8628HMACStrategy().GenerateDeviceCode(ctx) + require.NoError(t, err) userCode, sig, err := reg.RFC8628HMACStrategy().GenerateUserCode(ctx) require.NoError(t, err) - reg.OAuth2Storage().CreateUserCodeSession(ctx, sig, deviceRequest) + reg.OAuth2Storage().CreateDeviceAuthSession(ctx, deviceCodesig, sig, deviceRequest) require.NoError(t, err) acceptUserCode := &hydra.AcceptDeviceUserCodeRequest{UserCode: &userCode} @@ -489,7 +491,6 @@ func TestAcceptCodeDeviceRequestFailure(t *testing.T) { DefaultSession: &openid.DefaultSession{ Headers: &jwt.Headers{}, }, - BrowserFlowCompleted: false, }, ) userCode, _, err := reg.RFC8628HMACStrategy().GenerateUserCode(ctx) @@ -513,7 +514,6 @@ func TestAcceptCodeDeviceRequestFailure(t *testing.T) { DefaultSession: &openid.DefaultSession{ Headers: &jwt.Headers{}, }, - BrowserFlowCompleted: false, }, ) userCode := "" @@ -536,7 +536,6 @@ func TestAcceptCodeDeviceRequestFailure(t *testing.T) { DefaultSession: &openid.DefaultSession{ Headers: &jwt.Headers{}, }, - BrowserFlowCompleted: false, }, ) userCode, _, err := reg.RFC8628HMACStrategy().GenerateUserCode(ctx) @@ -560,7 +559,6 @@ func TestAcceptCodeDeviceRequestFailure(t *testing.T) { DefaultSession: &openid.DefaultSession{ Headers: &jwt.Headers{}, }, - BrowserFlowCompleted: false, }, ) userCode, _, err := reg.RFC8628HMACStrategy().GenerateUserCode(ctx) @@ -584,9 +582,10 @@ func TestAcceptCodeDeviceRequestFailure(t *testing.T) { DefaultSession: &openid.DefaultSession{ Headers: &jwt.Headers{}, }, - BrowserFlowCompleted: false, }, ) + _, deviceCodesig, err := reg.RFC8628HMACStrategy().GenerateDeviceCode(ctx) + require.NoError(t, err) userCode, sig, err := reg.RFC8628HMACStrategy().GenerateUserCode(ctx) require.NoError(t, err) deviceRequest.SetSession( @@ -594,12 +593,11 @@ func TestAcceptCodeDeviceRequestFailure(t *testing.T) { DefaultSession: &openid.DefaultSession{ Headers: &jwt.Headers{}, }, - BrowserFlowCompleted: false, }, ) exp := time.Now().UTC() deviceRequest.Session.SetExpiresAt(fosite.UserCode, exp) - err = reg.OAuth2Storage().CreateUserCodeSession(ctx, sig, deviceRequest) + err = reg.OAuth2Storage().CreateDeviceAuthSession(ctx, deviceCodesig, sig, deviceRequest) require.NoError(t, err) return json.Marshal(&hydra.AcceptDeviceUserCodeRequest{UserCode: &userCode}) }, @@ -623,7 +621,6 @@ func TestAcceptCodeDeviceRequestFailure(t *testing.T) { DefaultSession: &openid.DefaultSession{ Headers: &jwt.Headers{}, }, - BrowserFlowCompleted: false, }, ) userCode, _, err := reg.RFC8628HMACStrategy().GenerateUserCode(ctx) diff --git a/consent/manager.go b/consent/manager.go index 8396550b2bb..e2787d0de79 100644 --- a/consent/manager.go +++ b/consent/manager.go @@ -6,7 +6,6 @@ package consent import ( "context" - "github.com/gobuffalo/pop/v6" "github.com/gofrs/uuid" "github.com/ory/hydra/v2/client" @@ -67,8 +66,6 @@ type ( GetDeviceUserAuthRequest(ctx context.Context, challenge string) (*flow.DeviceUserAuthRequest, error) HandleDeviceUserAuthRequest(ctx context.Context, f *flow.Flow, challenge string, r *flow.HandledDeviceUserAuthRequest) (*flow.DeviceUserAuthRequest, error) VerifyAndInvalidateDeviceUserAuthRequest(ctx context.Context, verifier string) (*flow.HandledDeviceUserAuthRequest, error) - - Transaction(context.Context, func(ctx context.Context, c *pop.Connection) error) error } ManagerProvider interface { diff --git a/consent/strategy_default.go b/consent/strategy_default.go index c72329d8987..fb508c31f2b 100644 --- a/consent/strategy_default.go +++ b/consent/strategy_default.go @@ -14,7 +14,6 @@ import ( "strings" "time" - "github.com/gobuffalo/pop/v6" "github.com/gorilla/sessions" "github.com/hashicorp/go-retryablehttp" "github.com/pborman/uuid" @@ -1247,15 +1246,10 @@ func (s *DefaultStrategy) HandleOAuth2DeviceAuthorizationRequest( var consentSession *flow.AcceptOAuth2ConsentRequest var f *flow.Flow - err = s.r.ConsentManager().Transaction(ctx, func(ctx context.Context, c *pop.Connection) error { - consentSession, f, err = s.verifyConsent(ctx, w, r, consentVerifier) - if err != nil { - return err - } - err = s.r.OAuth2Storage().UpdateAndInvalidateUserCodeSessionByRequestID(ctx, string(f.DeviceCodeRequestID), f.ID) - - return err - }) + consentSession, f, err = s.verifyConsent(ctx, w, r, consentVerifier) + if err != nil { + return nil, nil, err + } return consentSession, f, err } 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 a687c788d99..61dfba78726 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,8 +30,7 @@ "consent_challenge": "", "exclude_not_before_claim": false, "allowed_top_level_claims": [], - "mirror_top_level_claims": true, - "browser_flow_completed": false + "mirror_top_level_claims": true }, "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 59ca3fc86e2..85da3a9fb18 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,15 +31,14 @@ "consent_challenge": "", "exclude_not_before_claim": false, "allowed_top_level_claims": [], - "mirror_top_level_claims": true, - "browser_flow_completed": false + "mirror_top_level_claims": true }, "request": { "client_id": "app-client", "requested_scopes": [ - "hydra.*", "offline", - "openid" + "openid", + "hydra.*" ], "granted_scopes": [ "offline", 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 a687c788d99..61dfba78726 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,8 +30,7 @@ "consent_challenge": "", "exclude_not_before_claim": false, "allowed_top_level_claims": [], - "mirror_top_level_claims": true, - "browser_flow_completed": false + "mirror_top_level_claims": true }, "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 59ca3fc86e2..85da3a9fb18 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,15 +31,14 @@ "consent_challenge": "", "exclude_not_before_claim": false, "allowed_top_level_claims": [], - "mirror_top_level_claims": true, - "browser_flow_completed": false + "mirror_top_level_claims": true }, "request": { "client_id": "app-client", "requested_scopes": [ - "hydra.*", "offline", - "openid" + "openid", + "hydra.*" ], "granted_scopes": [ "offline", 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 a687c788d99..61dfba78726 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,8 +30,7 @@ "consent_challenge": "", "exclude_not_before_claim": false, "allowed_top_level_claims": [], - "mirror_top_level_claims": true, - "browser_flow_completed": false + "mirror_top_level_claims": true }, "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 59ca3fc86e2..85da3a9fb18 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,15 +31,14 @@ "consent_challenge": "", "exclude_not_before_claim": false, "allowed_top_level_claims": [], - "mirror_top_level_claims": true, - "browser_flow_completed": false + "mirror_top_level_claims": true }, "request": { "client_id": "app-client", "requested_scopes": [ - "hydra.*", "offline", - "openid" + "openid", + "hydra.*" ], "granted_scopes": [ "offline", 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 a687c788d99..61dfba78726 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,8 +30,7 @@ "consent_challenge": "", "exclude_not_before_claim": false, "allowed_top_level_claims": [], - "mirror_top_level_claims": true, - "browser_flow_completed": false + "mirror_top_level_claims": true }, "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 59ca3fc86e2..85da3a9fb18 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,15 +31,14 @@ "consent_challenge": "", "exclude_not_before_claim": false, "allowed_top_level_claims": [], - "mirror_top_level_claims": true, - "browser_flow_completed": false + "mirror_top_level_claims": true }, "request": { "client_id": "app-client", "requested_scopes": [ - "hydra.*", "offline", - "openid" + "openid", + "hydra.*" ], "granted_scopes": [ "offline", 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 a687c788d99..61dfba78726 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,8 +30,7 @@ "consent_challenge": "", "exclude_not_before_claim": false, "allowed_top_level_claims": [], - "mirror_top_level_claims": true, - "browser_flow_completed": false + "mirror_top_level_claims": true }, "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 59ca3fc86e2..85da3a9fb18 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,15 +31,14 @@ "consent_challenge": "", "exclude_not_before_claim": false, "allowed_top_level_claims": [], - "mirror_top_level_claims": true, - "browser_flow_completed": false + "mirror_top_level_claims": true }, "request": { "client_id": "app-client", "requested_scopes": [ - "hydra.*", "offline", - "openid" + "openid", + "hydra.*" ], "granted_scopes": [ "offline", 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 a687c788d99..61dfba78726 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,8 +30,7 @@ "consent_challenge": "", "exclude_not_before_claim": false, "allowed_top_level_claims": [], - "mirror_top_level_claims": true, - "browser_flow_completed": false + "mirror_top_level_claims": true }, "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 59ca3fc86e2..85da3a9fb18 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,15 +31,14 @@ "consent_challenge": "", "exclude_not_before_claim": false, "allowed_top_level_claims": [], - "mirror_top_level_claims": true, - "browser_flow_completed": false + "mirror_top_level_claims": true }, "request": { "client_id": "app-client", "requested_scopes": [ - "hydra.*", "offline", - "openid" + "openid", + "hydra.*" ], "granted_scopes": [ "offline", diff --git a/oauth2/.snapshots/TestUnmarshalSession-v1.11.8.json b/oauth2/.snapshots/TestUnmarshalSession-v1.11.8.json index 341c3556d09..03e8881ee72 100644 --- a/oauth2/.snapshots/TestUnmarshalSession-v1.11.8.json +++ b/oauth2/.snapshots/TestUnmarshalSession-v1.11.8.json @@ -47,6 +47,5 @@ "zone", "login_session_id" ], - "mirror_top_level_claims": false, - "browser_flow_completed": false + "mirror_top_level_claims": false } diff --git a/oauth2/.snapshots/TestUnmarshalSession-v1.11.9.json b/oauth2/.snapshots/TestUnmarshalSession-v1.11.9.json index 341c3556d09..03e8881ee72 100644 --- a/oauth2/.snapshots/TestUnmarshalSession-v1.11.9.json +++ b/oauth2/.snapshots/TestUnmarshalSession-v1.11.9.json @@ -47,6 +47,5 @@ "zone", "login_session_id" ], - "mirror_top_level_claims": false, - "browser_flow_completed": false + "mirror_top_level_claims": false } diff --git a/oauth2/fixtures/v1.11.8-session.json b/oauth2/fixtures/v1.11.8-session.json index 8f7f9a13125..4608026d74e 100644 --- a/oauth2/fixtures/v1.11.8-session.json +++ b/oauth2/fixtures/v1.11.8-session.json @@ -44,6 +44,5 @@ "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 10bd3ec8d87..9636d07b8d6 100644 --- a/oauth2/fixtures/v1.11.9-session.json +++ b/oauth2/fixtures/v1.11.9-session.json @@ -44,6 +44,5 @@ "market", "zone", "login_session_id" - ], - "browser_flow_completed": false + ] } diff --git a/oauth2/handler.go b/oauth2/handler.go index 9aad6374844..19a28da878d 100644 --- a/oauth2/handler.go +++ b/oauth2/handler.go @@ -729,6 +729,15 @@ func (h *Handler) getOidcUserInfo(w http.ResponseWriter, r *http.Request) { func (h *Handler) performOAuth2DeviceVerificationFlow(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { ctx := r.Context() + // When this endpoint is called with a valid consent_verifier (meaning that the login flow completed successfully) + // there are 3 writes happening to the database: + // - The flow is created + // - The device auth session is updated (user_code is marked as accepted) + // - The OpenID session is created + // If there were multiple flows created for the same user_code then we may end up with multiple flow objects + // persisted to the database, while only one of them was actually used to validate the user_code + // (see https://github.com/ory/hydra/pull/3851#discussion_r1843678761) + // TODO: We should wrap these queries in a transaction consentSession, f, err := h.r.ConsentStrategy().HandleOAuth2DeviceAuthorizationRequest(ctx, w, r) if errors.Is(err, consent.ErrAbortOAuth2Request) { x.LogAudit(r, nil, h.r.AuditLogger()) @@ -747,26 +756,26 @@ func (h *Handler) performOAuth2DeviceVerificationFlow(w http.ResponseWriter, r * return } - req, err := h.r.OAuth2Storage().GetDeviceCodeSessionByRequestID(ctx, f.DeviceCodeRequestID.String(), &Session{}) + req, sig, err := h.r.OAuth2Storage().GetDeviceCodeSessionByRequestID(ctx, f.DeviceCodeRequestID.String(), &Session{}) if err != nil { x.LogError(r, err, h.r.Logger()) h.r.Writer().WriteError(w, r, err) return } + req.SetUserCodeState(fosite.UserCodeAccepted) session, err := h.updateSessionWithRequest(ctx, consentSession, f, r, req, req.GetSession().(*Session)) if err != nil { h.r.Writer().WriteError(w, r, err) return } - session.SetBrowserFlowCompleted(true) - req.SetSession(session) // Update the device code session with // - the claims for which the user gave consent // - the granted scopes // - the granted audiences + // - the user_code_state set to accepted // This marks it as ready to be used for the token exchange endpoint. - err = h.r.OAuth2Storage().UpdateDeviceCodeSessionByRequestID(ctx, f.DeviceCodeRequestID.String(), req) + err = h.r.OAuth2Storage().UpdateDeviceCodeSessionBySignature(ctx, sig, req) if err != nil { x.LogError(r, err, h.r.Logger()) h.r.Writer().WriteError(w, r, err) @@ -775,7 +784,7 @@ func (h *Handler) performOAuth2DeviceVerificationFlow(w http.ResponseWriter, r * // Update the OpenID Connect session if "openid" scope is granted if req.GetGrantedScopes().Has("openid") { - err = h.r.OAuth2Storage().CreateOpenIDConnectSession(ctx, req.GetID(), req.Sanitize([]string{"grant_type", + err = h.r.OAuth2Storage().CreateOpenIDConnectSession(ctx, sig, req.Sanitize([]string{"grant_type", "max_age", "prompt", "acr_values", @@ -868,7 +877,6 @@ func (h *Handler) oAuth2DeviceFlow(w http.ResponseWriter, r *http.Request) { DefaultSession: &openid.DefaultSession{ Headers: &jwt.Headers{}, }, - BrowserFlowCompleted: false, } resp, err := h.r.OAuth2Provider().NewDeviceResponse(ctx, request, session) diff --git a/oauth2/oauth2_device_code_test.go b/oauth2/oauth2_device_code_test.go index d8ed58a3bf3..23e2dcccf97 100644 --- a/oauth2/oauth2_device_code_test.go +++ b/oauth2/oauth2_device_code_test.go @@ -128,14 +128,15 @@ func TestDeviceTokenRequest(t *testing.T) { testCases := []struct { description string - setUp func(signature string) + setUp func(signature, userCodeSignature string) check func(t *testing.T, token *oauth2.Token, err error) cleanUp func() }{ { description: "should pass with refresh token", - setUp: func(signature string) { + setUp: func(signature, userCodeSignature string) { authreq := &fosite.DeviceRequest{ + UserCodeState: fosite.UserCodeAccepted, Request: fosite.Request{ Client: &fosite.DefaultClient{ ID: c.GetID(), @@ -152,13 +153,12 @@ func TestDeviceTokenRequest(t *testing.T) { fosite.DeviceCode: time.Now().Add(time.Hour).UTC(), }, }, - BrowserFlowCompleted: true, }, RequestedAt: time.Now(), }, } - require.NoError(t, reg.OAuth2Storage().CreateDeviceCodeSession(context.TODO(), signature, authreq)) + require.NoError(t, reg.OAuth2Storage().CreateDeviceAuthSession(context.TODO(), signature, userCodeSignature, authreq)) }, check: func(t *testing.T, token *oauth2.Token, err error) { assert.NotEmpty(t, token.AccessToken) @@ -167,8 +167,9 @@ func TestDeviceTokenRequest(t *testing.T) { }, { description: "should pass with ID token", - setUp: func(signature string) { + setUp: func(signature, userCodeSignature string) { authreq := &fosite.DeviceRequest{ + UserCodeState: fosite.UserCodeAccepted, Request: fosite.Request{ Client: &fosite.DefaultClient{ ID: c.GetID(), @@ -185,13 +186,12 @@ func TestDeviceTokenRequest(t *testing.T) { fosite.DeviceCode: time.Now().Add(time.Hour).UTC(), }, }, - BrowserFlowCompleted: true, }, RequestedAt: time.Now(), }, } - require.NoError(t, reg.OAuth2Storage().CreateDeviceCodeSession(context.TODO(), signature, authreq)) + require.NoError(t, reg.OAuth2Storage().CreateDeviceAuthSession(context.TODO(), signature, userCodeSignature, authreq)) require.NoError(t, reg.OAuth2Storage().CreateOpenIDConnectSession(context.TODO(), signature, authreq)) }, check: func(t *testing.T, token *oauth2.Token, err error) { @@ -205,10 +205,11 @@ func TestDeviceTokenRequest(t *testing.T) { for _, testCase := range testCases { t.Run("case="+testCase.description, func(t *testing.T) { code, signature, err := reg.RFC8628HMACStrategy().GenerateDeviceCode(context.TODO()) + _, userCodeSignature, err := reg.RFC8628HMACStrategy().GenerateUserCode(context.TODO()) require.NoError(t, err) if testCase.setUp != nil { - testCase.setUp(signature) + testCase.setUp(signature, userCodeSignature) } var token *oauth2.Token diff --git a/oauth2/session.go b/oauth2/session.go index cc067916a34..0630cb09142 100644 --- a/oauth2/session.go +++ b/oauth2/session.go @@ -33,7 +33,6 @@ 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:"-"` } @@ -209,11 +208,3 @@ func (s *Session) GetExtraClaims() map[string]interface{} { return s.Extra } - -func (s *Session) GetBrowserFlowCompleted() bool { - return s.BrowserFlowCompleted -} - -func (s *Session) SetBrowserFlowCompleted(flag bool) { - s.BrowserFlowCompleted = flag -} diff --git a/oauth2/session_test.go b/oauth2/session_test.go index a5094b4d9cd..461d753581a 100644 --- a/oauth2/session_test.go +++ b/oauth2/session_test.go @@ -77,7 +77,6 @@ func TestUnmarshalSession(t *testing.T) { "zone", "login_session_id", }, - BrowserFlowCompleted: false, } t.Run("v1.11.8", func(t *testing.T) { diff --git a/persistence/sql/persister_device.go b/persistence/sql/persister_device.go new file mode 100644 index 00000000000..50675646b45 --- /dev/null +++ b/persistence/sql/persister_device.go @@ -0,0 +1,296 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sql + +import ( + "context" + "database/sql" + "encoding/json" + "fmt" + "net/url" + "strings" + "time" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + "github.com/tidwall/gjson" + + "github.com/ory/fosite" + "github.com/ory/hydra/v2/oauth2" + "github.com/ory/x/errorsx" + "github.com/ory/x/otelx" + "github.com/ory/x/sqlcon" + "github.com/ory/x/sqlxx" + "github.com/ory/x/stringsx" +) + +const ( + sqlTableDeviceAuthCodes tableName = "hydra_oauth2_device_auth_codes" +) + +type DeviceRequestSQL struct { + ID string `db:"device_code_signature"` + UserCodeID string `db:"user_code_signature"` + NID uuid.UUID `db:"nid"` + Request string `db:"request_id"` + ConsentChallenge sql.NullString `db:"challenge_id"` + RequestedAt time.Time `db:"requested_at"` + Client string `db:"client_id"` + Scopes string `db:"scope"` + GrantedScope string `db:"granted_scope"` + RequestedAudience string `db:"requested_audience"` + GrantedAudience string `db:"granted_audience"` + Form string `db:"form_data"` + Subject string `db:"subject"` + DeviceCodeActive bool `db:"device_code_active"` + UserCodeState fosite.UserCodeState `db:"user_code_state"` + Session []byte `db:"session_data"` + // InternalExpiresAt denormalizes the expiry from the session to additionally store it as a row. + InternalExpiresAt sqlxx.NullTime `db:"expires_at" json:"-"` +} + +func (r DeviceRequestSQL) TableName() string { + return string(sqlTableDeviceAuthCodes) +} + +func (r *DeviceRequestSQL) toRequest(ctx context.Context, session fosite.Session, p *Persister) (_ *fosite.DeviceRequest, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.DeviceRequestSQL.toRequest") + defer otelx.End(span, &err) + + sess := r.Session + if !gjson.ValidBytes(sess) { + var err error + sess, err = p.r.KeyCipher().Decrypt(ctx, string(sess), nil) + if err != nil { + return nil, errorsx.WithStack(err) + } + } + + if session != nil { + if err := json.Unmarshal(sess, session); err != nil { + return nil, errorsx.WithStack(err) + } + } else { + p.l.Debugf("Got an empty session in toRequest") + } + + c, err := p.GetClient(ctx, r.Client) + if err != nil { + return nil, err + } + + val, err := url.ParseQuery(r.Form) + if err != nil { + return nil, errorsx.WithStack(err) + } + + return &fosite.DeviceRequest{ + UserCodeState: fosite.UserCodeState(r.UserCodeState), + Request: fosite.Request{ + ID: r.Request, + RequestedAt: r.RequestedAt, + // ExpiresAt does not need to be populated as we get the expiry time from the session. + Client: c, + RequestedScope: stringsx.Splitx(r.Scopes, "|"), + GrantedScope: stringsx.Splitx(r.GrantedScope, "|"), + RequestedAudience: stringsx.Splitx(r.RequestedAudience, "|"), + GrantedAudience: stringsx.Splitx(r.GrantedAudience, "|"), + Form: val, + Session: session, + }, + }, nil +} + +func (p *Persister) sqlDeviceSchemaFromRequest(ctx context.Context, deviceCodeSignature, userCodeSignature string, r fosite.DeviceRequester, expiresAt time.Time) (*DeviceRequestSQL, error) { + subject := "" + if r.GetSession() == nil { + p.l.Debugf("Got an empty session in sqlSchemaFromRequest") + } else { + subject = r.GetSession().GetSubject() + } + + session, err := json.Marshal(r.GetSession()) + if err != nil { + return nil, errorsx.WithStack(err) + } + + if p.config.EncryptSessionData(ctx) { + ciphertext, err := p.r.KeyCipher().Encrypt(ctx, session, nil) + if err != nil { + return nil, errorsx.WithStack(err) + } + session = []byte(ciphertext) + } + + var challenge sql.NullString + rr, ok := r.GetSession().(*oauth2.Session) + if !ok && r.GetSession() != nil { + return nil, errors.Errorf("Expected request to be of type *Session, but got: %T", r.GetSession()) + } else if ok { + if len(rr.ConsentChallenge) > 0 { + challenge = sql.NullString{Valid: true, String: rr.ConsentChallenge} + } + } + + return &DeviceRequestSQL{ + Request: r.GetID(), + ConsentChallenge: challenge, + ID: deviceCodeSignature, + UserCodeID: userCodeSignature, + RequestedAt: r.GetRequestedAt(), + InternalExpiresAt: sqlxx.NullTime(expiresAt), + Client: r.GetClient().GetID(), + Scopes: strings.Join(r.GetRequestedScopes(), "|"), + GrantedScope: strings.Join(r.GetGrantedScopes(), "|"), + GrantedAudience: strings.Join(r.GetGrantedAudience(), "|"), + RequestedAudience: strings.Join(r.GetRequestedAudience(), "|"), + Form: r.GetRequestForm().Encode(), + Session: session, + Subject: subject, + DeviceCodeActive: true, + UserCodeState: r.GetUserCodeState(), + }, nil +} + +func (p *Persister) createDeviceAuthSession(ctx context.Context, deviceCodeSignature, userCodeSignature string, requester fosite.DeviceRequester, expiresAt time.Time) error { + req, err := p.sqlDeviceSchemaFromRequest(ctx, deviceCodeSignature, userCodeSignature, requester, expiresAt) + if err != nil { + return err + } + + if err = sqlcon.HandleError(p.CreateWithNetwork(ctx, req)); errors.Is(err, sqlcon.ErrConcurrentUpdate) { + return errors.Wrap(fosite.ErrSerializationFailure, err.Error()) + } else if err != nil { + return err + } + return nil +} + +// CreateDeviceCodeSession creates a new device code session and stores it in the database +func (p *Persister) CreateDeviceAuthSession(ctx context.Context, deviceCodeSignature, userCodeSignature string, requester fosite.DeviceRequester) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateDeviceCodeSession") + defer otelx.End(span, &err) + return p.createDeviceAuthSession(ctx, deviceCodeSignature, userCodeSignature, requester, requester.GetSession().GetExpiresAt(fosite.DeviceCode).UTC()) +} + +// UpdateDeviceCodeSessionBySignature updates a device code session by the device_code signature +func (p *Persister) UpdateDeviceCodeSessionBySignature(ctx context.Context, signature string, requester fosite.DeviceRequester) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.UpdateDeviceCodeSessionBySignature") + defer otelx.End(span, &err) + + req, err := p.sqlDeviceSchemaFromRequest(ctx, signature, "", requester, requester.GetSession().GetExpiresAt(fosite.DeviceCode).UTC()) + if err != nil { + return err + } + + stmt := fmt.Sprintf( + "UPDATE %s SET granted_scope=?, granted_audience=?, session_data=?, user_code_state=? WHERE device_code_signature=? AND nid = ?", + sqlTableDeviceAuthCodes, + ) + + /* #nosec G201 table is static */ + err = p.Connection(ctx).RawQuery(stmt, req.GrantedScope, req.GrantedAudience, req.Session, req.UserCodeState, signature, p.NetworkID(ctx)).Exec() + if err != nil { + return sqlcon.HandleError(err) + } + + return nil +} + +// GetDeviceCodeSession returns a device code session from the database +func (p *Persister) GetDeviceCodeSession(ctx context.Context, signature string, session fosite.Session) (_ fosite.DeviceRequester, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.GetDeviceCodeSession") + defer otelx.End(span, &err) + + r := DeviceRequestSQL{} + err = p.QueryWithNetwork(ctx).Where("device_code_signature = ?", signature).First(&r) + if errors.Is(err, sql.ErrNoRows) { + return nil, errorsx.WithStack(fosite.ErrNotFound) + } + if err != nil { + return nil, sqlcon.HandleError(err) + } + if !r.DeviceCodeActive { + fr, err := r.toRequest(ctx, session, p) + if err != nil { + return nil, err + } + return fr, errorsx.WithStack(fosite.ErrInactiveToken) + } + + return r.toRequest(ctx, session, p) +} + +// GetDeviceCodeSessionByRequestID returns a device code session from the database +func (p *Persister) GetDeviceCodeSessionByRequestID(ctx context.Context, requestID string, session fosite.Session) (_ fosite.DeviceRequester, deviceCodeSignature string, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.GetDeviceCodeSessionByRequestID") + defer otelx.End(span, &err) + + r := DeviceRequestSQL{} + err = p.QueryWithNetwork(ctx).Where("request_id = ?", requestID).First(&r) + if errors.Is(err, sql.ErrNoRows) { + return nil, "", errorsx.WithStack(fosite.ErrNotFound) + } + if err != nil { + return nil, "", sqlcon.HandleError(err) + } + if !r.DeviceCodeActive { + fr, err := r.toRequest(ctx, session, p) + if err != nil { + return nil, "", err + } + return fr, r.ID, errorsx.WithStack(fosite.ErrInactiveToken) + } + + fr, err := r.toRequest(ctx, session, p) + if err != nil { + return nil, "", err + } + return fr, r.ID, nil +} + +// InvalidateDeviceCodeSession invalidates a device code session +func (p *Persister) InvalidateDeviceCodeSession(ctx context.Context, signature string) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.InvalidateDeviceCodeSession") + defer otelx.End(span, &err) + + /* #nosec G201 table is static */ + return sqlcon.HandleError( + p.Connection(ctx). + RawQuery( + fmt.Sprintf("UPDATE %s SET device_code_active=false WHERE device_code_signature=? AND nid = ?", sqlTableDeviceAuthCodes), + signature, + p.NetworkID(ctx), + ). + Exec(), + ) +} + +// GetUserCodeSession returns a user code session from the database +func (p *Persister) GetUserCodeSession(ctx context.Context, signature string, session fosite.Session) (_ fosite.DeviceRequester, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.GetUserCodeSession") + defer otelx.End(span, &err) + + r := DeviceRequestSQL{} + if session == nil { + session = oauth2.NewSession("") + } + err = p.QueryWithNetwork(ctx).Where("user_code_signature = ?", signature).First(&r) + if errors.Is(err, sql.ErrNoRows) { + return nil, errorsx.WithStack(fosite.ErrNotFound) + } + if err != nil { + return nil, sqlcon.HandleError(err) + } + + fr, err := r.toRequest(ctx, session, p) + if err != nil { + return nil, err + } + if r.UserCodeState != fosite.UserCodeUnused { + return fr, errorsx.WithStack(fosite.ErrInactiveToken) + } + + return fr, err +} diff --git a/persistence/sql/persister_oauth2.go b/persistence/sql/persister_oauth2.go index 8718c93ef2d..851367e3411 100644 --- a/persistence/sql/persister_oauth2.go +++ b/persistence/sql/persister_oauth2.go @@ -65,13 +65,11 @@ type ( ) const ( - sqlTableOpenID tableName = "oidc" - sqlTableAccess tableName = "access" - sqlTableRefresh tableName = "refresh" - sqlTableCode tableName = "code" - sqlTablePKCE tableName = "pkce" - sqlTableDeviceCode tableName = "device_code" - sqlTableUserCode tableName = "user_code" + sqlTableOpenID tableName = "oidc" + sqlTableAccess tableName = "access" + sqlTableRefresh tableName = "refresh" + sqlTableCode tableName = "code" + sqlTablePKCE tableName = "pkce" ) func (r OAuth2RequestSQL) TableName() string { @@ -286,29 +284,6 @@ func (p *Persister) findSessionBySignature(ctx context.Context, signature string return r.toRequest(ctx, session, p) } -func (p *Persister) findSessionByRequestID(ctx context.Context, requestID string, session fosite.Session, table tableName) (fosite.Requester, error) { - r := OAuth2RequestSQL{Table: table} - err := p.QueryWithNetwork(ctx).Where("request_id = ?", requestID).First(&r) - if errors.Is(err, sql.ErrNoRows) { - return nil, errorsx.WithStack(fosite.ErrNotFound) - } - if err != nil { - return nil, sqlcon.HandleError(err) - } - if !r.Active { - fr, err := r.toRequest(ctx, session, p) - if err != nil { - return nil, err - } - if table == sqlTableCode { - return fr, errorsx.WithStack(fosite.ErrInvalidatedAuthorizeCode) - } - return fr, errorsx.WithStack(fosite.ErrInactiveToken) - } - - return r.toRequest(ctx, session, p) -} - func (p *Persister) deleteSessionBySignature(ctx context.Context, signature string, table tableName) error { err := sqlcon.HandleError( p.QueryWithNetwork(ctx). @@ -778,117 +753,3 @@ func (p *Persister) RotateRefreshToken(ctx context.Context, requestID string, re return handleRetryError(p.strictRefreshRotation(ctx, requestID)) } - -// CreateDeviceCodeSession creates a new device code session and stores it in the database -func (p *Persister) CreateDeviceCodeSession(ctx context.Context, signature string, requester fosite.Requester) (err error) { - ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateDeviceCodeSession") - defer otelx.End(span, &err) - return p.createSession(ctx, signature, requester, sqlTableDeviceCode, requester.GetSession().GetExpiresAt(fosite.DeviceCode).UTC()) -} - -// UpdateDeviceCodeSessionByRequestID updates a device code session by requestID -func (p *Persister) UpdateDeviceCodeSessionByRequestID(ctx context.Context, requestID string, requester fosite.Requester) (err error) { - ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.UpdateDeviceCodeSessionByRequestID") - defer otelx.End(span, &err) - - req, err := p.sqlSchemaFromRequest(ctx, requestID, requester, sqlTableDeviceCode, requester.GetSession().GetExpiresAt(fosite.DeviceCode).UTC()) - if err != nil { - return err - } - - stmt := fmt.Sprintf( - "UPDATE %s SET granted_scope=?, granted_audience=?, session_data=? WHERE request_id=? AND nid = ?", - OAuth2RequestSQL{Table: sqlTableDeviceCode}.TableName(), - ) - - /* #nosec G201 table is static */ - err = p.Connection(ctx).RawQuery(stmt, req.GrantedScope, req.GrantedAudience, req.Session, requestID, p.NetworkID(ctx)).Exec() - if err != nil { - return sqlcon.HandleError(err) - } - - return nil -} - -// GetDeviceCodeSession returns a device code session from the database -func (p *Persister) GetDeviceCodeSession(ctx context.Context, signature string, session fosite.Session) (_ fosite.Requester, err error) { - ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.GetDeviceCodeSession") - defer otelx.End(span, &err) - return p.findSessionBySignature(ctx, signature, session, sqlTableDeviceCode) -} - -// GetDeviceCodeSessionByRequestID returns a device code session from the database -func (p *Persister) GetDeviceCodeSessionByRequestID(ctx context.Context, requestID string, session fosite.Session) (_ fosite.Requester, err error) { - ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.GetDeviceCodeSessionByRequestID") - defer otelx.End(span, &err) - return p.findSessionByRequestID(ctx, requestID, session, sqlTableDeviceCode) -} - -// InvalidateDeviceCodeSession invalidates a device code session -func (p *Persister) InvalidateDeviceCodeSession(ctx context.Context, signature string) (err error) { - ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.InvalidateDeviceCodeSession") - defer otelx.End(span, &err) - - /* #nosec G201 table is static */ - return sqlcon.HandleError( - p.Connection(ctx). - RawQuery( - fmt.Sprintf("UPDATE %s SET active=false WHERE signature=? AND nid = ?", OAuth2RequestSQL{Table: sqlTableDeviceCode}.TableName()), - signature, - p.NetworkID(ctx), - ). - Exec(), - ) -} - -// CreateUserCodeSession creates a new user code session and stores it in the database -func (p *Persister) CreateUserCodeSession(ctx context.Context, signature string, requester fosite.Requester) (err error) { - ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateUserCodeSession") - defer otelx.End(span, &err) - return p.createSession(ctx, signature, requester, sqlTableUserCode, requester.GetSession().GetExpiresAt(fosite.UserCode).UTC()) -} - -// GetUserCodeSession returns a user code session from the database -func (p *Persister) GetUserCodeSession(ctx context.Context, signature string, session fosite.Session) (_ fosite.Requester, err error) { - ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.GetUserCodeSession") - defer otelx.End(span, &err) - if session == nil { - session = oauth2.NewSession("") - } - return p.findSessionBySignature(ctx, signature, session, sqlTableUserCode) -} - -// InvalidateUserCodeSession invalidates a user code session -func (p *Persister) InvalidateUserCodeSession(ctx context.Context, signature string) (err error) { - ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.InvalidateUserCodeSession") - defer otelx.End(span, &err) - - /* #nosec G201 table is static */ - return sqlcon.HandleError( - p.Connection(ctx). - RawQuery( - fmt.Sprintf("UPDATE %s SET active=false WHERE signature=? AND nid = ?", OAuth2RequestSQL{Table: sqlTableUserCode}.TableName()), - signature, - p.NetworkID(ctx), - ). - Exec(), - ) -} - -// UpdateAndInvalidateUserCodeSession invalidates a user code session and connects it with the device flow request ID -func (p *Persister) UpdateAndInvalidateUserCodeSessionByRequestID(ctx context.Context, request_id, challenge_id string) (err error) { - ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.UpdateAndInvalidateUserCodeSession") - defer otelx.End(span, &err) - - if count, err := p.Connection(ctx).RawQuery( - fmt.Sprintf("UPDATE %s SET active=false, challenge_id=? WHERE request_id=? AND nid = ? AND active=true", OAuth2RequestSQL{Table: sqlTableUserCode}.TableName()), - challenge_id, - request_id, - p.NetworkID(ctx), - ).ExecWithCount(); count == 0 && err == nil { - return errorsx.WithStack(x.ErrNotFound) - } else if err != nil { - return sqlcon.HandleError(err) - } - return nil -} diff --git a/x/clean_sql.go b/x/clean_sql.go index 243d65033d8..2b51ec2cde3 100644 --- a/x/clean_sql.go +++ b/x/clean_sql.go @@ -16,8 +16,7 @@ func DeleteHydraRows(t *testing.T, c *pop.Connection) { "hydra_oauth2_code", "hydra_oauth2_oidc", "hydra_oauth2_pkce", - "hydra_oauth2_device_code", - "hydra_oauth2_user_code", + "hydra_oauth2_device_auth_codes", "hydra_oauth2_flow", "hydra_oauth2_authentication_session", "hydra_oauth2_obfuscated_authentication_session", @@ -41,8 +40,7 @@ func CleanSQLPop(t *testing.T, c *pop.Connection) { "hydra_oauth2_code", "hydra_oauth2_oidc", "hydra_oauth2_pkce", - "hydra_oauth2_device_code", - "hydra_oauth2_user_code", + "hydra_oauth2_device_auth_codes", "hydra_oauth2_flow", "hydra_oauth2_authentication_session", "hydra_oauth2_obfuscated_authentication_session", diff --git a/x/fosite_storer.go b/x/fosite_storer.go index 8b879e11da7..2313ca199d8 100644 --- a/x/fosite_storer.go +++ b/x/fosite_storer.go @@ -44,7 +44,7 @@ type FositeStorer interface { // This is duplicated from Ory Fosite to help against deprecation linting errors. DeleteOpenIDConnectSession(ctx context.Context, authorizeCode string) error - GetDeviceCodeSessionByRequestID(ctx context.Context, requestID string, requester fosite.Session) (fosite.Requester, error) - UpdateDeviceCodeSessionByRequestID(ctx context.Context, requestID string, requester fosite.Requester) error - UpdateAndInvalidateUserCodeSessionByRequestID(ctx context.Context, signature, request_id string) (err error) + GetUserCodeSession(context.Context, string, fosite.Session) (fosite.DeviceRequester, error) + GetDeviceCodeSessionByRequestID(ctx context.Context, requestID string, requester fosite.Session) (fosite.DeviceRequester, string, error) + UpdateDeviceCodeSessionBySignature(ctx context.Context, requestID string, requester fosite.DeviceRequester) error } From ce6e722a5f775fd289c4fb63423711aca5a3e555 Mon Sep 17 00:00:00 2001 From: Nikos Date: Tue, 10 Dec 2024 18:24:31 +0100 Subject: [PATCH 36/52] fix: update oauth persister logic --- persistence/sql/persister_consent.go | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/persistence/sql/persister_consent.go b/persistence/sql/persister_consent.go index e8f91d72882..9f820623ffc 100644 --- a/persistence/sql/persister_consent.go +++ b/persistence/sql/persister_consent.go @@ -225,14 +225,11 @@ func (p *Persister) GetConsentRequest(ctx context.Context, challenge string) (_ } // CreateDeviceUserAuthRequest creates a new flow from a DeviceUserAuthRequest. -func (p *Persister) CreateDeviceUserAuthRequest(ctx context.Context, req *flow.DeviceUserAuthRequest) (*flow.Flow, error) { +func (p *Persister) CreateDeviceUserAuthRequest(ctx context.Context, req *flow.DeviceUserAuthRequest) (_ *flow.Flow, err error) { ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateDeviceUserAuthRequest") - defer span.End() + defer otelx.End(span, &err) nid := p.NetworkID(ctx) - if nid == uuid.Nil { - return nil, errorsx.WithStack(x.ErrNotFound) - } f := flow.NewDeviceFlow(req) f.NID = nid @@ -240,9 +237,9 @@ func (p *Persister) CreateDeviceUserAuthRequest(ctx context.Context, req *flow.D } // GetDeviceUserAuthRequest decodes a challenge into a new DeviceUserAuthRequest. -func (p *Persister) GetDeviceUserAuthRequest(ctx context.Context, challenge string) (*flow.DeviceUserAuthRequest, error) { +func (p *Persister) GetDeviceUserAuthRequest(ctx context.Context, challenge string) (_ *flow.DeviceUserAuthRequest, err error) { ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.GetDeviceUserAuthRequest") - defer span.End() + defer otelx.End(span, &err) f, err := flowctx.Decode[flow.Flow](ctx, p.r.FlowCipher(), challenge, flowctx.AsDeviceChallenge) if err != nil { @@ -260,9 +257,9 @@ func (p *Persister) GetDeviceUserAuthRequest(ctx context.Context, challenge stri } // HandleDeviceUserAuthRequest uses a HandledDeviceUserAuthRequest to update the flow and returns a DeviceUserAuthRequest. -func (p *Persister) HandleDeviceUserAuthRequest(ctx context.Context, f *flow.Flow, challenge string, r *flow.HandledDeviceUserAuthRequest) (*flow.DeviceUserAuthRequest, error) { +func (p *Persister) HandleDeviceUserAuthRequest(ctx context.Context, f *flow.Flow, challenge string, r *flow.HandledDeviceUserAuthRequest) (_ *flow.DeviceUserAuthRequest, err error) { ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.HandleDeviceUserAuthRequest") - defer span.End() + defer otelx.End(span, &err) if f == nil { return nil, errorsx.WithStack(fosite.ErrInvalidRequest.WithDebug("Flow was nil")) @@ -270,7 +267,7 @@ func (p *Persister) HandleDeviceUserAuthRequest(ctx context.Context, f *flow.Flo if f.NID != p.NetworkID(ctx) { return nil, errorsx.WithStack(x.ErrNotFound) } - err := f.HandleDeviceUserAuthRequest(r) + err = f.HandleDeviceUserAuthRequest(r) if err != nil { return nil, err } @@ -279,9 +276,9 @@ func (p *Persister) HandleDeviceUserAuthRequest(ctx context.Context, f *flow.Flo } // VerifyAndInvalidateDeviceUserAuthRequest verifies a verifier and invalidates the flow. -func (p *Persister) VerifyAndInvalidateDeviceUserAuthRequest(ctx context.Context, verifier string) (*flow.HandledDeviceUserAuthRequest, error) { +func (p *Persister) VerifyAndInvalidateDeviceUserAuthRequest(ctx context.Context, verifier string) (_ *flow.HandledDeviceUserAuthRequest, err error) { ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.VerifyAndInvalidateDeviceUserAuthRequest") - defer span.End() + defer otelx.End(span, &err) f, err := flowctx.Decode[flow.Flow](ctx, p.r.FlowCipher(), verifier, flowctx.AsDeviceVerifier) if err != nil { From 03f5928bfdb6330a3a03c849407ab2fe9f88b588 Mon Sep 17 00:00:00 2001 From: Nikos Date: Wed, 8 Jan 2025 16:28:39 +0100 Subject: [PATCH 37/52] fix: handle user_code collisions --- persistence/sql/persister_device.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/persistence/sql/persister_device.go b/persistence/sql/persister_device.go index 50675646b45..c6663236cb1 100644 --- a/persistence/sql/persister_device.go +++ b/persistence/sql/persister_device.go @@ -161,6 +161,8 @@ func (p *Persister) createDeviceAuthSession(ctx context.Context, deviceCodeSigna if err = sqlcon.HandleError(p.CreateWithNetwork(ctx, req)); errors.Is(err, sqlcon.ErrConcurrentUpdate) { return errors.Wrap(fosite.ErrSerializationFailure, err.Error()) + } else if errors.Is(err, sqlcon.ErrUniqueViolation) { + return errors.Wrap(fosite.ErrExistingUserCodeSignature, err.Error()) } else if err != nil { return err } From aff0979838b5c072a86efb490eb3a88c0314fe85 Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Thu, 16 Jan 2025 12:54:11 +0100 Subject: [PATCH 38/52] chore: code review --- .schema/config.schema.json | 6 +- consent/handler.go | 21 +- consent/strategy_default.go | 4 +- contrib/quickstart/5-min/hydra.yml | 2 +- driver/config/provider.go | 2 +- internal/.hydra.yaml | 2 +- internal/httpclient/go.sum | 347 +++++++++++++++++++++++++++++ spec/config.json | 2 +- 8 files changed, 368 insertions(+), 18 deletions(-) diff --git a/.schema/config.schema.json b/.schema/config.schema.json index df510c34645..ab3e9448837 100644 --- a/.schema/config.schema.json +++ b/.schema/config.schema.json @@ -645,7 +645,7 @@ }, "userinfo_url": { "type": "string", - "description": "A URL of the userinfo endpoint to be advertised at the OpenID Connect Discovery endpoint /.well-known/openid-configuration. Defaults to Ory Hydra's userinfo endpoint at /userinfo. Set this value if you want to handle this endpoint yourself.", + "description": "A URL of the userinfo endpoint to be advertised at the OpenID Connect Discovery endpoint `/.well-known/openid-configuration`. Defaults to Ory Hydra's userinfo endpoint at `/userinfo`. Set this value if you want to handle this endpoint yourself.", "format": "uri-reference", "examples": [ "https://example.org/my-custom-userinfo-endpoint" @@ -653,7 +653,7 @@ }, "device_authorization_url": { "type": "string", - "description": "A URL of the device authorization endpoint to be advertised at the OpenID Connect Discovery endpoint /.well-known/openid-configuration. Defaults to Ory Hydra's device authorizatoin endpoint at /oauth2/device/auth. Set this value if you want to handle this endpoint yourself.", + "description": "A URL of the device authorization endpoint to be advertised at the OpenID Connect Discovery endpoint `/.well-known/openid-configuration`. Defaults to Ory Hydra's device authorization endpoint at `/oauth2/device/auth`. Set this value if you want to handle this endpoint yourself.", "format": "uri-reference", "examples": [ "https://example.org/oauth2/device/auth" @@ -825,7 +825,7 @@ "/ui/device_verification" ] }, - "post_device_done": { + "device_verification_success": { "type": "string", "description": "Sets the post device authentication endpoint. Defaults to an internal fallback URL showing an error.", "format": "uri-reference", diff --git a/consent/handler.go b/consent/handler.go index 53b26611257..54715f8e83b 100644 --- a/consent/handler.go +++ b/consent/handler.go @@ -1112,7 +1112,7 @@ func (h *Handler) acceptUserCodeRequest(w http.ResponseWriter, r *http.Request, d := json.NewDecoder(r.Body) d.DisallowUnknownFields() if err := d.Decode(&reqBody); err != nil { - h.r.Writer().WriteErrorCode(w, r, http.StatusBadRequest, errorsx.WithStack(err)) + h.r.Writer().WriteError(w, r, errorsx.WithStack(fosite.ErrInvalidRequest.WithWrap(err).WithHintf("Unable to decode request body: %s", err.Error()))) return } @@ -1123,7 +1123,7 @@ func (h *Handler) acceptUserCodeRequest(w http.ResponseWriter, r *http.Request, cr, err := h.r.ConsentManager().GetDeviceUserAuthRequest(ctx, challenge) if err != nil { - h.r.Writer().WriteError(w, r, errorsx.WithStack(err)) + h.r.Writer().WriteError(w, r, err) return } @@ -1135,17 +1135,18 @@ func (h *Handler) acceptUserCodeRequest(w http.ResponseWriter, r *http.Request, userCodeSignature, err := h.r.RFC8628HMACStrategy().UserCodeSignature(r.Context(), reqBody.UserCode) if err != nil { - h.r.Writer().WriteError(w, r, errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithHint(`'user_code' signature could not be computed`))) + h.r.Writer().WriteError(w, r, fosite.ErrServerError.WithWrap(err).WithHint(`The 'user_code' signature could not be computed.`)) return } + userCodeRequest, err := h.r.OAuth2Storage().GetUserCodeSession(r.Context(), userCodeSignature, nil) if err != nil { - h.r.Writer().WriteError(w, r, errorsx.WithStack(fosite.ErrNotFound.WithWrap(err).WithHint(`'user_code' session not found`))) + h.r.Writer().WriteError(w, r, fosite.ErrInvalidRequest.WithWrap(err).WithHint(`The 'user_code' session could not be found or has expired or is otherwise malformed.`)) return } - err = h.r.RFC8628HMACStrategy().ValidateUserCode(ctx, userCodeRequest, reqBody.UserCode) - if err != nil { - h.r.Writer().WriteError(w, r, errorsx.WithStack(fosite.ErrTokenExpired.WithWrap(err).WithHint(`'user_code' has expired`))) + + if err := h.r.RFC8628HMACStrategy().ValidateUserCode(ctx, userCodeRequest, reqBody.UserCode); err != nil { + h.r.Writer().WriteError(w, r, fosite.ErrInvalidRequest.WithWrap(err).WithHint(`The 'user_code' session could not be found or has expired or is otherwise malformed.`)) return } @@ -1165,22 +1166,24 @@ func (h *Handler) acceptUserCodeRequest(w http.ResponseWriter, r *http.Request, h.r.Writer().WriteError(w, r, errorsx.WithStack(err)) return } + if reqURL.Query().Get("client_id") == "" { q := reqURL.Query() q.Add("client_id", userCodeRequest.GetClient().GetID()) reqURL.RawQuery = q.Encode() } + f.RequestURL = reqURL.String() hr, err := h.r.ConsentManager().HandleDeviceUserAuthRequest(ctx, f, challenge, &p) if err != nil { - h.r.Writer().WriteError(w, r, errorsx.WithStack(err)) + h.r.Writer().WriteError(w, r, err) return } ru, err := url.Parse(hr.RequestURL) if err != nil { - h.r.Writer().WriteError(w, r, err) + h.r.Writer().WriteError(w, r, fosite.ErrInvalidRequest.WithWrap(err).WithHint(`Unable to parse the request_url.`)) return } diff --git a/consent/strategy_default.go b/consent/strategy_default.go index fb508c31f2b..de49476b20e 100644 --- a/consent/strategy_default.go +++ b/consent/strategy_default.go @@ -1212,11 +1212,11 @@ func (s *DefaultStrategy) HandleOAuth2DeviceAuthorizationRequest( // Validate client_id clientID := r.URL.Query().Get("client_id") if clientID == "" { - return nil, nil, errorsx.WithStack(fosite.ErrInvalidClient.WithHintf(`client_id query parameter is missing`)) + return nil, nil, errorsx.WithStack(fosite.ErrInvalidClient.WithHintf(`Query parameter 'client_id' is missing.`)) } c, err := s.r.ClientManager().GetConcreteClient(r.Context(), clientID) if errors.Is(err, x.ErrNotFound) { - return nil, nil, errorsx.WithStack(fosite.ErrInvalidClient.WithHintf(`Unknown client_id %s`, clientID)) + return nil, nil, errorsx.WithStack(fosite.ErrInvalidClient.WithWrap(err).WithHintf(`Client does not exist`)) } else if err != nil { return nil, nil, err } diff --git a/contrib/quickstart/5-min/hydra.yml b/contrib/quickstart/5-min/hydra.yml index 3becd68594f..e5cb3c8033c 100644 --- a/contrib/quickstart/5-min/hydra.yml +++ b/contrib/quickstart/5-min/hydra.yml @@ -9,7 +9,7 @@ urls: login: http://127.0.0.1:3000/login logout: http://127.0.0.1:3000/logout device_verification: http://127.0.0.1:3000/device_code - post_device_done: http://127.0.0.1:3000/device_complete + device_verification_success: http://127.0.0.1:3000/device_complete secrets: system: diff --git a/driver/config/provider.go b/driver/config/provider.go index 5d3d9a062bb..779084da5df 100644 --- a/driver/config/provider.go +++ b/driver/config/provider.go @@ -87,7 +87,7 @@ const ( KeyConsentURL = "urls.consent" KeyErrorURL = "urls.error" KeyDeviceVerificationURL = "urls.device_verification" - KeyDeviceDoneURL = "urls.post_device_done" + KeyDeviceDoneURL = "urls.device_verification_success" KeyPublicURL = "urls.self.public" KeyAdminURL = "urls.self.admin" KeyIssuerURL = "urls.self.issuer" diff --git a/internal/.hydra.yaml b/internal/.hydra.yaml index 4e7cbb0143c..1d8390f4b0d 100644 --- a/internal/.hydra.yaml +++ b/internal/.hydra.yaml @@ -102,7 +102,7 @@ urls: logout: https://logout error: https://error device_verification: https://device - post_device_done: https://device/callback + device_verification_success: https://device/callback post_logout_redirect: https://post_logout strategies: diff --git a/internal/httpclient/go.sum b/internal/httpclient/go.sum index 734252e6815..3dee6d68163 100644 --- a/internal/httpclient/go.sum +++ b/internal/httpclient/go.sum @@ -1,13 +1,360 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/spec/config.json b/spec/config.json index 46e9c4a549d..916131165eb 100644 --- a/spec/config.json +++ b/spec/config.json @@ -825,7 +825,7 @@ "/ui/device" ] }, - "post_device_done": { + "device_verification_success": { "type": "string", "description": "When a user completes an authentication flow initiated by a device, they will be redirected to this url afterwards.", "format": "uri-reference", From dbe83d4fca1b4191187d2e9cece325a5b9c7d069 Mon Sep 17 00:00:00 2001 From: Nikos Date: Fri, 24 Jan 2025 14:27:19 +0100 Subject: [PATCH 39/52] fixup! dfa4c99b --- .../migrations/20241609000001000000_device_flow.cockroach.up.sql | 1 + .../sql/migrations/20241609000001000000_device_flow.mysql.up.sql | 1 + .../migrations/20241609000001000000_device_flow.postgres.up.sql | 1 + 3 files changed, 3 insertions(+) diff --git a/persistence/sql/migrations/20241609000001000000_device_flow.cockroach.up.sql b/persistence/sql/migrations/20241609000001000000_device_flow.cockroach.up.sql index 5f2d1b3a53c..2739ecc7d68 100644 --- a/persistence/sql/migrations/20241609000001000000_device_flow.cockroach.up.sql +++ b/persistence/sql/migrations/20241609000001000000_device_flow.cockroach.up.sql @@ -22,6 +22,7 @@ CREATE TABLE IF NOT EXISTS hydra_oauth2_device_auth_codes FOREIGN KEY (client_id, nid) REFERENCES hydra_client(id, nid) ON DELETE CASCADE, FOREIGN KEY (nid) REFERENCES networks(id) ON UPDATE RESTRICT ON DELETE CASCADE, + FOREIGN KEY (challenge_id) REFERENCES hydra_oauth2_flow(consent_challenge_id) ON DELETE CASCADE, PRIMARY KEY (device_code_signature, nid) ); diff --git a/persistence/sql/migrations/20241609000001000000_device_flow.mysql.up.sql b/persistence/sql/migrations/20241609000001000000_device_flow.mysql.up.sql index 1343058ff3a..90f0aaaede6 100644 --- a/persistence/sql/migrations/20241609000001000000_device_flow.mysql.up.sql +++ b/persistence/sql/migrations/20241609000001000000_device_flow.mysql.up.sql @@ -22,6 +22,7 @@ CREATE TABLE IF NOT EXISTS hydra_oauth2_device_auth_codes FOREIGN KEY (client_id, nid) REFERENCES hydra_client(id, nid) ON DELETE CASCADE, FOREIGN KEY (nid) REFERENCES networks(id) ON UPDATE RESTRICT ON DELETE CASCADE, + FOREIGN KEY (challenge_id) REFERENCES hydra_oauth2_flow(consent_challenge_id) ON DELETE CASCADE, PRIMARY KEY (device_code_signature, nid) ); diff --git a/persistence/sql/migrations/20241609000001000000_device_flow.postgres.up.sql b/persistence/sql/migrations/20241609000001000000_device_flow.postgres.up.sql index 8e3a76794f1..1631bd3ca26 100644 --- a/persistence/sql/migrations/20241609000001000000_device_flow.postgres.up.sql +++ b/persistence/sql/migrations/20241609000001000000_device_flow.postgres.up.sql @@ -22,6 +22,7 @@ CREATE TABLE IF NOT EXISTS hydra_oauth2_device_auth_codes FOREIGN KEY (client_id, nid) REFERENCES hydra_client(id, nid) ON DELETE CASCADE, FOREIGN KEY (nid) REFERENCES networks(id) ON UPDATE RESTRICT ON DELETE CASCADE, + FOREIGN KEY (challenge_id) REFERENCES hydra_oauth2_flow(consent_challenge_id) ON DELETE CASCADE, PRIMARY KEY (device_code_signature, nid) ); From 05d275a4bd185bfd4c7a6473ea14e8ab25d87c37 Mon Sep 17 00:00:00 2001 From: Nikos Date: Fri, 24 Jan 2025 14:29:32 +0100 Subject: [PATCH 40/52] fixup! 04c25d8b4f6ed7f687fc5186732f9008bedce811 --- consent/strategy_default.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/consent/strategy_default.go b/consent/strategy_default.go index de49476b20e..781974b0334 100644 --- a/consent/strategy_default.go +++ b/consent/strategy_default.go @@ -1161,8 +1161,8 @@ func (s *DefaultStrategy) HandleOAuth2AuthorizationRequest( ctx, span := trace.SpanFromContext(ctx).TracerProvider().Tracer("").Start(ctx, "DefaultStrategy.HandleOAuth2AuthorizationRequest") defer otelx.End(span, &err) - loginVerifier := strings.TrimSpace(r.URL.Query().Get("login_verifier")) - consentVerifier := strings.TrimSpace(r.URL.Query().Get("consent_verifier")) + loginVerifier := strings.TrimSpace(req.GetRequestForm().Get("login_verifier")) + consentVerifier := strings.TrimSpace(req.GetRequestForm().Get("consent_verifier")) if loginVerifier == "" && consentVerifier == "" { // ok, we need to process this request and redirect to the original endpoint return nil, nil, s.requestAuthentication(ctx, w, r, req, nil) From 039f9c0d91dbf763f1eb81cde9dfc861539793f7 Mon Sep 17 00:00:00 2001 From: Nikos Date: Fri, 24 Jan 2025 14:29:50 +0100 Subject: [PATCH 41/52] chore: add more tests --- consent/handler_test.go | 86 +++++++++++++++++- consent/strategy_oauth_test.go | 97 +++++++++++++++++++- oauth2/oauth2_device_code_test.go | 143 +++++++++++++++++++++++++++--- 3 files changed, 309 insertions(+), 17 deletions(-) diff --git a/consent/handler_test.go b/consent/handler_test.go index 9b07c400d08..3487e3f7442 100644 --- a/consent/handler_test.go +++ b/consent/handler_test.go @@ -501,7 +501,7 @@ func TestAcceptCodeDeviceRequestFailure(t *testing.T) { return ts.URL + "/admin" + DevicePath + "/accept?device_challenge=" + challenge }, validateResponse: func(resp *http.Response) { - require.EqualValues(t, http.StatusNotFound, resp.StatusCode) + require.EqualValues(t, http.StatusBadRequest, resp.StatusCode) }, }, { @@ -605,10 +605,90 @@ func TestAcceptCodeDeviceRequestFailure(t *testing.T) { return ts.URL + "/admin" + DevicePath + "/accept?device_challenge=" + challenge }, validateResponse: func(resp *http.Response) { - require.EqualValues(t, http.StatusUnauthorized, resp.StatusCode) + require.EqualValues(t, http.StatusBadRequest, resp.StatusCode) + result := &fosite.RFC6749Error{} + require.NoError(t, json.NewDecoder(resp.Body).Decode(&result)) + require.EqualValues(t, result.ErrorField, fosite.ErrInvalidRequest.ErrorField) + }, + }, + { + desc: "accepted user_code", + getBody: func() ([]byte, error) { + deviceRequest := fosite.NewDeviceRequest() + deviceRequest.Client = cl + deviceRequest.SetSession( + &oauth2.Session{ + DefaultSession: &openid.DefaultSession{ + Headers: &jwt.Headers{}, + }, + }, + ) + _, deviceCodesig, err := reg.RFC8628HMACStrategy().GenerateDeviceCode(ctx) + require.NoError(t, err) + userCode, sig, err := reg.RFC8628HMACStrategy().GenerateUserCode(ctx) + require.NoError(t, err) + deviceRequest.SetSession( + &oauth2.Session{ + DefaultSession: &openid.DefaultSession{ + Headers: &jwt.Headers{}, + }, + }, + ) + exp := time.Now().UTC() + deviceRequest.Session.SetExpiresAt(fosite.UserCode, exp) + err = reg.OAuth2Storage().CreateDeviceAuthSession(ctx, deviceCodesig, sig, deviceRequest) + require.NoError(t, err) + deviceRequest.UserCodeState = fosite.UserCodeAccepted + return json.Marshal(&hydra.AcceptDeviceUserCodeRequest{UserCode: &userCode}) + }, + getURL: func() string { + return ts.URL + "/admin" + DevicePath + "/accept?device_challenge=" + challenge + }, + validateResponse: func(resp *http.Response) { + require.EqualValues(t, http.StatusBadRequest, resp.StatusCode) + result := &fosite.RFC6749Error{} + require.NoError(t, json.NewDecoder(resp.Body).Decode(&result)) + require.EqualValues(t, result.ErrorField, fosite.ErrInvalidRequest.ErrorField) + }, + }, + { + desc: "rejected user_code", + getBody: func() ([]byte, error) { + deviceRequest := fosite.NewDeviceRequest() + deviceRequest.Client = cl + deviceRequest.SetSession( + &oauth2.Session{ + DefaultSession: &openid.DefaultSession{ + Headers: &jwt.Headers{}, + }, + }, + ) + _, deviceCodesig, err := reg.RFC8628HMACStrategy().GenerateDeviceCode(ctx) + require.NoError(t, err) + userCode, sig, err := reg.RFC8628HMACStrategy().GenerateUserCode(ctx) + require.NoError(t, err) + deviceRequest.SetSession( + &oauth2.Session{ + DefaultSession: &openid.DefaultSession{ + Headers: &jwt.Headers{}, + }, + }, + ) + exp := time.Now().UTC() + deviceRequest.Session.SetExpiresAt(fosite.UserCode, exp) + err = reg.OAuth2Storage().CreateDeviceAuthSession(ctx, deviceCodesig, sig, deviceRequest) + require.NoError(t, err) + deviceRequest.UserCodeState = fosite.UserCodeRejected + return json.Marshal(&hydra.AcceptDeviceUserCodeRequest{UserCode: &userCode}) + }, + getURL: func() string { + return ts.URL + "/admin" + DevicePath + "/accept?device_challenge=" + challenge + }, + validateResponse: func(resp *http.Response) { + require.EqualValues(t, http.StatusBadRequest, resp.StatusCode) result := &fosite.RFC6749Error{} require.NoError(t, json.NewDecoder(resp.Body).Decode(&result)) - require.EqualValues(t, result.ErrorField, fosite.ErrTokenExpired.ErrorField) + require.EqualValues(t, result.ErrorField, fosite.ErrInvalidRequest.ErrorField) }, }, { diff --git a/consent/strategy_oauth_test.go b/consent/strategy_oauth_test.go index f91e7c7f380..319e350dc2e 100644 --- a/consent/strategy_oauth_test.go +++ b/consent/strategy_oauth_test.go @@ -1136,6 +1136,7 @@ func TestStrategyDeviceLoginConsent(t *testing.T) { } } + now := 1723546027 // Unix timestamps must round-trip through Hydra without converting to floats or similar acceptDeviceHandler := func(t *testing.T) http.HandlerFunc { return checkAndAcceptDeviceHandler(t, adminClient) } @@ -1174,13 +1175,20 @@ func TestStrategyDeviceLoginConsent(t *testing.T) { Remember: pointerx.Bool(true), GrantScope: []string{"openid"}, Session: &hydra.AcceptOAuth2ConsentRequestSession{ - AccessToken: map[string]interface{}{"foo": "bar"}, - IdToken: map[string]interface{}{"bar": "baz"}, + AccessToken: map[string]interface{}{ + "foo": "bar", + "ts1": now, + }, + IdToken: map[string]interface{}{ + "bar": "baz", + "ts1": now, + }, }, })) hc := testhelpers.NewEmptyJarClient(t) + var sid string var run = func(t *testing.T) { res, resp := makeOAuth2DeviceAuthRequest(t, reg, hc, c, "openid") assert.EqualValues(t, http.StatusOK, resp.StatusCode) @@ -1202,12 +1210,95 @@ func TestStrategyDeviceLoginConsent(t *testing.T) { idClaims := testhelpers.DecodeIDToken(t, token) assert.Equal(t, "baz", idClaims.Get("bar").String(), "%s", idClaims.Raw) - sid := idClaims.Get("sid").String() + sid = idClaims.Get("sid").String() assert.NotNil(t, sid) } t.Run("perform first flow", run) + t.Run("perform follow up flows and check if session values are set", func(t *testing.T) { + testhelpers.NewLoginConsentUI(t, reg.Config(), + checkAndAcceptLoginHandler(t, adminClient, subject, func(t *testing.T, res *hydra.OAuth2LoginRequest, err error) hydra.AcceptOAuth2LoginRequest { + require.NoError(t, err) + assert.True(t, res.Skip) + assert.Equal(t, sid, *res.SessionId) + assert.Equal(t, subject, res.Subject) + assert.Empty(t, pointerx.StringR(res.Client.ClientSecret)) + return hydra.AcceptOAuth2LoginRequest{ + Subject: subject, + Context: map[string]interface{}{"xyz": "abc"}, + } + }), + checkAndAcceptConsentHandler(t, adminClient, func(t *testing.T, req *hydra.OAuth2ConsentRequest, err error) hydra.AcceptOAuth2ConsentRequest { + require.NoError(t, err) + assert.True(t, *req.Skip) + assert.Equal(t, sid, *req.LoginSessionId) + assert.Equal(t, subject, *req.Subject) + assert.Empty(t, pointerx.StringR(req.Client.ClientSecret)) + assert.Equal(t, map[string]interface{}{"xyz": "abc"}, req.Context) + return hydra.AcceptOAuth2ConsentRequest{ + Remember: pointerx.Bool(true), + GrantScope: []string{"openid"}, + Session: &hydra.AcceptOAuth2ConsentRequestSession{ + AccessToken: map[string]interface{}{ + "foo": "bar", + "ts1": now, + }, + IdToken: map[string]interface{}{ + "bar": "baz", + "ts2": now, + }, + }, + } + })) + + for k := 0; k < 3; k++ { + t.Run(fmt.Sprintf("case=%d", k), run) + } + }) + }) + t.Run("case=should fail because we are reusing the same verifier", func(t *testing.T) { + subject := "aeneas-rekkas" + c := createDefaultClient(t) + testhelpers.NewDeviceLoginConsentUI(t, reg.Config(), + acceptDeviceHandler(t), + acceptLoginHandler(t, subject, &hydra.AcceptOAuth2LoginRequest{ + Remember: pointerx.Bool(true), + }), + acceptConsentHandler(t, &hydra.AcceptOAuth2ConsentRequest{ + Remember: pointerx.Bool(true), + GrantScope: []string{"openid"}, + Session: &hydra.AcceptOAuth2ConsentRequestSession{ + AccessToken: map[string]interface{}{"foo": "bar"}, + IdToken: map[string]interface{}{"bar": "baz"}, + }, + })) + + hc := testhelpers.NewEmptyJarClient(t) + + res, resp := makeOAuth2DeviceAuthRequest(t, reg, hc, c, "openid") + assert.EqualValues(t, http.StatusOK, resp.StatusCode) + + devResp := new(oauth2.DeviceAuthResponse) + require.NoError(t, json.Unmarshal([]byte(res.Raw), devResp)) + + resp, err := hc.Get(devResp.VerificationURIComplete) + require.NoError(t, err) + require.Contains(t, reg.Config().DeviceDoneURL(ctx).String(), resp.Request.URL.Path, "did not end up in post device URL") + require.Equal(t, resp.Request.URL.Query().Get("client_id"), c.ID) + + conf := oauth2Config(t, c) + token, err := conf.DeviceAccessToken(ctx, devResp) + require.NoError(t, err) + + claims := testhelpers.IntrospectToken(t, conf, token.AccessToken, adminTS) + assert.Equal(t, "bar", claims.Get("ext.foo").String(), "%s", claims.Raw) + + idClaims := testhelpers.DecodeIDToken(t, token) + assert.Equal(t, "baz", idClaims.Get("bar").String(), "%s", idClaims.Raw) + sid := idClaims.Get("sid").String() + assert.NotNil(t, sid) + }) t.Run("case=should fail because a device verifier was given that doesn't exist in the store", func(t *testing.T) { testhelpers.NewDeviceLoginConsentUI(t, reg.Config(), testhelpers.HTTPServerNoExpectedCallHandler(t), testhelpers.HTTPServerNoExpectedCallHandler(t), testhelpers.HTTPServerNoExpectedCallHandler(t)) diff --git a/oauth2/oauth2_device_code_test.go b/oauth2/oauth2_device_code_test.go index 23e2dcccf97..1a03155cb25 100644 --- a/oauth2/oauth2_device_code_test.go +++ b/oauth2/oauth2_device_code_test.go @@ -276,7 +276,7 @@ func TestDeviceCodeWithDefaultStrategy(t *testing.T) { } } - acceptLoginHandler := func(t *testing.T, c *client.Client, subject string, checkRequestPayload func(request *hydra.OAuth2LoginRequest) *hydra.AcceptOAuth2LoginRequest) http.HandlerFunc { + acceptLoginHandler := func(t *testing.T, c *client.Client, subject string, scopes []string, checkRequestPayload func(request *hydra.OAuth2LoginRequest) *hydra.AcceptOAuth2LoginRequest) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { rr, _, err := adminClient.OAuth2API.GetOAuth2LoginRequest(context.Background()).LoginChallenge(r.URL.Query().Get("login_challenge")).Execute() require.NoError(t, err) @@ -286,7 +286,7 @@ func TestDeviceCodeWithDefaultStrategy(t *testing.T) { assert.EqualValues(t, c.GrantTypes, rr.Client.GrantTypes) assert.EqualValues(t, c.LogoURI, pointerx.Deref(rr.Client.LogoUri)) assert.EqualValues(t, r.URL.Query().Get("login_challenge"), rr.Challenge) - assert.EqualValues(t, []string{"hydra", "offline", "openid"}, rr.RequestedScope) + assert.EqualValues(t, scopes, rr.RequestedScope) assert.Contains(t, rr.RequestUrl, hydraoauth2.DeviceVerificationPath) acceptBody := hydra.AcceptOAuth2LoginRequest{ @@ -312,7 +312,7 @@ func TestDeviceCodeWithDefaultStrategy(t *testing.T) { } } - acceptConsentHandler := func(t *testing.T, c *client.Client, subject string, checkRequestPayload func(*hydra.OAuth2ConsentRequest)) http.HandlerFunc { + acceptConsentHandler := func(t *testing.T, c *client.Client, subject string, scopes []string, checkRequestPayload func(*hydra.OAuth2ConsentRequest)) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { rr, _, err := adminClient.OAuth2API.GetOAuth2ConsentRequest(context.Background()).ConsentChallenge(r.URL.Query().Get("consent_challenge")).Execute() require.NoError(t, err) @@ -322,7 +322,7 @@ func TestDeviceCodeWithDefaultStrategy(t *testing.T) { assert.EqualValues(t, c.GrantTypes, rr.Client.GrantTypes) assert.EqualValues(t, c.LogoURI, pointerx.Deref(rr.Client.LogoUri)) assert.EqualValues(t, subject, pointerx.Deref(rr.Subject)) - assert.EqualValues(t, []string{"hydra", "offline", "openid"}, rr.RequestedScope) + assert.EqualValues(t, scopes, rr.RequestedScope) assert.EqualValues(t, r.URL.Query().Get("consent_challenge"), rr.Challenge) assert.Contains(t, *rr.RequestUrl, hydraoauth2.DeviceVerificationPath) if checkRequestPayload != nil { @@ -333,7 +333,7 @@ func TestDeviceCodeWithDefaultStrategy(t *testing.T) { v, _, err := adminClient.OAuth2API.AcceptOAuth2ConsentRequest(context.Background()). ConsentChallenge(r.URL.Query().Get("consent_challenge")). AcceptOAuth2ConsentRequest(hydra.AcceptOAuth2ConsentRequest{ - GrantScope: []string{"hydra", "offline", "openid"}, Remember: pointerx.Ptr(true), RememberFor: pointerx.Ptr[int64](0), + GrantScope: scopes, Remember: pointerx.Ptr(true), RememberFor: pointerx.Ptr[int64](0), GrantAccessTokenAudience: rr.RequestedAccessTokenAudience, Session: &hydra.AcceptOAuth2ConsentRequestSession{ AccessToken: map[string]interface{}{"foo": "bar"}, @@ -433,13 +433,90 @@ func TestDeviceCodeWithDefaultStrategy(t *testing.T) { subject := "aeneas-rekkas" nonce := uuid.New() + t.Run("case=perform device flow without ID and refresh tokens", func(t *testing.T) { + + c, conf := newDeviceClient(t, reg) + conf.Scopes = []string{"hydra"} + testhelpers.NewDeviceLoginConsentUI(t, reg.Config(), + acceptDeviceHandler(t, c), + acceptLoginHandler(t, c, subject, conf.Scopes, nil), + acceptConsentHandler(t, c, subject, conf.Scopes, nil), + ) + + resp, err := getDeviceCode(t, conf, nil) + require.NoError(t, err) + require.NotEmpty(t, resp.DeviceCode) + require.NotEmpty(t, resp.UserCode) + loginFlowResp := acceptUserCode(t, conf, nil, resp) + require.NotNil(t, loginFlowResp) + token, err := conf.DeviceAccessToken(context.Background(), resp) + require.NoError(t, err) + + assert.Empty(t, token.Extra("c_nonce_draft_00"), "should not be set if not requested") + assert.Empty(t, token.Extra("c_nonce_expires_in_draft_00"), "should not be set if not requested") + introspectAccessToken(t, conf, token, subject) + assert.Empty(t, token.Extra("id_token")) + assert.Empty(t, token.RefreshToken) + }) + t.Run("case=perform device flow with ID token", func(t *testing.T) { + + c, conf := newDeviceClient(t, reg) + conf.Scopes = []string{"openid", "hydra"} + testhelpers.NewDeviceLoginConsentUI(t, reg.Config(), + acceptDeviceHandler(t, c), + acceptLoginHandler(t, c, subject, conf.Scopes, nil), + acceptConsentHandler(t, c, subject, conf.Scopes, nil), + ) + + resp, err := getDeviceCode(t, conf, nil) + require.NoError(t, err) + require.NotEmpty(t, resp.DeviceCode) + require.NotEmpty(t, resp.UserCode) + loginFlowResp := acceptUserCode(t, conf, nil, resp) + require.NotNil(t, loginFlowResp) + token, err := conf.DeviceAccessToken(context.Background(), resp) + iat := time.Now() + require.NoError(t, err) + + assert.Empty(t, token.Extra("c_nonce_draft_00"), "should not be set if not requested") + assert.Empty(t, token.Extra("c_nonce_expires_in_draft_00"), "should not be set if not requested") + introspectAccessToken(t, conf, token, subject) + assertIDToken(t, token, conf, subject, nonce, iat.Add(reg.Config().GetIDTokenLifespan(ctx))) + assert.Empty(t, token.RefreshToken) + }) + t.Run("case=perform device flow with refresh token", func(t *testing.T) { + + c, conf := newDeviceClient(t, reg) + conf.Scopes = []string{"hydra", "offline"} + testhelpers.NewDeviceLoginConsentUI(t, reg.Config(), + acceptDeviceHandler(t, c), + acceptLoginHandler(t, c, subject, conf.Scopes, nil), + acceptConsentHandler(t, c, subject, conf.Scopes, nil), + ) + + resp, err := getDeviceCode(t, conf, nil) + require.NoError(t, err) + require.NotEmpty(t, resp.DeviceCode) + require.NotEmpty(t, resp.UserCode) + loginFlowResp := acceptUserCode(t, conf, nil, resp) + require.NotNil(t, loginFlowResp) + token, err := conf.DeviceAccessToken(context.Background(), resp) + iat := time.Now() + require.NoError(t, err) + + assert.Empty(t, token.Extra("c_nonce_draft_00"), "should not be set if not requested") + assert.Empty(t, token.Extra("c_nonce_expires_in_draft_00"), "should not be set if not requested") + introspectAccessToken(t, conf, token, subject) + assert.Empty(t, token.Extra("id_token")) + assertRefreshToken(t, token, conf, iat.Add(reg.Config().GetRefreshTokenLifespan(ctx))) + }) t.Run("case=perform device flow with ID token and refresh tokens", func(t *testing.T) { run := func(t *testing.T, strategy string) { c, conf := newDeviceClient(t, reg) testhelpers.NewDeviceLoginConsentUI(t, reg.Config(), acceptDeviceHandler(t, c), - acceptLoginHandler(t, c, subject, nil), - acceptConsentHandler(t, c, subject, nil), + acceptLoginHandler(t, c, subject, conf.Scopes, nil), + acceptConsentHandler(t, c, subject, conf.Scopes, nil), ) resp, err := getDeviceCode(t, conf, nil) @@ -515,12 +592,12 @@ func TestDeviceCodeWithDefaultStrategy(t *testing.T) { t, reg.Config(), acceptDeviceHandler(t, c), - acceptLoginHandler(t, c, subject, func(r *hydra.OAuth2LoginRequest) *hydra.AcceptOAuth2LoginRequest { + acceptLoginHandler(t, c, subject, conf.Scopes, func(r *hydra.OAuth2LoginRequest) *hydra.AcceptOAuth2LoginRequest { assert.False(t, r.Skip) assert.EqualValues(t, []string{expectAud}, r.RequestedAccessTokenAudience) return nil }), - acceptConsentHandler(t, c, subject, func(r *hydra.OAuth2ConsentRequest) { + acceptConsentHandler(t, c, subject, conf.Scopes, func(r *hydra.OAuth2ConsentRequest) { assert.False(t, *r.Skip) assert.EqualValues(t, []string{expectAud}, r.RequestedAccessTokenAudience) }), @@ -550,8 +627,8 @@ func TestDeviceCodeWithDefaultStrategy(t *testing.T) { t, reg.Config(), acceptDeviceHandler(t, c), - acceptLoginHandler(t, c, subject, nil), - acceptConsentHandler(t, c, subject, nil), + acceptLoginHandler(t, c, subject, conf.Scopes, nil), + acceptConsentHandler(t, c, subject, conf.Scopes, nil), ) resp, err := getDeviceCode(t, conf, nil) @@ -656,6 +733,50 @@ func TestDeviceCodeWithDefaultStrategy(t *testing.T) { run(t, "opaque", c, conf, expectedLifespans) }) }) + t.Run("case=cannot reuse user_code", func(t *testing.T) { + c, conf := newDeviceClient(t, reg) + testhelpers.NewDeviceLoginConsentUI(t, reg.Config(), + func(w http.ResponseWriter, r *http.Request) { + userCode := r.URL.Query().Get("user_code") + payload := hydra.AcceptDeviceUserCodeRequest{ + UserCode: &userCode, + } + + v, _, err := adminClient.OAuth2API.AcceptUserCodeRequest(context.Background()). + DeviceChallenge(r.URL.Query().Get("device_challenge")). + AcceptDeviceUserCodeRequest(payload). + Execute() + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + require.NotEmpty(t, v.RedirectTo) + http.Redirect(w, r, v.RedirectTo, http.StatusFound) + }, + acceptLoginHandler(t, c, subject, conf.Scopes, nil), + acceptConsentHandler(t, c, subject, conf.Scopes, nil), + ) + + resp, err := getDeviceCode(t, conf, nil) + require.NoError(t, err) + require.NotEmpty(t, resp.DeviceCode) + require.NotEmpty(t, resp.UserCode) + loginFlowResp := acceptUserCode(t, conf, nil, resp) + require.NotNil(t, loginFlowResp) + token, err := conf.DeviceAccessToken(context.Background(), resp) + iat := time.Now() + require.NoError(t, err) + + introspectAccessToken(t, conf, token, subject) + assertIDToken(t, token, conf, subject, nonce, iat.Add(reg.Config().GetIDTokenLifespan(ctx))) + assertRefreshToken(t, token, conf, iat.Add(reg.Config().GetRefreshTokenLifespan(ctx))) + + hc := testhelpers.NewEmptyJarClient(t) + + loginFlowResp2, err := hc.Get(resp.VerificationURIComplete) + require.NoError(t, err) + require.Equal(t, loginFlowResp2.StatusCode, http.StatusBadRequest) + }) } func newDeviceClient( From 7dc0ed7f513bf2939ce8d4ab268c1d5de6ff22c0 Mon Sep 17 00:00:00 2001 From: Nikos Date: Fri, 24 Jan 2025 17:09:44 +0100 Subject: [PATCH 42/52] fix: update device session fields --- persistence/sql/persister_device.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/persistence/sql/persister_device.go b/persistence/sql/persister_device.go index c6663236cb1..af658c77978 100644 --- a/persistence/sql/persister_device.go +++ b/persistence/sql/persister_device.go @@ -187,12 +187,12 @@ func (p *Persister) UpdateDeviceCodeSessionBySignature(ctx context.Context, sign } stmt := fmt.Sprintf( - "UPDATE %s SET granted_scope=?, granted_audience=?, session_data=?, user_code_state=? WHERE device_code_signature=? AND nid = ?", + "UPDATE %s SET granted_scope=?, granted_audience=?, session_data=?, user_code_state=?, subject=?, challenge_id=? WHERE device_code_signature=? AND nid = ?", sqlTableDeviceAuthCodes, ) /* #nosec G201 table is static */ - err = p.Connection(ctx).RawQuery(stmt, req.GrantedScope, req.GrantedAudience, req.Session, req.UserCodeState, signature, p.NetworkID(ctx)).Exec() + err = p.Connection(ctx).RawQuery(stmt, req.GrantedScope, req.GrantedAudience, req.Session, req.UserCodeState, req.Subject, req.ConsentChallenge, signature, p.NetworkID(ctx)).Exec() if err != nil { return sqlcon.HandleError(err) } From b657f00280ceea27d2922122855b62b8f9a3382e Mon Sep 17 00:00:00 2001 From: Nikos Date: Mon, 27 Jan 2025 15:58:06 +0100 Subject: [PATCH 43/52] fix: make user_code entropy configurable --- .schema/config.schema.json | 6 +++++ driver/config/provider.go | 40 ++++++++++++++++++++++++++++++---- driver/config/provider_test.go | 3 +++ go.mod | 4 +--- go.sum | 4 ++-- internal/.hydra.yaml | 1 + spec/config.json | 6 +++++ 7 files changed, 55 insertions(+), 9 deletions(-) diff --git a/.schema/config.schema.json b/.schema/config.schema.json index ab3e9448837..f4f0c3ca782 100644 --- a/.schema/config.schema.json +++ b/.schema/config.schema.json @@ -1113,6 +1113,12 @@ "$ref": "#/definitions/duration" } ] + }, + "user_code_entropy": { + "type": "string", + "description": "Sets the entropy for the user codes.", + "default": "medium", + "enum": ["high", "medium", "low"] } }, "grant": { diff --git a/driver/config/provider.go b/driver/config/provider.go index 779084da5df..6d3a2c10f63 100644 --- a/driver/config/provider.go +++ b/driver/config/provider.go @@ -100,6 +100,7 @@ const ( KeySubjectIdentifierAlgorithmSalt = "oidc.subject_identifiers.pairwise.salt" KeyPublicAllowDynamicRegistration = "oidc.dynamic_client_registration.enabled" KeyDeviceAuthTokenPollingInterval = "oauth2.device_authorization.token_polling_interval" // #nosec G101 + KeyDeviceAuthUserCodeEntropy = "oauth2.device_authorization.user_code_entropy" KeyPKCEEnforced = "oauth2.pkce.enforced" KeyPKCEEnforcedForPublicClients = "oauth2.pkce.enforced_for_public_clients" KeyLogLevel = "log.level" @@ -120,6 +121,15 @@ const ( const DSNMemory = "memory" +var userCodeEtropy = map[string]struct { + Length int + Symbols []rune +}{ + "high": {Length: 8, Symbols: []rune(randx.AlphaNumNoAmbiguous)}, + "medium": {Length: 8, Symbols: []rune(randx.AlphaUpper)}, + "low": {Length: 9, Symbols: []rune(randx.Numeric)}, +} + var ( _ hasherx.PBKDF2Configurator = (*DefaultProvider)(nil) _ hasherx.BCryptConfigurator = (*DefaultProvider)(nil) @@ -415,13 +425,35 @@ func (p *DefaultProvider) GetDeviceAuthTokenPollingInterval(ctx context.Context) } // GetUserCodeLength returns configured user_code length -func (c *DefaultProvider) GetUserCodeLength(ctx context.Context) int { - return 8 +func (p *DefaultProvider) GetUserCodeLength(ctx context.Context) int { + k := p.getProvider(ctx).StringF(KeyDeviceAuthUserCodeEntropy, "medium") + profile, ok := userCodeEtropy[k] + if !ok { + keys := []string{} + for k := range userCodeEtropy { + keys = append(keys, k) + } + + p.l.WithError(errors.Errorf("Invalid user_code entropy: %s, allowed entropy values are: %s", k, keys)) + return 0 + } + return profile.Length } // GetDeviceAuthTokenPollingInterval returns configured user_code allowed symbols -func (c *DefaultProvider) GetUserCodeSymbols(ctx context.Context) []rune { - return []rune(randx.AlphaUpper) +func (p *DefaultProvider) GetUserCodeSymbols(ctx context.Context) []rune { + k := p.getProvider(ctx).StringF(KeyDeviceAuthUserCodeEntropy, "medium") + profile, ok := userCodeEtropy[k] + if !ok { + keys := []string{} + for k := range userCodeEtropy { + keys = append(keys, k) + } + + p.l.WithError(errors.Errorf("Invalid user_code entropy: %s, allowed entropy values are: %s", k, keys)) + return nil + } + return profile.Symbols } func (p *DefaultProvider) LoginURL(ctx context.Context) *url.URL { diff --git a/driver/config/provider_test.go b/driver/config/provider_test.go index a4385a03e7a..a1ced7a590a 100644 --- a/driver/config/provider_test.go +++ b/driver/config/provider_test.go @@ -17,6 +17,7 @@ import ( "github.com/ory/fosite/token/jwt" "github.com/ory/x/configx" "github.com/ory/x/otelx" + "github.com/ory/x/randx" "github.com/rs/cors" "github.com/stretchr/testify/assert" @@ -332,6 +333,8 @@ func TestViperProviderValidates(t *testing.T) { assert.Equal(t, true, c.GetEnforcePKCE(ctx)) assert.Equal(t, true, c.GetEnforcePKCEForPublicClients(ctx)) assert.Equal(t, 2*time.Hour, c.GetDeviceAuthTokenPollingInterval(ctx)) + assert.Equal(t, 8, c.GetUserCodeLength(ctx)) + assert.Equal(t, []rune(randx.AlphaUpper), c.GetUserCodeSymbols(ctx)) // secrets secret, err := c.GetGlobalSecret(ctx) diff --git a/go.mod b/go.mod index 5141495cf69..ac7c2364b51 100644 --- a/go.mod +++ b/go.mod @@ -33,7 +33,7 @@ require ( github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 github.com/oleiade/reflections v1.0.1 github.com/ory/analytics-go/v5 v5.0.1 - github.com/ory/fosite v0.49.0 + github.com/ory/fosite v0.49.1-0.20250203124447-75b904ddbee4 github.com/ory/go-acc v0.2.9-0.20230103102148-6b1c9a70dbbe github.com/ory/graceful v0.1.3 github.com/ory/herodot v0.10.3-0.20230626083119-d7e5192f0d88 @@ -248,5 +248,3 @@ require ( gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) - -replace github.com/ory/fosite => github.com/canonical/fosite v0.0.0-20250124155649-b77efc392574 diff --git a/go.sum b/go.sum index 83edb2dde7f..51e6007fb36 100644 --- a/go.sum +++ b/go.sum @@ -47,8 +47,6 @@ github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dR 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/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= -github.com/canonical/fosite v0.0.0-20250124155649-b77efc392574 h1:ZH9mv26aX0FTVXM6jenoVZfsBBxKxnjiq8c44sMMAUE= -github.com/canonical/fosite v0.0.0-20250124155649-b77efc392574/go.mod h1:rwKPYm0bLjxRQ+J7/CVAIwpnwRMaRrwRzdGqgbjwNK8= 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.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= @@ -380,6 +378,8 @@ github.com/ory/analytics-go/v5 v5.0.1 h1:LX8T5B9FN8KZXOtxgN+R3I4THRRVB6+28IKgKBp github.com/ory/analytics-go/v5 v5.0.1/go.mod h1:lWCiCjAaJkKfgR/BN5DCLMol8BjKS1x+4jxBxff/FF0= github.com/ory/dockertest/v3 v3.10.1-0.20240704115616-d229e74b748d h1:By96ZSVuH5LyjXLVVMfvJoLVGHaT96LdOnwgFSLVf0E= github.com/ory/dockertest/v3 v3.10.1-0.20240704115616-d229e74b748d/go.mod h1:F2FIjwwAk6CsNAs//B8+aPFQF0t84pbM8oliyNXwQrk= +github.com/ory/fosite v0.49.1-0.20250203124447-75b904ddbee4 h1:VnazT+N30kfg5TxJQ2bg4Fa1pCWT7A1i3FffmX0fhAA= +github.com/ory/fosite v0.49.1-0.20250203124447-75b904ddbee4/go.mod h1:IhAwHrxwNgB3smKB75jkMVQjFTHq9HveITItLGX8/GU= github.com/ory/go-acc v0.2.9-0.20230103102148-6b1c9a70dbbe h1:rvu4obdvqR0fkSIJ8IfgzKOWwZ5kOT2UNfLq81Qk7rc= github.com/ory/go-acc v0.2.9-0.20230103102148-6b1c9a70dbbe/go.mod h1:z4n3u6as84LbV4YmgjHhnwtccQqzf4cZlSk9f1FhygI= github.com/ory/go-convenience v0.1.0 h1:zouLKfF2GoSGnJwGq+PE/nJAE6dj2Zj5QlTgmMTsTS8= diff --git a/internal/.hydra.yaml b/internal/.hydra.yaml index 1d8390f4b0d..ba08b291840 100644 --- a/internal/.hydra.yaml +++ b/internal/.hydra.yaml @@ -124,6 +124,7 @@ oauth2: cost: 20 device_authorization: token_polling_interval: 2h + user_code_entropy: medium pkce: enforced: true enforced_for_public_clients: true diff --git a/spec/config.json b/spec/config.json index 916131165eb..b246998d2cb 100644 --- a/spec/config.json +++ b/spec/config.json @@ -1176,6 +1176,12 @@ "default": "5s", "description": "configure how often a non-interactive device should poll the device token endpoint", "examples": ["5s", "15s", "1m"] + }, + "user_code_entropy": { + "type": "string", + "description": "Sets the entropy for the user codes.", + "default": "medium", + "enum": ["high", "medium", "low"] } } }, From 1b37e0871225d0c6819dbeeb6e9e9943aa43e456 Mon Sep 17 00:00:00 2001 From: Nikos Date: Mon, 27 Jan 2025 16:00:21 +0100 Subject: [PATCH 44/52] fix: make device_challenge unique --- oauth2/oauth2_device_code_test.go | 67 +++++++++++++++++++ ...9000001000000_device_flow.cockroach.up.sql | 4 +- ...41609000001000000_device_flow.mysql.up.sql | 4 +- ...09000001000000_device_flow.postgres.up.sql | 4 +- .../20241609000001000000_device_flow.up.sql | 4 +- 5 files changed, 75 insertions(+), 8 deletions(-) diff --git a/oauth2/oauth2_device_code_test.go b/oauth2/oauth2_device_code_test.go index 1a03155cb25..fbb71f02c11 100644 --- a/oauth2/oauth2_device_code_test.go +++ b/oauth2/oauth2_device_code_test.go @@ -777,6 +777,73 @@ func TestDeviceCodeWithDefaultStrategy(t *testing.T) { require.NoError(t, err) require.Equal(t, loginFlowResp2.StatusCode, http.StatusBadRequest) }) + t.Run("case=cannot reuse device_challenge", func(t *testing.T) { + var deviceChallenge string + c, conf := newDeviceClient(t, reg) + testhelpers.NewDeviceLoginConsentUI(t, reg.Config(), + func(w http.ResponseWriter, r *http.Request) { + userCode := r.URL.Query().Get("user_code") + payload := hydra.AcceptDeviceUserCodeRequest{ + UserCode: &userCode, + } + + if deviceChallenge == "" { + deviceChallenge = r.URL.Query().Get("device_challenge") + } + v, _, err := adminClient.OAuth2API.AcceptUserCodeRequest(context.Background()). + DeviceChallenge(deviceChallenge). + AcceptDeviceUserCodeRequest(payload). + Execute() + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + require.NoError(t, err) + require.NotEmpty(t, v.RedirectTo) + http.Redirect(w, r, v.RedirectTo, http.StatusFound) + }, + acceptLoginHandler(t, c, subject, conf.Scopes, nil), + acceptConsentHandler(t, c, subject, conf.Scopes, nil), + ) + + resp, err := getDeviceCode(t, conf, nil) + require.NoError(t, err) + require.NotEmpty(t, resp.DeviceCode) + require.NotEmpty(t, resp.UserCode) + + hc := testhelpers.NewEmptyJarClient(t) + loginFlowResp := acceptUserCode(t, conf, hc, resp) + require.NoError(t, err) + require.Contains(t, reg.Config().DeviceDoneURL(ctx).String(), loginFlowResp.Request.URL.Path, "did not end up in post device URL") + require.Equal(t, loginFlowResp.Request.URL.Query().Get("client_id"), conf.ClientID) + + require.NotNil(t, loginFlowResp) + token, err := conf.DeviceAccessToken(context.Background(), resp) + iat := time.Now() + require.NoError(t, err) + + introspectAccessToken(t, conf, token, subject) + assertIDToken(t, token, conf, subject, nonce, iat.Add(reg.Config().GetIDTokenLifespan(ctx))) + assertRefreshToken(t, token, conf, iat.Add(reg.Config().GetRefreshTokenLifespan(ctx))) + + resp2, err := getDeviceCode(t, conf, nil) + require.NoError(t, err) + require.NotEmpty(t, resp2.DeviceCode) + require.NotEmpty(t, resp2.UserCode) + + payload := hydra.AcceptDeviceUserCodeRequest{ + UserCode: &resp2.UserCode, + } + + acceptResp, _, err := adminClient.OAuth2API.AcceptUserCodeRequest(context.Background()). + DeviceChallenge(deviceChallenge). + AcceptDeviceUserCodeRequest(payload). + Execute() + + loginFlowResp2, err := hc.Get(acceptResp.RedirectTo) + require.NoError(t, err) + require.Equal(t, http.StatusForbidden, loginFlowResp2.StatusCode) + }) } func newDeviceClient( diff --git a/persistence/sql/migrations/20241609000001000000_device_flow.cockroach.up.sql b/persistence/sql/migrations/20241609000001000000_device_flow.cockroach.up.sql index 2739ecc7d68..447f68372cf 100644 --- a/persistence/sql/migrations/20241609000001000000_device_flow.cockroach.up.sql +++ b/persistence/sql/migrations/20241609000001000000_device_flow.cockroach.up.sql @@ -29,7 +29,7 @@ CREATE TABLE IF NOT EXISTS hydra_oauth2_device_auth_codes CREATE INDEX hydra_oauth2_device_auth_codes_request_id_idx ON hydra_oauth2_device_auth_codes (request_id, nid); CREATE INDEX hydra_oauth2_device_auth_codes_client_id_idx ON hydra_oauth2_device_auth_codes (client_id, nid); CREATE INDEX hydra_oauth2_device_auth_codes_challenge_id_idx ON hydra_oauth2_device_auth_codes (challenge_id); -CREATE UNIQUE INDEX hydra_oauth2_device_auth_codes_user_code_signature_idx ON hydra_oauth2_device_auth_codes (user_code_signature, nid); +CREATE UNIQUE INDEX hydra_oauth2_device_auth_codes_user_code_signature_idx ON hydra_oauth2_device_auth_codes (nid, user_code_signature); ALTER TABLE hydra_oauth2_flow ADD COLUMN device_challenge_id VARCHAR(255) NULL; ALTER TABLE hydra_oauth2_flow ADD COLUMN device_code_request_id VARCHAR(255) NULL; @@ -40,7 +40,7 @@ ALTER TABLE hydra_oauth2_flow ADD COLUMN device_was_used BOOL NULL; ALTER TABLE hydra_oauth2_flow ADD COLUMN device_handled_at TIMESTAMP NULL; ALTER TABLE hydra_oauth2_flow ADD COLUMN device_error TEXT NULL; -CREATE INDEX hydra_oauth2_flow_device_challenge_idx ON hydra_oauth2_flow (device_challenge_id); +CREATE UNIQUE INDEX hydra_oauth2_flow_device_challenge_idx ON hydra_oauth2_flow (device_challenge_id); ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_id_token_lifespan BIGINT NULL DEFAULT NULL; ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_access_token_lifespan BIGINT NULL DEFAULT NULL; diff --git a/persistence/sql/migrations/20241609000001000000_device_flow.mysql.up.sql b/persistence/sql/migrations/20241609000001000000_device_flow.mysql.up.sql index 90f0aaaede6..81267a62435 100644 --- a/persistence/sql/migrations/20241609000001000000_device_flow.mysql.up.sql +++ b/persistence/sql/migrations/20241609000001000000_device_flow.mysql.up.sql @@ -29,7 +29,7 @@ CREATE TABLE IF NOT EXISTS hydra_oauth2_device_auth_codes CREATE INDEX hydra_oauth2_device_auth_codes_request_id_idx ON hydra_oauth2_device_auth_codes (request_id, nid); CREATE INDEX hydra_oauth2_device_auth_codes_client_id_idx ON hydra_oauth2_device_auth_codes (client_id, nid); CREATE INDEX hydra_oauth2_device_auth_codes_challenge_id_idx ON hydra_oauth2_device_auth_codes (challenge_id); -CREATE UNIQUE INDEX hydra_oauth2_device_auth_codes_user_code_signature_idx ON hydra_oauth2_device_auth_codes (user_code_signature, nid); +CREATE UNIQUE INDEX hydra_oauth2_device_auth_codes_user_code_signature_idx ON hydra_oauth2_device_auth_codes (nid, user_code_signature); ALTER TABLE hydra_oauth2_flow ADD COLUMN device_challenge_id VARCHAR(255) NULL; ALTER TABLE hydra_oauth2_flow ADD COLUMN device_code_request_id VARCHAR(255) NULL; @@ -40,7 +40,7 @@ ALTER TABLE hydra_oauth2_flow ADD COLUMN device_was_used BOOL NULL; ALTER TABLE hydra_oauth2_flow ADD COLUMN device_handled_at TIMESTAMP NULL; ALTER TABLE hydra_oauth2_flow ADD COLUMN device_error TEXT NULL; -CREATE INDEX hydra_oauth2_flow_device_challenge_idx ON hydra_oauth2_flow (device_challenge_id); +CREATE UNIQUE INDEX hydra_oauth2_flow_device_challenge_idx ON hydra_oauth2_flow (device_challenge_id); ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_id_token_lifespan BIGINT NULL DEFAULT NULL; ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_access_token_lifespan BIGINT NULL DEFAULT NULL; diff --git a/persistence/sql/migrations/20241609000001000000_device_flow.postgres.up.sql b/persistence/sql/migrations/20241609000001000000_device_flow.postgres.up.sql index 1631bd3ca26..ea274805ac5 100644 --- a/persistence/sql/migrations/20241609000001000000_device_flow.postgres.up.sql +++ b/persistence/sql/migrations/20241609000001000000_device_flow.postgres.up.sql @@ -29,7 +29,7 @@ CREATE TABLE IF NOT EXISTS hydra_oauth2_device_auth_codes CREATE INDEX hydra_oauth2_device_auth_codes_request_id_idx ON hydra_oauth2_device_auth_codes (request_id, nid); CREATE INDEX hydra_oauth2_device_auth_codes_client_id_idx ON hydra_oauth2_device_auth_codes (client_id, nid); CREATE INDEX hydra_oauth2_device_auth_codes_challenge_id_idx ON hydra_oauth2_device_auth_codes (challenge_id); -CREATE UNIQUE INDEX hydra_oauth2_device_auth_codes_user_code_signature_idx ON hydra_oauth2_device_auth_codes (user_code_signature, nid); +CREATE UNIQUE INDEX hydra_oauth2_device_auth_codes_user_code_signature_idx ON hydra_oauth2_device_auth_codes (nid, user_code_signature); ALTER TABLE hydra_oauth2_flow ADD COLUMN device_challenge_id VARCHAR(255) NULL; ALTER TABLE hydra_oauth2_flow ADD COLUMN device_code_request_id VARCHAR(255) NULL; @@ -40,7 +40,7 @@ ALTER TABLE hydra_oauth2_flow ADD COLUMN device_was_used BOOLEAN NULL; ALTER TABLE hydra_oauth2_flow ADD COLUMN device_handled_at TIMESTAMP NULL; ALTER TABLE hydra_oauth2_flow ADD COLUMN device_error TEXT NULL; -CREATE INDEX hydra_oauth2_flow_device_challenge_idx ON hydra_oauth2_flow (device_challenge_id); +CREATE UNIQUE INDEX hydra_oauth2_flow_device_challenge_idx ON hydra_oauth2_flow (device_challenge_id); ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_id_token_lifespan BIGINT NULL DEFAULT NULL; ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_access_token_lifespan BIGINT NULL DEFAULT NULL; diff --git a/persistence/sql/migrations/20241609000001000000_device_flow.up.sql b/persistence/sql/migrations/20241609000001000000_device_flow.up.sql index 47b8f54061e..fa9cc10a524 100644 --- a/persistence/sql/migrations/20241609000001000000_device_flow.up.sql +++ b/persistence/sql/migrations/20241609000001000000_device_flow.up.sql @@ -24,7 +24,7 @@ CREATE TABLE IF NOT EXISTS hydra_oauth2_device_auth_codes CREATE INDEX hydra_oauth2_device_auth_codes_request_id_idx ON hydra_oauth2_device_auth_codes (request_id, nid); CREATE INDEX hydra_oauth2_device_auth_codes_client_id_idx ON hydra_oauth2_device_auth_codes (client_id, nid); CREATE INDEX hydra_oauth2_device_auth_codes_challenge_id_idx ON hydra_oauth2_device_auth_codes (challenge_id); -CREATE UNIQUE INDEX hydra_oauth2_device_auth_codes_user_code_signature_idx ON hydra_oauth2_device_auth_codes (user_code_signature, nid); +CREATE UNIQUE INDEX hydra_oauth2_device_auth_codes_user_code_signature_idx ON hydra_oauth2_device_auth_codes (nid, user_code_signature); ALTER TABLE hydra_oauth2_flow ADD COLUMN device_challenge_id VARCHAR(255) NULL; ALTER TABLE hydra_oauth2_flow ADD COLUMN device_code_request_id VARCHAR(255) NULL; @@ -35,7 +35,7 @@ ALTER TABLE hydra_oauth2_flow ADD COLUMN device_was_used BOOLEAN NULL; ALTER TABLE hydra_oauth2_flow ADD COLUMN device_handled_at TIMESTAMP NULL; ALTER TABLE hydra_oauth2_flow ADD COLUMN device_error TEXT NULL; -CREATE INDEX hydra_oauth2_flow_device_challenge_idx ON hydra_oauth2_flow (device_challenge_id); +CREATE UNIQUE INDEX hydra_oauth2_flow_device_challenge_idx ON hydra_oauth2_flow (device_challenge_id); ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_id_token_lifespan BIGINT NULL DEFAULT NULL; ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_access_token_lifespan BIGINT NULL DEFAULT NULL; From 17b682704327010885f87d244645072b877e9ada Mon Sep 17 00:00:00 2001 From: Nikos Date: Wed, 29 Jan 2025 11:08:26 +0100 Subject: [PATCH 45/52] chore: move array to variable --- oauth2/handler.go | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/oauth2/handler.go b/oauth2/handler.go index 19a28da878d..76a07ed1573 100644 --- a/oauth2/handler.go +++ b/oauth2/handler.go @@ -70,6 +70,15 @@ const ( DeviceVerificationPath = "/oauth2/device/verify" ) +// Taken from https://github.com/ory/fosite/blob/049ed1924cd0b41f12357b0fe617530c264421ac/handler/openid/flow_explicit_auth.go#L29 +var oidcParameters = []string{"grant_type", + "max_age", + "prompt", + "acr_values", + "id_token_hint", + "nonce", +} + type Handler struct { r InternalRegistry c *config.DefaultProvider @@ -784,13 +793,7 @@ func (h *Handler) performOAuth2DeviceVerificationFlow(w http.ResponseWriter, r * // Update the OpenID Connect session if "openid" scope is granted if req.GetGrantedScopes().Has("openid") { - err = h.r.OAuth2Storage().CreateOpenIDConnectSession(ctx, sig, req.Sanitize([]string{"grant_type", - "max_age", - "prompt", - "acr_values", - "id_token_hint", - "nonce", - })) + err = h.r.OAuth2Storage().CreateOpenIDConnectSession(ctx, sig, req.Sanitize(oidcParameters)) if err != nil { x.LogError(r, err, h.r.Logger()) h.r.Writer().WriteError(w, r, err) From fa281f7c4c684509704f5c5c99c5e2a740b986c2 Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Sun, 9 Feb 2025 13:29:13 -0300 Subject: [PATCH 46/52] fix: device flow cli --- cmd/cmd_perform_device_flow.go | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/cmd/cmd_perform_device_flow.go b/cmd/cmd_perform_device_flow.go index 74e9a33a4b3..a880dcdc98d 100644 --- a/cmd/cmd_perform_device_flow.go +++ b/cmd/cmd_perform_device_flow.go @@ -25,7 +25,7 @@ func NewPerformDeviceCodeCmd() *cobra.Command { Example: "{{ .CommandPath }} --client-id ... --client-secret ...", Short: "An exemplary OAuth 2.0 Client performing the OAuth 2.0 Device Code Flow", Long: `Performs the device code flow. Useful for getting an access token and an ID token in machines without a browser. - The client that will be used MUST support the "client_secret_post" token-endpoint-auth-method +The client that will be used MUST support the "client_secret_post" token-endpoint-auth-method. `, RunE: func(cmd *cobra.Command, args []string) error { client, endpoint, err := cliclient.NewClient(cmd) @@ -68,16 +68,22 @@ func NewPerformDeviceCodeCmd() *cobra.Command { Scopes: scopes, } + params := []oauth2.AuthCodeOption{oauth2.SetAuthURLParam("audience", strings.Join(audience, "+"))} + if clientSecret != "" { + params = append(params, oauth2.SetAuthURLParam("client_secret", clientSecret)) + } + deviceAuthResponse, err := conf.DeviceAuth( ctx, - oauth2.SetAuthURLParam("audience", strings.Join(audience, "+")), - oauth2.SetAuthURLParam("client_secret", clientSecret), + params..., ) if err != nil { - cmdx.Fatalf("Failed to perform the device authorization request", err.Error()) + _, _ = fmt.Fprintf( + cmd.ErrOrStderr(), "Failed to perform the device authorization request: %s", err) + return cmdx.FailSilently(cmd) } - fmt.Fprintln( + _, _ = fmt.Fprintln( cmd.OutOrStdout(), "To login please go to:\n\t", deviceAuthResponse.VerificationURIComplete, @@ -85,7 +91,9 @@ func NewPerformDeviceCodeCmd() *cobra.Command { token, err := conf.DeviceAccessToken(ctx, deviceAuthResponse) if err != nil { - cmdx.Fatalf("Failed to perform the device token request: %e", err.Error()) + _, _ = fmt.Fprintf( + cmd.ErrOrStderr(), "Failed to perform the device token request: %s", err) + return cmdx.FailSilently(cmd) } fmt.Println("Successfully signed in!") From 31372ebc3c65f66569f3617a547f3c7cc0dfa2c5 Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Sun, 9 Feb 2025 13:31:04 -0300 Subject: [PATCH 47/52] fix: update device urls --- contrib/quickstart/5-min/hydra.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contrib/quickstart/5-min/hydra.yml b/contrib/quickstart/5-min/hydra.yml index e5cb3c8033c..0a110cdb325 100644 --- a/contrib/quickstart/5-min/hydra.yml +++ b/contrib/quickstart/5-min/hydra.yml @@ -8,8 +8,8 @@ urls: consent: http://127.0.0.1:3000/consent login: http://127.0.0.1:3000/login logout: http://127.0.0.1:3000/logout - device_verification: http://127.0.0.1:3000/device_code - device_verification_success: http://127.0.0.1:3000/device_complete + device_verification: http://127.0.0.1:3000/device/code + device_verification_success: http://127.0.0.1:3000/device/complete secrets: system: From fff438e132a8daef88882f6caf60e43cb04f9ca2 Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Mon, 10 Feb 2025 13:04:40 -0300 Subject: [PATCH 48/52] chore: code review --- .schema/config.schema.json | 107 +++--- cmd/cmd_perform_device_flow.go | 17 +- consent/handler.go | 1 - consent/strategy_default.go | 25 +- contrib/quickstart/5-min/hydra.yml | 5 +- driver/config/provider.go | 4 +- internal/.hydra.yaml | 5 +- internal/httpclient/go.sum | 347 ------------------ oauth2/handler.go | 59 +-- oauth2/registry.go | 1 + ...9000001000000_device_flow.cockroach.up.sql | 47 --- .../20241609000001000000_device_flow.down.sql | 5 +- ...41609000001000000_device_flow.mysql.up.sql | 77 ++-- ...09000001000000_device_flow.postgres.up.sql | 47 --- ...1609000001000000_device_flow.sqlite.up.sql | 56 +++ .../20241609000001000000_device_flow.up.sql | 74 ++-- persistence/sql/persister_consent.go | 6 +- persistence/sql/persister_device.go | 54 ++- persistence/sql/persister_nid_test.go | 2 +- spec/config.json | 39 +- 20 files changed, 325 insertions(+), 653 deletions(-) delete mode 100644 persistence/sql/migrations/20241609000001000000_device_flow.cockroach.up.sql delete mode 100644 persistence/sql/migrations/20241609000001000000_device_flow.postgres.up.sql create mode 100644 persistence/sql/migrations/20241609000001000000_device_flow.sqlite.up.sql diff --git a/.schema/config.schema.json b/.schema/config.schema.json index f4f0c3ca782..d4aca510dab 100644 --- a/.schema/config.schema.json +++ b/.schema/config.schema.json @@ -619,6 +619,14 @@ "https://my-service.com/oauth2/auth" ] }, + "device_authorization_url": { + "type": "string", + "description": "Overwrites the OAuth2 Device Auth URL", + "format": "uri-reference", + "examples": [ + "https://my-service.com/oauth2/device/auth" + ] + }, "client_registration_url": { "description": "Sets the OpenID Connect Dynamic Client Registration Endpoint", "type": "string", @@ -645,19 +653,11 @@ }, "userinfo_url": { "type": "string", - "description": "A URL of the userinfo endpoint to be advertised at the OpenID Connect Discovery endpoint `/.well-known/openid-configuration`. Defaults to Ory Hydra's userinfo endpoint at `/userinfo`. Set this value if you want to handle this endpoint yourself.", + "description": "A URL of the userinfo endpoint to be advertised at the OpenID Connect Discovery endpoint /.well-known/openid-configuration. Defaults to Ory Hydra's userinfo endpoint at /userinfo. Set this value if you want to handle this endpoint yourself.", "format": "uri-reference", "examples": [ "https://example.org/my-custom-userinfo-endpoint" ] - }, - "device_authorization_url": { - "type": "string", - "description": "A URL of the device authorization endpoint to be advertised at the OpenID Connect Discovery endpoint `/.well-known/openid-configuration`. Defaults to Ory Hydra's device authorization endpoint at `/oauth2/device/auth`. Set this value if you want to handle this endpoint yourself.", - "format": "uri-reference", - "examples": [ - "https://example.org/oauth2/device/auth" - ] } } } @@ -816,23 +816,29 @@ "/ui/logout" ] }, - "device_verification": { - "type": "string", - "description": "Sets the device user code verification endpoint. Defaults to an internal fallback URL showing an error.", - "format": "uri-reference", - "examples": [ - "https://my-logout.app/device_verification", - "/ui/device_verification" - ] - }, - "device_verification_success": { - "type": "string", - "description": "Sets the post device authentication endpoint. Defaults to an internal fallback URL showing an error.", - "format": "uri-reference", - "examples": [ - "https://my-logout.app/device_done", - "/ui/device_done" - ] + "device": { + "type": "object", + "description": "Configure URLs for the OAuth 2.0 Device Code Flow.", + "properties": { + "verification": { + "type": "string", + "description": "Sets the device user code verification endpoint. Defaults to an internal fallback URL showing an error.", + "format": "uri-reference", + "examples": [ + "https://my-logout.app/device_verification", + "/ui/device_verification" + ] + }, + "success": { + "type": "string", + "description": "Sets the post device authentication endpoint. Defaults to an internal fallback URL showing an error.", + "format": "uri-reference", + "examples": [ + "https://my-logout.app/device_done", + "/ui/device_done" + ] + } + } }, "error": { "type": "string", @@ -980,8 +986,8 @@ ] }, "device_user_code": { - "description": "Configures how long device and user codes are valid.", - "default": "15m", + "description": "Configures how long device & user codes are valid.", + "default": "10m", "allOf": [ { "$ref": "#/definitions/duration" @@ -1104,23 +1110,6 @@ } } }, - "device_authorization": { - "token_polling_interval": { - "description": "Sets the starting token polling interval.", - "default": "5s", - "allOf": [ - { - "$ref": "#/definitions/duration" - } - ] - }, - "user_code_entropy": { - "type": "string", - "description": "Sets the entropy for the user codes.", - "default": "medium", - "enum": ["high", "medium", "low"] - } - }, "grant": { "type": "object", "additionalProperties": false, @@ -1181,6 +1170,28 @@ } ] }, + "device_authorization": { + "type": "object", + "additionalProperties": false, + "properties": { + "token_polling_interval": { + "allOf": [ + { + "$ref": "#/definitions/duration" + } + ], + "default": "5s", + "description": "configure how often a non-interactive device should poll the device token endpoint", + "examples": ["5s", "15s", "1m"] + }, + "user_code_entropy": { + "type": "string", + "description": "Sets the entropy for the user codes.", + "default": "medium", + "enum": ["high", "medium", "low"] + } + } + }, "token_hook": { "description": "Sets the token hook endpoint for all grant types. If set it will be called while providing token to customize claims.", "examples": ["https://my-example.app/token-hook"], @@ -1194,8 +1205,8 @@ } ] } - } - }, + } + }, "secrets": { "type": "object", "additionalProperties": false, @@ -1240,7 +1251,7 @@ "examples": ["cpu"] }, "tracing": { - "$ref": "https://raw.githubusercontent.com/ory/x/v0.0.675/otelx/config.schema.json" + "$ref": "ory://tracing-config" }, "sqa": { "type": "object", diff --git a/cmd/cmd_perform_device_flow.go b/cmd/cmd_perform_device_flow.go index a880dcdc98d..f697722e65f 100644 --- a/cmd/cmd_perform_device_flow.go +++ b/cmd/cmd_perform_device_flow.go @@ -22,11 +22,10 @@ import ( func NewPerformDeviceCodeCmd() *cobra.Command { cmd := &cobra.Command{ Use: "device-code", - Example: "{{ .CommandPath }} --client-id ... --client-secret ...", + Example: "{{ .CommandPath }} --client-id ...", Short: "An exemplary OAuth 2.0 Client performing the OAuth 2.0 Device Code Flow", Long: `Performs the device code flow. Useful for getting an access token and an ID token in machines without a browser. -The client that will be used MUST support the "client_secret_post" token-endpoint-auth-method. - `, +The client that will be used MUST use the "none" or "client_secret_post" token-endpoint-auth-method.`, RunE: func(cmd *cobra.Command, args []string) error { client, endpoint, err := cliclient.NewClient(cmd) if err != nil { @@ -43,8 +42,8 @@ The client that will be used MUST support the "client_secret_post" token-endpoin clientID := flagx.MustGetString(cmd, "client-id") if clientID == "" { - _, _ = fmt.Fprint(cmd.OutOrStdout(), cmd.UsageString()) - _, _ = fmt.Fprintln(cmd.OutOrStdout(), "Please provide a Client ID using --client-id flag, or OAUTH2_CLIENT_ID environment variable.") + _, _ = fmt.Fprintln(cmd.ErrOrStderr(), cmd.UsageString()) + _, _ = fmt.Fprintln(cmd.ErrOrStderr(), "Please provide a Client ID using --client-id flag, or OAUTH2_CLIENT_ID environment variable.") return cmdx.FailSilently(cmd) } @@ -79,12 +78,12 @@ The client that will be used MUST support the "client_secret_post" token-endpoin ) if err != nil { _, _ = fmt.Fprintf( - cmd.ErrOrStderr(), "Failed to perform the device authorization request: %s", err) + cmd.ErrOrStderr(), "Failed to perform the device authorization request: %s\n", err) return cmdx.FailSilently(cmd) } _, _ = fmt.Fprintln( - cmd.OutOrStdout(), + cmd.ErrOrStderr(), "To login please go to:\n\t", deviceAuthResponse.VerificationURIComplete, ) @@ -92,11 +91,11 @@ The client that will be used MUST support the "client_secret_post" token-endpoin token, err := conf.DeviceAccessToken(ctx, deviceAuthResponse) if err != nil { _, _ = fmt.Fprintf( - cmd.ErrOrStderr(), "Failed to perform the device token request: %s", err) + cmd.ErrOrStderr(), "Failed to perform the device token request: %s\n", err) return cmdx.FailSilently(cmd) } - fmt.Println("Successfully signed in!") + _, _ = fmt.Fprintln(cmd.ErrOrStderr(), "Successfully signed in!") cmdx.PrintRow(cmd, outputOAuth2Token(*token)) return nil diff --git a/consent/handler.go b/consent/handler.go index 54715f8e83b..8178cf0cc6d 100644 --- a/consent/handler.go +++ b/consent/handler.go @@ -1174,7 +1174,6 @@ func (h *Handler) acceptUserCodeRequest(w http.ResponseWriter, r *http.Request, } f.RequestURL = reqURL.String() - hr, err := h.r.ConsentManager().HandleDeviceUserAuthRequest(ctx, f, challenge, &p) if err != nil { h.r.Writer().WriteError(w, r, err) diff --git a/consent/strategy_default.go b/consent/strategy_default.go index 781974b0334..88c837c6da1 100644 --- a/consent/strategy_default.go +++ b/consent/strategy_default.go @@ -1193,20 +1193,33 @@ func (s *DefaultStrategy) HandleOAuth2DeviceAuthorizationRequest( ctx, span := trace.SpanFromContext(ctx).TracerProvider().Tracer("").Start(ctx, "DefaultStrategy.HandleOAuth2DeviceAuthorizationRequest") defer otelx.End(span, &err) + // This handler has the following validation states: + // + // 1. The flow is initiated (no verifiers) -> we request a device verifier (can only be achieved by solving the device challenge) + // 2. Device verifier is given -> we request login verifier (can only be achieved by solving the login challenge) + // 3. Login verifier is given -> we request consent verifier (can only be achieved by solving the consent challenge) + // 4. Consent verifier is given -> done. + deviceVerifier := strings.TrimSpace(r.URL.Query().Get("device_verifier")) loginVerifier := strings.TrimSpace(r.URL.Query().Get("login_verifier")) consentVerifier := strings.TrimSpace(r.URL.Query().Get("consent_verifier")) + ar := fosite.NewAuthorizeRequest() + var deviceFlow *flow.Flow if deviceVerifier == "" && loginVerifier == "" && consentVerifier == "" { - // ok, we need to process this request and redirect to device auth endpoint + // No verifiers are set, let's start by requesting the device verifier first. return nil, nil, s.requestDevice(ctx, w, r) } else if deviceVerifier != "" && loginVerifier == "" && consentVerifier == "" { + // Device verifier is set, but login and consent are not. So we need to verify the device. var err error deviceFlow, err = s.verifyDevice(ctx, w, r, deviceVerifier) if err != nil { return nil, nil, err } + + ar.RequestedScope = fosite.Arguments(deviceFlow.RequestedScope) + ar.RequestedAudience = fosite.Arguments(deviceFlow.RequestedAudience) } // Validate client_id @@ -1222,18 +1235,15 @@ func (s *DefaultStrategy) HandleOAuth2DeviceAuthorizationRequest( } // Fake an authorization request to instantiate the flow. - ar := fosite.NewAuthorizeRequest() ar.Client = c ar.Form = r.Form - if deviceFlow != nil { - ar.RequestedScope = fosite.Arguments(deviceFlow.RequestedScope) - ar.RequestedAudience = fosite.Arguments(deviceFlow.RequestedAudience) - } if loginVerifier == "" && consentVerifier == "" { - // ok, we need to process this request and redirect to the authentication endpoint + // Here we end up if the device has been verified, but login and verification are still missing. + // Let's request authentication. return nil, nil, s.requestAuthentication(ctx, w, r, ar, deviceFlow) } else if loginVerifier != "" { + // Login verification was given, let's verify! f, err := s.verifyAuthentication(ctx, w, r, ar, loginVerifier) if err != nil { return nil, nil, err @@ -1243,6 +1253,7 @@ func (s *DefaultStrategy) HandleOAuth2DeviceAuthorizationRequest( return nil, f, s.requestConsent(ctx, w, r, ar, f) } + // Here we end up when consent verifier is set, so we verify the consent. var consentSession *flow.AcceptOAuth2ConsentRequest var f *flow.Flow diff --git a/contrib/quickstart/5-min/hydra.yml b/contrib/quickstart/5-min/hydra.yml index 0a110cdb325..30c7862bc77 100644 --- a/contrib/quickstart/5-min/hydra.yml +++ b/contrib/quickstart/5-min/hydra.yml @@ -8,8 +8,9 @@ urls: consent: http://127.0.0.1:3000/consent login: http://127.0.0.1:3000/login logout: http://127.0.0.1:3000/logout - device_verification: http://127.0.0.1:3000/device/code - device_verification_success: http://127.0.0.1:3000/device/complete + device: + verification: http://127.0.0.1:3000/device/verify + success: http://127.0.0.1:3000/device/success secrets: system: diff --git a/driver/config/provider.go b/driver/config/provider.go index 6d3a2c10f63..0bb47620b2a 100644 --- a/driver/config/provider.go +++ b/driver/config/provider.go @@ -86,8 +86,8 @@ const ( KeyLogoutURL = "urls.logout" KeyConsentURL = "urls.consent" KeyErrorURL = "urls.error" - KeyDeviceVerificationURL = "urls.device_verification" - KeyDeviceDoneURL = "urls.device_verification_success" + KeyDeviceVerificationURL = "urls.device.verification" + KeyDeviceDoneURL = "urls.device.success" KeyPublicURL = "urls.self.public" KeyAdminURL = "urls.self.admin" KeyIssuerURL = "urls.self.issuer" diff --git a/internal/.hydra.yaml b/internal/.hydra.yaml index ba08b291840..9515aa64296 100644 --- a/internal/.hydra.yaml +++ b/internal/.hydra.yaml @@ -101,8 +101,9 @@ urls: consent: https://consent logout: https://logout error: https://error - device_verification: https://device - device_verification_success: https://device/callback + device: + verification: https://device + success: https://device/callback post_logout_redirect: https://post_logout strategies: diff --git a/internal/httpclient/go.sum b/internal/httpclient/go.sum index 3dee6d68163..734252e6815 100644 --- a/internal/httpclient/go.sum +++ b/internal/httpclient/go.sum @@ -1,360 +1,13 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/oauth2/handler.go b/oauth2/handler.go index 76a07ed1573..7bfd3a4802a 100644 --- a/oauth2/handler.go +++ b/oauth2/handler.go @@ -8,6 +8,7 @@ import ( "encoding/base64" "encoding/json" "fmt" + "github.com/ory/x/otelx" "html/template" "net/http" "net/url" @@ -736,7 +737,13 @@ func (h *Handler) getOidcUserInfo(w http.ResponseWriter, r *http.Request) { // 302: emptyResponse // default: errorOAuth2 func (h *Handler) performOAuth2DeviceVerificationFlow(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - ctx := r.Context() + var ( + ctx = r.Context() + err error + ) + + ctx, span := h.r.Tracer(ctx).Tracer().Start(ctx, "oauth2.handler.performOAuth2DeviceVerificationFlow") + defer otelx.End(span, &err) // When this endpoint is called with a valid consent_verifier (meaning that the login flow completed successfully) // there are 3 writes happening to the database: @@ -746,20 +753,15 @@ func (h *Handler) performOAuth2DeviceVerificationFlow(w http.ResponseWriter, r * // If there were multiple flows created for the same user_code then we may end up with multiple flow objects // persisted to the database, while only one of them was actually used to validate the user_code // (see https://github.com/ory/hydra/pull/3851#discussion_r1843678761) - // TODO: We should wrap these queries in a transaction consentSession, f, err := h.r.ConsentStrategy().HandleOAuth2DeviceAuthorizationRequest(ctx, w, r) if errors.Is(err, consent.ErrAbortOAuth2Request) { x.LogAudit(r, nil, h.r.AuditLogger()) return - } - - if e := &(fosite.RFC6749Error{}); errors.As(err, &e) { + } else if e := &(fosite.RFC6749Error{}); errors.As(err, &e) { x.LogAudit(r, err, h.r.AuditLogger()) h.r.Writer().WriteError(w, r, err) return - } - - if err != nil { + } else if err != nil { x.LogError(r, err, h.r.Logger()) h.r.Writer().WriteError(w, r, err) return @@ -771,36 +773,41 @@ func (h *Handler) performOAuth2DeviceVerificationFlow(w http.ResponseWriter, r * h.r.Writer().WriteError(w, r, err) return } + req.SetUserCodeState(fosite.UserCodeAccepted) session, err := h.updateSessionWithRequest(ctx, consentSession, f, r, req, req.GetSession().(*Session)) if err != nil { + x.LogError(r, err, h.r.Logger()) h.r.Writer().WriteError(w, r, err) return } + req.SetSession(session) - // Update the device code session with - // - the claims for which the user gave consent - // - the granted scopes - // - the granted audiences - // - the user_code_state set to accepted - // This marks it as ready to be used for the token exchange endpoint. - err = h.r.OAuth2Storage().UpdateDeviceCodeSessionBySignature(ctx, sig, req) - if err != nil { + if err := h.r.Persister().Transaction(ctx, func(ctx context.Context, _ *pop.Connection) error { + // Update the device code session with + // - the claims for which the user gave consent + // - the granted scopes + // - the granted audiences + // - the user_code_state set to `accepted` + // This marks it as ready to be used for the token exchange endpoint. + if err = h.r.OAuth2Storage().UpdateDeviceCodeSessionBySignature(ctx, sig, req); err != nil { + return err + } + + // Update the OpenID Connect session if "openid" scope is granted + if req.GetGrantedScopes().Has("openid") { + if err := h.r.OAuth2Storage().CreateOpenIDConnectSession(ctx, sig, req.Sanitize(oidcParameters)); err != nil { + return err + } + } + + return nil + }); err != nil { x.LogError(r, err, h.r.Logger()) h.r.Writer().WriteError(w, r, err) return } - // Update the OpenID Connect session if "openid" scope is granted - if req.GetGrantedScopes().Has("openid") { - err = h.r.OAuth2Storage().CreateOpenIDConnectSession(ctx, sig, req.Sanitize(oidcParameters)) - if err != nil { - x.LogError(r, err, h.r.Logger()) - h.r.Writer().WriteError(w, r, err) - return - } - } - redirectURL := urlx.SetQuery(h.c.DeviceDoneURL(ctx), url.Values{"client_id": {f.Client.GetID()}}).String() http.Redirect(w, r, redirectURL, http.StatusFound) } diff --git a/oauth2/registry.go b/oauth2/registry.go index ffb7b642541..ec0fb806354 100644 --- a/oauth2/registry.go +++ b/oauth2/registry.go @@ -26,6 +26,7 @@ type InternalRegistry interface { persistence.Provider Registry FlowCipher() *aead.XChaCha20Poly1305 + x.TracingProvider } type Registry interface { diff --git a/persistence/sql/migrations/20241609000001000000_device_flow.cockroach.up.sql b/persistence/sql/migrations/20241609000001000000_device_flow.cockroach.up.sql deleted file mode 100644 index 447f68372cf..00000000000 --- a/persistence/sql/migrations/20241609000001000000_device_flow.cockroach.up.sql +++ /dev/null @@ -1,47 +0,0 @@ --- Migration generated by the command below; DO NOT EDIT. --- hydra:generate hydra migrate gen -CREATE TABLE IF NOT EXISTS hydra_oauth2_device_auth_codes -( - device_code_signature VARCHAR(255) NOT NULL, - user_code_signature VARCHAR(255) NOT NULL, - request_id VARCHAR(40) NOT NULL, - requested_at TIMESTAMP NOT NULL DEFAULT NOW(), - client_id VARCHAR(255) NOT NULL, - scope TEXT NOT NULL, - granted_scope TEXT NOT NULL, - form_data TEXT NOT NULL, - session_data TEXT NOT NULL, - subject VARCHAR(255) NOT NULL DEFAULT '', - device_code_active BOOL NOT NULL DEFAULT true, - user_code_state SMALLINT NOT NULL DEFAULT 0, - requested_audience TEXT NULL DEFAULT '', - granted_audience TEXT NULL DEFAULT '', - challenge_id VARCHAR(40) NULL, - expires_at TIMESTAMP NULL, - nid UUID NULL, - - FOREIGN KEY (client_id, nid) REFERENCES hydra_client(id, nid) ON DELETE CASCADE, - FOREIGN KEY (nid) REFERENCES networks(id) ON UPDATE RESTRICT ON DELETE CASCADE, - FOREIGN KEY (challenge_id) REFERENCES hydra_oauth2_flow(consent_challenge_id) ON DELETE CASCADE, - PRIMARY KEY (device_code_signature, nid) -); - -CREATE INDEX hydra_oauth2_device_auth_codes_request_id_idx ON hydra_oauth2_device_auth_codes (request_id, nid); -CREATE INDEX hydra_oauth2_device_auth_codes_client_id_idx ON hydra_oauth2_device_auth_codes (client_id, nid); -CREATE INDEX hydra_oauth2_device_auth_codes_challenge_id_idx ON hydra_oauth2_device_auth_codes (challenge_id); -CREATE UNIQUE INDEX hydra_oauth2_device_auth_codes_user_code_signature_idx ON hydra_oauth2_device_auth_codes (nid, user_code_signature); - -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_challenge_id VARCHAR(255) NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_code_request_id VARCHAR(255) NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_verifier VARCHAR(40) NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_csrf VARCHAR(40) NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_user_code_accepted_at TIMESTAMP NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_was_used BOOL NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_handled_at TIMESTAMP NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_error TEXT NULL; - -CREATE UNIQUE INDEX hydra_oauth2_flow_device_challenge_idx ON hydra_oauth2_flow (device_challenge_id); - -ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_id_token_lifespan BIGINT NULL DEFAULT NULL; -ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_access_token_lifespan BIGINT NULL DEFAULT NULL; -ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_refresh_token_lifespan BIGINT NULL DEFAULT NULL; diff --git a/persistence/sql/migrations/20241609000001000000_device_flow.down.sql b/persistence/sql/migrations/20241609000001000000_device_flow.down.sql index 74f893a46cd..5e38432e890 100644 --- a/persistence/sql/migrations/20241609000001000000_device_flow.down.sql +++ b/persistence/sql/migrations/20241609000001000000_device_flow.down.sql @@ -1,5 +1,3 @@ --- Migration generated by the command below; DO NOT EDIT. --- hydra:generate hydra migrate gen ALTER TABLE hydra_oauth2_device_auth_codes DROP FOREIGN KEY IF EXISTS hydra_oauth2_device_auth_codes_challenge_id_fk; ALTER TABLE hydra_oauth2_device_auth_codes DROP FOREIGN KEY IF EXISTS hydra_oauth2_device_auth_codes_client_id_fk; ALTER TABLE hydra_oauth2_device_auth_codes DROP FOREIGN KEY IF EXISTS hydra_oauth2_device_auth_codes_nid_fk_idx; @@ -15,7 +13,6 @@ ALTER TABLE hydra_oauth2_flow DROP COLUMN IF EXISTS device_was_used; ALTER TABLE hydra_oauth2_flow DROP COLUMN IF EXISTS device_handled_at; ALTER TABLE hydra_oauth2_flow DROP COLUMN IF EXISTS device_error; - ALTER TABLE hydra_client DROP COLUMN device_authorization_grant_id_token_lifespan; ALTER TABLE hydra_client DROP COLUMN device_authorization_grant_access_token_lifespan; -ALTER TABLE hydra_client DROP COLUMN device_authorization_grant_refresh_token_lifespan; \ No newline at end of file +ALTER TABLE hydra_client DROP COLUMN device_authorization_grant_refresh_token_lifespan; diff --git a/persistence/sql/migrations/20241609000001000000_device_flow.mysql.up.sql b/persistence/sql/migrations/20241609000001000000_device_flow.mysql.up.sql index 81267a62435..a4721b83975 100644 --- a/persistence/sql/migrations/20241609000001000000_device_flow.mysql.up.sql +++ b/persistence/sql/migrations/20241609000001000000_device_flow.mysql.up.sql @@ -1,29 +1,27 @@ --- Migration generated by the command below; DO NOT EDIT. --- hydra:generate hydra migrate gen CREATE TABLE IF NOT EXISTS hydra_oauth2_device_auth_codes ( - device_code_signature VARCHAR(255) NOT NULL, - user_code_signature VARCHAR(255) NOT NULL, - request_id VARCHAR(40) NOT NULL DEFAULT '', - requested_at TIMESTAMP NOT NULL DEFAULT NOW(), - client_id VARCHAR(255) NOT NULL, - scope TEXT NOT NULL, - granted_scope TEXT NOT NULL, - form_data TEXT NOT NULL, - session_data TEXT NOT NULL, - subject VARCHAR(255) NOT NULL DEFAULT '', - device_code_active BOOL NOT NULL DEFAULT true, - user_code_state SMALLINT NOT NULL DEFAULT 0, - requested_audience TEXT NOT NULL, - granted_audience TEXT NOT NULL, - challenge_id VARCHAR(40) NULL, - expires_at TIMESTAMP NULL, - nid CHAR(36) NOT NULL, + device_code_signature VARCHAR(255) NOT NULL, + user_code_signature VARCHAR(255) NOT NULL, + request_id VARCHAR(40) NOT NULL, + requested_at TIMESTAMP NOT NULL DEFAULT NOW(), + client_id VARCHAR(255) NOT NULL, + scope VARCHAR(1024) NOT NULL, + granted_scope VARCHAR(1024) NOT NULL, + form_data VARCHAR(4096) NOT NULL, + session_data VARCHAR(8192) NOT NULL, + subject VARCHAR(255) NOT NULL DEFAULT '', + device_code_active BOOL NOT NULL DEFAULT true, + user_code_state SMALLINT NOT NULL DEFAULT 0, + requested_audience VARCHAR(1024) NOT NULL, + granted_audience VARCHAR(1024) NOT NULL, + challenge_id VARCHAR(40) NULL, + expires_at TIMESTAMP NULL, + nid CHAR(36) NOT NULL, - FOREIGN KEY (client_id, nid) REFERENCES hydra_client(id, nid) ON DELETE CASCADE, - FOREIGN KEY (nid) REFERENCES networks(id) ON UPDATE RESTRICT ON DELETE CASCADE, - FOREIGN KEY (challenge_id) REFERENCES hydra_oauth2_flow(consent_challenge_id) ON DELETE CASCADE, - PRIMARY KEY (device_code_signature, nid) + FOREIGN KEY (client_id, nid) REFERENCES hydra_client (id, nid) ON DELETE CASCADE, + FOREIGN KEY (nid) REFERENCES networks (id) ON UPDATE RESTRICT ON DELETE CASCADE, + FOREIGN KEY (challenge_id) REFERENCES hydra_oauth2_flow (consent_challenge_id) ON DELETE CASCADE, + PRIMARY KEY (device_code_signature, nid) ); CREATE INDEX hydra_oauth2_device_auth_codes_request_id_idx ON hydra_oauth2_device_auth_codes (request_id, nid); @@ -31,17 +29,28 @@ CREATE INDEX hydra_oauth2_device_auth_codes_client_id_idx ON hydra_oauth2_device CREATE INDEX hydra_oauth2_device_auth_codes_challenge_id_idx ON hydra_oauth2_device_auth_codes (challenge_id); CREATE UNIQUE INDEX hydra_oauth2_device_auth_codes_user_code_signature_idx ON hydra_oauth2_device_auth_codes (nid, user_code_signature); -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_challenge_id VARCHAR(255) NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_code_request_id VARCHAR(255) NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_verifier VARCHAR(40) NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_csrf VARCHAR(40) NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_user_code_accepted_at TIMESTAMP NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_was_used BOOL NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_handled_at TIMESTAMP NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_error TEXT NULL; +ALTER TABLE hydra_oauth2_flow + ADD COLUMN device_challenge_id VARCHAR(255) NULL; +ALTER TABLE hydra_oauth2_flow + ADD COLUMN device_code_request_id VARCHAR(255) NULL; +ALTER TABLE hydra_oauth2_flow + ADD COLUMN device_verifier VARCHAR(40) NULL; +ALTER TABLE hydra_oauth2_flow + ADD COLUMN device_csrf VARCHAR(40) NULL; +ALTER TABLE hydra_oauth2_flow + ADD COLUMN device_user_code_accepted_at TIMESTAMP NULL; +ALTER TABLE hydra_oauth2_flow + ADD COLUMN device_was_used BOOL NULL; +ALTER TABLE hydra_oauth2_flow + ADD COLUMN device_handled_at TIMESTAMP NULL; +ALTER TABLE hydra_oauth2_flow + ADD COLUMN device_error VARCHAR(2048) NULL; CREATE UNIQUE INDEX hydra_oauth2_flow_device_challenge_idx ON hydra_oauth2_flow (device_challenge_id); -ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_id_token_lifespan BIGINT NULL DEFAULT NULL; -ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_access_token_lifespan BIGINT NULL DEFAULT NULL; -ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_refresh_token_lifespan BIGINT NULL DEFAULT NULL; +ALTER TABLE hydra_client + ADD COLUMN device_authorization_grant_id_token_lifespan BIGINT NULL DEFAULT NULL; +ALTER TABLE hydra_client + ADD COLUMN device_authorization_grant_access_token_lifespan BIGINT NULL DEFAULT NULL; +ALTER TABLE hydra_client + ADD COLUMN device_authorization_grant_refresh_token_lifespan BIGINT NULL DEFAULT NULL; diff --git a/persistence/sql/migrations/20241609000001000000_device_flow.postgres.up.sql b/persistence/sql/migrations/20241609000001000000_device_flow.postgres.up.sql deleted file mode 100644 index ea274805ac5..00000000000 --- a/persistence/sql/migrations/20241609000001000000_device_flow.postgres.up.sql +++ /dev/null @@ -1,47 +0,0 @@ --- Migration generated by the command below; DO NOT EDIT. --- hydra:generate hydra migrate gen -CREATE TABLE IF NOT EXISTS hydra_oauth2_device_auth_codes -( - device_code_signature VARCHAR(255) NOT NULL, - user_code_signature VARCHAR(255) NOT NULL, - request_id VARCHAR(40) NOT NULL, - requested_at TIMESTAMP NOT NULL DEFAULT NOW(), - client_id VARCHAR(255) NOT NULL, - scope TEXT NOT NULL, - granted_scope TEXT NOT NULL, - form_data TEXT NOT NULL, - session_data TEXT NOT NULL, - subject VARCHAR(255) NOT NULL DEFAULT '', - device_code_active BOOL NOT NULL DEFAULT true, - user_code_state SMALLINT NOT NULL DEFAULT 0, - requested_audience TEXT NULL DEFAULT '', - granted_audience TEXT NULL DEFAULT '', - challenge_id VARCHAR(40) NULL, - expires_at TIMESTAMP NULL, - nid UUID NULL, - - FOREIGN KEY (client_id, nid) REFERENCES hydra_client(id, nid) ON DELETE CASCADE, - FOREIGN KEY (nid) REFERENCES networks(id) ON UPDATE RESTRICT ON DELETE CASCADE, - FOREIGN KEY (challenge_id) REFERENCES hydra_oauth2_flow(consent_challenge_id) ON DELETE CASCADE, - PRIMARY KEY (device_code_signature, nid) -); - -CREATE INDEX hydra_oauth2_device_auth_codes_request_id_idx ON hydra_oauth2_device_auth_codes (request_id, nid); -CREATE INDEX hydra_oauth2_device_auth_codes_client_id_idx ON hydra_oauth2_device_auth_codes (client_id, nid); -CREATE INDEX hydra_oauth2_device_auth_codes_challenge_id_idx ON hydra_oauth2_device_auth_codes (challenge_id); -CREATE UNIQUE INDEX hydra_oauth2_device_auth_codes_user_code_signature_idx ON hydra_oauth2_device_auth_codes (nid, user_code_signature); - -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_challenge_id VARCHAR(255) NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_code_request_id VARCHAR(255) NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_verifier VARCHAR(40) NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_csrf VARCHAR(40) NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_user_code_accepted_at TIMESTAMP NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_was_used BOOLEAN NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_handled_at TIMESTAMP NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_error TEXT NULL; - -CREATE UNIQUE INDEX hydra_oauth2_flow_device_challenge_idx ON hydra_oauth2_flow (device_challenge_id); - -ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_id_token_lifespan BIGINT NULL DEFAULT NULL; -ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_access_token_lifespan BIGINT NULL DEFAULT NULL; -ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_refresh_token_lifespan BIGINT NULL DEFAULT NULL; diff --git a/persistence/sql/migrations/20241609000001000000_device_flow.sqlite.up.sql b/persistence/sql/migrations/20241609000001000000_device_flow.sqlite.up.sql new file mode 100644 index 00000000000..dc61ee00578 --- /dev/null +++ b/persistence/sql/migrations/20241609000001000000_device_flow.sqlite.up.sql @@ -0,0 +1,56 @@ +CREATE TABLE IF NOT EXISTS hydra_oauth2_device_auth_codes +( + device_code_signature VARCHAR(255) NOT NULL, + user_code_signature VARCHAR(255) NOT NULL, + request_id VARCHAR(40) NOT NULL, + requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + client_id VARCHAR(255) NOT NULL, + scope VARCHAR(1024) NOT NULL, + granted_scope VARCHAR(1024) NOT NULL, + form_data VARCHAR(4096) NOT NULL, + session_data VARCHAR(8192) NOT NULL, + subject VARCHAR(255) NOT NULL DEFAULT '', + device_code_active BOOL NOT NULL DEFAULT true, + user_code_state SMALLINT NOT NULL DEFAULT 0, + requested_audience VARCHAR(1024) NOT NULL, + granted_audience VARCHAR(1024) NOT NULL, + challenge_id VARCHAR(40) NULL, + expires_at TIMESTAMP NULL, + nid UUID NOT NULL, + + FOREIGN KEY (client_id, nid) REFERENCES hydra_client (id, nid) ON DELETE CASCADE, + FOREIGN KEY (nid) REFERENCES networks (id) ON UPDATE RESTRICT ON DELETE CASCADE, + FOREIGN KEY (challenge_id) REFERENCES hydra_oauth2_flow (consent_challenge_id) ON DELETE CASCADE, + PRIMARY KEY (device_code_signature, nid) +); + +CREATE INDEX hydra_oauth2_device_auth_codes_request_id_idx ON hydra_oauth2_device_auth_codes (request_id, nid); +CREATE INDEX hydra_oauth2_device_auth_codes_client_id_idx ON hydra_oauth2_device_auth_codes (client_id, nid); +CREATE INDEX hydra_oauth2_device_auth_codes_challenge_id_idx ON hydra_oauth2_device_auth_codes (challenge_id); +CREATE UNIQUE INDEX hydra_oauth2_device_auth_codes_user_code_signature_idx ON hydra_oauth2_device_auth_codes (nid, user_code_signature); + +ALTER TABLE hydra_oauth2_flow + ADD COLUMN device_challenge_id VARCHAR(255) NULL; +ALTER TABLE hydra_oauth2_flow + ADD COLUMN device_code_request_id VARCHAR(255) NULL; +ALTER TABLE hydra_oauth2_flow + ADD COLUMN device_verifier VARCHAR(40) NULL; +ALTER TABLE hydra_oauth2_flow + ADD COLUMN device_csrf VARCHAR(40) NULL; +ALTER TABLE hydra_oauth2_flow + ADD COLUMN device_user_code_accepted_at TIMESTAMP NULL; +ALTER TABLE hydra_oauth2_flow + ADD COLUMN device_was_used BOOLEAN NULL; +ALTER TABLE hydra_oauth2_flow + ADD COLUMN device_handled_at TIMESTAMP NULL; +ALTER TABLE hydra_oauth2_flow + ADD COLUMN device_error VARCHAR(2048) NULL; + +CREATE UNIQUE INDEX hydra_oauth2_flow_device_challenge_idx ON hydra_oauth2_flow (device_challenge_id); + +ALTER TABLE hydra_client + ADD COLUMN device_authorization_grant_id_token_lifespan BIGINT NULL DEFAULT NULL; +ALTER TABLE hydra_client + ADD COLUMN device_authorization_grant_access_token_lifespan BIGINT NULL DEFAULT NULL; +ALTER TABLE hydra_client + ADD COLUMN device_authorization_grant_refresh_token_lifespan BIGINT NULL DEFAULT NULL; diff --git a/persistence/sql/migrations/20241609000001000000_device_flow.up.sql b/persistence/sql/migrations/20241609000001000000_device_flow.up.sql index fa9cc10a524..a607701d0fd 100644 --- a/persistence/sql/migrations/20241609000001000000_device_flow.up.sql +++ b/persistence/sql/migrations/20241609000001000000_device_flow.up.sql @@ -1,24 +1,27 @@ --- Migration generated by the command below; DO NOT EDIT. --- hydra:generate hydra migrate gen CREATE TABLE IF NOT EXISTS hydra_oauth2_device_auth_codes ( - device_code_signature VARCHAR(255) NOT NULL PRIMARY KEY, - user_code_signature VARCHAR(255) NOT NULL, - request_id VARCHAR(40) NOT NULL, - requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - client_id VARCHAR(255) NOT NULL, - scope TEXT NOT NULL, - granted_scope TEXT NOT NULL, - form_data TEXT NOT NULL, - session_data TEXT NOT NULL, - subject VARCHAR(255) NOT NULL DEFAULT '', - device_code_active BOOL NOT NULL DEFAULT true, - user_code_state SMALLINT NOT NULL DEFAULT 0, - requested_audience TEXT NULL DEFAULT '', - granted_audience TEXT NULL DEFAULT '', - challenge_id VARCHAR(40) NULL, - expires_at TIMESTAMP NULL, - nid UUID NULL + device_code_signature VARCHAR(255) NOT NULL, + user_code_signature VARCHAR(255) NOT NULL, + request_id VARCHAR(40) NOT NULL, + requested_at TIMESTAMP NOT NULL DEFAULT NOW(), + client_id VARCHAR(255) NOT NULL, + scope VARCHAR(1024) NOT NULL, + granted_scope VARCHAR(1024) NOT NULL, + form_data VARCHAR(4096) NOT NULL, + session_data VARCHAR(8192) NOT NULL, + subject VARCHAR(255) NOT NULL DEFAULT '', + device_code_active BOOL NOT NULL DEFAULT true, + user_code_state SMALLINT NOT NULL DEFAULT 0, + requested_audience VARCHAR(1024) NOT NULL, + granted_audience VARCHAR(1024) NOT NULL, + challenge_id VARCHAR(40) NULL, + expires_at TIMESTAMP NULL, + nid UUID NOT NULL, + + FOREIGN KEY (client_id, nid) REFERENCES hydra_client (id, nid) ON DELETE CASCADE, + FOREIGN KEY (nid) REFERENCES networks (id) ON UPDATE RESTRICT ON DELETE CASCADE, + FOREIGN KEY (challenge_id) REFERENCES hydra_oauth2_flow (consent_challenge_id) ON DELETE CASCADE, + PRIMARY KEY (device_code_signature, nid) ); CREATE INDEX hydra_oauth2_device_auth_codes_request_id_idx ON hydra_oauth2_device_auth_codes (request_id, nid); @@ -26,17 +29,28 @@ CREATE INDEX hydra_oauth2_device_auth_codes_client_id_idx ON hydra_oauth2_device CREATE INDEX hydra_oauth2_device_auth_codes_challenge_id_idx ON hydra_oauth2_device_auth_codes (challenge_id); CREATE UNIQUE INDEX hydra_oauth2_device_auth_codes_user_code_signature_idx ON hydra_oauth2_device_auth_codes (nid, user_code_signature); -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_challenge_id VARCHAR(255) NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_code_request_id VARCHAR(255) NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_verifier VARCHAR(40) NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_csrf VARCHAR(40) NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_user_code_accepted_at TIMESTAMP NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_was_used BOOLEAN NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_handled_at TIMESTAMP NULL; -ALTER TABLE hydra_oauth2_flow ADD COLUMN device_error TEXT NULL; +ALTER TABLE hydra_oauth2_flow + ADD COLUMN device_challenge_id VARCHAR(255) NULL; +ALTER TABLE hydra_oauth2_flow + ADD COLUMN device_code_request_id VARCHAR(255) NULL; +ALTER TABLE hydra_oauth2_flow + ADD COLUMN device_verifier VARCHAR(40) NULL; +ALTER TABLE hydra_oauth2_flow + ADD COLUMN device_csrf VARCHAR(40) NULL; +ALTER TABLE hydra_oauth2_flow + ADD COLUMN device_user_code_accepted_at TIMESTAMP NULL; +ALTER TABLE hydra_oauth2_flow + ADD COLUMN device_was_used BOOLEAN NULL; +ALTER TABLE hydra_oauth2_flow + ADD COLUMN device_handled_at TIMESTAMP NULL; +ALTER TABLE hydra_oauth2_flow + ADD COLUMN device_error VARCHAR(2048) NULL; CREATE UNIQUE INDEX hydra_oauth2_flow_device_challenge_idx ON hydra_oauth2_flow (device_challenge_id); -ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_id_token_lifespan BIGINT NULL DEFAULT NULL; -ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_access_token_lifespan BIGINT NULL DEFAULT NULL; -ALTER TABLE hydra_client ADD COLUMN device_authorization_grant_refresh_token_lifespan BIGINT NULL DEFAULT NULL; +ALTER TABLE hydra_client + ADD COLUMN device_authorization_grant_id_token_lifespan BIGINT NULL DEFAULT NULL; +ALTER TABLE hydra_client + ADD COLUMN device_authorization_grant_access_token_lifespan BIGINT NULL DEFAULT NULL; +ALTER TABLE hydra_client + ADD COLUMN device_authorization_grant_refresh_token_lifespan BIGINT NULL DEFAULT NULL; diff --git a/persistence/sql/persister_consent.go b/persistence/sql/persister_consent.go index 9f820623ffc..a69e8cc7297 100644 --- a/persistence/sql/persister_consent.go +++ b/persistence/sql/persister_consent.go @@ -251,9 +251,8 @@ func (p *Persister) GetDeviceUserAuthRequest(ctx context.Context, challenge stri if f.RequestedAt.Add(p.config.ConsentRequestMaxAge(ctx)).Before(time.Now()) { return nil, errorsx.WithStack(fosite.ErrRequestUnauthorized.WithHint("The device request has expired, please try again.")) } - dr := f.GetDeviceUserAuthRequest() - return dr, nil + return f.GetDeviceUserAuthRequest(), nil } // HandleDeviceUserAuthRequest uses a HandledDeviceUserAuthRequest to update the flow and returns a DeviceUserAuthRequest. @@ -299,6 +298,8 @@ func (p *Persister) CreateLoginRequest(ctx context.Context, f *flow.Flow, req *f ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateLoginRequest") defer otelx.End(span, &err) + // TODO - this is quite confusing - if f is not nil we re-create the flow object with the only + // TODO difference being f.State? if f == nil { f = flow.NewFlow(req) } else { @@ -320,6 +321,7 @@ func (p *Persister) CreateLoginRequest(ctx context.Context, f *flow.Flow, req *f f.RequestedAt = req.RequestedAt f.State = flow.FlowStateLoginInitialized } + nid := p.NetworkID(ctx) if nid == uuid.Nil { return nil, errorsx.WithStack(x.ErrNotFound) diff --git a/persistence/sql/persister_device.go b/persistence/sql/persister_device.go index af658c77978..dbbcbfe1d08 100644 --- a/persistence/sql/persister_device.go +++ b/persistence/sql/persister_device.go @@ -153,27 +153,25 @@ func (p *Persister) sqlDeviceSchemaFromRequest(ctx context.Context, deviceCodeSi }, nil } -func (p *Persister) createDeviceAuthSession(ctx context.Context, deviceCodeSignature, userCodeSignature string, requester fosite.DeviceRequester, expiresAt time.Time) error { - req, err := p.sqlDeviceSchemaFromRequest(ctx, deviceCodeSignature, userCodeSignature, requester, expiresAt) +// CreateDeviceCodeSession creates a new device code session and stores it in the database +func (p *Persister) CreateDeviceAuthSession(ctx context.Context, deviceCodeSignature, userCodeSignature string, requester fosite.DeviceRequester) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateDeviceCodeSession") + defer otelx.End(span, &err) + + req, err := p.sqlDeviceSchemaFromRequest(ctx, deviceCodeSignature, userCodeSignature, requester, requester.GetSession().GetExpiresAt(fosite.DeviceCode).UTC()) if err != nil { return err } - if err = sqlcon.HandleError(p.CreateWithNetwork(ctx, req)); errors.Is(err, sqlcon.ErrConcurrentUpdate) { + if err := sqlcon.HandleError(p.CreateWithNetwork(ctx, req)); errors.Is(err, sqlcon.ErrConcurrentUpdate) { return errors.Wrap(fosite.ErrSerializationFailure, err.Error()) } else if errors.Is(err, sqlcon.ErrUniqueViolation) { return errors.Wrap(fosite.ErrExistingUserCodeSignature, err.Error()) } else if err != nil { return err } - return nil -} -// CreateDeviceCodeSession creates a new device code session and stores it in the database -func (p *Persister) CreateDeviceAuthSession(ctx context.Context, deviceCodeSignature, userCodeSignature string, requester fosite.DeviceRequester) (err error) { - ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateDeviceCodeSession") - defer otelx.End(span, &err) - return p.createDeviceAuthSession(ctx, deviceCodeSignature, userCodeSignature, requester, requester.GetSession().GetExpiresAt(fosite.DeviceCode).UTC()) + return nil } // UpdateDeviceCodeSessionBySignature updates a device code session by the device_code signature @@ -192,12 +190,14 @@ func (p *Persister) UpdateDeviceCodeSessionBySignature(ctx context.Context, sign ) /* #nosec G201 table is static */ - err = p.Connection(ctx).RawQuery(stmt, req.GrantedScope, req.GrantedAudience, req.Session, req.UserCodeState, req.Subject, req.ConsentChallenge, signature, p.NetworkID(ctx)).Exec() - if err != nil { - return sqlcon.HandleError(err) - } - - return nil + return sqlcon.HandleError( + p.Connection(ctx).RawQuery(stmt, + req.GrantedScope, req.GrantedAudience, + req.Session, req.UserCodeState, + req.Subject, req.ConsentChallenge, + signature, p.NetworkID(ctx), + ).Exec(), + ) } // GetDeviceCodeSession returns a device code session from the database @@ -206,13 +206,12 @@ func (p *Persister) GetDeviceCodeSession(ctx context.Context, signature string, defer otelx.End(span, &err) r := DeviceRequestSQL{} - err = p.QueryWithNetwork(ctx).Where("device_code_signature = ?", signature).First(&r) - if errors.Is(err, sql.ErrNoRows) { + if err = p.QueryWithNetwork(ctx).Where("device_code_signature = ?", signature).First(&r); errors.Is(err, sql.ErrNoRows) { return nil, errorsx.WithStack(fosite.ErrNotFound) - } - if err != nil { + } else if err != nil { return nil, sqlcon.HandleError(err) } + if !r.DeviceCodeActive { fr, err := r.toRequest(ctx, session, p) if err != nil { @@ -230,13 +229,12 @@ func (p *Persister) GetDeviceCodeSessionByRequestID(ctx context.Context, request defer otelx.End(span, &err) r := DeviceRequestSQL{} - err = p.QueryWithNetwork(ctx).Where("request_id = ?", requestID).First(&r) - if errors.Is(err, sql.ErrNoRows) { + if err = p.QueryWithNetwork(ctx).Where("request_id = ?", requestID).First(&r); errors.Is(err, sql.ErrNoRows) { return nil, "", errorsx.WithStack(fosite.ErrNotFound) - } - if err != nil { + } else if err != nil { return nil, "", sqlcon.HandleError(err) } + if !r.DeviceCodeActive { fr, err := r.toRequest(ctx, session, p) if err != nil { @@ -278,11 +276,10 @@ func (p *Persister) GetUserCodeSession(ctx context.Context, signature string, se if session == nil { session = oauth2.NewSession("") } - err = p.QueryWithNetwork(ctx).Where("user_code_signature = ?", signature).First(&r) - if errors.Is(err, sql.ErrNoRows) { + + if err = p.QueryWithNetwork(ctx).Where("user_code_signature = ?", signature).First(&r); errors.Is(err, sql.ErrNoRows) { return nil, errorsx.WithStack(fosite.ErrNotFound) - } - if err != nil { + } else if err != nil { return nil, sqlcon.HandleError(err) } @@ -290,6 +287,7 @@ func (p *Persister) GetUserCodeSession(ctx context.Context, signature string, se if err != nil { return nil, err } + if r.UserCodeState != fosite.UserCodeUnused { return fr, errorsx.WithStack(fosite.ErrInactiveToken) } diff --git a/persistence/sql/persister_nid_test.go b/persistence/sql/persister_nid_test.go index b45142404e5..cdab3d05b30 100644 --- a/persistence/sql/persister_nid_test.go +++ b/persistence/sql/persister_nid_test.go @@ -1365,7 +1365,7 @@ func (s *PersisterTestSuite) TestGetPublicKeys() { ks := newKeySet(issuer, "use") grant := trust.Grant{ ID: uuid.Must(uuid.NewV4()).String(), - ExpiresAt: time.Now().Add(time.Hour), + ExpiresAt: time.Now().UTC().Add(time.Hour), Issuer: issuer, PublicKey: trust.PublicKey{Set: issuer, KeyID: ks.Keys[0].KeyID}, } diff --git a/spec/config.json b/spec/config.json index b246998d2cb..c212f4b2637 100644 --- a/spec/config.json +++ b/spec/config.json @@ -816,22 +816,29 @@ "/ui/logout" ] }, - "device_verification": { - "type": "string", - "description": "Sets the device verification URL. Defaults to an internal fallback URL showing an error.", - "format": "uri-reference", - "examples": [ - "https://my-app/device", - "/ui/device" - ] - }, - "device_verification_success": { - "type": "string", - "description": "When a user completes an authentication flow initiated by a device, they will be redirected to this url afterwards.", - "format": "uri-reference", - "examples": [ - "https://my-app/device/post" - ] + "device": { + "type": "object", + "description": "Configure URLs for the OAuth 2.0 Device Code Flow.", + "properties": { + "verification": { + "type": "string", + "description": "Sets the device user code verification endpoint. Defaults to an internal fallback URL showing an error.", + "format": "uri-reference", + "examples": [ + "https://my-logout.app/device_verification", + "/ui/device_verification" + ] + }, + "success": { + "type": "string", + "description": "Sets the post device authentication endpoint. Defaults to an internal fallback URL showing an error.", + "format": "uri-reference", + "examples": [ + "https://my-logout.app/device_done", + "/ui/device_done" + ] + } + } }, "error": { "type": "string", From a3e3e0572b9f32f7dd5c79af22b7d909bf1b931c Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Mon, 10 Feb 2025 13:58:20 -0500 Subject: [PATCH 49/52] u --- internal/httpclient/go.sum | 347 +++++++++++++++++++++++++++++++++++++ oauth2/handler.go | 3 +- 2 files changed, 349 insertions(+), 1 deletion(-) diff --git a/internal/httpclient/go.sum b/internal/httpclient/go.sum index 734252e6815..3dee6d68163 100644 --- a/internal/httpclient/go.sum +++ b/internal/httpclient/go.sum @@ -1,13 +1,360 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/oauth2/handler.go b/oauth2/handler.go index 7bfd3a4802a..eb80e859278 100644 --- a/oauth2/handler.go +++ b/oauth2/handler.go @@ -8,7 +8,6 @@ import ( "encoding/base64" "encoding/json" "fmt" - "github.com/ory/x/otelx" "html/template" "net/http" "net/url" @@ -16,6 +15,8 @@ import ( "strings" "time" + "github.com/ory/x/otelx" + "github.com/gobuffalo/pop/v6" "github.com/tidwall/gjson" From 33d997f38f17da6cd28e0d5057366581a6c78c9a Mon Sep 17 00:00:00 2001 From: Nikos Date: Tue, 11 Feb 2025 15:20:04 +0100 Subject: [PATCH 50/52] chore: address review comments --- .schema/config.schema.json | 2 +- consent/handler_test.go | 6 +-- consent/manager.go | 3 +- consent/strategy_default.go | 18 ++++--- consent/test/manager_test_helpers.go | 16 +++--- flow/flow.go | 2 - internal/testhelpers/janitor_test_helper.go | 14 ++--- oauth2/fosite_store_helpers_test.go | 2 +- oauth2/handler.go | 20 ++++---- oauth2/oauth2_device_code_test.go | 1 - .../hydra_oauth2_flow/challenge-0001.json | 1 - .../hydra_oauth2_flow/challenge-0002.json | 1 - .../hydra_oauth2_flow/challenge-0003.json | 1 - .../hydra_oauth2_flow/challenge-0004.json | 1 - .../hydra_oauth2_flow/challenge-0005.json | 1 - .../hydra_oauth2_flow/challenge-0006.json | 1 - .../hydra_oauth2_flow/challenge-0007.json | 1 - .../hydra_oauth2_flow/challenge-0008.json | 1 - .../hydra_oauth2_flow/challenge-0009.json | 1 - .../hydra_oauth2_flow/challenge-0010.json | 1 - .../hydra_oauth2_flow/challenge-0011.json | 1 - .../hydra_oauth2_flow/challenge-0012.json | 1 - .../hydra_oauth2_flow/challenge-0013.json | 1 - .../hydra_oauth2_flow/challenge-0014.json | 1 - .../hydra_oauth2_flow/challenge-0015.json | 1 - .../hydra_oauth2_flow/challenge-0016.json | 1 - .../hydra_oauth2_flow/challenge-0017.json | 1 - .../hydra_oauth2_flow/challenge-0018.json | 1 - .../20241609000001000000_device_flow.down.sql | 1 - ...41609000001000000_device_flow.mysql.up.sql | 4 +- ...1609000001000000_device_flow.sqlite.up.sql | 4 +- .../20241609000001000000_device_flow.up.sql | 4 +- persistence/sql/persister_consent.go | 51 ++++++++++--------- persistence/sql/persister_nid_test.go | 4 +- spec/config.json | 2 +- 35 files changed, 78 insertions(+), 94 deletions(-) diff --git a/.schema/config.schema.json b/.schema/config.schema.json index d4aca510dab..0dc6cbe3ed2 100644 --- a/.schema/config.schema.json +++ b/.schema/config.schema.json @@ -1181,7 +1181,7 @@ } ], "default": "5s", - "description": "configure how often a non-interactive device should poll the device token endpoint", + "description": "Configures how often a non-interactive device should poll the device token endpoint, this is a purely informational configuration and does not enforce rate-limiting.", "examples": ["5s", "15s", "1m"] }, "user_code_entropy": { diff --git a/consent/handler_test.go b/consent/handler_test.go index 3487e3f7442..f8638c07454 100644 --- a/consent/handler_test.go +++ b/consent/handler_test.go @@ -108,7 +108,7 @@ func TestGetLoginRequest(t *testing.T) { if tc.exists { cl := &client.Client{ID: "client" + key} require.NoError(t, reg.ClientManager().CreateClient(context.Background(), cl)) - f, err := reg.ConsentManager().CreateLoginRequest(context.Background(), nil, &flow.LoginRequest{ + f, err := reg.ConsentManager().CreateLoginRequest(context.Background(), &flow.LoginRequest{ Client: cl, ID: challenge, RequestURL: requestURL, @@ -180,7 +180,7 @@ func TestGetConsentRequest(t *testing.T) { RequestURL: requestURL, RequestedAt: time.Now(), } - f, err := reg.ConsentManager().CreateLoginRequest(ctx, nil, lr) + f, err := reg.ConsentManager().CreateLoginRequest(ctx, lr) require.NoError(t, err) challenge, err = f.ToLoginChallenge(ctx, reg) require.NoError(t, err) @@ -247,7 +247,7 @@ func TestGetLoginRequestWithDuplicateAccept(t *testing.T) { cl := &client.Client{ID: "client"} require.NoError(t, reg.ClientManager().CreateClient(ctx, cl)) - f, err := reg.ConsentManager().CreateLoginRequest(ctx, nil, &flow.LoginRequest{ + f, err := reg.ConsentManager().CreateLoginRequest(ctx, &flow.LoginRequest{ Client: cl, ID: challenge, RequestURL: requestURL, diff --git a/consent/manager.go b/consent/manager.go index e2787d0de79..b5b2aba5691 100644 --- a/consent/manager.go +++ b/consent/manager.go @@ -45,7 +45,8 @@ type ( RevokeSubjectLoginSession(ctx context.Context, user string) error ConfirmLoginSession(ctx context.Context, loginSession *flow.LoginSession) error - CreateLoginRequest(ctx context.Context, f *flow.Flow, req *flow.LoginRequest) (*flow.Flow, error) + CreateLoginRequest(ctx context.Context, req *flow.LoginRequest) (*flow.Flow, error) + CreateLoginRequestFromDeviceRequest(ctx context.Context, f *flow.Flow, req *flow.LoginRequest) (*flow.Flow, error) GetLoginRequest(ctx context.Context, challenge string) (*flow.LoginRequest, error) HandleLoginRequest(ctx context.Context, f *flow.Flow, challenge string, r *flow.HandledLoginRequest) (*flow.LoginRequest, error) VerifyAndInvalidateLoginRequest(ctx context.Context, verifier string) (*flow.HandledLoginRequest, error) diff --git a/consent/strategy_default.go b/consent/strategy_default.go index 88c837c6da1..6244ea0a269 100644 --- a/consent/strategy_default.go +++ b/consent/strategy_default.go @@ -279,11 +279,12 @@ func (s *DefaultStrategy) forwardAuthenticationRequest( LoginHint: ar.GetRequestForm().Get("login_hint"), }, } - f, err := s.r.ConsentManager().CreateLoginRequest( - ctx, - f, - loginRequest, - ) + var err error + if f == nil { + f, err = s.r.ConsentManager().CreateLoginRequest(ctx, loginRequest) + } else { + f, err = s.r.ConsentManager().CreateLoginRequestFromDeviceRequest(ctx, f, loginRequest) + } if err != nil { return errorsx.WithStack(err) } @@ -1295,7 +1296,12 @@ func (s *DefaultStrategy) forwardDeviceRequest(ctx context.Context, w http.Respo // Generate the request URL iu := s.getDeviceVerificationPath(ctx) - iu.RawQuery = r.URL.RawQuery + // We don't want the user_code persisted in the database + q := r.URL.Query() + if q.Has("user_code") { + q.Set("user_code", "****") + } + iu.RawQuery = q.Encode() f, err := s.r.ConsentManager().CreateDeviceUserAuthRequest( r.Context(), diff --git a/consent/test/manager_test_helpers.go b/consent/test/manager_test_helpers.go index 7b820eb3245..2f2b280c0e5 100644 --- a/consent/test/manager_test_helpers.go +++ b/consent/test/manager_test_helpers.go @@ -301,7 +301,7 @@ func SaneMockAuthRequest(t *testing.T, m consent.Manager, ls *flow.LoginSession, ID: uuid.New().String(), Verifier: uuid.New().String(), } - _, err := m.CreateLoginRequest(context.Background(), nil, c) + _, err := m.CreateLoginRequest(context.Background(), c) require.NoError(t, err) return c } @@ -346,9 +346,9 @@ func TestHelperNID(r interface { require.Error(t, t2InvalidNID.CreateLoginSession(ctx, &testLS)) require.NoError(t, t1ValidNID.CreateLoginSession(ctx, &testLS)) - _, err := t2InvalidNID.CreateLoginRequest(ctx, nil, &testLR) + _, err := t2InvalidNID.CreateLoginRequest(ctx, &testLR) require.Error(t, err) - f, err := t1ValidNID.CreateLoginRequest(ctx, nil, &testLR) + f, err := t1ValidNID.CreateLoginRequest(ctx, &testLR) require.NoError(t, err) testLR.ID = x.Must(f.ToLoginChallenge(ctx, r)) @@ -406,7 +406,7 @@ func ManagerTests(deps Deps, m consent.Manager, clientManager client.Manager, fo RequestedAt: time.Now(), } - _, err := m.CreateLoginRequest(ctx, nil, lr[k]) + _, err := m.CreateLoginRequest(ctx, lr[k]) require.NoError(t, err) } }) @@ -581,7 +581,7 @@ func ManagerTests(deps Deps, m consent.Manager, clientManager client.Manager, fo _, err := m.GetLoginRequest(ctx, loginChallenge) require.Error(t, err) - f, err = m.CreateLoginRequest(ctx, nil, c) + f, err = m.CreateLoginRequest(ctx, c) require.NoError(t, err) loginChallenge = x.Must(f.ToLoginChallenge(ctx, deps)) @@ -850,9 +850,9 @@ func ManagerTests(deps Deps, m consent.Manager, clientManager client.Manager, fo }) t.Run("case=list-used-consent-requests", func(t *testing.T) { - f1, err := m.CreateLoginRequest(ctx, nil, lr["rv1"]) + f1, err := m.CreateLoginRequest(ctx, lr["rv1"]) require.NoError(t, err) - f2, err := m.CreateLoginRequest(ctx, nil, lr["rv2"]) + f2, err := m.CreateLoginRequest(ctx, lr["rv2"]) require.NoError(t, err) cr1, hcr1, _ := MockConsentRequest("rv1", true, 0, false, false, false, "fk-login-challenge", network) @@ -1172,7 +1172,7 @@ func ManagerTests(deps Deps, m consent.Manager, clientManager client.Manager, fo SessionID: sqlxx.NullString(s.ID), } - f, err := m.CreateLoginRequest(ctx, nil, lr) + f, err := m.CreateLoginRequest(ctx, lr) require.NoError(t, err) expected := &flow.OAuth2ConsentRequest{ ConsentRequestID: uuid.NewString(), diff --git a/flow/flow.go b/flow/flow.go index 8d8b21d3305..46cb4b46675 100644 --- a/flow/flow.go +++ b/flow/flow.go @@ -233,8 +233,6 @@ type Flow struct { DeviceVerifier sqlxx.NullString `db:"device_verifier" json:"dv,omitempty"` // DeviceVerifier is the device request's CSRF DeviceCSRF sqlxx.NullString `db:"device_csrf" json:"dc,omitempty"` - // DeviceUserCodeAcceptedAt is the time when device user_code was accepted - DeviceUserCodeAcceptedAt sqlxx.NullTime `db:"device_user_code_accepted_at" json:"da,omitempty"` // DeviceWasUsed set to true means that the device request was already handled DeviceWasUsed sqlxx.NullBool `db:"device_was_used" json:"du,omitempty"` // DeviceHandledAt contains the timestamp the device user_code verification request was handled diff --git a/internal/testhelpers/janitor_test_helper.go b/internal/testhelpers/janitor_test_helper.go index e4692cb76e6..89007a12585 100644 --- a/internal/testhelpers/janitor_test_helper.go +++ b/internal/testhelpers/janitor_test_helper.go @@ -192,7 +192,7 @@ func (j *JanitorConsentTestHelper) LoginRejectionSetup(ctx context.Context, reg // Create login requests for _, r := range j.flushLoginRequests { require.NoError(t, cl.CreateClient(ctx, r.Client)) - f, err := cm.CreateLoginRequest(ctx, nil, r) + f, err := cm.CreateLoginRequest(ctx, r) require.NoError(t, err) f.RequestedAt = time.Now() // we won't handle expired flows @@ -246,7 +246,7 @@ func (j *JanitorConsentTestHelper) LimitSetup(ctx context.Context, reg interface // Create login requests for _, r := range j.flushLoginRequests { require.NoError(t, cl.CreateClient(ctx, r.Client)) - f, err = cm.CreateLoginRequest(ctx, nil, r) + f, err = cm.CreateLoginRequest(ctx, r) require.NoError(t, err) // Reject each request @@ -290,7 +290,7 @@ func (j *JanitorConsentTestHelper) ConsentRejectionSetup(ctx context.Context, re // Create login requests for i, loginRequest := range j.flushLoginRequests { require.NoError(t, cl.CreateClient(ctx, loginRequest.Client)) - f, err = cm.CreateLoginRequest(ctx, nil, loginRequest) + f, err = cm.CreateLoginRequest(ctx, loginRequest) require.NoError(t, err) // Create consent requests @@ -344,7 +344,7 @@ func (j *JanitorConsentTestHelper) LoginTimeoutSetup(ctx context.Context, reg in // Create login requests for i, loginRequest := range j.flushLoginRequests { require.NoError(t, cl.CreateClient(ctx, loginRequest.Client)) - f, err = cm.CreateLoginRequest(ctx, nil, loginRequest) + f, err = cm.CreateLoginRequest(ctx, loginRequest) require.NoError(t, err) if i == 0 { @@ -385,7 +385,7 @@ func (j *JanitorConsentTestHelper) ConsentTimeoutSetup(ctx context.Context, reg // Let's reset and accept all login requests to test the consent requests for i, loginRequest := range j.flushLoginRequests { require.NoError(t, cl.CreateClient(ctx, loginRequest.Client)) - f, err := cm.CreateLoginRequest(ctx, nil, loginRequest) + f, err := cm.CreateLoginRequest(ctx, loginRequest) require.NoError(t, err) f.RequestedAt = time.Now() // we won't handle expired flows challenge := x.Must(f.ToLoginChallenge(ctx, reg)) @@ -437,7 +437,7 @@ func (j *JanitorConsentTestHelper) LoginConsentNotAfterSetup(ctx context.Context ) for _, r := range j.flushLoginRequests { require.NoError(t, cl.CreateClient(ctx, r.Client)) - f, err = cm.CreateLoginRequest(ctx, nil, r) + f, err = cm.CreateLoginRequest(ctx, r) require.NoError(t, err) } @@ -469,7 +469,7 @@ func (j *JanitorConsentTestHelper) LoginConsentNotAfterValidate( t.Logf("login flush check:\nNotAfter: %s\nLoginRequest: %s\nis expired: %v\n%+v\n", notAfter.String(), consentRequestLifespan.String(), isExpired, r) - f = x.Must(reg.ConsentManager().CreateLoginRequest(ctx, nil, r)) + f = x.Must(reg.ConsentManager().CreateLoginRequest(ctx, r)) loginChallenge := x.Must(f.ToLoginChallenge(ctx, reg)) _, err = reg.ConsentManager().GetLoginRequest(ctx, loginChallenge) diff --git a/oauth2/fosite_store_helpers_test.go b/oauth2/fosite_store_helpers_test.go index e3309f0fdb3..d8eeffbd95f 100644 --- a/oauth2/fosite_store_helpers_test.go +++ b/oauth2/fosite_store_helpers_test.go @@ -126,7 +126,7 @@ func mockRequestForeignKey(t *testing.T, id string, x oauth2.InternalRegistry) { } f, err := x.ConsentManager().CreateLoginRequest( - ctx, nil, &flow.LoginRequest{ + ctx, &flow.LoginRequest{ Client: cl, OpenIDConnectContext: new(flow.OAuth2ConsentRequestOpenIDConnectContext), ID: id, diff --git a/oauth2/handler.go b/oauth2/handler.go index eb80e859278..ea76f10b4d4 100644 --- a/oauth2/handler.go +++ b/oauth2/handler.go @@ -47,15 +47,16 @@ import ( ) const ( - DefaultLoginPath = "/oauth2/fallbacks/login" - DefaultConsentPath = "/oauth2/fallbacks/consent" - DefaultPostLogoutPath = "/oauth2/fallbacks/logout/callback" - DefaultPostDevicePath = "/oauth2/fallbacks/device/done" - DefaultLogoutPath = "/oauth2/fallbacks/logout" - DefaultErrorPath = "/oauth2/fallbacks/error" - TokenPath = "/oauth2/token" // #nosec G101 - AuthPath = "/oauth2/auth" - LogoutPath = "/oauth2/sessions/logout" + DefaultLoginPath = "/oauth2/fallbacks/login" + DefaultConsentPath = "/oauth2/fallbacks/consent" + DefaultPostLogoutPath = "/oauth2/fallbacks/logout/callback" + DefaultDeviceVerificationPath = "/oauth2/fallbacks/device" + DefaultPostDevicePath = "/oauth2/fallbacks/device/done" + DefaultLogoutPath = "/oauth2/fallbacks/logout" + DefaultErrorPath = "/oauth2/fallbacks/error" + TokenPath = "/oauth2/token" // #nosec G101 + AuthPath = "/oauth2/auth" + LogoutPath = "/oauth2/sessions/logout" VerifiableCredentialsPath = "/credentials" UserinfoPath = "/userinfo" @@ -111,6 +112,7 @@ func (h *Handler) SetRoutes(admin *httprouterx.RouterAdmin, public *httprouterx. http.StatusOK, config.KeyLogoutRedirectURL, )) + public.GET(DefaultDeviceVerificationPath, h.fallbackHandler("", "", http.StatusOK, config.KeyDeviceVerificationURL)) public.GET(DefaultPostDevicePath, h.fallbackHandler( "You successfully authenticated on your device!", "The Default Post Device URL is not set which is why you are seeing this fallback page. Your device login request however succeeded.", diff --git a/oauth2/oauth2_device_code_test.go b/oauth2/oauth2_device_code_test.go index fbb71f02c11..6fd21bca9fc 100644 --- a/oauth2/oauth2_device_code_test.go +++ b/oauth2/oauth2_device_code_test.go @@ -323,7 +323,6 @@ func TestDeviceCodeWithDefaultStrategy(t *testing.T) { assert.EqualValues(t, c.LogoURI, pointerx.Deref(rr.Client.LogoUri)) assert.EqualValues(t, subject, pointerx.Deref(rr.Subject)) assert.EqualValues(t, scopes, rr.RequestedScope) - assert.EqualValues(t, r.URL.Query().Get("consent_challenge"), rr.Challenge) assert.Contains(t, *rr.RequestUrl, hydraoauth2.DeviceVerificationPath) if checkRequestPayload != nil { checkRequestPayload(rr) diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0001.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0001.json index 1431c94066c..1611399b0d1 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0001.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0001.json @@ -29,7 +29,6 @@ "valid": false }, "la": null, - "da": null, "du": null, "dh": null, "cc": "challenge-0001", diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0002.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0002.json index e454e282434..6689bda2fb5 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0002.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0002.json @@ -30,7 +30,6 @@ "valid": false }, "la": null, - "da": null, "du": null, "dh": null, "cc": "challenge-0002", diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0003.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0003.json index aa4d250f5c2..8214625e79d 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0003.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0003.json @@ -31,7 +31,6 @@ "valid": false }, "la": null, - "da": null, "du": null, "dh": null, "cc": "challenge-0003", diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0004.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0004.json index c95e9dd963c..2b401d6f02e 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0004.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0004.json @@ -34,7 +34,6 @@ "valid": false }, "la": null, - "da": null, "du": null, "dh": null, "cc": "challenge-0004", diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0005.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0005.json index 14fa4483bdd..d7c7c9eeb2e 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0005.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0005.json @@ -34,7 +34,6 @@ "valid": false }, "la": null, - "da": null, "du": null, "dh": null, "cc": "challenge-0005", diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0006.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0006.json index 12157ef0300..6c6f67e1925 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0006.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0006.json @@ -34,7 +34,6 @@ "valid": false }, "la": null, - "da": null, "du": null, "dh": null, "cc": "challenge-0006", diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0007.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0007.json index 9efbdcc49b3..927c4395df1 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0007.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0007.json @@ -34,7 +34,6 @@ "valid": false }, "la": null, - "da": null, "du": null, "dh": null, "cc": "challenge-0007", diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0008.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0008.json index b240dce7127..4eafe51c612 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0008.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0008.json @@ -36,7 +36,6 @@ "valid": false }, "la": null, - "da": null, "du": null, "dh": null, "cc": "challenge-0008", diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0009.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0009.json index 1887b28b1f1..ba284a35de7 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0009.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0009.json @@ -36,7 +36,6 @@ "valid": false }, "la": null, - "da": null, "du": null, "dh": null, "cc": "challenge-0009", diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0010.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0010.json index 06922c8709f..2c620ceb353 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0010.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0010.json @@ -36,7 +36,6 @@ "valid": false }, "la": null, - "da": null, "du": null, "dh": null, "cc": "challenge-0010", diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0011.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0011.json index 8298eea26c1..fe02c17b38c 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0011.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0011.json @@ -36,7 +36,6 @@ "valid": false }, "la": null, - "da": null, "du": null, "dh": null, "cc": "challenge-0011", diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0012.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0012.json index 689bf6cec8f..908b8d6d6f5 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0012.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0012.json @@ -36,7 +36,6 @@ "valid": false }, "la": null, - "da": null, "du": null, "dh": null, "cc": "challenge-0012", diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0013.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0013.json index 5c7db729136..367e83e24ed 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0013.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0013.json @@ -36,7 +36,6 @@ "valid": false }, "la": null, - "da": null, "du": null, "dh": null, "cc": "challenge-0013", diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0014.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0014.json index 596894f09d9..55ab3eef0df 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0014.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0014.json @@ -36,7 +36,6 @@ "valid": false }, "la": null, - "da": null, "du": null, "dh": null, "cc": "challenge-0014", diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0015.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0015.json index be20015e244..433ce5c8763 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0015.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0015.json @@ -42,7 +42,6 @@ "valid": false }, "la": null, - "da": null, "du": null, "dh": null, "cc": "challenge-0015", diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0016.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0016.json index 5e8d25b2c76..0c576c04a34 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0016.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0016.json @@ -43,7 +43,6 @@ "valid": false }, "la": null, - "da": null, "du": null, "dh": null, "cc": "challenge-0016", diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0017.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0017.json index 1e26b6038b3..bf289f469cc 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0017.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0017.json @@ -44,7 +44,6 @@ "valid": false }, "la": null, - "da": null, "du": null, "dh": null, "cc": "challenge-0017", diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0018.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0018.json index b21344dfcbf..210d63747c2 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0018.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0018.json @@ -42,7 +42,6 @@ "dr": "request-0018", "dv": "verifier-0018", "dc": "csrf-0018", - "da": "0001-01-01T00:00:00Z", "du": true, "dh": "0001-01-01T00:00:00Z", "de": null, diff --git a/persistence/sql/migrations/20241609000001000000_device_flow.down.sql b/persistence/sql/migrations/20241609000001000000_device_flow.down.sql index 5e38432e890..d7a243c77e6 100644 --- a/persistence/sql/migrations/20241609000001000000_device_flow.down.sql +++ b/persistence/sql/migrations/20241609000001000000_device_flow.down.sql @@ -8,7 +8,6 @@ ALTER TABLE hydra_oauth2_flow DROP COLUMN IF EXISTS device_challenge_id; ALTER TABLE hydra_oauth2_flow DROP COLUMN IF EXISTS device_code_request_id; ALTER TABLE hydra_oauth2_flow DROP COLUMN IF EXISTS device_verifier; ALTER TABLE hydra_oauth2_flow DROP COLUMN IF EXISTS device_csrf; -ALTER TABLE hydra_oauth2_flow DROP COLUMN IF EXISTS device_user_code_accepted_at; ALTER TABLE hydra_oauth2_flow DROP COLUMN IF EXISTS device_was_used; ALTER TABLE hydra_oauth2_flow DROP COLUMN IF EXISTS device_handled_at; ALTER TABLE hydra_oauth2_flow DROP COLUMN IF EXISTS device_error; diff --git a/persistence/sql/migrations/20241609000001000000_device_flow.mysql.up.sql b/persistence/sql/migrations/20241609000001000000_device_flow.mysql.up.sql index a4721b83975..89ae76d125f 100644 --- a/persistence/sql/migrations/20241609000001000000_device_flow.mysql.up.sql +++ b/persistence/sql/migrations/20241609000001000000_device_flow.mysql.up.sql @@ -8,7 +8,7 @@ CREATE TABLE IF NOT EXISTS hydra_oauth2_device_auth_codes scope VARCHAR(1024) NOT NULL, granted_scope VARCHAR(1024) NOT NULL, form_data VARCHAR(4096) NOT NULL, - session_data VARCHAR(8192) NOT NULL, + session_data TEXT NOT NULL, subject VARCHAR(255) NOT NULL DEFAULT '', device_code_active BOOL NOT NULL DEFAULT true, user_code_state SMALLINT NOT NULL DEFAULT 0, @@ -37,8 +37,6 @@ ALTER TABLE hydra_oauth2_flow ADD COLUMN device_verifier VARCHAR(40) NULL; ALTER TABLE hydra_oauth2_flow ADD COLUMN device_csrf VARCHAR(40) NULL; -ALTER TABLE hydra_oauth2_flow - ADD COLUMN device_user_code_accepted_at TIMESTAMP NULL; ALTER TABLE hydra_oauth2_flow ADD COLUMN device_was_used BOOL NULL; ALTER TABLE hydra_oauth2_flow diff --git a/persistence/sql/migrations/20241609000001000000_device_flow.sqlite.up.sql b/persistence/sql/migrations/20241609000001000000_device_flow.sqlite.up.sql index dc61ee00578..d6fe883f137 100644 --- a/persistence/sql/migrations/20241609000001000000_device_flow.sqlite.up.sql +++ b/persistence/sql/migrations/20241609000001000000_device_flow.sqlite.up.sql @@ -8,7 +8,7 @@ CREATE TABLE IF NOT EXISTS hydra_oauth2_device_auth_codes scope VARCHAR(1024) NOT NULL, granted_scope VARCHAR(1024) NOT NULL, form_data VARCHAR(4096) NOT NULL, - session_data VARCHAR(8192) NOT NULL, + session_data TEXT NOT NULL, subject VARCHAR(255) NOT NULL DEFAULT '', device_code_active BOOL NOT NULL DEFAULT true, user_code_state SMALLINT NOT NULL DEFAULT 0, @@ -37,8 +37,6 @@ ALTER TABLE hydra_oauth2_flow ADD COLUMN device_verifier VARCHAR(40) NULL; ALTER TABLE hydra_oauth2_flow ADD COLUMN device_csrf VARCHAR(40) NULL; -ALTER TABLE hydra_oauth2_flow - ADD COLUMN device_user_code_accepted_at TIMESTAMP NULL; ALTER TABLE hydra_oauth2_flow ADD COLUMN device_was_used BOOLEAN NULL; ALTER TABLE hydra_oauth2_flow diff --git a/persistence/sql/migrations/20241609000001000000_device_flow.up.sql b/persistence/sql/migrations/20241609000001000000_device_flow.up.sql index a607701d0fd..0e946b5d0d2 100644 --- a/persistence/sql/migrations/20241609000001000000_device_flow.up.sql +++ b/persistence/sql/migrations/20241609000001000000_device_flow.up.sql @@ -8,7 +8,7 @@ CREATE TABLE IF NOT EXISTS hydra_oauth2_device_auth_codes scope VARCHAR(1024) NOT NULL, granted_scope VARCHAR(1024) NOT NULL, form_data VARCHAR(4096) NOT NULL, - session_data VARCHAR(8192) NOT NULL, + session_data TEXT NOT NULL, subject VARCHAR(255) NOT NULL DEFAULT '', device_code_active BOOL NOT NULL DEFAULT true, user_code_state SMALLINT NOT NULL DEFAULT 0, @@ -37,8 +37,6 @@ ALTER TABLE hydra_oauth2_flow ADD COLUMN device_verifier VARCHAR(40) NULL; ALTER TABLE hydra_oauth2_flow ADD COLUMN device_csrf VARCHAR(40) NULL; -ALTER TABLE hydra_oauth2_flow - ADD COLUMN device_user_code_accepted_at TIMESTAMP NULL; ALTER TABLE hydra_oauth2_flow ADD COLUMN device_was_used BOOLEAN NULL; ALTER TABLE hydra_oauth2_flow diff --git a/persistence/sql/persister_consent.go b/persistence/sql/persister_consent.go index a69e8cc7297..76ef0c9096c 100644 --- a/persistence/sql/persister_consent.go +++ b/persistence/sql/persister_consent.go @@ -294,33 +294,36 @@ func (p *Persister) VerifyAndInvalidateDeviceUserAuthRequest(ctx context.Context return f.GetHandledDeviceUserAuthRequest(), nil } -func (p *Persister) CreateLoginRequest(ctx context.Context, f *flow.Flow, req *flow.LoginRequest) (_ *flow.Flow, err error) { +func (p *Persister) CreateLoginRequestFromDeviceRequest(ctx context.Context, f *flow.Flow, req *flow.LoginRequest) (_ *flow.Flow, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateLoginRequestFromDeviceRequest") + defer otelx.End(span, &err) + + f.ID = req.ID + f.LoginSkip = req.Skip + f.Subject = req.Subject + f.SessionID = req.SessionID + f.LoginWasUsed = req.WasHandled + f.ForceSubjectIdentifier = req.ForceSubjectIdentifier + f.LoginVerifier = req.Verifier + f.LoginCSRF = req.CSRF + f.LoginAuthenticatedAt = req.AuthenticatedAt + f.RequestedAt = req.RequestedAt + f.State = flow.FlowStateLoginInitialized + + nid := p.NetworkID(ctx) + if nid == uuid.Nil { + return nil, errorsx.WithStack(x.ErrNotFound) + } + f.NID = nid + + return f, nil +} + +func (p *Persister) CreateLoginRequest(ctx context.Context, req *flow.LoginRequest) (_ *flow.Flow, err error) { ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateLoginRequest") defer otelx.End(span, &err) - // TODO - this is quite confusing - if f is not nil we re-create the flow object with the only - // TODO difference being f.State? - if f == nil { - f = flow.NewFlow(req) - } else { - f.ID = req.ID - f.RequestedScope = req.RequestedScope - f.RequestedAudience = req.RequestedAudience - f.LoginSkip = req.Skip - f.Subject = req.Subject - f.OpenIDConnectContext = req.OpenIDConnectContext - f.Client = req.Client - f.ClientID = req.ClientID - f.RequestURL = req.RequestURL - f.SessionID = req.SessionID - f.LoginWasUsed = req.WasHandled - f.ForceSubjectIdentifier = req.ForceSubjectIdentifier - f.LoginVerifier = req.Verifier - f.LoginCSRF = req.CSRF - f.LoginAuthenticatedAt = req.AuthenticatedAt - f.RequestedAt = req.RequestedAt - f.State = flow.FlowStateLoginInitialized - } + f := flow.NewFlow(req) nid := p.NetworkID(ctx) if nid == uuid.Nil { diff --git a/persistence/sql/persister_nid_test.go b/persistence/sql/persister_nid_test.go index cdab3d05b30..108ae42e7cc 100644 --- a/persistence/sql/persister_nid_test.go +++ b/persistence/sql/persister_nid_test.go @@ -437,7 +437,7 @@ func (s *PersisterTestSuite) TestCreateLoginRequest() { lr := flow.LoginRequest{ID: "lr-id", ClientID: client.ID, RequestedAt: time.Now()} require.NoError(t, r.Persister().CreateClient(s.t1, client)) - f, err := r.ConsentManager().CreateLoginRequest(s.t1, nil, &lr) + f, err := r.ConsentManager().CreateLoginRequest(s.t1, &lr) require.NoError(t, err) require.Equal(t, s.t1NID, f.NID) }) @@ -1219,7 +1219,7 @@ func (s *PersisterTestSuite) TestGetLoginRequest() { lr := flow.LoginRequest{ID: "lr-id", ClientID: client.ID, RequestedAt: time.Now()} require.NoError(t, r.Persister().CreateClient(s.t1, client)) - f, err := r.ConsentManager().CreateLoginRequest(s.t1, nil, &lr) + f, err := r.ConsentManager().CreateLoginRequest(s.t1, &lr) require.NoError(t, err) require.Equal(t, s.t1NID, f.NID) diff --git a/spec/config.json b/spec/config.json index c212f4b2637..275e1a1110a 100644 --- a/spec/config.json +++ b/spec/config.json @@ -1181,7 +1181,7 @@ } ], "default": "5s", - "description": "configure how often a non-interactive device should poll the device token endpoint", + "description": "Configures how often a non-interactive device should poll the device token endpoint, this is a purely informational configuration and does not enforce rate-limiting.", "examples": ["5s", "15s", "1m"] }, "user_code_entropy": { From df22c9ed91b808e3895b652c230f31b065b5d1c2 Mon Sep 17 00:00:00 2001 From: Nikos Date: Wed, 12 Feb 2025 11:50:30 +0100 Subject: [PATCH 51/52] fix: remove device session when used --- persistence/sql/persister_device.go | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/persistence/sql/persister_device.go b/persistence/sql/persister_device.go index dbbcbfe1d08..0bf9d8ef74d 100644 --- a/persistence/sql/persister_device.go +++ b/persistence/sql/persister_device.go @@ -257,14 +257,9 @@ func (p *Persister) InvalidateDeviceCodeSession(ctx context.Context, signature s /* #nosec G201 table is static */ return sqlcon.HandleError( - p.Connection(ctx). - RawQuery( - fmt.Sprintf("UPDATE %s SET device_code_active=false WHERE device_code_signature=? AND nid = ?", sqlTableDeviceAuthCodes), - signature, - p.NetworkID(ctx), - ). - Exec(), - ) + p.QueryWithNetwork(ctx). + Where("device_code_signature = ?", signature). + Delete(DeviceRequestSQL{})) } // GetUserCodeSession returns a user code session from the database From ee0738f97881ee9aaea110f046c7b9b91b02616e Mon Sep 17 00:00:00 2001 From: Nikos Date: Thu, 13 Feb 2025 10:29:49 +0100 Subject: [PATCH 52/52] chore: add cypress tests --- cypress/integration/oauth2/device_auth.js | 118 ++ cypress/support/commands.js | 87 ++ test/e2e/circle-ci.bash | 2 + test/e2e/oauth2-client/package-lock.json | 1527 +++++++++------------ test/e2e/oauth2-client/package.json | 2 +- test/e2e/oauth2-client/src/index.js | 81 ++ 6 files changed, 945 insertions(+), 872 deletions(-) create mode 100644 cypress/integration/oauth2/device_auth.js diff --git a/cypress/integration/oauth2/device_auth.js b/cypress/integration/oauth2/device_auth.js new file mode 100644 index 00000000000..4e26abde8a0 --- /dev/null +++ b/cypress/integration/oauth2/device_auth.js @@ -0,0 +1,118 @@ +// Copyright © 2022 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { prng } from "../../helpers" + +const accessTokenStrategies = ["opaque", "jwt"] + +describe("The OAuth 2.0 Device Authorization Grant", function () { + accessTokenStrategies.forEach((accessTokenStrategy) => { + describe("access_token_strategy=" + accessTokenStrategy, function () { + const nc = (extradata) => ({ + client_secret: prng(), + scope: "offline_access openid", + subject_type: "public", + token_endpoint_auth_method: "client_secret_basic", + grant_types: [ + "urn:ietf:params:oauth:grant-type:device_code", + "refresh_token", + ], + access_token_strategy: accessTokenStrategy, + ...extradata, + }) + + it("should return an Access, Refresh, and ID Token when scope offline_access and openid are granted", function () { + const client = nc() + cy.deviceAuthFlow(client, { + consent: { scope: ["offline_access", "openid"] }, + }) + + cy.postDeviceAuthFlow().then((resp) => { + const { + result, + token: { access_token, id_token, refresh_token }, + } = resp.body + + expect(result).to.equal("success") + expect(access_token).to.not.be.empty + expect(id_token).to.not.be.empty + expect(refresh_token).to.not.be.empty + }) + }) + + it("should return an Access and Refresh Token when scope offline_access is granted", function () { + const client = nc() + cy.deviceAuthFlow(client, { consent: { scope: ["offline_access"] } }) + + cy.postDeviceAuthFlow().then((resp) => { + console.log(resp) + const { + result, + token: { access_token, id_token, refresh_token }, + } = resp.body + + expect(result).to.equal("success") + expect(access_token).to.not.be.empty + expect(id_token).to.be.undefined + expect(refresh_token).to.not.be.empty + }) + }) + + it("should return an Access and ID Token when scope offline_access is granted", function () { + const client = nc() + cy.deviceAuthFlow(client, { consent: { scope: ["openid"] } }) + + cy.postDeviceAuthFlow().then((resp) => { + console.log(resp) + const { + result, + token: { access_token, id_token, refresh_token }, + } = resp.body + + expect(result).to.equal("success") + expect(access_token).to.not.be.empty + expect(id_token).to.not.be.empty + expect(refresh_token).to.be.undefined + }) + }) + + it("should return an Access Token when no scope is granted", function () { + const client = nc() + cy.deviceAuthFlow(client, { consent: { scope: [] } }) + + cy.postDeviceAuthFlow().then((resp) => { + console.log(resp) + const { + result, + token: { access_token, id_token, refresh_token }, + } = resp.body + + expect(result).to.equal("success") + expect(access_token).to.not.be.empty + expect(id_token).to.be.undefined + expect(refresh_token).to.be.undefined + }) + }) + + it("should skip consent if the client is confgured thus", function () { + const client = nc({ skip_consent: true }) + cy.deviceAuthFlow(client, { + consent: { scope: ["offline_access", "openid"], skip: true }, + }) + + cy.postDeviceAuthFlow().then((resp) => { + console.log(resp) + const { + result, + token: { access_token, id_token, refresh_token }, + } = resp.body + + expect(result).to.equal("success") + expect(access_token).to.not.be.empty + expect(id_token).to.not.be.empty + expect(refresh_token).to.not.be.empty + }) + }) + }) + }) +}) diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 2f75293404d..0e8700177dc 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -216,3 +216,90 @@ Cypress.Commands.add("refreshTokenBrowser", (client, token) => failOnStatusCode: false, }), ) + +Cypress.Commands.add( + "deviceAuthFlow", + ( + client, + { + override: { scope, client_id, client_secret } = {}, + consent: { + accept: acceptConsent = true, + skip: skipConsent = false, + remember: rememberConsent = false, + scope: acceptScope = [], + } = {}, + login: { + accept: acceptLogin = true, + skip: skipLogin = false, + remember: rememberLogin = false, + username = "foo@bar.com", + password = "foobar", + } = {}, + prompt = "", + createClient: doCreateClient = true, + } = {}, + path = "oauth2", + ) => { + const run = (client) => { + cy.visit( + `${Cypress.env("client_url")}/${path}/device?client_id=${ + client_id || client.client_id + }&client_secret=${client_secret || client.client_secret}&scope=${ + scope || client.scope + }`, + { failOnStatusCode: false }, + ) + + cy.get("#verify").click() + + if (!skipLogin) { + cy.get("#email").type(username, { delay: 1 }) + cy.get("#password").type(password, { delay: 1 }) + + if (rememberLogin) { + cy.get("#remember").click() + } + + if (acceptLogin) { + cy.get("#accept").click() + } else { + cy.get("#reject").click() + } + } + + if (!skipConsent) { + acceptScope.forEach((s) => { + cy.get(`#${s}`).click() + }) + + if (rememberConsent) { + cy.get("#remember").click() + } + + if (acceptConsent) { + cy.get("#accept").click() + } else { + cy.get("#reject").click() + } + + cy.location().should((loc) => { + expect(loc.origin).to.eq(Cypress.env("consent_url")) + expect(loc.pathname).to.eq("/oauth2/device/success") + }) + } + } + + if (doCreateClient) { + createClient(client).should((client) => { + run(client) + }) + return + } + run(client) + }, +) + +Cypress.Commands.add("postDeviceAuthFlow", (path = "oauth2") => + cy.request(`${Cypress.env("client_url")}/${path}/device/success`), +) diff --git a/test/e2e/circle-ci.bash b/test/e2e/circle-ci.bash index 03b8f70ac79..bad46d4adcf 100755 --- a/test/e2e/circle-ci.bash +++ b/test/e2e/circle-ci.bash @@ -38,6 +38,8 @@ fi (cd oauth2-client; PORT=5002 HYDRA_ADMIN_URL=http://127.0.0.1:5001 npm run consent > ../login-consent-logout.e2e.log 2>&1 &) export URLS_SELF_ISSUER=http://127.0.0.1:5004/ +export URLS_DEVICE_VERIFICATION=http://127.0.0.1:5002/device/verify +export URLS_DEVICE_SUCCESS=http://127.0.0.1:5002/oauth2/device/success export URLS_CONSENT=http://127.0.0.1:5002/consent export URLS_LOGIN=http://127.0.0.1:5002/login export URLS_LOGOUT=http://127.0.0.1:5002/logout diff --git a/test/e2e/oauth2-client/package-lock.json b/test/e2e/oauth2-client/package-lock.json index ffbd7675356..b208455fa0a 100644 --- a/test/e2e/oauth2-client/package-lock.json +++ b/test/e2e/oauth2-client/package-lock.json @@ -13,7 +13,7 @@ "express": "^4.21.2", "express-session": "^1.17.0", "express-winston": "^3.4.0", - "hydra-login-consent-logout": "2.0.4-pre.2", + "hydra-login-consent-logout": "2.4.0-pre.3", "jsonwebtoken": "^8.5.1", "jwks-rsa": "^2.1.4", "node-fetch": "^2.6.0", @@ -27,6 +27,52 @@ "nodemon": "^2.0.22" } }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.9.tgz", + "integrity": "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.9" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.9.tgz", + "integrity": "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@hapi/address": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz", @@ -66,13 +112,11 @@ "@hapi/hoek": "^8.3.0" } }, - "node_modules/@ory/client": { - "version": "0.2.0-alpha.60", - "resolved": "https://registry.npmjs.org/@ory/client/-/client-0.2.0-alpha.60.tgz", - "integrity": "sha512-fGovJ/xIl7dvJJP9/IL4Xu1yiOCy9pvmkfj2xnHZbPrIbL9c9tqVcC3CSlzBq6zJQZMC3XI7VmZ8uEQ+cF4suw==", - "dependencies": { - "axios": "^0.21.4" - } + "node_modules/@ory/hydra-client-fetch": { + "version": "2.4.0-alpha.1", + "resolved": "https://registry.npmjs.org/@ory/hydra-client-fetch/-/hydra-client-fetch-2.4.0-alpha.1.tgz", + "integrity": "sha512-TTuw+1DdIFskz4JU7yP2OSOHP3pVr7HjLnr8YI4S1pQJ91JaGwBQxCPQLrqdNuffs7d8JU1VRkcgdSWOo+89eA==", + "license": "Apache-2.0" }, "node_modules/@panva/asn1.js": { "version": "1.0.0", @@ -90,19 +134,6 @@ "node": ">=4" } }, - "node_modules/@types/babel-types": { - "version": "7.0.7", - "resolved": "https://registry.npmjs.org/@types/babel-types/-/babel-types-7.0.7.tgz", - "integrity": "sha512-dBtBbrc+qTHy1WdfHYjBwRln4+LWqASWakLHsWHR2NWHIFkv4W3O070IGoGLEBrJBvct3r0L1BUPuvURi7kYUQ==" - }, - "node_modules/@types/babylon": { - "version": "6.16.5", - "resolved": "https://registry.npmjs.org/@types/babylon/-/babylon-6.16.5.tgz", - "integrity": "sha512-xH2e58elpj1X4ynnKp9qSnWlsRTIs6n3tgLGNfwAGHwePw0mulHQllV34n0T25uYSu1k0hRKkWXF890B1yS47w==", - "dependencies": { - "@types/babel-types": "*" - } - }, "node_modules/@types/body-parser": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", @@ -226,28 +257,10 @@ } }, "node_modules/acorn": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", - "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-globals": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-3.1.0.tgz", - "integrity": "sha1-/YJw9x+7SZawBPqIDuXUZXOnMb8=", - "dependencies": { - "acorn": "^4.0.4" - } - }, - "node_modules/acorn-globals/node_modules/acorn": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", - "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=", + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -267,30 +280,6 @@ "node": ">=4" } }, - "node_modules/align-text": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", - "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", - "dependencies": { - "kind-of": "^3.0.2", - "longest": "^1.0.1", - "repeat-string": "^1.5.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/align-text/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", @@ -323,7 +312,14 @@ "node_modules/asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "license": "MIT" + }, + "node_modules/assert-never": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.4.0.tgz", + "integrity": "sha512-5oJg84os6NMQNl27T9LnZkvvqzvAnHu03ShCnoj6bsJwS7L8AO4lf+C/XjK/nvzEqQB744moC6V128RucQd1jA==", + "license": "MIT" }, "node_modules/async": { "version": "2.6.4", @@ -333,40 +329,16 @@ "lodash": "^4.17.14" } }, - "node_modules/axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", - "dependencies": { - "follow-redirects": "^1.14.0" - } - }, - "node_modules/babel-runtime": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", - "dependencies": { - "core-js": "^2.4.0", - "regenerator-runtime": "^0.11.0" - } - }, - "node_modules/babel-types": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", - "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "node_modules/babel-walk": { + "version": "3.0.0-canary-5", + "resolved": "https://registry.npmjs.org/babel-walk/-/babel-walk-3.0.0-canary-5.tgz", + "integrity": "sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==", + "license": "MIT", "dependencies": { - "babel-runtime": "^6.26.0", - "esutils": "^2.0.2", - "lodash": "^4.17.4", - "to-fast-properties": "^1.0.3" - } - }, - "node_modules/babylon": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", - "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", - "bin": { - "babylon": "bin/babylon.js" + "@babel/types": "^7.9.6" + }, + "engines": { + "node": ">= 10.0.0" } }, "node_modules/balanced-match": { @@ -557,16 +529,33 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/center-align": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", - "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", "dependencies": { - "align-text": "^0.1.3", - "lazy-cache": "^1.0.3" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", + "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/chalk": { @@ -585,7 +574,8 @@ "node_modules/character-parser": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz", - "integrity": "sha1-x84o821LzZdE5f/CxfzeHHMmH8A=", + "integrity": "sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==", + "license": "MIT", "dependencies": { "is-regex": "^1.0.3" } @@ -617,25 +607,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/clean-css": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz", - "integrity": "sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA==", - "dependencies": { - "source-map": "~0.6.0" - }, - "engines": { - "node": ">= 4.0" - } - }, - "node_modules/clean-css/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/clean-stack": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-1.3.0.tgz", @@ -644,16 +615,6 @@ "node": ">=4" } }, - "node_modules/cliui": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", - "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", - "dependencies": { - "center-align": "^0.1.1", - "right-align": "^0.1.1", - "wordwrap": "0.0.2" - } - }, "node_modules/clone-response": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", @@ -722,14 +683,13 @@ "dev": true }, "node_modules/constantinople": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-3.1.2.tgz", - "integrity": "sha512-yePcBqEFhLOqSBtwYOGGS1exHo/s1xjekXiinh4itpNQGCu4KA1euPh1fg07N2wMITZXQkBz75Ntdt1ctGZouw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-4.0.1.tgz", + "integrity": "sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==", + "license": "MIT", "dependencies": { - "@types/babel-types": "^7.0.0", - "@types/babylon": "^6.16.2", - "babel-types": "^6.26.0", - "babylon": "^6.18.0" + "@babel/parser": "^7.6.0", + "@babel/types": "^7.6.1" } }, "node_modules/content-disposition": { @@ -779,29 +739,32 @@ } }, "node_modules/cookie-parser": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.5.tgz", - "integrity": "sha512-f13bPUj/gG/5mDr+xLmSxxDsB9DQiTIfhJS/sqjrmfAWiAN+x2O4i/XguTL9yDZ+/IFDanJ+5x7hC4CXT9Tdzw==", + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", "dependencies": { - "cookie": "0.4.0", + "cookie": "0.7.2", "cookie-signature": "1.0.6" }, "engines": { "node": ">= 0.8.0" } }, + "node_modules/cookie-parser/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" }, - "node_modules/core-js": { - "version": "2.6.11", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz", - "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==", - "deprecated": "core-js@<3.4 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Please, upgrade your dependencies to the actual version of core-js.", - "hasInstallScript": true - }, "node_modules/core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -898,14 +861,6 @@ "ms": "2.0.0" } }, - "node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/decode-uri-component": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", @@ -971,7 +926,8 @@ "node_modules/doctypes": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz", - "integrity": "sha1-6oCxBqh1OHdOijpKWv4pPeSJ4Kk=" + "integrity": "sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==", + "license": "MIT" }, "node_modules/dotenv": { "version": "7.0.0", @@ -981,6 +937,20 @@ "node": ">=6" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/duplexer3": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", @@ -1021,12 +991,10 @@ "integrity": "sha512-zoB603vQReOFvTg5xMl9I1P2PnHsHQQKTEowsKKD7nseUfJq6UWzK+4YtlWUO1nhiQUxe6XMkk+JleSZD1NZFA==" }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -1039,6 +1007,18 @@ "node": ">= 0.4" } }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es6-promise": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", @@ -1057,14 +1037,6 @@ "node": ">=0.8.0" } }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -1281,25 +1253,6 @@ "node": ">= 0.8" } }, - "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -1370,15 +1323,21 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -1387,6 +1346,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", @@ -1408,27 +1380,17 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dependencies": { - "get-intrinsic": "^1.1.3" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -1448,17 +1410,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/has-symbol-support-x": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz", @@ -1468,9 +1419,10 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -1489,6 +1441,21 @@ "node": "*" } }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -1556,27 +1523,27 @@ } }, "node_modules/hydra-login-consent-logout": { - "version": "2.0.4-pre.2", - "resolved": "https://registry.npmjs.org/hydra-login-consent-logout/-/hydra-login-consent-logout-2.0.4-pre.2.tgz", - "integrity": "sha512-nB3JKffjiTyQZzr0DPdkdoUAg7mPlNTv7c/jZrC5IrIyodc3X4s16LzcZJcs/e2U3pZyu3CoWGUrnF//wPzmqQ==", + "version": "2.4.0-pre.3", + "resolved": "https://registry.npmjs.org/hydra-login-consent-logout/-/hydra-login-consent-logout-2.4.0-pre.3.tgz", + "integrity": "sha512-Dtoop55BOQ/z+DQunVblxcFC5IMLw9NOw4vqTm2WmvMn7TjWwCfq+gA03ifGPAiFuqbyprxUnUaILCSJn9Hqdg==", "dependencies": { - "@ory/client": "^0.2.0-alpha.24", + "@ory/hydra-client-fetch": "^2.4.0-alpha.1", "@types/cookie-parser": "^1.4.2", "@types/csurf": "^1.9.36", "@types/express": "^4.17.7", "@types/morgan": "^1.9.1", "@types/url-join": "^4.0.0", - "body-parser": "^1.19.0", - "cookie-parser": "^1.4.5", + "body-parser": "^1.20.3", + "cookie-parser": "^1.4.7", "csurf": "^1.11.0", "debug": "^4.1.1", - "express": "^4.17.1", + "express": "^4.21.2", "morgan": "^1.10.0", "node-fetch": "^2.6.7", - "pug": "^2.0.4", + "pug": "^3.0.3", "querystring": "^0.2.0", "serve-favicon": "^2.5.0", - "typescript": "^3.7.5", + "typescript": "^5.7.3", "url-join": "^4.0.1" }, "bin": { @@ -1669,29 +1636,29 @@ "node": ">=8" } }, - "node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" - }, - "node_modules/is-expression": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-3.0.0.tgz", - "integrity": "sha1-Oayqa+f9HzRx3ELHQW5hwkMXrJ8=", + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", "dependencies": { - "acorn": "~4.0.2", - "object-assign": "^4.0.1" - } - }, - "node_modules/is-expression/node_modules/acorn": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", - "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=", - "bin": { - "acorn": "bin/acorn" + "hasown": "^2.0.2" }, "engines": { - "node": ">=0.4.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-expression": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-4.0.0.tgz", + "integrity": "sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==", + "license": "MIT", + "dependencies": { + "acorn": "^7.1.1", + "object-assign": "^4.1.1" } }, "node_modules/is-extglob": { @@ -1738,16 +1705,21 @@ } }, "node_modules/is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", - "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "license": "MIT" }, "node_modules/is-regex": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", - "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "license": "MIT", "dependencies": { - "has": "^1.0.3" + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -1812,7 +1784,8 @@ "node_modules/js-stringify": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz", - "integrity": "sha1-Fzb939lyTyijaCrcYjCufk6Weds=" + "integrity": "sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==", + "license": "MIT" }, "node_modules/json-buffer": { "version": "3.0.0", @@ -1848,7 +1821,8 @@ "node_modules/jstransformer": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-1.0.0.tgz", - "integrity": "sha1-7Yvwkh4vPx7U1cGkT2hwntJHIsM=", + "integrity": "sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==", + "license": "MIT", "dependencies": { "is-promise": "^2.0.0", "promise": "^7.0.1" @@ -1926,14 +1900,6 @@ "colornames": "^1.1.1" } }, - "node_modules/lazy-cache": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", - "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/limiter": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", @@ -2006,14 +1972,6 @@ "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" }, - "node_modules/longest": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", - "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/lowercase-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", @@ -2040,6 +1998,15 @@ "lru-cache": "~4.0.0" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -2527,9 +2494,10 @@ } }, "node_modules/path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" }, "node_modules/path-to-regexp": { "version": "0.1.12", @@ -2573,6 +2541,7 @@ "version": "7.3.1", "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "license": "MIT", "dependencies": { "asap": "~2.0.3" } @@ -2601,118 +2570,128 @@ "dev": true }, "node_modules/pug": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pug/-/pug-2.0.4.tgz", - "integrity": "sha512-XhoaDlvi6NIzL49nu094R2NA6P37ijtgMDuWE+ofekDChvfKnzFal60bhSdiy8y2PBO6fmz3oMEIcfpBVRUdvw==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pug/-/pug-3.0.3.tgz", + "integrity": "sha512-uBi6kmc9f3SZ3PXxqcHiUZLmIXgfgWooKWXcwSGwQd2Zi5Rb0bT14+8CJjJgI8AB+nndLaNgHGrcc6bPIB665g==", + "license": "MIT", "dependencies": { - "pug-code-gen": "^2.0.2", - "pug-filters": "^3.1.1", - "pug-lexer": "^4.1.0", - "pug-linker": "^3.0.6", - "pug-load": "^2.0.12", - "pug-parser": "^5.0.1", - "pug-runtime": "^2.0.5", - "pug-strip-comments": "^1.0.4" + "pug-code-gen": "^3.0.3", + "pug-filters": "^4.0.0", + "pug-lexer": "^5.0.1", + "pug-linker": "^4.0.0", + "pug-load": "^3.0.0", + "pug-parser": "^6.0.0", + "pug-runtime": "^3.0.1", + "pug-strip-comments": "^2.0.0" } }, "node_modules/pug-attrs": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pug-attrs/-/pug-attrs-2.0.4.tgz", - "integrity": "sha512-TaZ4Z2TWUPDJcV3wjU3RtUXMrd3kM4Wzjbe3EWnSsZPsJ3LDI0F3yCnf2/W7PPFF+edUFQ0HgDL1IoxSz5K8EQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pug-attrs/-/pug-attrs-3.0.0.tgz", + "integrity": "sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==", + "license": "MIT", "dependencies": { - "constantinople": "^3.0.1", - "js-stringify": "^1.0.1", - "pug-runtime": "^2.0.5" + "constantinople": "^4.0.1", + "js-stringify": "^1.0.2", + "pug-runtime": "^3.0.0" } }, "node_modules/pug-code-gen": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-2.0.3.tgz", - "integrity": "sha512-r9sezXdDuZJfW9J91TN/2LFbiqDhmltTFmGpHTsGdrNGp3p4SxAjjXEfnuK2e4ywYsRIVP0NeLbSAMHUcaX1EA==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-3.0.3.tgz", + "integrity": "sha512-cYQg0JW0w32Ux+XTeZnBEeuWrAY7/HNE6TWnhiHGnnRYlCgyAUPoyh9KzCMa9WhcJlJ1AtQqpEYHc+vbCzA+Aw==", + "license": "MIT", "dependencies": { - "constantinople": "^3.1.2", + "constantinople": "^4.0.1", "doctypes": "^1.1.0", - "js-stringify": "^1.0.1", - "pug-attrs": "^2.0.4", - "pug-error": "^1.3.3", - "pug-runtime": "^2.0.5", - "void-elements": "^2.0.1", - "with": "^5.0.0" + "js-stringify": "^1.0.2", + "pug-attrs": "^3.0.0", + "pug-error": "^2.1.0", + "pug-runtime": "^3.0.1", + "void-elements": "^3.1.0", + "with": "^7.0.0" } }, "node_modules/pug-error": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-1.3.3.tgz", - "integrity": "sha512-qE3YhESP2mRAWMFJgKdtT5D7ckThRScXRwkfo+Erqga7dyJdY3ZquspprMCj/9sJ2ijm5hXFWQE/A3l4poMWiQ==" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-2.1.0.tgz", + "integrity": "sha512-lv7sU9e5Jk8IeUheHata6/UThZ7RK2jnaaNztxfPYUY+VxZyk/ePVaNZ/vwmH8WqGvDz3LrNYt/+gA55NDg6Pg==", + "license": "MIT" }, "node_modules/pug-filters": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-3.1.1.tgz", - "integrity": "sha512-lFfjNyGEyVWC4BwX0WyvkoWLapI5xHSM3xZJFUhx4JM4XyyRdO8Aucc6pCygnqV2uSgJFaJWW3Ft1wCWSoQkQg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-4.0.0.tgz", + "integrity": "sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==", + "license": "MIT", "dependencies": { - "clean-css": "^4.1.11", - "constantinople": "^3.0.1", + "constantinople": "^4.0.1", "jstransformer": "1.0.0", - "pug-error": "^1.3.3", - "pug-walk": "^1.1.8", - "resolve": "^1.1.6", - "uglify-js": "^2.6.1" + "pug-error": "^2.0.0", + "pug-walk": "^2.0.0", + "resolve": "^1.15.1" } }, "node_modules/pug-lexer": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-4.1.0.tgz", - "integrity": "sha512-i55yzEBtjm0mlplW4LoANq7k3S8gDdfC6+LThGEvsK4FuobcKfDAwt6V4jKPH9RtiE3a2Akfg5UpafZ1OksaPA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-5.0.1.tgz", + "integrity": "sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w==", + "license": "MIT", "dependencies": { - "character-parser": "^2.1.1", - "is-expression": "^3.0.0", - "pug-error": "^1.3.3" + "character-parser": "^2.2.0", + "is-expression": "^4.0.0", + "pug-error": "^2.0.0" } }, "node_modules/pug-linker": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-3.0.6.tgz", - "integrity": "sha512-bagfuHttfQOpANGy1Y6NJ+0mNb7dD2MswFG2ZKj22s8g0wVsojpRlqveEQHmgXXcfROB2RT6oqbPYr9EN2ZWzg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-4.0.0.tgz", + "integrity": "sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==", + "license": "MIT", "dependencies": { - "pug-error": "^1.3.3", - "pug-walk": "^1.1.8" + "pug-error": "^2.0.0", + "pug-walk": "^2.0.0" } }, "node_modules/pug-load": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/pug-load/-/pug-load-2.0.12.tgz", - "integrity": "sha512-UqpgGpyyXRYgJs/X60sE6SIf8UBsmcHYKNaOccyVLEuT6OPBIMo6xMPhoJnqtB3Q3BbO4Z3Bjz5qDsUWh4rXsg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pug-load/-/pug-load-3.0.0.tgz", + "integrity": "sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==", + "license": "MIT", "dependencies": { - "object-assign": "^4.1.0", - "pug-walk": "^1.1.8" + "object-assign": "^4.1.1", + "pug-walk": "^2.0.0" } }, "node_modules/pug-parser": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-5.0.1.tgz", - "integrity": "sha512-nGHqK+w07p5/PsPIyzkTQfzlYfuqoiGjaoqHv1LjOv2ZLXmGX1O+4Vcvps+P4LhxZ3drYSljjq4b+Naid126wA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-6.0.0.tgz", + "integrity": "sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==", + "license": "MIT", "dependencies": { - "pug-error": "^1.3.3", - "token-stream": "0.0.1" + "pug-error": "^2.0.0", + "token-stream": "1.0.0" } }, "node_modules/pug-runtime": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-2.0.5.tgz", - "integrity": "sha512-P+rXKn9un4fQY77wtpcuFyvFaBww7/91f3jHa154qU26qFAnOe6SW1CbIDcxiG5lLK9HazYrMCCuDvNgDQNptw==" + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-3.0.1.tgz", + "integrity": "sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg==", + "license": "MIT" }, "node_modules/pug-strip-comments": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-1.0.4.tgz", - "integrity": "sha512-i5j/9CS4yFhSxHp5iKPHwigaig/VV9g+FgReLJWWHEHbvKsbqL0oP/K5ubuLco6Wu3Kan5p7u7qk8A4oLLh6vw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-2.0.0.tgz", + "integrity": "sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==", + "license": "MIT", "dependencies": { - "pug-error": "^1.3.3" + "pug-error": "^2.0.0" } }, "node_modules/pug-walk": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-1.1.8.tgz", - "integrity": "sha512-GMu3M5nUL3fju4/egXwZO0XLi6fW/K3T3VTgFQ14GxNi8btlxgT5qZL//JwZFm/2Fa64J/PNS8AZeys3wiMkVA==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-2.0.0.tgz", + "integrity": "sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==", + "license": "MIT" }, "node_modules/qs": { "version": "6.13.0", @@ -2810,25 +2789,21 @@ "node": ">=8.10.0" } }, - "node_modules/regenerator-runtime": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" - }, - "node_modules/repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", - "engines": { - "node": ">=0.10" - } - }, "node_modules/resolve": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.16.0.tgz", - "integrity": "sha512-LarL/PIKJvc09k1jaeT4kQb/8/7P+qV4qSnN2K80AES+OHdfZELAKVOBjxsvtToT/uLOfFbvYvKfZmV8cee7nA==", + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "license": "MIT", "dependencies": { - "path-parse": "^1.0.6" + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2842,17 +2817,6 @@ "lowercase-keys": "^1.0.0" } }, - "node_modules/right-align": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", - "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", - "dependencies": { - "align-text": "^0.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/rndm": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz", @@ -3103,14 +3067,6 @@ "node": ">=4" } }, - "node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/stack-trace": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", @@ -3154,6 +3110,18 @@ "node": ">=4" } }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/text-hex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", @@ -3167,14 +3135,6 @@ "node": ">=0.10.0" } }, - "node_modules/to-fast-properties": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", - "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -3196,9 +3156,10 @@ } }, "node_modules/token-stream": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-0.0.1.tgz", - "integrity": "sha1-zu78cXp2xDFvEm0LnbqlXX598Bo=" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-1.0.0.tgz", + "integrity": "sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==", + "license": "MIT" }, "node_modules/touch": { "version": "3.1.0", @@ -3243,41 +3204,18 @@ } }, "node_modules/typescript": { - "version": "3.9.10", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz", - "integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==", + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" - } - }, - "node_modules/uglify-js": { - "version": "2.8.29", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", - "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", - "dependencies": { - "source-map": "~0.5.1", - "yargs": "~3.10.0" - }, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - }, - "optionalDependencies": { - "uglify-to-browserify": "~1.0.0" + "node": ">=14.17" } }, - "node_modules/uglify-to-browserify": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", - "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", - "optional": true - }, "node_modules/uid-safe": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", @@ -3338,9 +3276,10 @@ } }, "node_modules/void-elements": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", - "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -3371,14 +3310,6 @@ "which": "bin/which" } }, - "node_modules/window-size": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", - "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/winston": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/winston/-/winston-3.2.1.tgz", @@ -3433,20 +3364,18 @@ } }, "node_modules/with": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/with/-/with-5.1.1.tgz", - "integrity": "sha1-+k2qktrzLE6pTtRTyB8EaGtXXf4=", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/with/-/with-7.0.2.tgz", + "integrity": "sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==", + "license": "MIT", "dependencies": { - "acorn": "^3.1.0", - "acorn-globals": "^3.0.0" - } - }, - "node_modules/wordwrap": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", - "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", + "@babel/parser": "^7.9.6", + "@babel/types": "^7.9.6", + "assert-never": "^1.2.1", + "babel-walk": "3.0.0-canary-5" + }, "engines": { - "node": ">=0.4.0" + "node": ">= 10.0.0" } }, "node_modules/wreck": { @@ -3464,28 +3393,36 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" + } + }, + "dependencies": { + "@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==" }, - "node_modules/yargs": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", - "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", - "dependencies": { - "camelcase": "^1.0.2", - "cliui": "^2.1.0", - "decamelize": "^1.0.0", - "window-size": "0.1.0" + "@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==" + }, + "@babel/parser": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.9.tgz", + "integrity": "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==", + "requires": { + "@babel/types": "^7.26.9" } }, - "node_modules/yargs/node_modules/camelcase": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", - "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", - "engines": { - "node": ">=0.10.0" + "@babel/types": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.9.tgz", + "integrity": "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==", + "requires": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" } - } - }, - "dependencies": { + }, "@hapi/address": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz", @@ -3520,13 +3457,10 @@ "@hapi/hoek": "^8.3.0" } }, - "@ory/client": { - "version": "0.2.0-alpha.60", - "resolved": "https://registry.npmjs.org/@ory/client/-/client-0.2.0-alpha.60.tgz", - "integrity": "sha512-fGovJ/xIl7dvJJP9/IL4Xu1yiOCy9pvmkfj2xnHZbPrIbL9c9tqVcC3CSlzBq6zJQZMC3XI7VmZ8uEQ+cF4suw==", - "requires": { - "axios": "^0.21.4" - } + "@ory/hydra-client-fetch": { + "version": "2.4.0-alpha.1", + "resolved": "https://registry.npmjs.org/@ory/hydra-client-fetch/-/hydra-client-fetch-2.4.0-alpha.1.tgz", + "integrity": "sha512-TTuw+1DdIFskz4JU7yP2OSOHP3pVr7HjLnr8YI4S1pQJ91JaGwBQxCPQLrqdNuffs7d8JU1VRkcgdSWOo+89eA==" }, "@panva/asn1.js": { "version": "1.0.0", @@ -3538,19 +3472,6 @@ "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.7.0.tgz", "integrity": "sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==" }, - "@types/babel-types": { - "version": "7.0.7", - "resolved": "https://registry.npmjs.org/@types/babel-types/-/babel-types-7.0.7.tgz", - "integrity": "sha512-dBtBbrc+qTHy1WdfHYjBwRln4+LWqASWakLHsWHR2NWHIFkv4W3O070IGoGLEBrJBvct3r0L1BUPuvURi7kYUQ==" - }, - "@types/babylon": { - "version": "6.16.5", - "resolved": "https://registry.npmjs.org/@types/babylon/-/babylon-6.16.5.tgz", - "integrity": "sha512-xH2e58elpj1X4ynnKp9qSnWlsRTIs6n3tgLGNfwAGHwePw0mulHQllV34n0T25uYSu1k0hRKkWXF890B1yS47w==", - "requires": { - "@types/babel-types": "*" - } - }, "@types/body-parser": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", @@ -3671,24 +3592,9 @@ } }, "acorn": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", - "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=" - }, - "acorn-globals": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-3.1.0.tgz", - "integrity": "sha1-/YJw9x+7SZawBPqIDuXUZXOnMb8=", - "requires": { - "acorn": "^4.0.4" - }, - "dependencies": { - "acorn": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", - "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=" - } - } + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==" }, "aggregate-error": { "version": "1.0.0", @@ -3699,26 +3605,6 @@ "indent-string": "^3.0.0" } }, - "align-text": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", - "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", - "requires": { - "kind-of": "^3.0.2", - "longest": "^1.0.1", - "repeat-string": "^1.5.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, "ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", @@ -3745,7 +3631,12 @@ "asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" + }, + "assert-never": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.4.0.tgz", + "integrity": "sha512-5oJg84os6NMQNl27T9LnZkvvqzvAnHu03ShCnoj6bsJwS7L8AO4lf+C/XjK/nvzEqQB744moC6V128RucQd1jA==" }, "async": { "version": "2.6.4", @@ -3755,39 +3646,14 @@ "lodash": "^4.17.14" } }, - "axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", - "requires": { - "follow-redirects": "^1.14.0" - } - }, - "babel-runtime": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", - "requires": { - "core-js": "^2.4.0", - "regenerator-runtime": "^0.11.0" - } - }, - "babel-types": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", - "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "babel-walk": { + "version": "3.0.0-canary-5", + "resolved": "https://registry.npmjs.org/babel-walk/-/babel-walk-3.0.0-canary-5.tgz", + "integrity": "sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==", "requires": { - "babel-runtime": "^6.26.0", - "esutils": "^2.0.2", - "lodash": "^4.17.4", - "to-fast-properties": "^1.0.3" + "@babel/types": "^7.9.6" } }, - "babylon": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", - "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==" - }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -3944,13 +3810,22 @@ "set-function-length": "^1.2.1" } }, - "center-align": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", - "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", + "call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "requires": { - "align-text": "^0.1.3", - "lazy-cache": "^1.0.3" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + } + }, + "call-bound": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", + "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "requires": { + "call-bind-apply-helpers": "^1.0.1", + "get-intrinsic": "^1.2.6" } }, "chalk": { @@ -3966,7 +3841,7 @@ "character-parser": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz", - "integrity": "sha1-x84o821LzZdE5f/CxfzeHHMmH8A=", + "integrity": "sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==", "requires": { "is-regex": "^1.0.3" } @@ -3987,36 +3862,11 @@ "readdirp": "~3.6.0" } }, - "clean-css": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz", - "integrity": "sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA==", - "requires": { - "source-map": "~0.6.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - } - } - }, "clean-stack": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-1.3.0.tgz", "integrity": "sha1-noIVAa6XmYbEax1m0tQy2y/UrjE=" }, - "cliui": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", - "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", - "requires": { - "center-align": "^0.1.1", - "right-align": "^0.1.1", - "wordwrap": "0.0.2" - } - }, "clone-response": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", @@ -4082,14 +3932,12 @@ "dev": true }, "constantinople": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-3.1.2.tgz", - "integrity": "sha512-yePcBqEFhLOqSBtwYOGGS1exHo/s1xjekXiinh4itpNQGCu4KA1euPh1fg07N2wMITZXQkBz75Ntdt1ctGZouw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-4.0.1.tgz", + "integrity": "sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==", "requires": { - "@types/babel-types": "^7.0.0", - "@types/babylon": "^6.16.2", - "babel-types": "^6.26.0", - "babylon": "^6.18.0" + "@babel/parser": "^7.6.0", + "@babel/types": "^7.6.1" } }, "content-disposition": { @@ -4118,12 +3966,19 @@ "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" }, "cookie-parser": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.5.tgz", - "integrity": "sha512-f13bPUj/gG/5mDr+xLmSxxDsB9DQiTIfhJS/sqjrmfAWiAN+x2O4i/XguTL9yDZ+/IFDanJ+5x7hC4CXT9Tdzw==", + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", "requires": { - "cookie": "0.4.0", + "cookie": "0.7.2", "cookie-signature": "1.0.6" + }, + "dependencies": { + "cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==" + } } }, "cookie-signature": { @@ -4131,11 +3986,6 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" }, - "core-js": { - "version": "2.6.11", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz", - "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==" - }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -4211,11 +4061,6 @@ "ms": "2.0.0" } }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" - }, "decode-uri-component": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", @@ -4262,13 +4107,23 @@ "doctypes": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz", - "integrity": "sha1-6oCxBqh1OHdOijpKWv4pPeSJ4Kk=" + "integrity": "sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==" }, "dotenv": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-7.0.0.tgz", "integrity": "sha512-M3NhsLbV1i6HuGzBUH8vXrtxOk+tWmzWKDMbAVSUp3Zsjm7ywFeuwrUXhmhQyRK1q5B5GGy7hcXPbj3bnfZg2g==" }, + "dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "requires": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + } + }, "duplexer3": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", @@ -4306,18 +4161,23 @@ "integrity": "sha512-zoB603vQReOFvTg5xMl9I1P2PnHsHQQKTEowsKKD7nseUfJq6UWzK+4YtlWUO1nhiQUxe6XMkk+JleSZD1NZFA==" }, "es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "requires": { - "get-intrinsic": "^1.2.4" - } + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" }, "es-errors": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" }, + "es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "requires": { + "es-errors": "^1.3.0" + } + }, "es6-promise": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", @@ -4333,11 +4193,6 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, - "esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" - }, "etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -4500,11 +4355,6 @@ } } }, - "follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==" - }, "forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -4561,15 +4411,29 @@ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" }, "get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "requires": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + } + }, + "get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "requires": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" } }, "get-stream": { @@ -4587,20 +4451,9 @@ } }, "gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "requires": { - "get-intrinsic": "^1.1.3" - } - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "requires": { - "function-bind": "^1.1.1" - } + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" }, "has-flag": { "version": "3.0.0", @@ -4615,20 +4468,15 @@ "es-define-property": "^1.0.0" } }, - "has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==" - }, "has-symbol-support-x": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz", "integrity": "sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw==" }, "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" }, "has-to-string-tag-x": { "version": "1.4.1", @@ -4638,6 +4486,14 @@ "has-symbol-support-x": "^1.4.1" } }, + "has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "requires": { + "has-symbols": "^1.0.3" + } + }, "hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -4691,27 +4547,27 @@ } }, "hydra-login-consent-logout": { - "version": "2.0.4-pre.2", - "resolved": "https://registry.npmjs.org/hydra-login-consent-logout/-/hydra-login-consent-logout-2.0.4-pre.2.tgz", - "integrity": "sha512-nB3JKffjiTyQZzr0DPdkdoUAg7mPlNTv7c/jZrC5IrIyodc3X4s16LzcZJcs/e2U3pZyu3CoWGUrnF//wPzmqQ==", + "version": "2.4.0-pre.3", + "resolved": "https://registry.npmjs.org/hydra-login-consent-logout/-/hydra-login-consent-logout-2.4.0-pre.3.tgz", + "integrity": "sha512-Dtoop55BOQ/z+DQunVblxcFC5IMLw9NOw4vqTm2WmvMn7TjWwCfq+gA03ifGPAiFuqbyprxUnUaILCSJn9Hqdg==", "requires": { - "@ory/client": "^0.2.0-alpha.24", + "@ory/hydra-client-fetch": "^2.4.0-alpha.1", "@types/cookie-parser": "^1.4.2", "@types/csurf": "^1.9.36", "@types/express": "^4.17.7", "@types/morgan": "^1.9.1", "@types/url-join": "^4.0.0", - "body-parser": "^1.19.0", - "cookie-parser": "^1.4.5", + "body-parser": "^1.20.3", + "cookie-parser": "^1.4.7", "csurf": "^1.11.0", "debug": "^4.1.1", - "express": "^4.17.1", + "express": "^4.21.2", "morgan": "^1.10.0", "node-fetch": "^2.6.7", - "pug": "^2.0.4", + "pug": "^3.0.3", "querystring": "^0.2.0", "serve-favicon": "^2.5.0", - "typescript": "^3.7.5", + "typescript": "^5.7.3", "url-join": "^4.0.1" }, "dependencies": { @@ -4789,25 +4645,21 @@ "binary-extensions": "^2.0.0" } }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + "is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "requires": { + "hasown": "^2.0.2" + } }, "is-expression": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-3.0.0.tgz", - "integrity": "sha1-Oayqa+f9HzRx3ELHQW5hwkMXrJ8=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-4.0.0.tgz", + "integrity": "sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==", "requires": { - "acorn": "~4.0.2", - "object-assign": "^4.0.1" - }, - "dependencies": { - "acorn": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", - "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=" - } + "acorn": "^7.1.1", + "object-assign": "^4.1.1" } }, "is-extglob": { @@ -4842,16 +4694,19 @@ "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=" }, "is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", - "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==" }, "is-regex": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", - "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", "requires": { - "has": "^1.0.3" + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" } }, "is-retry-allowed": { @@ -4895,7 +4750,7 @@ "js-stringify": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz", - "integrity": "sha1-Fzb939lyTyijaCrcYjCufk6Weds=" + "integrity": "sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==" }, "json-buffer": { "version": "3.0.0", @@ -4929,7 +4784,7 @@ "jstransformer": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-1.0.0.tgz", - "integrity": "sha1-7Yvwkh4vPx7U1cGkT2hwntJHIsM=", + "integrity": "sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==", "requires": { "is-promise": "^2.0.0", "promise": "^7.0.1" @@ -4998,11 +4853,6 @@ "colornames": "^1.1.1" } }, - "lazy-cache": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", - "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=" - }, "limiter": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", @@ -5077,11 +4927,6 @@ "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" }, - "longest": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", - "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=" - }, "lowercase-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", @@ -5105,6 +4950,11 @@ "lru-cache": "~4.0.0" } }, + "math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" + }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -5462,9 +5312,9 @@ "dev": true }, "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "path-to-regexp": { "version": "0.1.12", @@ -5521,118 +5371,116 @@ "dev": true }, "pug": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pug/-/pug-2.0.4.tgz", - "integrity": "sha512-XhoaDlvi6NIzL49nu094R2NA6P37ijtgMDuWE+ofekDChvfKnzFal60bhSdiy8y2PBO6fmz3oMEIcfpBVRUdvw==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pug/-/pug-3.0.3.tgz", + "integrity": "sha512-uBi6kmc9f3SZ3PXxqcHiUZLmIXgfgWooKWXcwSGwQd2Zi5Rb0bT14+8CJjJgI8AB+nndLaNgHGrcc6bPIB665g==", "requires": { - "pug-code-gen": "^2.0.2", - "pug-filters": "^3.1.1", - "pug-lexer": "^4.1.0", - "pug-linker": "^3.0.6", - "pug-load": "^2.0.12", - "pug-parser": "^5.0.1", - "pug-runtime": "^2.0.5", - "pug-strip-comments": "^1.0.4" + "pug-code-gen": "^3.0.3", + "pug-filters": "^4.0.0", + "pug-lexer": "^5.0.1", + "pug-linker": "^4.0.0", + "pug-load": "^3.0.0", + "pug-parser": "^6.0.0", + "pug-runtime": "^3.0.1", + "pug-strip-comments": "^2.0.0" } }, "pug-attrs": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pug-attrs/-/pug-attrs-2.0.4.tgz", - "integrity": "sha512-TaZ4Z2TWUPDJcV3wjU3RtUXMrd3kM4Wzjbe3EWnSsZPsJ3LDI0F3yCnf2/W7PPFF+edUFQ0HgDL1IoxSz5K8EQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pug-attrs/-/pug-attrs-3.0.0.tgz", + "integrity": "sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==", "requires": { - "constantinople": "^3.0.1", - "js-stringify": "^1.0.1", - "pug-runtime": "^2.0.5" + "constantinople": "^4.0.1", + "js-stringify": "^1.0.2", + "pug-runtime": "^3.0.0" } }, "pug-code-gen": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-2.0.3.tgz", - "integrity": "sha512-r9sezXdDuZJfW9J91TN/2LFbiqDhmltTFmGpHTsGdrNGp3p4SxAjjXEfnuK2e4ywYsRIVP0NeLbSAMHUcaX1EA==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-3.0.3.tgz", + "integrity": "sha512-cYQg0JW0w32Ux+XTeZnBEeuWrAY7/HNE6TWnhiHGnnRYlCgyAUPoyh9KzCMa9WhcJlJ1AtQqpEYHc+vbCzA+Aw==", "requires": { - "constantinople": "^3.1.2", + "constantinople": "^4.0.1", "doctypes": "^1.1.0", - "js-stringify": "^1.0.1", - "pug-attrs": "^2.0.4", - "pug-error": "^1.3.3", - "pug-runtime": "^2.0.5", - "void-elements": "^2.0.1", - "with": "^5.0.0" + "js-stringify": "^1.0.2", + "pug-attrs": "^3.0.0", + "pug-error": "^2.1.0", + "pug-runtime": "^3.0.1", + "void-elements": "^3.1.0", + "with": "^7.0.0" } }, "pug-error": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-1.3.3.tgz", - "integrity": "sha512-qE3YhESP2mRAWMFJgKdtT5D7ckThRScXRwkfo+Erqga7dyJdY3ZquspprMCj/9sJ2ijm5hXFWQE/A3l4poMWiQ==" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-2.1.0.tgz", + "integrity": "sha512-lv7sU9e5Jk8IeUheHata6/UThZ7RK2jnaaNztxfPYUY+VxZyk/ePVaNZ/vwmH8WqGvDz3LrNYt/+gA55NDg6Pg==" }, "pug-filters": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-3.1.1.tgz", - "integrity": "sha512-lFfjNyGEyVWC4BwX0WyvkoWLapI5xHSM3xZJFUhx4JM4XyyRdO8Aucc6pCygnqV2uSgJFaJWW3Ft1wCWSoQkQg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-4.0.0.tgz", + "integrity": "sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==", "requires": { - "clean-css": "^4.1.11", - "constantinople": "^3.0.1", + "constantinople": "^4.0.1", "jstransformer": "1.0.0", - "pug-error": "^1.3.3", - "pug-walk": "^1.1.8", - "resolve": "^1.1.6", - "uglify-js": "^2.6.1" + "pug-error": "^2.0.0", + "pug-walk": "^2.0.0", + "resolve": "^1.15.1" } }, "pug-lexer": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-4.1.0.tgz", - "integrity": "sha512-i55yzEBtjm0mlplW4LoANq7k3S8gDdfC6+LThGEvsK4FuobcKfDAwt6V4jKPH9RtiE3a2Akfg5UpafZ1OksaPA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-5.0.1.tgz", + "integrity": "sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w==", "requires": { - "character-parser": "^2.1.1", - "is-expression": "^3.0.0", - "pug-error": "^1.3.3" + "character-parser": "^2.2.0", + "is-expression": "^4.0.0", + "pug-error": "^2.0.0" } }, "pug-linker": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-3.0.6.tgz", - "integrity": "sha512-bagfuHttfQOpANGy1Y6NJ+0mNb7dD2MswFG2ZKj22s8g0wVsojpRlqveEQHmgXXcfROB2RT6oqbPYr9EN2ZWzg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-4.0.0.tgz", + "integrity": "sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==", "requires": { - "pug-error": "^1.3.3", - "pug-walk": "^1.1.8" + "pug-error": "^2.0.0", + "pug-walk": "^2.0.0" } }, "pug-load": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/pug-load/-/pug-load-2.0.12.tgz", - "integrity": "sha512-UqpgGpyyXRYgJs/X60sE6SIf8UBsmcHYKNaOccyVLEuT6OPBIMo6xMPhoJnqtB3Q3BbO4Z3Bjz5qDsUWh4rXsg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pug-load/-/pug-load-3.0.0.tgz", + "integrity": "sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==", "requires": { - "object-assign": "^4.1.0", - "pug-walk": "^1.1.8" + "object-assign": "^4.1.1", + "pug-walk": "^2.0.0" } }, "pug-parser": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-5.0.1.tgz", - "integrity": "sha512-nGHqK+w07p5/PsPIyzkTQfzlYfuqoiGjaoqHv1LjOv2ZLXmGX1O+4Vcvps+P4LhxZ3drYSljjq4b+Naid126wA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-6.0.0.tgz", + "integrity": "sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==", "requires": { - "pug-error": "^1.3.3", - "token-stream": "0.0.1" + "pug-error": "^2.0.0", + "token-stream": "1.0.0" } }, "pug-runtime": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-2.0.5.tgz", - "integrity": "sha512-P+rXKn9un4fQY77wtpcuFyvFaBww7/91f3jHa154qU26qFAnOe6SW1CbIDcxiG5lLK9HazYrMCCuDvNgDQNptw==" + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-3.0.1.tgz", + "integrity": "sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg==" }, "pug-strip-comments": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-1.0.4.tgz", - "integrity": "sha512-i5j/9CS4yFhSxHp5iKPHwigaig/VV9g+FgReLJWWHEHbvKsbqL0oP/K5ubuLco6Wu3Kan5p7u7qk8A4oLLh6vw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-2.0.0.tgz", + "integrity": "sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==", "requires": { - "pug-error": "^1.3.3" + "pug-error": "^2.0.0" } }, "pug-walk": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-1.1.8.tgz", - "integrity": "sha512-GMu3M5nUL3fju4/egXwZO0XLi6fW/K3T3VTgFQ14GxNi8btlxgT5qZL//JwZFm/2Fa64J/PNS8AZeys3wiMkVA==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-2.0.0.tgz", + "integrity": "sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==" }, "qs": { "version": "6.13.0", @@ -5702,22 +5550,14 @@ "picomatch": "^2.2.1" } }, - "regenerator-runtime": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" - }, "resolve": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.16.0.tgz", - "integrity": "sha512-LarL/PIKJvc09k1jaeT4kQb/8/7P+qV4qSnN2K80AES+OHdfZELAKVOBjxsvtToT/uLOfFbvYvKfZmV8cee7nA==", + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", "requires": { - "path-parse": "^1.0.6" + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" } }, "responselike": { @@ -5728,14 +5568,6 @@ "lowercase-keys": "^1.0.0" } }, - "right-align": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", - "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", - "requires": { - "align-text": "^0.1.1" - } - }, "rndm": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz", @@ -5944,11 +5776,6 @@ "is-plain-obj": "^1.0.0" } }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" - }, "stack-trace": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", @@ -5980,6 +5807,11 @@ "has-flag": "^3.0.0" } }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" + }, "text-hex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", @@ -5990,11 +5822,6 @@ "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=" }, - "to-fast-properties": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", - "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=" - }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -6010,9 +5837,9 @@ "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" }, "token-stream": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-0.0.1.tgz", - "integrity": "sha1-zu78cXp2xDFvEm0LnbqlXX598Bo=" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-1.0.0.tgz", + "integrity": "sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==" }, "touch": { "version": "3.1.0", @@ -6048,25 +5875,9 @@ } }, "typescript": { - "version": "3.9.10", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz", - "integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==" - }, - "uglify-js": { - "version": "2.8.29", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", - "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", - "requires": { - "source-map": "~0.5.1", - "uglify-to-browserify": "~1.0.0", - "yargs": "~3.10.0" - } - }, - "uglify-to-browserify": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", - "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", - "optional": true + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==" }, "uid-safe": { "version": "2.1.5", @@ -6113,9 +5924,9 @@ "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" }, "void-elements": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", - "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=" + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==" }, "webidl-conversions": { "version": "3.0.1", @@ -6140,11 +5951,6 @@ "isexe": "^2.0.0" } }, - "window-size": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", - "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=" - }, "winston": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/winston/-/winston-3.2.1.tgz", @@ -6195,19 +6001,16 @@ } }, "with": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/with/-/with-5.1.1.tgz", - "integrity": "sha1-+k2qktrzLE6pTtRTyB8EaGtXXf4=", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/with/-/with-7.0.2.tgz", + "integrity": "sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==", "requires": { - "acorn": "^3.1.0", - "acorn-globals": "^3.0.0" + "@babel/parser": "^7.9.6", + "@babel/types": "^7.9.6", + "assert-never": "^1.2.1", + "babel-walk": "3.0.0-canary-5" } }, - "wordwrap": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", - "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=" - }, "wreck": { "version": "14.2.0", "resolved": "https://registry.npmjs.org/wreck/-/wreck-14.2.0.tgz", @@ -6222,24 +6025,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" - }, - "yargs": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", - "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", - "requires": { - "camelcase": "^1.0.2", - "cliui": "^2.1.0", - "decamelize": "^1.0.0", - "window-size": "0.1.0" - }, - "dependencies": { - "camelcase": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", - "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=" - } - } } } } diff --git a/test/e2e/oauth2-client/package.json b/test/e2e/oauth2-client/package.json index 90412a46396..0ddd96f7002 100644 --- a/test/e2e/oauth2-client/package.json +++ b/test/e2e/oauth2-client/package.json @@ -14,7 +14,7 @@ "express": "^4.21.2", "express-session": "^1.17.0", "express-winston": "^3.4.0", - "hydra-login-consent-logout": "2.0.4-pre.2", + "hydra-login-consent-logout": "2.4.0-pre.3", "jsonwebtoken": "^8.5.1", "jwks-rsa": "^2.1.4", "node-fetch": "^2.6.0", diff --git a/test/e2e/oauth2-client/src/index.js b/test/e2e/oauth2-client/src/index.js index b27512bedeb..d868fbc0d84 100644 --- a/test/e2e/oauth2-client/src/index.js +++ b/test/e2e/oauth2-client/src/index.js @@ -152,6 +152,87 @@ app.get("/oauth2/callback", async (req, res) => { }) }) +app.get("/oauth2/device", async (req, res) => { + const client = { + id: req.query.client_id, + secret: req.query.client_secret, + } + + const state = uuid.v4() + const scope = req.query.scope || "" + + req.session.client = client + req.session.scope = scope.split(" ") + + const params = new URLSearchParams() + params.append("client_id", req.query.client_id) + params.append("scope", scope) + + let headers = new Headers() + headers.set( + "Authorization", + "Basic " + + Buffer.from(req.query.client_id + ":" + req.query.client_secret).toString( + "base64", + ), + ) + + fetch(new URL("/oauth2/device/auth", config.public).toString(), { + method: "POST", + body: params, + headers: headers, + }) + .then(isStatusOk) + .then((res) => res.json()) + .then((body) => { + // Store the device_code to use after authentication to get the tokens + req.session.device_code = body?.device_code + res.redirect(body?.verification_uri_complete) + }) + .catch((err) => { + res.send(JSON.stringify({ error: err.toString() })) + }) +}) + +app.get("/oauth2/device/success", async (req, res) => { + const clientId = req.session?.client?.id + const clientSecret = req.session?.client?.secret + + if (clientId === undefined || clientSecret === undefined) { + res.send( + JSON.stringify({ + result: "error", + error: "no client credentials in session", + }), + ) + return + } + + const params = new URLSearchParams() + params.append("client_id", clientId) + params.append("device_code", req.session?.device_code) + params.append("grant_type", "urn:ietf:params:oauth:grant-type:device_code") + let headers = new Headers() + headers.set( + "Authorization", + "Basic " + Buffer.from(clientId + ":" + clientSecret).toString("base64"), + ) + + fetch(new URL("/oauth2/token", config.public).toString(), { + method: "POST", + body: params, + headers: headers, + }) + .then(isStatusOk) + .then((resp) => resp.json()) + .then((data) => { + res.send({ result: "success", token: data }) + }) + .catch((err) => { + res.send(JSON.stringify({ error: err.toString() })) + }) +}) + app.get("/oauth2/refresh", function (req, res) { oauth2 .create(req.session.credentials)