From 07b5313d4be628d72ff8167b7a2647cd632423f1 Mon Sep 17 00:00:00 2001 From: Nikos Date: Wed, 25 Sep 2024 15:02:39 +0300 Subject: [PATCH 01/33] chore: install fosite from branch (remove) --- go.mod | 5 ++++- go.sum | 6 ++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index a17e5606a7..20cd4a888e 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ replace github.com/gobuffalo/pop/v6 => github.com/ory/pop/v6 v6.2.0 // // This is needed until we release the next version of the master branch, as that branch already contains the redirect URI validation fix, which // may be breaking for some users. -replace github.com/ory/fosite => github.com/ory/fosite v0.47.1-0.20241101073333-eab241e153a4 +// replace github.com/ory/fosite => github.com/ory/fosite v0.47.1-0.20241101073333-eab241e153a4 require ( github.com/ThalesIgnite/crypto11 v1.2.5 @@ -239,6 +239,7 @@ require ( go.opentelemetry.io/otel/exporters/zipkin v1.21.0 // indirect go.opentelemetry.io/otel/metric v1.28.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect + go.uber.org/mock v0.4.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/mod v0.19.0 // indirect golang.org/x/net v0.27.0 // indirect @@ -254,3 +255,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-20241018095821-24db6b931174 diff --git a/go.sum b/go.sum index 5f527a114e..caba3b9a5e 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-20241018095821-24db6b931174 h1:BwWAPln4uGXSGHJauaotSOk4+zJGN53uXMBI+9oC2Jw= +github.com/canonical/fosite v0.0.0-20241018095821-24db6b931174/go.mod h1:pKDsjcvWgjB4EBNdrGfic5Z9tyyOt8D15ykLLPZdOow= 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= @@ -382,8 +384,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.47.1-0.20241101073333-eab241e153a4 h1:1pEVHGC+Dx2xMPMgpRgG3lyejyK8iU9KKfSnLowLYd8= -github.com/ory/fosite v0.47.1-0.20241101073333-eab241e153a4/go.mod h1:AZyn1jrABUaGN12RHcWorRLbqLn52gTdHaIYY81m5J0= 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= @@ -564,6 +564,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.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= +go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= 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 f038745a8cc798c7b7b8880c960506e5bcfb63a3 Mon Sep 17 00:00:00 2001 From: Nikos Date: Wed, 25 Sep 2024 21:40:34 +0300 Subject: [PATCH 02/33] 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 5d556d44b4..1d8f831d7e 100644 --- a/persistence/sql/persister_nid_test.go +++ b/persistence/sql/persister_nid_test.go @@ -2107,7 +2107,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 366fe01a56bd5939efd044aaa71c4ebaac5610a2 Mon Sep 17 00:00:00 2001 From: Nikos Date: Wed, 25 Sep 2024 22:14:35 +0300 Subject: [PATCH 03/33] fix: add redirect_uri to test --- consent/strategy_default_test.go | 1 + oauth2/oauth2_auth_code_test.go | 1 + 2 files changed, 2 insertions(+) diff --git a/consent/strategy_default_test.go b/consent/strategy_default_test.go index e1746d4bf0..9ad2a99b8d 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() diff --git a/oauth2/oauth2_auth_code_test.go b/oauth2/oauth2_auth_code_test.go index 0d89e14ac9..feea2451e2 100644 --- a/oauth2/oauth2_auth_code_test.go +++ b/oauth2/oauth2_auth_code_test.go @@ -2120,6 +2120,7 @@ func newOAuth2Client( return c, &oauth2.Config{ ClientID: c.GetID(), ClientSecret: secret, + RedirectURL: callbackURL, Endpoint: oauth2.Endpoint{ AuthURL: reg.Config().OAuth2AuthURL(ctx).String(), TokenURL: reg.Config().OAuth2TokenURL(ctx).String(), From 32112468de9369d8aaf98b6c8c6a6a576c6fc3da Mon Sep 17 00:00:00 2001 From: Nikos Date: Wed, 25 Sep 2024 15:02:08 +0300 Subject: [PATCH 04/33] chore: update go.mod --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 20cd4a888e..16e2cbd4c3 100644 --- a/go.mod +++ b/go.mod @@ -239,7 +239,7 @@ require ( go.opentelemetry.io/otel/exporters/zipkin v1.21.0 // indirect go.opentelemetry.io/otel/metric v1.28.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect - go.uber.org/mock v0.4.0 // 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.27.0 // indirect @@ -256,4 +256,4 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect ) -replace github.com/ory/fosite => github.com/canonical/fosite v0.0.0-20241018095821-24db6b931174 +replace github.com/ory/fosite => github.com/canonical/fosite v0.0.0-20241106141421-904e20c5f3b0 diff --git a/go.sum b/go.sum index caba3b9a5e..02ae04a49c 100644 --- a/go.sum +++ b/go.sum @@ -47,8 +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-20241018095821-24db6b931174 h1:BwWAPln4uGXSGHJauaotSOk4+zJGN53uXMBI+9oC2Jw= -github.com/canonical/fosite v0.0.0-20241018095821-24db6b931174/go.mod h1:pKDsjcvWgjB4EBNdrGfic5Z9tyyOt8D15ykLLPZdOow= +github.com/canonical/fosite v0.0.0-20241106141421-904e20c5f3b0 h1:SFNBHXfFxR0cQoq+KEJNYI6JpB2CPFfn9VmYJmm2Nx4= +github.com/canonical/fosite v0.0.0-20241106141421-904e20c5f3b0/go.mod h1:A8nrQ4txReSzKzg/QRCBL13Agy61kt65EJb3iH/vRE0= 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= @@ -564,8 +564,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.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= -go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +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 b9ccf0672ffb3fef080b12173d257bb3a639475c Mon Sep 17 00:00:00 2001 From: Nikos Date: Fri, 9 Feb 2024 17:01:36 +0200 Subject: [PATCH 05/33] fix: add rfc8628 providers to registry --- .schema/config.schema.json | 5 +++ 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, 151 insertions(+), 13 deletions(-) diff --git a/.schema/config.schema.json b/.schema/config.schema.json index c4a933b85c..344e7ee4e5 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", diff --git a/client/client.go b/client/client.go index 52ee86b558..be4b9c4668 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 4f66e4448a..39c8c28b70 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" @@ -393,6 +398,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"))) } @@ -413,6 +436,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))) } @@ -470,6 +497,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 168ca81d69..fdc5d60c49 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 4377efb1f6..7c2018971f 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 5fab6d1d7d..d6898a7b8d 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 c305fd1816..68de41ca30 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 83d584eb12..e99c959fc4 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 0e7cdfae81..ffb7b64254 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 72f81534c6..31f451c813 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 467d9c3bbe6e66aece0e5653205081d70dbb91ed Mon Sep 17 00:00:00 2001 From: Nikos Date: Fri, 9 Feb 2024 17:02:28 +0200 Subject: [PATCH 06/33] 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 13a940c841..49c9fb5ea9 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 fae8513d60..1431c94066 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 bc73e23fc2..e454e28243 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 f04dee3726..aa4d250f5c 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 e3b5d630dd..c95e9dd963 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 db4e078729..14fa4483bd 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 7a8b9fd889..12157ef030 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 b5f6814ea4..9efbdcc49b 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 e821518707..b240dce712 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 be51195ca6..1887b28b1f 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 353ed37ffe..06922c8709 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 ed92bbce29..8298eea26c 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 6375e36928..689bf6cec8 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 3939f00e95..5c7db72913 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 38e0af5405..596894f09d 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 f55d9d59c0..be20015e24 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 be6ca67a2d..5e8d25b2c7 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 e8f9235696..1e26b6038b 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 0000000000..b21344dfcb --- /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 0000000000..69d926ba28 --- /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 0000000000..54ceefff83 --- /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 0000000000..c0b6446a0e --- /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 0000000000..29bc24f69e --- /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 0000000000..ceb69a1078 --- /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 0000000000..59fae8ea86 --- /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 0000000000..2547f10e76 --- /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 0000000000..19892b22d8 --- /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 0000000000..62b894fd6b --- /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 0000000000..15f7417c62 --- /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 8c11107c920f458caa1154dce6042bd9306661e5 Mon Sep 17 00:00:00 2001 From: Nikos Date: Tue, 24 Sep 2024 13:17:55 +0300 Subject: [PATCH 07/33] 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 39c8c28b70..cdd82b4aa0 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" @@ -685,6 +686,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 76159e4c80..3d8160a42d 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 @@ -588,6 +590,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 @@ -614,6 +626,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{ @@ -628,6 +641,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 399938a802..17b97c7545 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" ) @@ -542,6 +543,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 0502bc544b..2f2c65a8d6 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) ) @@ -202,6 +221,22 @@ type Flow struct { LoginError *RequestDeniedError `db:"login_error" json:"le,omitempty"` LoginAuthenticatedAt sqlxx.NullTime `db:"login_authenticated_at" json:"la,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"` + // ConsentChallengeID is the identifier ("authorization challenge") of the consent authorization request. It is used to // identify the session. // @@ -263,6 +298,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.")) @@ -365,6 +416,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") @@ -509,6 +601,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) (string, error) { if f.Client != nil { diff --git a/oauth2/flowctx/encoding.go b/oauth2/flowctx/encoding.go index 8a1f8cbf27..8c659ad724 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 5bd9713938..eeadcfcebd 100644 --- a/persistence/sql/persister_consent.go +++ b/persistence/sql/persister_consent.go @@ -220,11 +220,105 @@ func (p *Persister) GetConsentRequest(ctx context.Context, challenge string) (_ return f.GetConsentRequest(), 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 083e67ac5d..9ebfc8cd73 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 { @@ -612,3 +614,101 @@ func (p *Persister) DeleteAccessTokens(ctx context.Context, clientID string) (er p.QueryWithNetwork(ctx).Where("client_id=?", clientID).Delete(&OAuth2RequestSQL{Table: sqlTableAccess}), ) } + +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 a02a9a054c..243d65033d 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 09a00ee9e6dc77c2c971cf20f4d1072fc6f12b3b Mon Sep 17 00:00:00 2001 From: Nikos Date: Fri, 9 Feb 2024 17:04:19 +0200 Subject: [PATCH 08/33] 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 cdd82b4aa0..796936d4c4 100644 --- a/driver/config/provider.go +++ b/driver/config/provider.go @@ -399,10 +399,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) } @@ -437,6 +439,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"))) } @@ -498,6 +501,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 7c2018971f..f699eb5ab5 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 bb02d986ad..7442fe036f 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 288ed1f16f..3ad50b844b 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 e99c959fc4..8149e206b2 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 9ebfc8cd73..5a180792cf 100644 --- a/persistence/sql/persister_oauth2.go +++ b/persistence/sql/persister_oauth2.go @@ -615,13 +615,14 @@ func (p *Persister) DeleteAccessTokens(ctx context.Context, clientID string) (er ) } +// 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) @@ -644,6 +645,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) @@ -667,18 +669,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 734ff1e4b9..4b5268143b 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -2314,6 +2314,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 27e029ce597835d23731c3b540399e888d5556c5 Mon Sep 17 00:00:00 2001 From: Nikos Date: Wed, 28 Feb 2024 16:06:05 +0200 Subject: [PATCH 09/33] 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 3ad50b844b..4ee54756ac 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" @@ -1160,7 +1161,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 @@ -1175,83 +1176,15 @@ 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) + authorizeRequest.SetID(acceptConsentSession.ID) + 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.ID) - 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.ID, - 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()) @@ -1323,6 +1256,80 @@ 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 + } + + 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.ID, + 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 f6da36269b1608809600bcaebbad93fb15937668 Mon Sep 17 00:00:00 2001 From: Nikos Date: Wed, 28 Feb 2024 16:04:08 +0200 Subject: [PATCH 10/33] 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 4ee54756ac..0758c5e0ff 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 76fd069b0d15337cb0bfb0fcf1f3928f4580d535 Mon Sep 17 00:00:00 2001 From: Nikos Date: Wed, 28 Feb 2024 16:09:43 +0200 Subject: [PATCH 11/33] 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 796936d4c4..ebb9844027 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" @@ -506,6 +507,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 f699eb5ab5..4300077ea9 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 0758c5e0ff..1b1c70313e 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 her 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 52a30e5975..769679ec17 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 a488e835cb37ca16e59ad614170ef34d86ba0d3b Mon Sep 17 00:00:00 2001 From: Nikos Date: Wed, 28 Feb 2024 16:25:32 +0200 Subject: [PATCH 12/33] 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 fe4b018352..44b1f54564 100644 --- a/consent/manager.go +++ b/consent/manager.go @@ -60,6 +60,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 08e8788c75..0def2866e2 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 6d7f8c67e9..9095c472ce 100644 --- a/consent/strategy_default.go +++ b/consent/strategy_default.go @@ -39,7 +39,10 @@ import ( "github.com/ory/x/urlx" ) +type ctxKey int + const ( + DeviceVerificationPath = "/oauth2/device/verify" CookieAuthenticationSIDName = "sid" ) @@ -120,18 +123,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 } @@ -149,12 +158,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) @@ -166,7 +175,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) { @@ -193,7 +202,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.")) } @@ -215,8 +233,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 { @@ -244,7 +268,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), @@ -258,6 +282,7 @@ func (s *DefaultStrategy) forwardAuthenticationRequest(ctx context.Context, w ht } f, err := s.r.ConsentManager().CreateLoginRequest( ctx, + f, loginRequest, ) if err != nil { @@ -1130,11 +1155,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 { @@ -1153,6 +1188,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] @@ -1170,3 +1253,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 ebb9844027..796936d4c4 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" @@ -507,11 +506,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 a8233fb127fcdf9c8297050e02e5c67d24511781 Mon Sep 17 00:00:00 2001 From: Nikos Date: Fri, 1 Mar 2024 14:52:43 +0200 Subject: [PATCH 13/33] feat: update flow --- consent/sdk_test.go | 12 +- .../TestOAuth2ConsentRequest_MarshalJSON.json | 2 +- flow/consent_types.go | 76 ++++--- flow/flow.go | 206 +++++++++++------- flow/flow_test.go | 88 ++++++++ internal/testhelpers/janitor_test_helper.go | 14 +- oauth2/fosite_store_helpers.go | 2 +- persistence/sql/persister_nid_test.go | 4 +- 8 files changed, 270 insertions(+), 134 deletions(-) diff --git a/consent/sdk_test.go b/consent/sdk_test.go index f749428d5d..f50e87b96b 100644 --- a/consent/sdk_test.go +++ b/consent/sdk_test.go @@ -70,9 +70,9 @@ func TestSDK(t *testing.T) { ID: ar2.SessionID.String(), Subject: ar2.Subject, })) - _, err := m.CreateLoginRequest(context.Background(), ar1) + _, err := m.CreateLoginRequest(context.Background(), nil, ar1) require.NoError(t, err) - _, err = m.CreateLoginRequest(context.Background(), ar2) + _, err = m.CreateLoginRequest(context.Background(), nil, ar2) require.NoError(t, err) cr1, hcr1, _ := test.MockConsentRequest("1", false, 0, false, false, false, "fk-login-challenge", network) @@ -84,7 +84,7 @@ func TestSDK(t *testing.T) { require.NoError(t, reg.ClientManager().CreateClient(context.Background(), cr3.Client)) require.NoError(t, reg.ClientManager().CreateClient(context.Background(), cr4.Client)) - cr1Flow, err := m.CreateLoginRequest(context.Background(), &LoginRequest{ + cr1Flow, err := m.CreateLoginRequest(context.Background(), nil, &LoginRequest{ ID: cr1.LoginChallenge.String(), Subject: cr1.Subject, Client: cr1.Client, @@ -94,7 +94,7 @@ func TestSDK(t *testing.T) { require.NoError(t, err) cr1Flow.LoginSkip = ar1.Skip - cr2Flow, err := m.CreateLoginRequest(context.Background(), &LoginRequest{ + cr2Flow, err := m.CreateLoginRequest(context.Background(), nil, &LoginRequest{ ID: cr2.LoginChallenge.String(), Subject: cr2.Subject, Client: cr2.Client, @@ -107,7 +107,7 @@ func TestSDK(t *testing.T) { loginSession3 := &LoginSession{ID: cr3.LoginSessionID.String()} require.NoError(t, m.CreateLoginSession(context.Background(), loginSession3)) require.NoError(t, m.ConfirmLoginSession(context.Background(), loginSession3)) - cr3Flow, err := m.CreateLoginRequest(context.Background(), &LoginRequest{ + cr3Flow, err := m.CreateLoginRequest(context.Background(), nil, &LoginRequest{ ID: cr3.LoginChallenge.String(), Subject: cr3.Subject, Client: cr3.Client, @@ -120,7 +120,7 @@ func TestSDK(t *testing.T) { loginSession4 := &LoginSession{ID: cr4.LoginSessionID.String()} require.NoError(t, m.CreateLoginSession(context.Background(), loginSession4)) require.NoError(t, m.ConfirmLoginSession(context.Background(), loginSession4)) - cr4Flow, err := m.CreateLoginRequest(context.Background(), &LoginRequest{ + cr4Flow, err := m.CreateLoginRequest(context.Background(), nil, &LoginRequest{ ID: cr4.LoginChallenge.String(), Client: cr4.Client, Verifier: cr4.ID, diff --git a/flow/.snapshots/TestOAuth2ConsentRequest_MarshalJSON.json b/flow/.snapshots/TestOAuth2ConsentRequest_MarshalJSON.json index 1a39fb2e6c..58c3808115 100644 --- a/flow/.snapshots/TestOAuth2ConsentRequest_MarshalJSON.json +++ b/flow/.snapshots/TestOAuth2ConsentRequest_MarshalJSON.json @@ -1 +1 @@ -"{\"challenge\":\"\",\"requested_scope\":[],\"requested_access_token_audience\":[],\"skip\":false,\"subject\":\"\",\"oidc_context\":null,\"client\":null,\"request_url\":\"\",\"login_challenge\":\"\",\"login_session_id\":\"\",\"acr\":\"\",\"amr\":[]}" +"{\"challenge\":\"\",\"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 17b97c7545..b93d238b24 100644 --- a/flow/consent_types.go +++ b/flow/consent_types.go @@ -551,40 +551,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. @@ -593,16 +576,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() } @@ -690,6 +686,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 @@ -738,6 +741,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 2f2c65a8d6..f424f707f7 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) ) @@ -221,21 +220,22 @@ type Flow struct { LoginError *RequestDeniedError `db:"login_error" json:"le,omitempty"` LoginAuthenticatedAt sqlxx.NullTime `db:"login_authenticated_at" json:"la,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"` + // 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"` // ConsentChallengeID is the identifier ("authorization challenge") of the consent authorization request. It is used to // identify the session. @@ -276,6 +276,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, @@ -298,22 +396,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.")) @@ -416,47 +498,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") @@ -526,6 +567,7 @@ func (f *Flow) GetConsentRequest() *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 43a5417608..43acf151a3 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 f70d7c2749..a13e21d808 100644 --- a/internal/testhelpers/janitor_test_helper.go +++ b/internal/testhelpers/janitor_test_helper.go @@ -193,7 +193,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 @@ -247,7 +247,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 @@ -291,7 +291,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 @@ -346,7 +346,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 { @@ -387,7 +387,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)) @@ -439,7 +439,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) } @@ -471,7 +471,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.go b/oauth2/fosite_store_helpers.go index 553a6bae62..8f740110a7 100644 --- a/oauth2/fosite_store_helpers.go +++ b/oauth2/fosite_store_helpers.go @@ -166,7 +166,7 @@ func mockRequestForeignKey(t *testing.T, id string, x 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 1d8f831d7e..322b6e6b36 100644 --- a/persistence/sql/persister_nid_test.go +++ b/persistence/sql/persister_nid_test.go @@ -436,7 +436,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) }) @@ -1217,7 +1217,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 1df9bcd47e9913fa1de9a009d66812f90a72c370 Mon Sep 17 00:00:00 2001 From: Nikos Date: Fri, 1 Mar 2024 14:54:58 +0200 Subject: [PATCH 14/33] fix: add post device auth handler --- driver/config/provider.go | 7 +++++++ oauth2/handler.go | 37 ++++++++++++++++++++++++++++++++----- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/driver/config/provider.go b/driver/config/provider.go index 796936d4c4..e27edfe7fc 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" @@ -444,6 +445,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))) } @@ -690,6 +696,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 1b1c70313e..f2a054a562 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) @@ -1313,7 +1340,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()) @@ -1321,7 +1348,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 @@ -1341,7 +1368,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 @@ -1360,7 +1387,7 @@ func (h *Handler) updateSessionWithRequest(ctx context.Context, session *flow.Ac }, Extra: session.Session.AccessToken, KID: accessTokenKeyID, - ClientID: flow.Client.GetID(), + ClientID: request.GetClient().GetID(), ConsentChallenge: session.ID, ExcludeNotBeforeClaim: h.c.ExcludeNotBeforeClaim(ctx), AllowedTopLevelClaims: h.c.AllowedTopLevelClaims(ctx), From 30678c2eda953f82644d7bb7df96662cc7469dfc Mon Sep 17 00:00:00 2001 From: Nikos Date: Fri, 1 Mar 2024 14:58:08 +0200 Subject: [PATCH 15/33] feat: add consent handler for accepting a user_code --- client/registry.go | 2 + consent/handler.go | 143 +++++++++++ consent/handler_test.go | 381 +++++++++++++++++++++++++++- 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, 554 insertions(+), 10 deletions(-) diff --git a/client/registry.go b/client/registry.go index bfec25ace9..c23efd231d 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 48167e7380..2c8e6b141e 100644 --- a/consent/handler.go +++ b/consent/handler.go @@ -10,6 +10,7 @@ import ( "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" @@ -36,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" @@ -67,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 @@ -1047,11 +1051,150 @@ 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 := stringsx.Coalesce( + r.URL.Query().Get("device_challenge"), + r.URL.Query().Get("challenge"), + ) + if challenge == "" { + h.r.Writer().WriteError(w, r, errorsx.WithStack(fosite.ErrInvalidRequest.WithHint(`Query parameter '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 } + if f.ClientID == "" { + return f, nil + } + + f.Client, err = h.r.ClientManager().GetConcreteClient(ctx, f.ClientID) + if err != nil { + return nil, err + } + return f, nil } diff --git a/consent/handler_test.go b/consent/handler_test.go index d5dfe5254a..7600afde01 100644 --- a/consent/handler_test.go +++ b/consent/handler_test.go @@ -15,11 +15,15 @@ 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/internal" + "github.com/ory/hydra/v2/oauth2" "github.com/ory/hydra/v2/x" "github.com/ory/x/contextx" "github.com/ory/x/pointerx" @@ -103,7 +107,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, @@ -175,7 +179,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,374 @@ 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 := internal.NewConfigurationWithDefaults() + reg := internal.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) + defer 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) + if err != nil { + panic(err) + } + + // set the HTTP method, url, and request body + req, err := http.NewRequest(http.MethodPut, ts.URL+"/admin"+DevicePath+"/accept?challenge="+challenge, bytes.NewBuffer(acceptUserCodeJson)) + if err != nil { + panic(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 := internal.NewConfigurationWithDefaults() + reg := internal.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) + defer 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) + if err != nil { + panic(err) + } + + // set the HTTP method, url, and request body + req, err := http.NewRequest(http.MethodPut, ts.URL+"/admin"+DevicePath+"/accept?challenge="+challenge, bytes.NewBuffer(acceptUserCodeJson)) + if err != nil { + panic(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?challenge="+challenge, bytes.NewBuffer(acceptUserCodeJson)) + if err != nil { + panic(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 := internal.NewConfigurationWithDefaults() + reg := internal.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) + defer 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?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?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?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?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?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() + if err != nil { + panic(err) + } + + // set the HTTP method, url, and request body + req, err := http.NewRequest(http.MethodPut, tc.getURL(), bytes.NewBuffer(acceptUserCodeJson)) + if err != nil { + panic(err) + } + + resp, err := c.Do(req) + require.NoError(t, err) + tc.validateResponse(resp) + }) + } + +} diff --git a/consent/manager.go b/consent/manager.go index 44b1f54564..f09c803c06 100644 --- a/consent/manager.go +++ b/consent/manager.go @@ -44,7 +44,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 9095c472ce..a4a11ac38c 100644 --- a/consent/strategy_default.go +++ b/consent/strategy_default.go @@ -560,9 +560,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 implicitly 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) } } @@ -1231,7 +1235,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 } @@ -1340,7 +1352,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 5a180792cf..c042520c95 100644 --- a/persistence/sql/persister_oauth2.go +++ b/persistence/sql/persister_oauth2.go @@ -680,6 +680,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) } @@ -700,18 +703,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 b93843f9db..9bf804ad34 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 546cfc9887..9d4833ddaa 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 a956f3212df743bd15c3b9290c2af85ed226dded Mon Sep 17 00:00:00 2001 From: Nikos Date: Thu, 7 Mar 2024 16:04:34 +0200 Subject: [PATCH 16/33] 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 fdc5d60c49..f52c81faf4 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 7442fe036f..4e7cbb0143 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 31f451c813..21a7d76f30 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 554b5dc98ed5fc51cd5a6ad7f8778aad33cd2b0c Mon Sep 17 00:00:00 2001 From: Nikos Date: Mon, 11 Mar 2024 19:03:20 +0200 Subject: [PATCH 17/33] chore: add e2e tests --- consent/strategy_default_test.go | 59 +++++++++++++ consent/strategy_oauth_test.go | 127 +++++++++++++++++++++++++++ consent/test/manager_test_helpers.go | 106 ++++++++++++++++++++-- 3 files changed, 284 insertions(+), 8 deletions(-) diff --git a/consent/strategy_default_test.go b/consent/strategy_default_test.go index 9ad2a99b8d..b4fa0498bd 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 370a337807..ed41df6c9e 100644 --- a/consent/strategy_oauth_test.go +++ b/consent/strategy_oauth_test.go @@ -1109,6 +1109,133 @@ func TestStrategyLoginConsentNext(t *testing.T) { }) } +func TestStrategyDeviceLoginConsent(t *testing.T) { + ctx := context.Background() + reg := internal.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 a5b141f535..fd94caf405 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,54 @@ func ManagerTests(deps Deps, m consent.Manager, clientManager client.Manager, fo } }) + t.Run("case=device-request", func(t *testing.T) { + for _, tc := range []struct { + key string + }{ + {"1"}, + {"2"}, + {"3"}, + {"4"}, + {"5"}, + {"6"}, + {"7"}, + } { + tc := tc + t.Run("key="+tc.key, func(t *testing.T) { + c, h, f := MockDeviceRequest(tc.key, 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) + + DeviceVerifier := x.Must(f.ToDeviceVerifier(ctx, deps)) + + got2, err := m.VerifyAndInvalidateDeviceUserAuthRequest(ctx, DeviceVerifier) + require.NoError(t, err) + compareDeviceRequest(t, c, got2.Request) + + deviceChallenge = x.Must(f.ToDeviceChallenge(ctx, deps)) + _, err = m.GetDeviceUserAuthRequest(ctx, deviceChallenge) + require.NoError(t, err) + }) + } + }) + t.Run("case=auth-request", func(t *testing.T) { for _, tc := range []struct { key string @@ -480,7 +562,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 +831,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 +1153,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{ ID: x.Must(f.ToConsentChallenge(ctx, deps)), @@ -1130,6 +1212,13 @@ 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) +} + func compareConsentRequest(t *testing.T, a, b *flow.OAuth2ConsentRequest) { assert.EqualValues(t, a.Client.GetID(), b.Client.GetID()) assert.EqualValues(t, a.ID, b.ID) @@ -1142,4 +1231,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 04ce2dfe8b99455483c923a018da5072732aedbf Mon Sep 17 00:00:00 2001 From: dushu Date: Sat, 23 Mar 2024 16:54:13 -0600 Subject: [PATCH 18/33] 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 4300077ea9..35c551c50a 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 61dfba7872..a687c788d9 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 3748c3744f..4be660d13c 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 61dfba7872..a687c788d9 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 3748c3744f..4be660d13c 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 61dfba7872..a687c788d9 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 3748c3744f..4be660d13c 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 61dfba7872..a687c788d9 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 3748c3744f..4be660d13c 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 61dfba7872..a687c788d9 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 3748c3744f..4be660d13c 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 61dfba7872..a687c788d9 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 3748c3744f..4be660d13c 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 03e8881ee7..341c3556d0 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 03e8881ee7..341c3556d0 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 4608026d74..8f7f9a1312 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 9636d07b8d..10bd3ec8d8 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 f2a054a562..12c1e1197d 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) @@ -1376,7 +1376,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{}{ @@ -1393,7 +1393,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 feea2451e2..43d42e2a19 100644 --- a/oauth2/oauth2_auth_code_test.go +++ b/oauth2/oauth2_auth_code_test.go @@ -1528,7 +1528,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 0000000000..022b79f45f --- /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 0630cb0914..cc067916a3 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 461d753581..a5094b4d9c 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 5ebeb511ee869b479d97380bb6934a9bdd446f2d Mon Sep 17 00:00:00 2001 From: Nikos Date: Thu, 21 Mar 2024 18:05:27 +0200 Subject: [PATCH 19/33] 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 8d69cc1d24..3becd68594 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 d4391d91e18fdf472090a86394d8da51c50a4996 Mon Sep 17 00:00:00 2001 From: dushu Date: Wed, 10 Apr 2024 20:28:42 -0600 Subject: [PATCH 20/33] 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 35c551c50a..40efcd33de 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 12c1e1197d..bfa7632aa4 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 022b79f45f..758b916be8 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 c042520c95..14ad040e0b 100644 --- a/persistence/sql/persister_oauth2.go +++ b/persistence/sql/persister_oauth2.go @@ -629,20 +629,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 0a2eaddfdf6e5084b33f9823d4fe47f7ccbae616 Mon Sep 17 00:00:00 2001 From: dushu Date: Thu, 11 Apr 2024 21:29:41 -0600 Subject: [PATCH 21/33] fix: update OpenID Connect session after user consent --- oauth2/handler.go | 11 +++++++++++ persistence/sql/persister_oauth2.go | 24 ++++++++++++++++++++++++ x/fosite_storer.go | 2 ++ 3 files changed, 37 insertions(+) diff --git a/oauth2/handler.go b/oauth2/handler.go index bfa7632aa4..accdafc5d4 100644 --- a/oauth2/handler.go +++ b/oauth2/handler.go @@ -761,6 +761,17 @@ func (h *Handler) performOAuth2DeviceVerificationFlow(w http.ResponseWriter, r * return } + // TODO evaluate if an OpenID Connect session is necessary for device flow. + // 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 14ad040e0b..581f264bcc 100644 --- a/persistence/sql/persister_oauth2.go +++ b/persistence/sql/persister_oauth2.go @@ -501,6 +501,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 9d4833ddaa..91c9ff3e69 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 da85bb1b7c3c1278ed9a16a83387d9b4f4380078 Mon Sep 17 00:00:00 2001 From: Nikos Date: Mon, 15 Apr 2024 14:02:19 +0300 Subject: [PATCH 22/33] 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 581f264bcc..c94281f769 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). @@ -677,6 +700,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 91c9ff3e69..d8c2d871b4 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 d874a9ff50b6de5b69125b70606a1034e2a16e0e Mon Sep 17 00:00:00 2001 From: Nikos Date: Mon, 15 Apr 2024 14:03:47 +0300 Subject: [PATCH 23/33] 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 ed41df6c9e..1037af5210 100644 --- a/consent/strategy_oauth_test.go +++ b/consent/strategy_oauth_test.go @@ -1190,19 +1190,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 accdafc5d4..ca897babf3 100644 --- a/oauth2/handler.go +++ b/oauth2/handler.go @@ -772,7 +772,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 f1d6341c462355292e0c7a315124616e0add875c Mon Sep 17 00:00:00 2001 From: Nikos Date: Mon, 15 Apr 2024 14:05:46 +0300 Subject: [PATCH 24/33] 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 | 81 +++++++++++++++++++++++++++-------------------- 1 file changed, 47 insertions(+), 34 deletions(-) diff --git a/oauth2/handler.go b/oauth2/handler.go index ca897babf3..a33ded10af 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 @@ -1261,7 +1268,7 @@ func (h *Handler) oAuth2Authorize(w http.ResponseWriter, r *http.Request, _ http } authorizeRequest.SetID(acceptConsentSession.ID) - session, err := h.updateSessionWithRequest(ctx, acceptConsentSession, flow, r, authorizeRequest) + session, err := h.updateSessionWithRequest(ctx, acceptConsentSession, flow, r, authorizeRequest, nil) if err != nil { h.writeAuthorizeError(w, r, authorizeRequest, err) return @@ -1342,12 +1349,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) } @@ -1366,7 +1380,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 @@ -1378,11 +1392,11 @@ func (h *Handler) updateSessionWithRequest(ctx context.Context, session *flow.Ac 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"), @@ -1392,32 +1406,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: request.GetClient().GetID(), - ConsentChallenge: session.ID, - 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.ID + 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 44ca5df397e81e93a1f10298ff3bcc4b6dcfe213 Mon Sep 17 00:00:00 2001 From: Nikos Date: Mon, 15 Apr 2024 14:06:49 +0300 Subject: [PATCH 25/33] fix: update tests --- oauth2/oauth2_device_code_test.go | 484 +++++++++++++++++++++++++++++- 1 file changed, 479 insertions(+), 5 deletions(-) diff --git a/oauth2/oauth2_device_code_test.go b/oauth2/oauth2_device_code_test.go index 758b916be8..8589f063b4 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,15 +17,21 @@ 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/driver/config" "github.com/ory/hydra/v2/internal" "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) { @@ -33,11 +41,9 @@ func TestDeviceAuthRequest(t *testing.T) { 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", @@ -219,3 +225,471 @@ func TestDeviceTokenRequest(t *testing.T) { }) } } + +func TestDeviceCodeWithDefaultStrategy(t *testing.T) { + ctx := context.Background() + reg := internal.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 20e1fe3ba8239c940e9ddb205dfdc4383178f19f Mon Sep 17 00:00:00 2001 From: Nikos Date: Tue, 23 Apr 2024 11:05:09 +0300 Subject: [PATCH 26/33] 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 5bc92ec79a..177300163a 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 5bc92ec79a..177300163a 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 a33ded10af..4822e81653 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 // @@ -828,7 +835,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 b32093c549ab87903d078ac13cbfa9f5b5fd4054 Mon Sep 17 00:00:00 2001 From: Nikos Date: Thu, 25 Apr 2024 17:58:00 +0300 Subject: [PATCH 27/33] 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 15cff8f77d..a9ac8197df 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 7956cbdb0b..75972d053b 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 bf89ac9fbb..19b5e5afae 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 80b03c03c1..16fb5b3114 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 51c70ec465..69682c0324 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 c21aa5b371..e23aa7bed8 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 f2b7a739e5..25e7e61522 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 8726a5b41a..e88c1c9d9b 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 96fa08bab1..91e85c55a5 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 c21aa5b371..e23aa7bed8 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 08bfd96862..1191ae414e 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 96fa08bab1..91e85c55a5 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 bf89ac9fbb..19b5e5afae 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 7ac99ae55c..9fc694022c 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 6f80f12335..d6544830e5 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 4472241967..aca2c7bbca 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 12b431ec4b..4953cd5422 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 24b0eecfeb..5727960363 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 281b21ecdb..b161bf055f 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 281b21ecdb..b161bf055f 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 0718f2d222..aa0b8b3ae7 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 be4b9c4668..1c994e904e 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 86477c90b0..e2ba8a218c 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 156886324a1e1c6b9ffbd9de5718223b280bb570 Mon Sep 17 00:00:00 2001 From: Nikos Date: Mon, 29 Apr 2024 16:53:30 +0300 Subject: [PATCH 28/33] 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 eb65327c43..92a6eb6b00 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 d58301981b..1cb9ff6e76 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 b0a9c4116b..b2d8a61222 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 ad8ddb8fa5..10e001ac97 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 295a11833a..c51c01b13e 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 9864869db0..f87065ee09 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 8186c89de2..6bf27b0d29 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 84bf09f357..51cbcaf1c5 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 afae63b866..ffe308afe0 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 5385cde2a5..573049c6c9 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 7e3e68023f..a49000472a 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 cd61f01cbb..1877d4b298 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 7eaff4d813..fb67f9202b 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 7571ef2353..1bc2ef1ea6 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 ab4ee61170..42b12e6b49 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 63339ce7da..fbc35aedfc 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 140d8a4202..40470238a5 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 6bc3911af9..7b3e67c777 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 5b6cc1f41355e02212561f17eb3cc886fd51cc2d Mon Sep 17 00:00:00 2001 From: Nikos Date: Wed, 22 May 2024 14:35:33 +0300 Subject: [PATCH 29/33] 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 0000000000..74e9a33a4b --- /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 6feabdb810..8fcaf54a41 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -62,6 +62,7 @@ func RegisterCommandRecursive(parent *cobra.Command, slOpts []servicelocatorx.Op performCmd.AddCommand( NewPerformClientCredentialsCmd(), NewPerformAuthorizationCodeCmd(), + NewPerformDeviceCodeCmd(), ) revokeCmd := NewRevokeCmd() From a5bb44b3e3138c9d607f99c52f7fd2efbfbd99a3 Mon Sep 17 00:00:00 2001 From: Nikos Date: Tue, 30 Jul 2024 14:56:56 +0300 Subject: [PATCH 30/33] 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 f09c803c06..577fffa27f 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" @@ -65,6 +66,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 a4a11ac38c..a17a6412b1 100644 --- a/consent/strategy_default.go +++ b/consent/strategy_default.go @@ -13,6 +13,7 @@ import ( "strings" "time" + "github.com/gobuffalo/pop/v6" "github.com/gorilla/sessions" "github.com/hashicorp/go-retryablehttp" "github.com/pborman/uuid" @@ -39,8 +40,6 @@ import ( "github.com/ory/x/urlx" ) -type ctxKey int - const ( DeviceVerificationPath = "/oauth2/device/verify" CookieAuthenticationSIDName = "sid" @@ -1159,21 +1158,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 { @@ -1197,7 +1186,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")) @@ -1235,16 +1227,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 } @@ -1325,7 +1333,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 4822e81653..b7250035e8 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 e8971687936cfca83bb7513b001a5b36c12cc58a Mon Sep 17 00:00:00 2001 From: Nikos Date: Wed, 25 Sep 2024 14:56:40 +0300 Subject: [PATCH 31/33] 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 d6898a7b8d..d146e10cd6 100644 --- a/internal/mock/config_cookie.go +++ b/internal/mock/config_cookie.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/hydra/v2/x (interfaces: CookieConfigProvider) diff --git a/jwk/registry_mock_test.go b/jwk/registry_mock_test.go index 68de41ca30..f9624dc2b7 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 8149e206b2..7dd35e6a15 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 111eea0979a14ae772c8fbdbb48254f8c19d2892 Mon Sep 17 00:00:00 2001 From: Nikos Date: Tue, 24 Sep 2024 13:40:48 +0300 Subject: [PATCH 32/33] 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 d0e465ce1c..395d900b30 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 a2f17fd7fe..f5ccc0e780 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 b0fa5a54ad..92edea6ebe 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: |- @@ -1499,6 +1533,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 + her 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 201. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/errorOAuth2' + description: errorOAuth2 + summary: OAuth 2.0 Device Verification Endpoint + tags: + - oAuth2 /oauth2/register: post: description: |- @@ -1866,6 +1943,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." @@ -1929,6 +2038,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: @@ -2125,6 +2240,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: @@ -2555,47 +2717,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 @@ -2603,21 +2746,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: |- @@ -2725,6 +2890,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 @@ -2979,6 +3162,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." @@ -3038,6 +3239,7 @@ components: - acr_values - acr_values display: display + device_challenge_id: device_challenge_id skip: true request_url: request_url acr: acr @@ -3045,47 +3247,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 @@ -3093,21 +3276,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 @@ -3134,6 +3339,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 @@ -3268,6 +3477,7 @@ components: - acr_values - acr_values display: display + device_challenge_id: device_challenge_id skip: true request_url: request_url acr: acr @@ -3275,47 +3485,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 @@ -3323,21 +3514,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 @@ -3428,47 +3641,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 @@ -3476,21 +3670,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 @@ -3559,47 +3775,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 @@ -3607,21 +3804,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 sid: sid @@ -3714,6 +3933,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 @@ -3836,6 +4056,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 @@ -4015,6 +4239,7 @@ components: type: array required: - authorization_endpoint + - device_authorization_endpoint - id_token_signed_response_alg - id_token_signing_alg_values_supported - issuer @@ -4482,6 +4707,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 90a496a643..59e687f0e2 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 her 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 0000000000..2f892922a7 --- /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 0000000000..4ba933a4b2 --- /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 0000000000..ae99e6223f --- /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 6f8c0aac52..0ca7f601f1 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 c9285372f9..de0c029e2f 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 cda6ca600c..b38aef35d7 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 f01dc3f79f..dfe3d0abec 100644 --- a/internal/httpclient/docs/OAuth2ConsentRequest.md +++ b/internal/httpclient/docs/OAuth2ConsentRequest.md @@ -9,6 +9,7 @@ Name | Type | Description | Notes **Challenge** | **string** | ID is the identifier (\"authorization challenge\") of the consent authorization request. It is used to identify the session. | **Client** | Pointer to [**OAuth2Client**](OAuth2Client.md) | | [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] @@ -167,6 +168,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 1b20c7d873..27f0134440 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 0000000000..09a2270ab4 --- /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 0000000000..c34d1cd504 --- /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 0000000000..975972a853 --- /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 0000000000..a101144b4a --- /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 96fc7da400..454579d0ec 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 2ed10b8508..16e925f679 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 78be5c543f..06fa79ba56 100644 --- a/internal/httpclient/model_o_auth2_consent_request.go +++ b/internal/httpclient/model_o_auth2_consent_request.go @@ -29,6 +29,8 @@ type OAuth2ConsentRequest struct { Challenge string `json:"challenge"` Client *OAuth2Client `json:"client,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. @@ -217,6 +219,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) { @@ -496,6 +530,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 240e40b307..465fa997f3 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 0000000000..692694e904 --- /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 41f0ddaec8..1ec0b266f7 100644 --- a/internal/testhelpers/oauth2.go +++ b/internal/testhelpers/oauth2.go @@ -169,6 +169,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 26ab8b4dfb..ad979c7dc5 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" }, @@ -848,6 +943,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" @@ -1147,6 +1246,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" @@ -1280,6 +1384,7 @@ "required": [ "issuer", "authorization_endpoint", + "device_authorization_endpoint", "token_endpoint", "jwks_uri", "subject_types_supported", @@ -1665,6 +1770,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": { @@ -2591,6 +2725,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.", @@ -3472,6 +3658,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 her 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 4b5268143b..3eac3dd839 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.", @@ -1713,6 +1762,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 her 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.", @@ -2119,6 +2227,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." @@ -2169,6 +2306,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.", @@ -2315,7 +2461,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": { @@ -2729,6 +2875,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" @@ -2866,6 +3021,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" }, @@ -2910,6 +3074,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" @@ -3141,6 +3309,7 @@ "required": [ "issuer", "authorization_endpoint", + "device_authorization_endpoint", "token_endpoint", "jwks_uri", "subject_types_supported", @@ -3192,6 +3361,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" @@ -3697,6 +3871,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 b7767f9eff02cd382727f81dce9c9dc7cf077d9f Mon Sep 17 00:00:00 2001 From: Nikos Date: Thu, 26 Sep 2024 13:25:27 +0300 Subject: [PATCH 33/33] 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 c94281f769..b5454b0eaa 100644 --- a/persistence/sql/persister_oauth2.go +++ b/persistence/sql/persister_oauth2.go @@ -763,17 +763,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 }