diff --git a/.schema/config.schema.json b/.schema/config.schema.json index 252433161c0..0dc6cbe3ed2 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", @@ -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,30 @@ "/ui/logout" ] }, + "device": { + "type": "object", + "description": "Configure URLs for the OAuth 2.0 Device Code Flow.", + "properties": { + "verification": { + "type": "string", + "description": "Sets the device user code verification endpoint. Defaults to an internal fallback URL showing an error.", + "format": "uri-reference", + "examples": [ + "https://my-logout.app/device_verification", + "/ui/device_verification" + ] + }, + "success": { + "type": "string", + "description": "Sets the post device authentication endpoint. Defaults to an internal fallback URL showing an error.", + "format": "uri-reference", + "examples": [ + "https://my-logout.app/device_done", + "/ui/device_done" + ] + } + } + }, "error": { "type": "string", "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 +984,15 @@ "$ref": "#/definitions/duration" } ] + }, + "device_user_code": { + "description": "Configures how long device & user codes are valid.", + "default": "10m", + "allOf": [ + { + "$ref": "#/definitions/duration" + } + ] } } }, @@ -1124,6 +1170,28 @@ } ] }, + "device_authorization": { + "type": "object", + "additionalProperties": false, + "properties": { + "token_polling_interval": { + "allOf": [ + { + "$ref": "#/definitions/duration" + } + ], + "default": "5s", + "description": "Configures how often a non-interactive device should poll the device token endpoint, this is a purely informational configuration and does not enforce rate-limiting.", + "examples": ["5s", "15s", "1m"] + }, + "user_code_entropy": { + "type": "string", + "description": "Sets the entropy for the user codes.", + "default": "medium", + "enum": ["high", "medium", "low"] + } + } + }, "token_hook": { "description": "Sets the token hook endpoint for all grant types. If set it will be called while providing token to customize claims.", "examples": ["https://my-example.app/token-hook"], @@ -1137,8 +1205,8 @@ } ] } - } - }, + } + }, "secrets": { "type": "object", "additionalProperties": false, @@ -1183,7 +1251,7 @@ "examples": ["cpu"] }, "tracing": { - "$ref": "https://raw.githubusercontent.com/ory/x/v0.0.675/otelx/config.schema.json" + "$ref": "ory://tracing-config" }, "sqa": { "type": "object", diff --git a/client/.snapshots/TestHandler-common-case=create_clients-case=0-description=basic_dynamic_client_registration.json b/client/.snapshots/TestHandler-common-case=create_clients-case=0-description=basic_dynamic_client_registration.json index 15cff8f77d2..a9ac8197dff 100644 --- a/client/.snapshots/TestHandler-common-case=create_clients-case=0-description=basic_dynamic_client_registration.json +++ b/client/.snapshots/TestHandler-common-case=create_clients-case=0-description=basic_dynamic_client_registration.json @@ -31,5 +31,8 @@ "jwt_bearer_grant_access_token_lifespan": null, "refresh_token_grant_id_token_lifespan": null, "refresh_token_grant_access_token_lifespan": null, - "refresh_token_grant_refresh_token_lifespan": null + "refresh_token_grant_refresh_token_lifespan": null, + "device_authorization_grant_id_token_lifespan": null, + "device_authorization_grant_access_token_lifespan": null, + "device_authorization_grant_refresh_token_lifespan": null } diff --git a/client/.snapshots/TestHandler-common-case=create_clients-case=1-description=basic_admin_registration.json b/client/.snapshots/TestHandler-common-case=create_clients-case=1-description=basic_admin_registration.json index 7956cbdb0bb..75972d053bb 100644 --- a/client/.snapshots/TestHandler-common-case=create_clients-case=1-description=basic_admin_registration.json +++ b/client/.snapshots/TestHandler-common-case=create_clients-case=1-description=basic_admin_registration.json @@ -34,5 +34,8 @@ "jwt_bearer_grant_access_token_lifespan": null, "refresh_token_grant_id_token_lifespan": null, "refresh_token_grant_access_token_lifespan": null, - "refresh_token_grant_refresh_token_lifespan": null + "refresh_token_grant_refresh_token_lifespan": null, + "device_authorization_grant_id_token_lifespan": null, + "device_authorization_grant_access_token_lifespan": null, + "device_authorization_grant_refresh_token_lifespan": null } diff --git a/client/.snapshots/TestHandler-common-case=create_clients-case=10-description=empty_ID_succeeds.json b/client/.snapshots/TestHandler-common-case=create_clients-case=10-description=empty_ID_succeeds.json index bf89ac9fbb8..19b5e5afae5 100644 --- a/client/.snapshots/TestHandler-common-case=create_clients-case=10-description=empty_ID_succeeds.json +++ b/client/.snapshots/TestHandler-common-case=create_clients-case=10-description=empty_ID_succeeds.json @@ -31,5 +31,8 @@ "jwt_bearer_grant_access_token_lifespan": null, "refresh_token_grant_id_token_lifespan": null, "refresh_token_grant_access_token_lifespan": null, - "refresh_token_grant_refresh_token_lifespan": null + "refresh_token_grant_refresh_token_lifespan": null, + "device_authorization_grant_id_token_lifespan": null, + "device_authorization_grant_access_token_lifespan": null, + "device_authorization_grant_refresh_token_lifespan": null } diff --git a/client/.snapshots/TestHandler-common-case=create_clients-case=10-description=setting_skip_logout_consent_succeeds_for_admin_registration.json b/client/.snapshots/TestHandler-common-case=create_clients-case=10-description=setting_skip_logout_consent_succeeds_for_admin_registration.json index 80b03c03c1e..16fb5b31144 100644 --- a/client/.snapshots/TestHandler-common-case=create_clients-case=10-description=setting_skip_logout_consent_succeeds_for_admin_registration.json +++ b/client/.snapshots/TestHandler-common-case=create_clients-case=10-description=setting_skip_logout_consent_succeeds_for_admin_registration.json @@ -32,5 +32,8 @@ "jwt_bearer_grant_access_token_lifespan": null, "refresh_token_grant_id_token_lifespan": null, "refresh_token_grant_access_token_lifespan": null, - "refresh_token_grant_refresh_token_lifespan": null + "refresh_token_grant_refresh_token_lifespan": null, + "device_authorization_grant_id_token_lifespan": null, + "device_authorization_grant_access_token_lifespan": null, + "device_authorization_grant_refresh_token_lifespan": null } diff --git a/client/.snapshots/TestHandler-common-case=create_clients-case=12-description=empty_ID_succeeds.json b/client/.snapshots/TestHandler-common-case=create_clients-case=12-description=empty_ID_succeeds.json index 51c70ec465c..69682c03242 100644 --- a/client/.snapshots/TestHandler-common-case=create_clients-case=12-description=empty_ID_succeeds.json +++ b/client/.snapshots/TestHandler-common-case=create_clients-case=12-description=empty_ID_succeeds.json @@ -32,5 +32,8 @@ "jwt_bearer_grant_access_token_lifespan": null, "refresh_token_grant_id_token_lifespan": null, "refresh_token_grant_access_token_lifespan": null, - "refresh_token_grant_refresh_token_lifespan": null + "refresh_token_grant_refresh_token_lifespan": null, + "device_authorization_grant_id_token_lifespan": null, + "device_authorization_grant_access_token_lifespan": null, + "device_authorization_grant_refresh_token_lifespan": null } diff --git a/client/.snapshots/TestHandler-common-case=create_clients-case=2-description=empty_ID_succeeds.json b/client/.snapshots/TestHandler-common-case=create_clients-case=2-description=empty_ID_succeeds.json index c21aa5b3710..e23aa7bed82 100644 --- a/client/.snapshots/TestHandler-common-case=create_clients-case=2-description=empty_ID_succeeds.json +++ b/client/.snapshots/TestHandler-common-case=create_clients-case=2-description=empty_ID_succeeds.json @@ -30,5 +30,8 @@ "jwt_bearer_grant_access_token_lifespan": null, "refresh_token_grant_id_token_lifespan": null, "refresh_token_grant_access_token_lifespan": null, - "refresh_token_grant_refresh_token_lifespan": null + "refresh_token_grant_refresh_token_lifespan": null, + "device_authorization_grant_id_token_lifespan": null, + "device_authorization_grant_access_token_lifespan": null, + "device_authorization_grant_refresh_token_lifespan": null } diff --git a/client/.snapshots/TestHandler-common-case=create_clients-case=4-description=non-uuid_works.json b/client/.snapshots/TestHandler-common-case=create_clients-case=4-description=non-uuid_works.json index f2b7a739e55..25e7e615220 100644 --- a/client/.snapshots/TestHandler-common-case=create_clients-case=4-description=non-uuid_works.json +++ b/client/.snapshots/TestHandler-common-case=create_clients-case=4-description=non-uuid_works.json @@ -34,5 +34,8 @@ "jwt_bearer_grant_access_token_lifespan": null, "refresh_token_grant_id_token_lifespan": null, "refresh_token_grant_access_token_lifespan": null, - "refresh_token_grant_refresh_token_lifespan": null + "refresh_token_grant_refresh_token_lifespan": null, + "device_authorization_grant_id_token_lifespan": null, + "device_authorization_grant_access_token_lifespan": null, + "device_authorization_grant_refresh_token_lifespan": null } diff --git a/client/.snapshots/TestHandler-common-case=create_clients-case=5-description=setting_client_id_as_uuid_works.json b/client/.snapshots/TestHandler-common-case=create_clients-case=5-description=setting_client_id_as_uuid_works.json index 8726a5b41a0..e88c1c9d9be 100644 --- a/client/.snapshots/TestHandler-common-case=create_clients-case=5-description=setting_client_id_as_uuid_works.json +++ b/client/.snapshots/TestHandler-common-case=create_clients-case=5-description=setting_client_id_as_uuid_works.json @@ -34,5 +34,8 @@ "jwt_bearer_grant_access_token_lifespan": null, "refresh_token_grant_id_token_lifespan": null, "refresh_token_grant_access_token_lifespan": null, - "refresh_token_grant_refresh_token_lifespan": null + "refresh_token_grant_refresh_token_lifespan": null, + "device_authorization_grant_id_token_lifespan": null, + "device_authorization_grant_access_token_lifespan": null, + "device_authorization_grant_refresh_token_lifespan": null } diff --git a/client/.snapshots/TestHandler-common-case=create_clients-case=7-description=setting_skip_consent_suceeds_for_admin_registration.json b/client/.snapshots/TestHandler-common-case=create_clients-case=7-description=setting_skip_consent_suceeds_for_admin_registration.json index 96fa08bab16..91e85c55a58 100644 --- a/client/.snapshots/TestHandler-common-case=create_clients-case=7-description=setting_skip_consent_suceeds_for_admin_registration.json +++ b/client/.snapshots/TestHandler-common-case=create_clients-case=7-description=setting_skip_consent_suceeds_for_admin_registration.json @@ -31,5 +31,8 @@ "jwt_bearer_grant_access_token_lifespan": null, "refresh_token_grant_id_token_lifespan": null, "refresh_token_grant_access_token_lifespan": null, - "refresh_token_grant_refresh_token_lifespan": null + "refresh_token_grant_refresh_token_lifespan": null, + "device_authorization_grant_id_token_lifespan": null, + "device_authorization_grant_access_token_lifespan": null, + "device_authorization_grant_refresh_token_lifespan": null } diff --git a/client/.snapshots/TestHandler-common-case=create_clients-case=8-description=empty_ID_succeeds.json b/client/.snapshots/TestHandler-common-case=create_clients-case=8-description=empty_ID_succeeds.json index c21aa5b3710..e23aa7bed82 100644 --- a/client/.snapshots/TestHandler-common-case=create_clients-case=8-description=empty_ID_succeeds.json +++ b/client/.snapshots/TestHandler-common-case=create_clients-case=8-description=empty_ID_succeeds.json @@ -30,5 +30,8 @@ "jwt_bearer_grant_access_token_lifespan": null, "refresh_token_grant_id_token_lifespan": null, "refresh_token_grant_access_token_lifespan": null, - "refresh_token_grant_refresh_token_lifespan": null + "refresh_token_grant_refresh_token_lifespan": null, + "device_authorization_grant_id_token_lifespan": null, + "device_authorization_grant_access_token_lifespan": null, + "device_authorization_grant_refresh_token_lifespan": null } diff --git a/client/.snapshots/TestHandler-common-case=create_clients-case=8-description=setting_skip_consent_succeeds_for_admin_registration.json b/client/.snapshots/TestHandler-common-case=create_clients-case=8-description=setting_skip_consent_succeeds_for_admin_registration.json index 08bfd968627..1191ae414eb 100644 --- a/client/.snapshots/TestHandler-common-case=create_clients-case=8-description=setting_skip_consent_succeeds_for_admin_registration.json +++ b/client/.snapshots/TestHandler-common-case=create_clients-case=8-description=setting_skip_consent_succeeds_for_admin_registration.json @@ -32,5 +32,8 @@ "jwt_bearer_grant_access_token_lifespan": null, "refresh_token_grant_id_token_lifespan": null, "refresh_token_grant_access_token_lifespan": null, - "refresh_token_grant_refresh_token_lifespan": null + "refresh_token_grant_refresh_token_lifespan": null, + "device_authorization_grant_id_token_lifespan": null, + "device_authorization_grant_access_token_lifespan": null, + "device_authorization_grant_refresh_token_lifespan": null } diff --git a/client/.snapshots/TestHandler-common-case=create_clients-case=8-description=setting_skip_consent_suceeds_for_admin_registration.json b/client/.snapshots/TestHandler-common-case=create_clients-case=8-description=setting_skip_consent_suceeds_for_admin_registration.json index 96fa08bab16..91e85c55a58 100644 --- a/client/.snapshots/TestHandler-common-case=create_clients-case=8-description=setting_skip_consent_suceeds_for_admin_registration.json +++ b/client/.snapshots/TestHandler-common-case=create_clients-case=8-description=setting_skip_consent_suceeds_for_admin_registration.json @@ -31,5 +31,8 @@ "jwt_bearer_grant_access_token_lifespan": null, "refresh_token_grant_id_token_lifespan": null, "refresh_token_grant_access_token_lifespan": null, - "refresh_token_grant_refresh_token_lifespan": null + "refresh_token_grant_refresh_token_lifespan": null, + "device_authorization_grant_id_token_lifespan": null, + "device_authorization_grant_access_token_lifespan": null, + "device_authorization_grant_refresh_token_lifespan": null } diff --git a/client/.snapshots/TestHandler-common-case=create_clients-case=9-description=empty_ID_succeeds.json b/client/.snapshots/TestHandler-common-case=create_clients-case=9-description=empty_ID_succeeds.json index bf89ac9fbb8..19b5e5afae5 100644 --- a/client/.snapshots/TestHandler-common-case=create_clients-case=9-description=empty_ID_succeeds.json +++ b/client/.snapshots/TestHandler-common-case=create_clients-case=9-description=empty_ID_succeeds.json @@ -31,5 +31,8 @@ "jwt_bearer_grant_access_token_lifespan": null, "refresh_token_grant_id_token_lifespan": null, "refresh_token_grant_access_token_lifespan": null, - "refresh_token_grant_refresh_token_lifespan": null + "refresh_token_grant_refresh_token_lifespan": null, + "device_authorization_grant_id_token_lifespan": null, + "device_authorization_grant_access_token_lifespan": null, + "device_authorization_grant_refresh_token_lifespan": null } diff --git a/client/.snapshots/TestHandler-common-case=fetching_existing_client-endpoint=admin.json b/client/.snapshots/TestHandler-common-case=fetching_existing_client-endpoint=admin.json index 7ac99ae55c2..9fc694022cd 100644 --- a/client/.snapshots/TestHandler-common-case=fetching_existing_client-endpoint=admin.json +++ b/client/.snapshots/TestHandler-common-case=fetching_existing_client-endpoint=admin.json @@ -32,7 +32,10 @@ "jwt_bearer_grant_access_token_lifespan": null, "refresh_token_grant_id_token_lifespan": null, "refresh_token_grant_access_token_lifespan": null, - "refresh_token_grant_refresh_token_lifespan": null + "refresh_token_grant_refresh_token_lifespan": null, + "device_authorization_grant_id_token_lifespan": null, + "device_authorization_grant_access_token_lifespan": null, + "device_authorization_grant_refresh_token_lifespan": null }, "status": 200 } diff --git a/client/.snapshots/TestHandler-common-case=fetching_existing_client-endpoint=selfservice.json b/client/.snapshots/TestHandler-common-case=fetching_existing_client-endpoint=selfservice.json index 6f80f123353..d6544830e52 100644 --- a/client/.snapshots/TestHandler-common-case=fetching_existing_client-endpoint=selfservice.json +++ b/client/.snapshots/TestHandler-common-case=fetching_existing_client-endpoint=selfservice.json @@ -31,7 +31,10 @@ "jwt_bearer_grant_access_token_lifespan": null, "refresh_token_grant_id_token_lifespan": null, "refresh_token_grant_access_token_lifespan": null, - "refresh_token_grant_refresh_token_lifespan": null + "refresh_token_grant_refresh_token_lifespan": null, + "device_authorization_grant_id_token_lifespan": null, + "device_authorization_grant_access_token_lifespan": null, + "device_authorization_grant_refresh_token_lifespan": null }, "status": 200 } diff --git a/client/.snapshots/TestHandler-common-case=update_the_lifespans_of_an_OAuth2_client.json b/client/.snapshots/TestHandler-common-case=update_the_lifespans_of_an_OAuth2_client.json index 4472241967d..aca2c7bbca9 100644 --- a/client/.snapshots/TestHandler-common-case=update_the_lifespans_of_an_OAuth2_client.json +++ b/client/.snapshots/TestHandler-common-case=update_the_lifespans_of_an_OAuth2_client.json @@ -32,7 +32,10 @@ "jwt_bearer_grant_access_token_lifespan": "37h0m0s", "refresh_token_grant_id_token_lifespan": "40h0m0s", "refresh_token_grant_access_token_lifespan": "41h0m0s", - "refresh_token_grant_refresh_token_lifespan": "42h0m0s" + "refresh_token_grant_refresh_token_lifespan": "42h0m0s", + "device_authorization_grant_id_token_lifespan": "45h0m0s", + "device_authorization_grant_access_token_lifespan": "46h0m0s", + "device_authorization_grant_refresh_token_lifespan": "47h0m0s" }, "status": 200 } diff --git a/client/.snapshots/TestHandler-common-case=updating_existing_client-endpoint=admin.json b/client/.snapshots/TestHandler-common-case=updating_existing_client-endpoint=admin.json index 12b431ec4b2..4953cd54220 100644 --- a/client/.snapshots/TestHandler-common-case=updating_existing_client-endpoint=admin.json +++ b/client/.snapshots/TestHandler-common-case=updating_existing_client-endpoint=admin.json @@ -34,7 +34,10 @@ "jwt_bearer_grant_access_token_lifespan": null, "refresh_token_grant_id_token_lifespan": null, "refresh_token_grant_access_token_lifespan": null, - "refresh_token_grant_refresh_token_lifespan": null + "refresh_token_grant_refresh_token_lifespan": null, + "device_authorization_grant_id_token_lifespan": null, + "device_authorization_grant_access_token_lifespan": null, + "device_authorization_grant_refresh_token_lifespan": null }, "status": 200 } diff --git a/client/.snapshots/TestHandler-common-case=updating_existing_client-endpoint=dynamic_client_registration.json b/client/.snapshots/TestHandler-common-case=updating_existing_client-endpoint=dynamic_client_registration.json index 24b0eecfeb7..5727960363b 100644 --- a/client/.snapshots/TestHandler-common-case=updating_existing_client-endpoint=dynamic_client_registration.json +++ b/client/.snapshots/TestHandler-common-case=updating_existing_client-endpoint=dynamic_client_registration.json @@ -33,7 +33,10 @@ "jwt_bearer_grant_access_token_lifespan": null, "refresh_token_grant_id_token_lifespan": null, "refresh_token_grant_access_token_lifespan": null, - "refresh_token_grant_refresh_token_lifespan": null + "refresh_token_grant_refresh_token_lifespan": null, + "device_authorization_grant_id_token_lifespan": null, + "device_authorization_grant_access_token_lifespan": null, + "device_authorization_grant_refresh_token_lifespan": null }, "status": 200 } diff --git a/client/.snapshots/TestHandler-create_client_registration_tokens-case=0-dynamic=true.json b/client/.snapshots/TestHandler-create_client_registration_tokens-case=0-dynamic=true.json index 281b21ecdbf..b161bf055fa 100644 --- a/client/.snapshots/TestHandler-create_client_registration_tokens-case=0-dynamic=true.json +++ b/client/.snapshots/TestHandler-create_client_registration_tokens-case=0-dynamic=true.json @@ -27,5 +27,8 @@ "jwt_bearer_grant_access_token_lifespan": null, "refresh_token_grant_id_token_lifespan": null, "refresh_token_grant_access_token_lifespan": null, - "refresh_token_grant_refresh_token_lifespan": null + "refresh_token_grant_refresh_token_lifespan": null, + "device_authorization_grant_id_token_lifespan": null, + "device_authorization_grant_access_token_lifespan": null, + "device_authorization_grant_refresh_token_lifespan": null } diff --git a/client/.snapshots/TestHandler-create_client_registration_tokens-case=1-dynamic=false.json b/client/.snapshots/TestHandler-create_client_registration_tokens-case=1-dynamic=false.json index 281b21ecdbf..b161bf055fa 100644 --- a/client/.snapshots/TestHandler-create_client_registration_tokens-case=1-dynamic=false.json +++ b/client/.snapshots/TestHandler-create_client_registration_tokens-case=1-dynamic=false.json @@ -27,5 +27,8 @@ "jwt_bearer_grant_access_token_lifespan": null, "refresh_token_grant_id_token_lifespan": null, "refresh_token_grant_access_token_lifespan": null, - "refresh_token_grant_refresh_token_lifespan": null + "refresh_token_grant_refresh_token_lifespan": null, + "device_authorization_grant_id_token_lifespan": null, + "device_authorization_grant_access_token_lifespan": null, + "device_authorization_grant_refresh_token_lifespan": null } diff --git a/client/.snapshots/TestHandler-create_client_registration_tokens-case=2-dynamic=false.json b/client/.snapshots/TestHandler-create_client_registration_tokens-case=2-dynamic=false.json index 0718f2d2227..aa0b8b3ae78 100644 --- a/client/.snapshots/TestHandler-create_client_registration_tokens-case=2-dynamic=false.json +++ b/client/.snapshots/TestHandler-create_client_registration_tokens-case=2-dynamic=false.json @@ -28,5 +28,8 @@ "jwt_bearer_grant_access_token_lifespan": null, "refresh_token_grant_id_token_lifespan": null, "refresh_token_grant_access_token_lifespan": null, - "refresh_token_grant_refresh_token_lifespan": null + "refresh_token_grant_refresh_token_lifespan": null, + "device_authorization_grant_id_token_lifespan": null, + "device_authorization_grant_access_token_lifespan": null, + "device_authorization_grant_refresh_token_lifespan": null } diff --git a/client/client.go b/client/client.go index 3394dd51c79..c027d505192 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 @@ -379,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 { @@ -549,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/client/registry.go b/client/registry.go index bfec25ace91..c23efd231db 100644 --- a/client/registry.go +++ b/client/registry.go @@ -8,6 +8,7 @@ import ( "github.com/ory/fosite" foauth2 "github.com/ory/fosite/handler/oauth2" + "github.com/ory/fosite/handler/rfc8628" enigma "github.com/ory/fosite/token/hmac" "github.com/ory/hydra/v2/jwk" "github.com/ory/hydra/v2/x" @@ -25,5 +26,6 @@ type Registry interface { OpenIDJWTStrategy() jwk.JWTSigner OAuth2HMACStrategy() foauth2.CoreStrategy OAuth2EnigmaStrategy() *enigma.HMACStrategy + RFC8628HMACStrategy() rfc8628.RFC8628CodeStrategy config.Provider } diff --git a/cmd/cmd_perform_device_flow.go b/cmd/cmd_perform_device_flow.go new file mode 100644 index 00000000000..f697722e65f --- /dev/null +++ b/cmd/cmd_perform_device_flow.go @@ -0,0 +1,115 @@ +// 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 ...", + 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 use the "none" or "client_secret_post" token-endpoint-auth-method.`, + RunE: func(cmd *cobra.Command, args []string) error { + client, endpoint, err := cliclient.NewClient(cmd) + if err != nil { + 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.Fprintln(cmd.ErrOrStderr(), cmd.UsageString()) + _, _ = fmt.Fprintln(cmd.ErrOrStderr(), "Please provide a Client ID using --client-id flag, or OAUTH2_CLIENT_ID environment variable.") + return cmdx.FailSilently(cmd) + } + + 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, + } + + params := []oauth2.AuthCodeOption{oauth2.SetAuthURLParam("audience", strings.Join(audience, "+"))} + if clientSecret != "" { + params = append(params, oauth2.SetAuthURLParam("client_secret", clientSecret)) + } + + deviceAuthResponse, err := conf.DeviceAuth( + ctx, + params..., + ) + if err != nil { + _, _ = fmt.Fprintf( + cmd.ErrOrStderr(), "Failed to perform the device authorization request: %s\n", err) + return cmdx.FailSilently(cmd) + } + + _, _ = fmt.Fprintln( + cmd.ErrOrStderr(), + "To login please go to:\n\t", + deviceAuthResponse.VerificationURIComplete, + ) + + token, err := conf.DeviceAccessToken(ctx, deviceAuthResponse) + if err != nil { + _, _ = fmt.Fprintf( + cmd.ErrOrStderr(), "Failed to perform the device token request: %s\n", err) + return cmdx.FailSilently(cmd) + } + + _, _ = fmt.Fprintln(cmd.ErrOrStderr(), "Successfully signed in!") + + cmdx.PrintRow(cmd, outputOAuth2Token(*token)) + return nil + }, + } + + cmd.Flags().StringSlice("scope", []string{"offline", "openid"}, "Request OAuth2 scope") + + cmd.Flags().String("client-id", os.Getenv("OAUTH2_CLIENT_ID"), "Use the provided OAuth 2.0 Client ID, defaults to environment variable OAUTH2_CLIENT_ID") + cmd.Flags().String("client-secret", os.Getenv("OAUTH2_CLIENT_SECRET"), "Use the provided OAuth 2.0 Client Secret, defaults to environment variable OAUTH2_CLIENT_SECRET") + + cmd.Flags().StringSlice("audience", []string{}, "Request a specific OAuth 2.0 Access Token Audience") + cmd.Flags().String("device-auth-url", "", "Usually it is enough to specify the `endpoint` flag, but if you want to force the device authorization url, use this flag") + cmd.Flags().String("token-url", "", "Usually it is enough to specify the `endpoint` flag, but if you want to force the token url, use this flag") + + return cmd +} diff --git a/cmd/root.go b/cmd/root.go index 20832d40546..12055fb0b85 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -64,6 +64,7 @@ func RegisterCommandRecursive(parent *cobra.Command, slOpts []servicelocatorx.Op performCmd.AddCommand( NewPerformClientCredentialsCmd(), NewPerformAuthorizationCodeCmd(), + NewPerformDeviceCodeCmd(), ) revokeCmd := NewRevokeCmd() diff --git a/consent/handler.go b/consent/handler.go index b7af2d49afb..8178cf0cc6d 100644 --- a/consent/handler.go +++ b/consent/handler.go @@ -4,11 +4,13 @@ package consent import ( + "context" "encoding/json" "net/http" "net/url" "time" + "github.com/ory/hydra/v2/client" "github.com/ory/hydra/v2/flow" "github.com/ory/hydra/v2/oauth2/flowctx" "github.com/ory/hydra/v2/x/events" @@ -35,6 +37,7 @@ type Handler struct { const ( LoginPath = "/oauth2/auth/requests/login" + DevicePath = "/oauth2/auth/requests/device" ConsentPath = "/oauth2/auth/requests/consent" LogoutPath = "/oauth2/auth/requests/logout" SessionsPath = "/oauth2/auth/sessions" @@ -66,6 +69,8 @@ func (h *Handler) SetRoutes(admin *httprouterx.RouterAdmin) { admin.GET(LogoutPath, h.getOAuth2LogoutRequest) admin.PUT(LogoutPath+"/accept", h.acceptOAuth2LogoutRequest) admin.PUT(LogoutPath+"/reject", h.rejectOAuth2LogoutRequest) + + admin.PUT(DevicePath+"/accept", h.acceptUserCodeRequest) } // Revoke OAuth 2.0 Consent Session Parameters @@ -1064,3 +1069,141 @@ func (h *Handler) getOAuth2LogoutRequest(w http.ResponseWriter, r *http.Request, h.r.Writer().Write(w, r, request) } + +// Verify OAuth 2.0 User Code Request +// +// swagger:parameters acceptUserCodeRequest +type verifyUserCodeRequest struct { + // in: query + // required: true + Challenge string `json:"device_challenge"` + + // in: body + Body flow.AcceptDeviceUserCodeRequest +} + +// swagger:route PUT /admin/oauth2/auth/requests/device/accept oAuth2 acceptUserCodeRequest +// +// # Accepts a device grant user_code request +// +// Accepts a device grant user_code request +// +// Consumes: +// - application/json +// +// Produces: +// - application/json +// +// Schemes: http, https +// +// Responses: +// 200: oAuth2RedirectTo +// default: errorOAuth2 +func (h *Handler) acceptUserCodeRequest(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + ctx := r.Context() + + challenge := r.URL.Query().Get("device_challenge") + if challenge == "" { + h.r.Writer().WriteError(w, r, errorsx.WithStack(fosite.ErrInvalidRequest.WithHint(`Query parameter 'device_challenge' is not defined but should have been.`))) + return + } + + var reqBody flow.AcceptDeviceUserCodeRequest + d := json.NewDecoder(r.Body) + d.DisallowUnknownFields() + if err := d.Decode(&reqBody); err != nil { + h.r.Writer().WriteError(w, r, errorsx.WithStack(fosite.ErrInvalidRequest.WithWrap(err).WithHintf("Unable to decode request body: %s", err.Error()))) + 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, 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, fosite.ErrServerError.WithWrap(err).WithHint(`The 'user_code' signature could not be computed.`)) + return + } + + userCodeRequest, err := h.r.OAuth2Storage().GetUserCodeSession(r.Context(), userCodeSignature, nil) + if err != nil { + h.r.Writer().WriteError(w, r, fosite.ErrInvalidRequest.WithWrap(err).WithHint(`The 'user_code' session could not be found or has expired or is otherwise malformed.`)) + return + } + + if err := h.r.RFC8628HMACStrategy().ValidateUserCode(ctx, userCodeRequest, reqBody.UserCode); err != nil { + h.r.Writer().WriteError(w, r, fosite.ErrInvalidRequest.WithWrap(err).WithHint(`The 'user_code' session could not be found or has expired or is otherwise malformed.`)) + return + } + + 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, err) + return + } + + ru, err := url.Parse(hr.RequestURL) + if err != nil { + h.r.Writer().WriteError(w, r, fosite.ErrInvalidRequest.WithWrap(err).WithHint(`Unable to parse the request_url.`)) + return + } + + verifier, err := f.ToDeviceVerifier(ctx, h.r) + if err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + + events.Trace(ctx, events.DeviceUserCodeAccepted, events.WithClientID(userCodeRequest.GetClient().GetID())) + + h.r.Writer().Write(w, r, &flow.OAuth2RedirectTo{ + RedirectTo: urlx.SetQuery(ru, url.Values{"device_verifier": {verifier}, "client_id": {userCodeRequest.GetClient().GetID()}}).String(), + }) +} + +func (h *Handler) decodeFlowWithClient(ctx context.Context, challenge string, opts ...flowctx.CodecOption) (*flow.Flow, error) { + f, err := flowctx.Decode[flow.Flow](ctx, h.r.FlowCipher(), challenge, opts...) + if err != nil { + return nil, err + } + + return f, nil +} diff --git a/consent/handler_test.go b/consent/handler_test.go index 273c6a48fe5..f8638c07454 100644 --- a/consent/handler_test.go +++ b/consent/handler_test.go @@ -17,10 +17,14 @@ import ( "github.com/stretchr/testify/require" + "github.com/ory/fosite" + "github.com/ory/fosite/handler/openid" + "github.com/ory/fosite/token/jwt" hydra "github.com/ory/hydra-client-go/v2" "github.com/ory/hydra/v2/client" . "github.com/ory/hydra/v2/consent" "github.com/ory/hydra/v2/flow" + "github.com/ory/hydra/v2/oauth2" "github.com/ory/hydra/v2/x" "github.com/ory/x/contextx" "github.com/ory/x/pointerx" @@ -300,3 +304,437 @@ func TestGetLoginRequestWithDuplicateAccept(t *testing.T) { require.Contains(t, result2.RedirectTo, "login_verifier") }) } + +func TestAcceptDeviceRequest(t *testing.T) { + ctx := context.Background() + challenge := "challenge" + requestURL := "https://hydra.example.com/" + oauth2.DeviceVerificationPath + + conf := testhelpers.NewConfigurationWithDefaults() + reg := testhelpers.NewRegistryMemory(t, conf, &contextx.Default{}) + + cl := &client.Client{ID: "client"} + require.NoError(t, reg.ClientManager().CreateClient(ctx, cl)) + f, err := reg.ConsentManager().CreateDeviceUserAuthRequest(ctx, &flow.DeviceUserAuthRequest{ + Client: cl, + ID: challenge, + RequestURL: requestURL, + RequestedAt: time.Now(), + }) + require.NoError(t, err) + challenge, err = f.ToDeviceChallenge(ctx, reg) + require.NoError(t, err) + + h := NewHandler(reg, conf) + r := x.NewRouterAdmin(conf.AdminURL) + h.SetRoutes(r) + ts := httptest.NewServer(r) + t.Cleanup(ts.Close) + + c := &http.Client{} + + deviceRequest := fosite.NewDeviceRequest() + deviceRequest.Client = cl + deviceRequest.SetSession( + &oauth2.Session{ + DefaultSession: &openid.DefaultSession{ + Headers: &jwt.Headers{}, + }, + }, + ) + _, deviceCodesig, err := reg.RFC8628HMACStrategy().GenerateDeviceCode(ctx) + require.NoError(t, err) + userCode, sig, err := reg.RFC8628HMACStrategy().GenerateUserCode(ctx) + require.NoError(t, err) + reg.OAuth2Storage().CreateDeviceAuthSession(ctx, deviceCodesig, sig, deviceRequest) + require.NoError(t, err) + + acceptUserCode := &hydra.AcceptDeviceUserCodeRequest{UserCode: &userCode} + + // marshal User to json + acceptUserCodeJson, err := json.Marshal(acceptUserCode) + require.NoError(t, err) + + // set the HTTP method, url, and request body + req, err := http.NewRequest(http.MethodPut, ts.URL+"/admin"+DevicePath+"/accept?device_challenge="+challenge, bytes.NewBuffer(acceptUserCodeJson)) + require.NoError(t, err) + + resp, err := c.Do(req) + require.NoError(t, err) + require.EqualValues(t, http.StatusOK, resp.StatusCode) + + var result flow.OAuth2RedirectTo + require.NoError(t, json.NewDecoder(resp.Body).Decode(&result)) + require.NotNil(t, result.RedirectTo) + require.Contains(t, result.RedirectTo, requestURL) + require.Contains(t, result.RedirectTo, "device_verifier") +} + +func TestAcceptDuplicateDeviceRequest(t *testing.T) { + ctx := context.Background() + challenge := "challenge" + requestURL := "https://hydra.example.com/" + oauth2.DeviceVerificationPath + + conf := testhelpers.NewConfigurationWithDefaults() + reg := testhelpers.NewRegistryMemory(t, conf, &contextx.Default{}) + + cl := &client.Client{ID: "client"} + require.NoError(t, reg.ClientManager().CreateClient(ctx, cl)) + f, err := reg.ConsentManager().CreateDeviceUserAuthRequest(ctx, &flow.DeviceUserAuthRequest{ + Client: cl, + ID: challenge, + RequestURL: requestURL, + RequestedAt: time.Now(), + }) + require.NoError(t, err) + challenge, err = f.ToDeviceChallenge(ctx, reg) + require.NoError(t, err) + + h := NewHandler(reg, conf) + r := x.NewRouterAdmin(conf.AdminURL) + h.SetRoutes(r) + ts := httptest.NewServer(r) + t.Cleanup(ts.Close) + + c := &http.Client{} + + deviceRequest := fosite.NewDeviceRequest() + deviceRequest.Client = cl + deviceRequest.SetSession( + &oauth2.Session{ + DefaultSession: &openid.DefaultSession{ + Headers: &jwt.Headers{}, + }, + }, + ) + _, deviceCodesig, err := reg.RFC8628HMACStrategy().GenerateDeviceCode(ctx) + require.NoError(t, err) + userCode, sig, err := reg.RFC8628HMACStrategy().GenerateUserCode(ctx) + require.NoError(t, err) + reg.OAuth2Storage().CreateDeviceAuthSession(ctx, deviceCodesig, sig, deviceRequest) + require.NoError(t, err) + + acceptUserCode := &hydra.AcceptDeviceUserCodeRequest{UserCode: &userCode} + + // marshal User to json + acceptUserCodeJson, err := json.Marshal(acceptUserCode) + require.NoError(t, err) + + // set the HTTP method, url, and request body + req, err := http.NewRequest(http.MethodPut, ts.URL+"/admin"+DevicePath+"/accept?device_challenge="+challenge, bytes.NewBuffer(acceptUserCodeJson)) + require.NoError(t, err) + + resp, err := c.Do(req) + require.NoError(t, err) + require.EqualValues(t, http.StatusOK, resp.StatusCode) + + var result flow.OAuth2RedirectTo + require.NoError(t, json.NewDecoder(resp.Body).Decode(&result)) + require.NotNil(t, result.RedirectTo) + require.Contains(t, result.RedirectTo, requestURL) + require.Contains(t, result.RedirectTo, "device_verifier") + + req2, err := http.NewRequest(http.MethodPut, ts.URL+"/admin"+DevicePath+"/accept?device_challenge="+challenge, bytes.NewBuffer(acceptUserCodeJson)) + require.NoError(t, err) + resp2, err := c.Do(req2) + require.NoError(t, err) + require.EqualValues(t, http.StatusOK, resp2.StatusCode) + + var result2 flow.OAuth2RedirectTo + require.NoError(t, json.NewDecoder(resp2.Body).Decode(&result2)) + require.NotNil(t, result2.RedirectTo) + require.Contains(t, result2.RedirectTo, requestURL) + require.Contains(t, result2.RedirectTo, "device_verifier") +} + +func TestAcceptCodeDeviceRequestFailure(t *testing.T) { + ctx := context.Background() + challenge := "challenge" + requestURL := "https://hydra.example.com/" + oauth2.DeviceVerificationPath + + conf := testhelpers.NewConfigurationWithDefaults() + reg := testhelpers.NewRegistryMemory(t, conf, &contextx.Default{}) + + cl := &client.Client{ID: "client"} + require.NoError(t, reg.ClientManager().CreateClient(ctx, cl)) + f, err := reg.ConsentManager().CreateDeviceUserAuthRequest(ctx, &flow.DeviceUserAuthRequest{ + Client: cl, + ID: challenge, + RequestURL: requestURL, + RequestedAt: time.Now(), + }) + require.NoError(t, err) + challenge, err = f.ToDeviceChallenge(ctx, reg) + require.NoError(t, err) + + h := NewHandler(reg, conf) + r := x.NewRouterAdmin(conf.AdminURL) + h.SetRoutes(r) + ts := httptest.NewServer(r) + t.Cleanup(ts.Close) + + c := &http.Client{} + + for _, tc := range []struct { + desc string + getBody func() ([]byte, error) + getURL func() string + validateResponse func(*http.Response) + }{ + { + desc: "random user_code, not persisted in the database", + getBody: func() ([]byte, error) { + deviceRequest := fosite.NewDeviceRequest() + deviceRequest.Client = cl + deviceRequest.SetSession( + &oauth2.Session{ + DefaultSession: &openid.DefaultSession{ + Headers: &jwt.Headers{}, + }, + }, + ) + userCode, _, err := reg.RFC8628HMACStrategy().GenerateUserCode(ctx) + require.NoError(t, err) + return json.Marshal(&hydra.AcceptDeviceUserCodeRequest{UserCode: &userCode}) + }, + getURL: func() string { + return ts.URL + "/admin" + DevicePath + "/accept?device_challenge=" + challenge + }, + validateResponse: func(resp *http.Response) { + require.EqualValues(t, http.StatusBadRequest, 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{}, + }, + }, + ) + userCode := "" + return json.Marshal(&hydra.AcceptDeviceUserCodeRequest{UserCode: &userCode}) + }, + getURL: func() string { + return ts.URL + "/admin" + DevicePath + "/accept?device_challenge=" + challenge + }, + validateResponse: func(resp *http.Response) { + require.EqualValues(t, http.StatusBadRequest, resp.StatusCode) + }, + }, + { + desc: "empty challenge", + getBody: func() ([]byte, error) { + deviceRequest := fosite.NewDeviceRequest() + deviceRequest.Client = cl + deviceRequest.SetSession( + &oauth2.Session{ + DefaultSession: &openid.DefaultSession{ + Headers: &jwt.Headers{}, + }, + }, + ) + 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{}, + }, + }, + ) + userCode, _, err := reg.RFC8628HMACStrategy().GenerateUserCode(ctx) + require.NoError(t, err) + return json.Marshal(&hydra.AcceptDeviceUserCodeRequest{UserCode: &userCode}) + }, + getURL: func() string { + return ts.URL + "/admin" + DevicePath + "/accept?device_challenge=abc" + }, + validateResponse: func(resp *http.Response) { + require.EqualValues(t, http.StatusNotFound, resp.StatusCode) + }, + }, + { + desc: "expired user_code", + getBody: func() ([]byte, error) { + deviceRequest := fosite.NewDeviceRequest() + deviceRequest.Client = cl + deviceRequest.SetSession( + &oauth2.Session{ + DefaultSession: &openid.DefaultSession{ + Headers: &jwt.Headers{}, + }, + }, + ) + _, deviceCodesig, err := reg.RFC8628HMACStrategy().GenerateDeviceCode(ctx) + require.NoError(t, err) + userCode, sig, err := reg.RFC8628HMACStrategy().GenerateUserCode(ctx) + require.NoError(t, err) + deviceRequest.SetSession( + &oauth2.Session{ + DefaultSession: &openid.DefaultSession{ + Headers: &jwt.Headers{}, + }, + }, + ) + exp := time.Now().UTC() + deviceRequest.Session.SetExpiresAt(fosite.UserCode, exp) + err = reg.OAuth2Storage().CreateDeviceAuthSession(ctx, deviceCodesig, sig, deviceRequest) + require.NoError(t, err) + return json.Marshal(&hydra.AcceptDeviceUserCodeRequest{UserCode: &userCode}) + }, + getURL: func() string { + return ts.URL + "/admin" + DevicePath + "/accept?device_challenge=" + challenge + }, + validateResponse: func(resp *http.Response) { + require.EqualValues(t, http.StatusBadRequest, resp.StatusCode) + result := &fosite.RFC6749Error{} + require.NoError(t, json.NewDecoder(resp.Body).Decode(&result)) + require.EqualValues(t, result.ErrorField, fosite.ErrInvalidRequest.ErrorField) + }, + }, + { + desc: "accepted user_code", + getBody: func() ([]byte, error) { + deviceRequest := fosite.NewDeviceRequest() + deviceRequest.Client = cl + deviceRequest.SetSession( + &oauth2.Session{ + DefaultSession: &openid.DefaultSession{ + Headers: &jwt.Headers{}, + }, + }, + ) + _, deviceCodesig, err := reg.RFC8628HMACStrategy().GenerateDeviceCode(ctx) + require.NoError(t, err) + userCode, sig, err := reg.RFC8628HMACStrategy().GenerateUserCode(ctx) + require.NoError(t, err) + deviceRequest.SetSession( + &oauth2.Session{ + DefaultSession: &openid.DefaultSession{ + Headers: &jwt.Headers{}, + }, + }, + ) + exp := time.Now().UTC() + deviceRequest.Session.SetExpiresAt(fosite.UserCode, exp) + err = reg.OAuth2Storage().CreateDeviceAuthSession(ctx, deviceCodesig, sig, deviceRequest) + require.NoError(t, err) + deviceRequest.UserCodeState = fosite.UserCodeAccepted + return json.Marshal(&hydra.AcceptDeviceUserCodeRequest{UserCode: &userCode}) + }, + getURL: func() string { + return ts.URL + "/admin" + DevicePath + "/accept?device_challenge=" + challenge + }, + validateResponse: func(resp *http.Response) { + require.EqualValues(t, http.StatusBadRequest, resp.StatusCode) + result := &fosite.RFC6749Error{} + require.NoError(t, json.NewDecoder(resp.Body).Decode(&result)) + require.EqualValues(t, result.ErrorField, fosite.ErrInvalidRequest.ErrorField) + }, + }, + { + desc: "rejected user_code", + getBody: func() ([]byte, error) { + deviceRequest := fosite.NewDeviceRequest() + deviceRequest.Client = cl + deviceRequest.SetSession( + &oauth2.Session{ + DefaultSession: &openid.DefaultSession{ + Headers: &jwt.Headers{}, + }, + }, + ) + _, deviceCodesig, err := reg.RFC8628HMACStrategy().GenerateDeviceCode(ctx) + require.NoError(t, err) + userCode, sig, err := reg.RFC8628HMACStrategy().GenerateUserCode(ctx) + require.NoError(t, err) + deviceRequest.SetSession( + &oauth2.Session{ + DefaultSession: &openid.DefaultSession{ + Headers: &jwt.Headers{}, + }, + }, + ) + exp := time.Now().UTC() + deviceRequest.Session.SetExpiresAt(fosite.UserCode, exp) + err = reg.OAuth2Storage().CreateDeviceAuthSession(ctx, deviceCodesig, sig, deviceRequest) + require.NoError(t, err) + deviceRequest.UserCodeState = fosite.UserCodeRejected + return json.Marshal(&hydra.AcceptDeviceUserCodeRequest{UserCode: &userCode}) + }, + getURL: func() string { + return ts.URL + "/admin" + DevicePath + "/accept?device_challenge=" + challenge + }, + validateResponse: func(resp *http.Response) { + require.EqualValues(t, http.StatusBadRequest, resp.StatusCode) + result := &fosite.RFC6749Error{} + require.NoError(t, json.NewDecoder(resp.Body).Decode(&result)) + require.EqualValues(t, result.ErrorField, fosite.ErrInvalidRequest.ErrorField) + }, + }, + { + desc: "extra fields", + getBody: func() ([]byte, error) { + deviceRequest := fosite.NewDeviceRequest() + deviceRequest.Client = cl + deviceRequest.SetSession( + &oauth2.Session{ + DefaultSession: &openid.DefaultSession{ + Headers: &jwt.Headers{}, + }, + }, + ) + userCode, _, err := reg.RFC8628HMACStrategy().GenerateUserCode(ctx) + require.NoError(t, err) + ret := struct { + UserCode *string + Extra string + }{ + UserCode: &userCode, + Extra: "extra", + } + return json.Marshal(ret) + }, + getURL: func() string { + return ts.URL + "/admin" + DevicePath + "/accept?device_challenge=" + challenge + }, + validateResponse: func(resp *http.Response) { + require.EqualValues(t, http.StatusBadRequest, resp.StatusCode) + }, + }, + } { + tc := tc + t.Run("case="+tc.desc, func(t *testing.T) { + acceptUserCodeJson, err := tc.getBody() + require.NoError(t, err) + + // set the HTTP method, url, and request body + req, err := http.NewRequest(http.MethodPut, tc.getURL(), bytes.NewBuffer(acceptUserCodeJson)) + require.NoError(t, err) + + resp, err := c.Do(req) + require.NoError(t, err) + tc.validateResponse(resp) + }) + } + +} diff --git a/consent/manager.go b/consent/manager.go index 182064028ba..b5b2aba5691 100644 --- a/consent/manager.go +++ b/consent/manager.go @@ -46,6 +46,7 @@ type ( ConfirmLoginSession(ctx context.Context, loginSession *flow.LoginSession) error CreateLoginRequest(ctx context.Context, req *flow.LoginRequest) (*flow.Flow, error) + CreateLoginRequestFromDeviceRequest(ctx context.Context, f *flow.Flow, req *flow.LoginRequest) (*flow.Flow, error) GetLoginRequest(ctx context.Context, challenge string) (*flow.LoginRequest, error) HandleLoginRequest(ctx context.Context, f *flow.Flow, challenge string, r *flow.HandledLoginRequest) (*flow.LoginRequest, error) VerifyAndInvalidateLoginRequest(ctx context.Context, verifier string) (*flow.HandledLoginRequest, error) @@ -61,6 +62,11 @@ type ( AcceptLogoutRequest(ctx context.Context, challenge string) (*flow.LogoutRequest, error) RejectLogoutRequest(ctx context.Context, challenge string) error VerifyAndInvalidateLogoutRequest(ctx context.Context, verifier string) (*flow.LogoutRequest, error) + + CreateDeviceUserAuthRequest(ctx context.Context, req *flow.DeviceUserAuthRequest) (*flow.Flow, error) + GetDeviceUserAuthRequest(ctx context.Context, challenge string) (*flow.DeviceUserAuthRequest, error) + HandleDeviceUserAuthRequest(ctx context.Context, f *flow.Flow, challenge string, r *flow.HandledDeviceUserAuthRequest) (*flow.DeviceUserAuthRequest, error) + VerifyAndInvalidateDeviceUserAuthRequest(ctx context.Context, verifier string) (*flow.HandledDeviceUserAuthRequest, error) } ManagerProvider interface { diff --git a/consent/strategy.go b/consent/strategy.go index 08e8788c756..0def2866e27 100644 --- a/consent/strategy.go +++ b/consent/strategy.go @@ -20,6 +20,11 @@ type Strategy interface { r *http.Request, req fosite.AuthorizeRequester, ) (*flow.AcceptOAuth2ConsentRequest, *flow.Flow, error) + HandleOAuth2DeviceAuthorizationRequest( + ctx context.Context, + w http.ResponseWriter, + r *http.Request, + ) (*flow.AcceptOAuth2ConsentRequest, *flow.Flow, error) HandleOpenIDConnectLogout(ctx context.Context, w http.ResponseWriter, r *http.Request) (*flow.LogoutResult, error) HandleHeadlessLogout(ctx context.Context, w http.ResponseWriter, r *http.Request, sid string) error ObfuscateSubjectIdentifier(ctx context.Context, cl fosite.Client, subject, forcedIdentifier string) (string, error) diff --git a/consent/strategy_default.go b/consent/strategy_default.go index ba8947a1ce5..6244ea0a269 100644 --- a/consent/strategy_default.go +++ b/consent/strategy_default.go @@ -41,6 +41,7 @@ import ( ) const ( + DeviceVerificationPath = "/oauth2/device/verify" CookieAuthenticationSIDName = "sid" ) @@ -121,18 +122,24 @@ func (s *DefaultStrategy) authenticationSession(ctx context.Context, _ http.Resp return session, nil } -func (s *DefaultStrategy) requestAuthentication(ctx context.Context, w http.ResponseWriter, r *http.Request, ar fosite.AuthorizeRequester) (err error) { +func (s *DefaultStrategy) requestAuthentication( + ctx context.Context, + w http.ResponseWriter, + r *http.Request, + ar fosite.AuthorizeRequester, + f *flow.Flow, +) (err error) { ctx, span := trace.SpanFromContext(ctx).TracerProvider().Tracer("").Start(ctx, "DefaultStrategy.requestAuthentication") defer otelx.End(span, &err) prompt := stringsx.Splitx(ar.GetRequestForm().Get("prompt"), " ") if stringslice.Has(prompt, "login") { - return s.forwardAuthenticationRequest(ctx, w, r, ar, "", time.Time{}, nil) + return s.forwardAuthenticationRequest(ctx, w, r, ar, "", time.Time{}, nil, f) } session, err := s.authenticationSession(ctx, w, r) if errors.Is(err, ErrNoAuthenticationSessionFound) { - return s.forwardAuthenticationRequest(ctx, w, r, ar, "", time.Time{}, nil) + return s.forwardAuthenticationRequest(ctx, w, r, ar, "", time.Time{}, nil, f) } else if err != nil { return err } @@ -150,12 +157,12 @@ func (s *DefaultStrategy) requestAuthentication(ctx context.Context, w http.Resp if stringslice.Has(prompt, "none") { return errorsx.WithStack(fosite.ErrLoginRequired.WithHint("Request failed because prompt is set to 'none' and authentication time reached 'max_age'.")) } - return s.forwardAuthenticationRequest(ctx, w, r, ar, "", time.Time{}, nil) + return s.forwardAuthenticationRequest(ctx, w, r, ar, "", time.Time{}, nil, f) } idTokenHint := ar.GetRequestForm().Get("id_token_hint") if idTokenHint == "" { - return s.forwardAuthenticationRequest(ctx, w, r, ar, session.Subject, time.Time(session.AuthenticatedAt), session) + return s.forwardAuthenticationRequest(ctx, w, r, ar, session.Subject, time.Time(session.AuthenticatedAt), session, f) } hintSub, err := s.getSubjectFromIDTokenHint(r.Context(), idTokenHint) @@ -167,7 +174,7 @@ func (s *DefaultStrategy) requestAuthentication(ctx context.Context, w http.Resp return errorsx.WithStack(fosite.ErrLoginRequired.WithHint("Request failed because subject claim from id_token_hint does not match subject from authentication session.")) } - return s.forwardAuthenticationRequest(ctx, w, r, ar, session.Subject, time.Time(session.AuthenticatedAt), session) + return s.forwardAuthenticationRequest(ctx, w, r, ar, session.Subject, time.Time(session.AuthenticatedAt), session, f) } func (s *DefaultStrategy) getIDTokenHintClaims(ctx context.Context, idTokenHint string) (jwt.MapClaims, error) { @@ -194,7 +201,16 @@ func (s *DefaultStrategy) getSubjectFromIDTokenHint(ctx context.Context, idToken return sub, nil } -func (s *DefaultStrategy) forwardAuthenticationRequest(ctx context.Context, w http.ResponseWriter, r *http.Request, ar fosite.AuthorizeRequester, subject string, authenticatedAt time.Time, session *flow.LoginSession) error { +func (s *DefaultStrategy) forwardAuthenticationRequest( + ctx context.Context, + w http.ResponseWriter, + r *http.Request, + ar fosite.AuthorizeRequester, + subject string, + authenticatedAt time.Time, + session *flow.LoginSession, + f *flow.Flow, +) error { if (subject != "" && authenticatedAt.IsZero()) || (subject == "" && !authenticatedAt.IsZero()) { return errorsx.WithStack(fosite.ErrServerError.WithHint("Consent strategy returned a non-empty subject with an empty auth date, or an empty subject with a non-empty auth date.")) } @@ -216,8 +232,14 @@ func (s *DefaultStrategy) forwardAuthenticationRequest(ctx context.Context, w ht csrf := strings.Replace(uuid.New(), "-", "", -1) // Generate the request URL - iu := s.c.OAuth2AuthURL(ctx) - iu.RawQuery = r.URL.RawQuery + var requestURL string + if f != nil { + requestURL = f.RequestURL + } else { + oauth2URL := s.c.OAuth2AuthURL(ctx) + oauth2URL.RawQuery = r.URL.RawQuery + requestURL = oauth2URL.String() + } var idTokenHintClaims jwt.MapClaims if idTokenHint := ar.GetRequestForm().Get("id_token_hint"); len(idTokenHint) > 0 { @@ -245,7 +267,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), @@ -257,10 +279,12 @@ func (s *DefaultStrategy) forwardAuthenticationRequest(ctx context.Context, w ht LoginHint: ar.GetRequestForm().Get("login_hint"), }, } - f, err := s.r.ConsentManager().CreateLoginRequest( - ctx, - loginRequest, - ) + var err error + if f == nil { + f, err = s.r.ConsentManager().CreateLoginRequest(ctx, loginRequest) + } else { + f, err = s.r.ConsentManager().CreateLoginRequestFromDeviceRequest(ctx, f, loginRequest) + } if err != nil { return errorsx.WithStack(err) } @@ -536,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 explicitly authorize the device. + // // This is tracked as issue: https://github.com/ory/hydra/issues/866 // This is also tracked as upstream issue: https://github.com/openid-certification/oidctest/issues/97 - if !(ar.GetRedirectURI().Scheme == "https" || (fosite.IsLocalhost(ar.GetRedirectURI()) && ar.GetRedirectURI().Scheme == "http")) { + if f.DeviceChallengeID != "" { + return s.forwardConsentRequest(ctx, w, r, ar, f, nil) + } else if !(ar.GetRedirectURI().Scheme == "https" || (fosite.IsLocalhost(ar.GetRedirectURI()) && ar.GetRedirectURI().Scheme == "http")) { return s.forwardConsentRequest(ctx, w, r, ar, f, nil) } } @@ -1137,8 +1165,8 @@ func (s *DefaultStrategy) HandleOAuth2AuthorizationRequest( loginVerifier := strings.TrimSpace(req.GetRequestForm().Get("login_verifier")) consentVerifier := strings.TrimSpace(req.GetRequestForm().Get("consent_verifier")) if loginVerifier == "" && consentVerifier == "" { - // ok, we need to process this request and redirect to 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, nil) } else if loginVerifier != "" { f, err := s.verifyAuthentication(ctx, w, r, req, loginVerifier) if err != nil { @@ -1157,6 +1185,87 @@ 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, err error) { + ctx, span := trace.SpanFromContext(ctx).TracerProvider().Tracer("").Start(ctx, "DefaultStrategy.HandleOAuth2DeviceAuthorizationRequest") + defer otelx.End(span, &err) + + // This handler has the following validation states: + // + // 1. The flow is initiated (no verifiers) -> we request a device verifier (can only be achieved by solving the device challenge) + // 2. Device verifier is given -> we request login verifier (can only be achieved by solving the login challenge) + // 3. Login verifier is given -> we request consent verifier (can only be achieved by solving the consent challenge) + // 4. Consent verifier is given -> done. + + deviceVerifier := strings.TrimSpace(r.URL.Query().Get("device_verifier")) + loginVerifier := strings.TrimSpace(r.URL.Query().Get("login_verifier")) + consentVerifier := strings.TrimSpace(r.URL.Query().Get("consent_verifier")) + + ar := fosite.NewAuthorizeRequest() + + var deviceFlow *flow.Flow + if deviceVerifier == "" && loginVerifier == "" && consentVerifier == "" { + // No verifiers are set, let's start by requesting the device verifier first. + return nil, nil, s.requestDevice(ctx, w, r) + } else if deviceVerifier != "" && loginVerifier == "" && consentVerifier == "" { + // Device verifier is set, but login and consent are not. So we need to verify the device. + var err error + deviceFlow, err = s.verifyDevice(ctx, w, r, deviceVerifier) + if err != nil { + return nil, nil, err + } + + ar.RequestedScope = fosite.Arguments(deviceFlow.RequestedScope) + ar.RequestedAudience = fosite.Arguments(deviceFlow.RequestedAudience) + } + + // Validate client_id + clientID := r.URL.Query().Get("client_id") + if clientID == "" { + return nil, nil, errorsx.WithStack(fosite.ErrInvalidClient.WithHintf(`Query parameter 'client_id' is missing.`)) + } + c, err := s.r.ClientManager().GetConcreteClient(r.Context(), clientID) + if errors.Is(err, x.ErrNotFound) { + return nil, nil, errorsx.WithStack(fosite.ErrInvalidClient.WithWrap(err).WithHintf(`Client does not exist`)) + } else if err != nil { + return nil, nil, err + } + + // Fake an authorization request to instantiate the flow. + ar.Client = c + ar.Form = r.Form + + if loginVerifier == "" && consentVerifier == "" { + // Here we end up if the device has been verified, but login and verification are still missing. + // Let's request authentication. + return nil, nil, s.requestAuthentication(ctx, w, r, ar, deviceFlow) + } else if loginVerifier != "" { + // Login verification was given, let's verify! + f, err := s.verifyAuthentication(ctx, w, r, ar, loginVerifier) + if err != nil { + return nil, nil, err + } + + // ok, we need to process this request and redirect to consent endpoint + return nil, f, s.requestConsent(ctx, w, r, ar, f) + } + + // Here we end up when consent verifier is set, so we verify the consent. + var consentSession *flow.AcceptOAuth2ConsentRequest + var f *flow.Flow + + consentSession, f, err = s.verifyConsent(ctx, w, r, consentVerifier) + if err != nil { + return nil, nil, err + } + + return consentSession, f, err +} + func (s *DefaultStrategy) ObfuscateSubjectIdentifier(ctx context.Context, cl fosite.Client, subject, forcedIdentifier string) (string, error) { if c, ok := cl.(*client.Client); ok && c.SubjectType == "pairwise" { algorithm, ok := s.r.SubjectIdentifierAlgorithm(ctx)[c.SubjectType] @@ -1174,3 +1283,105 @@ 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) + // We don't want the user_code persisted in the database + q := r.URL.Query() + if q.Has("user_code") { + q.Set("user_code", "****") + } + iu.RawQuery = q.Encode() + + f, err := s.r.ConsentManager().CreateDeviceUserAuthRequest( + r.Context(), + &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.verifyDevice") + 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, f); 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/consent/strategy_default_test.go b/consent/strategy_default_test.go index c4ba3d97107..a65c1306f85 100644 --- a/consent/strategy_default_test.go +++ b/consent/strategy_default_test.go @@ -9,6 +9,7 @@ import ( "net/http/cookiejar" "net/http/httptest" "net/url" + "strings" "testing" "github.com/google/uuid" @@ -21,10 +22,28 @@ import ( . "github.com/ory/hydra/v2/consent" "github.com/ory/hydra/v2/driver" "github.com/ory/hydra/v2/internal/testhelpers" + "github.com/ory/hydra/v2/oauth2" "github.com/ory/x/ioutilx" "github.com/ory/x/urlx" ) +func checkAndAcceptDeviceHandler(t *testing.T, apiClient *hydra.APIClient) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + userCode := r.URL.Query().Get("user_code") + payload := hydra.AcceptDeviceUserCodeRequest{ + UserCode: &userCode, + } + + v, _, err := apiClient.OAuth2API.AcceptUserCodeRequest(context.Background()). + DeviceChallenge(r.URL.Query().Get("device_challenge")). + AcceptDeviceUserCodeRequest(payload). + Execute() + require.NoError(t, err) + require.NotEmpty(t, v.RedirectTo) + http.Redirect(w, r, v.RedirectTo, http.StatusFound) + } +} + func checkAndAcceptLoginHandler(t *testing.T, apiClient *hydra.APIClient, subject string, cb func(*testing.T, *hydra.OAuth2LoginRequest, error) hydra.AcceptOAuth2LoginRequest) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { res, _, err := apiClient.OAuth2API.GetOAuth2LoginRequest(context.Background()).LoginChallenge(r.URL.Query().Get("login_challenge")).Execute() @@ -65,6 +84,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() @@ -72,6 +92,46 @@ func makeOAuth2Request(t *testing.T, reg driver.Registry, hc *http.Client, oc *c return gjson.ParseBytes(ioutilx.MustReadAll(res.Body)), res } +func makeOAuth2DeviceAuthRequest(t *testing.T, reg driver.Registry, hc *http.Client, oc *client.Client, scope string) (gjson.Result, *http.Response) { + ctx := context.Background() + if hc == nil { + hc = testhelpers.NewEmptyJarClient(t) + } + + data := url.Values{} + data.Set("scope", scope) + data.Set("client_id", oc.GetID()) + req, err := http.NewRequest( + http.MethodPost, + reg.Config().OAuth2DeviceAuthorisationURL(ctx).String(), + strings.NewReader(data.Encode()), + ) + require.NoError(t, err) + req.SetBasicAuth(oc.GetID(), oc.Secret) + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + + res, err := hc.Do(req) + require.NoError(t, err) + + defer res.Body.Close() + + return gjson.ParseBytes(ioutilx.MustReadAll(res.Body)), res +} + +func makeOAuth2DeviceVerificationRequest(t *testing.T, reg driver.Registry, hc *http.Client, oc *client.Client, values url.Values) (gjson.Result, *http.Response) { + ctx := context.Background() + if hc == nil { + hc = testhelpers.NewEmptyJarClient(t) + } + + values.Add("client_id", oc.GetID()) + res, err := hc.Get(urlx.CopyWithQuery(urlx.AppendPaths(reg.Config().PublicURL(ctx), oauth2.DeviceVerificationPath), values).String()) + require.NoError(t, err) + defer res.Body.Close() + + return gjson.ParseBytes(ioutilx.MustReadAll(res.Body)), res +} + func createClient(t *testing.T, reg driver.Registry, c *client.Client) *client.Client { secret := uuid.New().String() c.Secret = secret diff --git a/consent/strategy_oauth_test.go b/consent/strategy_oauth_test.go index 1c17d1bc814..319e350dc2e 100644 --- a/consent/strategy_oauth_test.go +++ b/consent/strategy_oauth_test.go @@ -1110,6 +1110,224 @@ func TestStrategyLoginConsentNext(t *testing.T) { }) } +func TestStrategyDeviceLoginConsent(t *testing.T) { + ctx := context.Background() + reg := testhelpers.NewMockedRegistry(t, &contextx.Default{}) + reg.Config().MustSet(ctx, config.KeyAccessTokenStrategy, "opaque") + reg.Config().MustSet(ctx, config.KeyConsentRequestMaxAge, time.Hour) + reg.Config().MustSet(ctx, config.KeyConsentRequestMaxAge, time.Hour) + reg.Config().MustSet(ctx, config.KeyScopeStrategy, "exact") + reg.Config().MustSet(ctx, config.KeySubjectTypesSupported, []string{"pairwise", "public"}) + reg.Config().MustSet(ctx, config.KeySubjectIdentifierAlgorithmSalt, "76d5d2bf-747f-4592-9fbd-d2b895a54b3a") + + publicTS, adminTS := testhelpers.NewOAuth2Server(ctx, t, reg) + adminClient := hydra.NewAPIClient(hydra.NewConfiguration()) + adminClient.GetConfig().Servers = hydra.ServerConfigurations{{URL: adminTS.URL}} + + oauth2Config := func(t *testing.T, c *client.Client) *oauth2.Config { + return &oauth2.Config{ + ClientID: c.GetID(), + ClientSecret: c.Secret, + Endpoint: oauth2.Endpoint{ + DeviceAuthURL: publicTS.URL + "/oauth2/device/auth", + TokenURL: publicTS.URL + "/oauth2/token", + AuthStyle: oauth2.AuthStyleInHeader, + }, + } + } + + now := 1723546027 // Unix timestamps must round-trip through Hydra without converting to floats or similar + acceptDeviceHandler := func(t *testing.T) http.HandlerFunc { + return checkAndAcceptDeviceHandler(t, adminClient) + } + + 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", + "ts1": now, + }, + IdToken: map[string]interface{}{ + "bar": "baz", + "ts1": now, + }, + }, + })) + + hc := testhelpers.NewEmptyJarClient(t) + + var sid string + var run = func(t *testing.T) { + res, resp := makeOAuth2DeviceAuthRequest(t, reg, hc, c, "openid") + assert.EqualValues(t, http.StatusOK, resp.StatusCode) + + devResp := new(oauth2.DeviceAuthResponse) + require.NoError(t, json.Unmarshal([]byte(res.Raw), devResp)) + + resp, err := hc.Get(devResp.VerificationURIComplete) + require.NoError(t, err) + require.Contains(t, reg.Config().DeviceDoneURL(ctx).String(), resp.Request.URL.Path, "did not end up in post device URL") + require.Equal(t, resp.Request.URL.Query().Get("client_id"), c.ID) + + conf := oauth2Config(t, c) + token, err := conf.DeviceAccessToken(ctx, devResp) + require.NoError(t, err) + + claims := testhelpers.IntrospectToken(t, conf, token.AccessToken, adminTS) + assert.Equal(t, "bar", claims.Get("ext.foo").String(), "%s", claims.Raw) + + idClaims := testhelpers.DecodeIDToken(t, token) + assert.Equal(t, "baz", idClaims.Get("bar").String(), "%s", idClaims.Raw) + sid = idClaims.Get("sid").String() + assert.NotNil(t, sid) + } + + t.Run("perform first flow", run) + + t.Run("perform follow up flows and check if session values are set", func(t *testing.T) { + testhelpers.NewLoginConsentUI(t, reg.Config(), + checkAndAcceptLoginHandler(t, adminClient, subject, func(t *testing.T, res *hydra.OAuth2LoginRequest, err error) hydra.AcceptOAuth2LoginRequest { + require.NoError(t, err) + assert.True(t, res.Skip) + assert.Equal(t, sid, *res.SessionId) + assert.Equal(t, subject, res.Subject) + assert.Empty(t, pointerx.StringR(res.Client.ClientSecret)) + return hydra.AcceptOAuth2LoginRequest{ + Subject: subject, + Context: map[string]interface{}{"xyz": "abc"}, + } + }), + checkAndAcceptConsentHandler(t, adminClient, func(t *testing.T, req *hydra.OAuth2ConsentRequest, err error) hydra.AcceptOAuth2ConsentRequest { + require.NoError(t, err) + assert.True(t, *req.Skip) + assert.Equal(t, sid, *req.LoginSessionId) + assert.Equal(t, subject, *req.Subject) + assert.Empty(t, pointerx.StringR(req.Client.ClientSecret)) + assert.Equal(t, map[string]interface{}{"xyz": "abc"}, req.Context) + return hydra.AcceptOAuth2ConsentRequest{ + Remember: pointerx.Bool(true), + GrantScope: []string{"openid"}, + Session: &hydra.AcceptOAuth2ConsentRequestSession{ + AccessToken: map[string]interface{}{ + "foo": "bar", + "ts1": now, + }, + IdToken: map[string]interface{}{ + "bar": "baz", + "ts2": now, + }, + }, + } + })) + + for k := 0; k < 3; k++ { + t.Run(fmt.Sprintf("case=%d", k), run) + } + }) + }) + t.Run("case=should fail because we are reusing the same verifier", func(t *testing.T) { + subject := "aeneas-rekkas" + c := createDefaultClient(t) + testhelpers.NewDeviceLoginConsentUI(t, reg.Config(), + acceptDeviceHandler(t), + acceptLoginHandler(t, subject, &hydra.AcceptOAuth2LoginRequest{ + Remember: pointerx.Bool(true), + }), + acceptConsentHandler(t, &hydra.AcceptOAuth2ConsentRequest{ + Remember: pointerx.Bool(true), + GrantScope: []string{"openid"}, + Session: &hydra.AcceptOAuth2ConsentRequestSession{ + AccessToken: map[string]interface{}{"foo": "bar"}, + IdToken: map[string]interface{}{"bar": "baz"}, + }, + })) + + hc := testhelpers.NewEmptyJarClient(t) + + res, resp := makeOAuth2DeviceAuthRequest(t, reg, hc, c, "openid") + assert.EqualValues(t, http.StatusOK, resp.StatusCode) + + devResp := new(oauth2.DeviceAuthResponse) + require.NoError(t, json.Unmarshal([]byte(res.Raw), devResp)) + + resp, err := hc.Get(devResp.VerificationURIComplete) + require.NoError(t, err) + require.Contains(t, reg.Config().DeviceDoneURL(ctx).String(), resp.Request.URL.Path, "did not end up in post device URL") + require.Equal(t, resp.Request.URL.Query().Get("client_id"), c.ID) + + conf := oauth2Config(t, c) + token, err := conf.DeviceAccessToken(ctx, devResp) + require.NoError(t, err) + + claims := testhelpers.IntrospectToken(t, conf, token.AccessToken, adminTS) + assert.Equal(t, "bar", claims.Get("ext.foo").String(), "%s", claims.Raw) + + idClaims := testhelpers.DecodeIDToken(t, token) + assert.Equal(t, "baz", idClaims.Get("bar").String(), "%s", idClaims.Raw) + sid := idClaims.Get("sid").String() + assert.NotNil(t, sid) + + }) + t.Run("case=should fail because a device verifier was given that doesn't exist in the store", func(t *testing.T) { + testhelpers.NewDeviceLoginConsentUI(t, reg.Config(), testhelpers.HTTPServerNoExpectedCallHandler(t), testhelpers.HTTPServerNoExpectedCallHandler(t), testhelpers.HTTPServerNoExpectedCallHandler(t)) + c := createDefaultClient(t) + hc := testhelpers.NewEmptyJarClient(t) + + _, res := makeOAuth2DeviceVerificationRequest(t, reg, hc, c, url.Values{"device_verifier": {"does-not-exist"}}) + assert.EqualValues(t, http.StatusForbidden, res.StatusCode) + }) + + t.Run("case=should fail because a login verifier was given that doesn't exist in the store", func(t *testing.T) { + testhelpers.NewLoginConsentUI(t, reg.Config(), testhelpers.HTTPServerNoExpectedCallHandler(t), testhelpers.HTTPServerNoExpectedCallHandler(t)) + c := createDefaultClient(t) + hc := testhelpers.NewEmptyJarClient(t) + + _, res := makeOAuth2DeviceVerificationRequest(t, reg, hc, c, url.Values{"login_verifier": {"does-not-exist"}}) + assert.EqualValues(t, http.StatusForbidden, res.StatusCode) + }) + + t.Run("case=should fail because a consent verifier was given that doesn't exist in the store", func(t *testing.T) { + testhelpers.NewLoginConsentUI(t, reg.Config(), testhelpers.HTTPServerNoExpectedCallHandler(t), testhelpers.HTTPServerNoExpectedCallHandler(t)) + c := createDefaultClient(t) + hc := testhelpers.NewEmptyJarClient(t) + + _, res := makeOAuth2DeviceVerificationRequest(t, reg, hc, c, url.Values{"consent_verifier": {"does-not-exist"}}) + assert.EqualValues(t, http.StatusForbidden, res.StatusCode) + }) +} + func DropCookieJar(drop *regexp.Regexp) http.CookieJar { jar, _ := cookiejar.New(nil) return &dropCSRFCookieJar{ diff --git a/consent/test/manager_test_helpers.go b/consent/test/manager_test_helpers.go index 003f666f056..2f2b280c0e5 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{ @@ -459,6 +493,73 @@ func ManagerTests(deps Deps, m consent.Manager, clientManager client.Manager, fo } }) + t.Run("case=device-request", func(t *testing.T) { + challenges := make([]string, 0) + + c, h, f := MockDeviceRequest("0", network) + _ = clientManager.CreateClient(ctx, c.Client) // Ignore errors that are caused by duplication + deviceChallenge := x.Must(f.ToDeviceChallenge(ctx, deps)) + + _, err := m.GetDeviceUserAuthRequest(ctx, deviceChallenge) + require.Error(t, err) + + f, err = m.CreateDeviceUserAuthRequest(ctx, c) + require.NoError(t, err) + + deviceChallenge = x.Must(f.ToDeviceChallenge(ctx, deps)) + + got1, err := m.GetDeviceUserAuthRequest(ctx, deviceChallenge) + require.NoError(t, err) + assert.False(t, got1.WasHandled) + compareDeviceRequest(t, c, got1) + + got1, err = m.HandleDeviceUserAuthRequest(ctx, f, deviceChallenge, h) + require.NoError(t, err) + compareDeviceRequest(t, c, got1) + + for _, key := range []string{"1", "2", "3", "4", "5", "6", "7"} { + c, h, f := MockDeviceRequest(key, network) + deviceChallenge := x.Must(f.ToDeviceChallenge(ctx, deps)) + + _, err := m.GetDeviceUserAuthRequest(ctx, deviceChallenge) + require.Error(t, err) + + f, err = m.CreateDeviceUserAuthRequest(ctx, c) + require.NoError(t, err) + + deviceChallenge = x.Must(f.ToDeviceChallenge(ctx, deps)) + challenges = append(challenges, deviceChallenge) + + got1, err := m.GetDeviceUserAuthRequest(ctx, deviceChallenge) + require.NoError(t, err) + assert.False(t, got1.WasHandled) + compareDeviceRequest(t, c, got1) + + got1, err = m.HandleDeviceUserAuthRequest(ctx, f, deviceChallenge, h) + require.NoError(t, err) + compareDeviceRequest(t, c, got1) + } + + DeviceVerifier := x.Must(f.ToDeviceVerifier(ctx, deps)) + + got2, err := m.VerifyAndInvalidateDeviceUserAuthRequest(ctx, DeviceVerifier) + require.NoError(t, err) + c.WasHandled = true + compareDeviceRequest(t, c, got2.Request) + + deviceChallenge = x.Must(f.ToDeviceChallenge(ctx, deps)) + authReq, err := m.GetDeviceUserAuthRequest(ctx, deviceChallenge) + require.NoError(t, err) + c.WasHandled = false + compareDeviceRequest(t, c, authReq) + + for _, challenge := range challenges { + authReq, err := m.GetDeviceUserAuthRequest(ctx, challenge) + require.NoError(t, err) + assert.Equal(t, authReq.WasHandled, false) + } + }) + t.Run("case=auth-request", func(t *testing.T) { for _, tc := range []struct { key string @@ -1133,6 +1234,17 @@ func compareAuthenticationRequest(t *testing.T, a, b *flow.LoginRequest) { assert.EqualValues(t, a.SessionID, b.SessionID) } +func compareDeviceRequest(t *testing.T, a, b *flow.DeviceUserAuthRequest) { + assert.EqualValues(t, a.Client.GetID(), b.Client.GetID()) + assert.EqualValues(t, a.CSRF, b.CSRF) + assert.EqualValues(t, a.RequestURL, b.RequestURL) + assert.EqualValues(t, a.Verifier, b.Verifier) + assert.EqualValues(t, a.HandledAt, b.HandledAt) + assert.EqualValues(t, a.RequestedAudience, b.RequestedAudience) + assert.EqualValues(t, a.RequestedScope, b.RequestedScope) + assert.EqualValues(t, a.WasHandled, b.WasHandled) +} + func compareConsentRequest(t *testing.T, a, b *flow.OAuth2ConsentRequest) { assert.EqualValues(t, a.Client.GetID(), b.Client.GetID()) assert.EqualValues(t, a.ConsentRequestID, b.ConsentRequestID) @@ -1145,4 +1257,5 @@ func compareConsentRequest(t *testing.T, a, b *flow.OAuth2ConsentRequest) { assert.EqualValues(t, a.Skip, b.Skip) assert.EqualValues(t, a.LoginChallenge, b.LoginChallenge) assert.EqualValues(t, a.LoginSessionID, b.LoginSessionID) + assert.EqualValues(t, a.DeviceChallenge, b.DeviceChallenge) } diff --git a/contrib/quickstart/5-min/hydra.yml b/contrib/quickstart/5-min/hydra.yml index 8d69cc1d243..30c7862bc77 100644 --- a/contrib/quickstart/5-min/hydra.yml +++ b/contrib/quickstart/5-min/hydra.yml @@ -8,6 +8,9 @@ urls: consent: http://127.0.0.1:3000/consent login: http://127.0.0.1:3000/login logout: http://127.0.0.1:3000/logout + device: + verification: http://127.0.0.1:3000/device/verify + success: http://127.0.0.1:3000/device/success secrets: system: diff --git a/cypress/integration/oauth2/device_auth.js b/cypress/integration/oauth2/device_auth.js new file mode 100644 index 00000000000..4e26abde8a0 --- /dev/null +++ b/cypress/integration/oauth2/device_auth.js @@ -0,0 +1,118 @@ +// Copyright © 2022 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { prng } from "../../helpers" + +const accessTokenStrategies = ["opaque", "jwt"] + +describe("The OAuth 2.0 Device Authorization Grant", function () { + accessTokenStrategies.forEach((accessTokenStrategy) => { + describe("access_token_strategy=" + accessTokenStrategy, function () { + const nc = (extradata) => ({ + client_secret: prng(), + scope: "offline_access openid", + subject_type: "public", + token_endpoint_auth_method: "client_secret_basic", + grant_types: [ + "urn:ietf:params:oauth:grant-type:device_code", + "refresh_token", + ], + access_token_strategy: accessTokenStrategy, + ...extradata, + }) + + it("should return an Access, Refresh, and ID Token when scope offline_access and openid are granted", function () { + const client = nc() + cy.deviceAuthFlow(client, { + consent: { scope: ["offline_access", "openid"] }, + }) + + cy.postDeviceAuthFlow().then((resp) => { + const { + result, + token: { access_token, id_token, refresh_token }, + } = resp.body + + expect(result).to.equal("success") + expect(access_token).to.not.be.empty + expect(id_token).to.not.be.empty + expect(refresh_token).to.not.be.empty + }) + }) + + it("should return an Access and Refresh Token when scope offline_access is granted", function () { + const client = nc() + cy.deviceAuthFlow(client, { consent: { scope: ["offline_access"] } }) + + cy.postDeviceAuthFlow().then((resp) => { + console.log(resp) + const { + result, + token: { access_token, id_token, refresh_token }, + } = resp.body + + expect(result).to.equal("success") + expect(access_token).to.not.be.empty + expect(id_token).to.be.undefined + expect(refresh_token).to.not.be.empty + }) + }) + + it("should return an Access and ID Token when scope offline_access is granted", function () { + const client = nc() + cy.deviceAuthFlow(client, { consent: { scope: ["openid"] } }) + + cy.postDeviceAuthFlow().then((resp) => { + console.log(resp) + const { + result, + token: { access_token, id_token, refresh_token }, + } = resp.body + + expect(result).to.equal("success") + expect(access_token).to.not.be.empty + expect(id_token).to.not.be.empty + expect(refresh_token).to.be.undefined + }) + }) + + it("should return an Access Token when no scope is granted", function () { + const client = nc() + cy.deviceAuthFlow(client, { consent: { scope: [] } }) + + cy.postDeviceAuthFlow().then((resp) => { + console.log(resp) + const { + result, + token: { access_token, id_token, refresh_token }, + } = resp.body + + expect(result).to.equal("success") + expect(access_token).to.not.be.empty + expect(id_token).to.be.undefined + expect(refresh_token).to.be.undefined + }) + }) + + it("should skip consent if the client is confgured thus", function () { + const client = nc({ skip_consent: true }) + cy.deviceAuthFlow(client, { + consent: { scope: ["offline_access", "openid"], skip: true }, + }) + + cy.postDeviceAuthFlow().then((resp) => { + console.log(resp) + const { + result, + token: { access_token, id_token, refresh_token }, + } = resp.body + + expect(result).to.equal("success") + expect(access_token).to.not.be.empty + expect(id_token).to.not.be.empty + expect(refresh_token).to.not.be.empty + }) + }) + }) + }) +}) diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 2f75293404d..0e8700177dc 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -216,3 +216,90 @@ Cypress.Commands.add("refreshTokenBrowser", (client, token) => failOnStatusCode: false, }), ) + +Cypress.Commands.add( + "deviceAuthFlow", + ( + client, + { + override: { scope, client_id, client_secret } = {}, + consent: { + accept: acceptConsent = true, + skip: skipConsent = false, + remember: rememberConsent = false, + scope: acceptScope = [], + } = {}, + login: { + accept: acceptLogin = true, + skip: skipLogin = false, + remember: rememberLogin = false, + username = "foo@bar.com", + password = "foobar", + } = {}, + prompt = "", + createClient: doCreateClient = true, + } = {}, + path = "oauth2", + ) => { + const run = (client) => { + cy.visit( + `${Cypress.env("client_url")}/${path}/device?client_id=${ + client_id || client.client_id + }&client_secret=${client_secret || client.client_secret}&scope=${ + scope || client.scope + }`, + { failOnStatusCode: false }, + ) + + cy.get("#verify").click() + + if (!skipLogin) { + cy.get("#email").type(username, { delay: 1 }) + cy.get("#password").type(password, { delay: 1 }) + + if (rememberLogin) { + cy.get("#remember").click() + } + + if (acceptLogin) { + cy.get("#accept").click() + } else { + cy.get("#reject").click() + } + } + + if (!skipConsent) { + acceptScope.forEach((s) => { + cy.get(`#${s}`).click() + }) + + if (rememberConsent) { + cy.get("#remember").click() + } + + if (acceptConsent) { + cy.get("#accept").click() + } else { + cy.get("#reject").click() + } + + cy.location().should((loc) => { + expect(loc.origin).to.eq(Cypress.env("consent_url")) + expect(loc.pathname).to.eq("/oauth2/device/success") + }) + } + } + + if (doCreateClient) { + createClient(client).should((client) => { + run(client) + }) + return + } + run(client) + }, +) + +Cypress.Commands.add("postDeviceAuthFlow", (path = "oauth2") => + cy.request(`${Cypress.env("client_url")}/${path}/device/success`), +) diff --git a/driver/config/provider.go b/driver/config/provider.go index b02d0ae1da4..0bb47620b2a 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" @@ -64,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" @@ -73,6 +76,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 +86,8 @@ const ( KeyLogoutURL = "urls.logout" KeyConsentURL = "urls.consent" KeyErrorURL = "urls.error" + KeyDeviceVerificationURL = "urls.device.verification" + KeyDeviceDoneURL = "urls.device.success" KeyPublicURL = "urls.self.public" KeyAdminURL = "urls.self.admin" KeyIssuerURL = "urls.self.issuer" @@ -93,6 +99,8 @@ 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 + KeyDeviceAuthUserCodeEntropy = "oauth2.device_authorization.user_code_entropy" KeyPKCEEnforced = "oauth2.pkce.enforced" KeyPKCEEnforcedForPublicClients = "oauth2.pkce.enforced_for_public_clients" KeyLogLevel = "log.level" @@ -113,6 +121,15 @@ const ( const DSNMemory = "memory" +var userCodeEtropy = map[string]struct { + Length int + Symbols []rune +}{ + "high": {Length: 8, Symbols: []rune(randx.AlphaNumNoAmbiguous)}, + "medium": {Length: 8, Symbols: []rune(randx.AlphaUpper)}, + "low": {Length: 9, Symbols: []rune(randx.Numeric)}, +} + var ( _ hasherx.PBKDF2Configurator = (*DefaultProvider)(nil) _ hasherx.BCryptConfigurator = (*DefaultProvider)(nil) @@ -397,6 +414,48 @@ 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) +} + +// GetUserCodeLength returns configured user_code length +func (p *DefaultProvider) GetUserCodeLength(ctx context.Context) int { + k := p.getProvider(ctx).StringF(KeyDeviceAuthUserCodeEntropy, "medium") + profile, ok := userCodeEtropy[k] + if !ok { + keys := []string{} + for k := range userCodeEtropy { + keys = append(keys, k) + } + + p.l.WithError(errors.Errorf("Invalid user_code entropy: %s, allowed entropy values are: %s", k, keys)) + return 0 + } + return profile.Length +} + +// GetDeviceAuthTokenPollingInterval returns configured user_code allowed symbols +func (p *DefaultProvider) GetUserCodeSymbols(ctx context.Context) []rune { + k := p.getProvider(ctx).StringF(KeyDeviceAuthUserCodeEntropy, "medium") + profile, ok := userCodeEtropy[k] + if !ok { + keys := []string{} + for k := range userCodeEtropy { + keys = append(keys, k) + } + + p.l.WithError(errors.Errorf("Invalid user_code entropy: %s, allowed entropy values are: %s", k, keys)) + return nil + } + return profile.Symbols +} + func (p *DefaultProvider) LoginURL(ctx context.Context) *url.URL { return urlRoot(p.getProvider(ctx).URIF(KeyLoginURL, p.publicFallbackURL(ctx, "oauth2/fallbacks/login"))) } @@ -417,6 +476,16 @@ 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"))) +} + +// 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))) } @@ -474,6 +543,11 @@ 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")) +} + func (p *DefaultProvider) JWKSURL(ctx context.Context) *url.URL { return p.getProvider(ctx).RequestURIF(KeyJWKSURL, urlx.AppendPaths(p.IssuerURL(ctx), "/.well-known/jwks.json")) } @@ -662,6 +736,11 @@ 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) +} + func (p *DefaultProvider) CookieNameConsentCSRF(ctx context.Context) string { return p.cookieSuffix(ctx, KeyCookieConsentCSRFName) } diff --git a/driver/config/provider_test.go b/driver/config/provider_test.go index 7ec1dce8df9..a1ced7a590a 100644 --- a/driver/config/provider_test.go +++ b/driver/config/provider_test.go @@ -17,6 +17,7 @@ import ( "github.com/ory/fosite/token/jwt" "github.com/ory/x/configx" "github.com/ory/x/otelx" + "github.com/ory/x/randx" "github.com/rs/cors" "github.com/stretchr/testify/assert" @@ -279,6 +280,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 +306,8 @@ 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://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)) @@ -321,12 +325,16 @@ 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)) + assert.Equal(t, 8, c.GetUserCodeLength(ctx)) + assert.Equal(t, []rune(randx.AlphaUpper), c.GetUserCodeSymbols(ctx)) // secrets secret, err := c.GetGlobalSecret(ctx) @@ -395,16 +403,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/driver/registry_sql.go b/driver/registry_sql.go index 7cef650f955..07832c3e022 100644 --- a/driver/registry_sql.go +++ b/driver/registry_sql.go @@ -22,6 +22,7 @@ import ( "github.com/ory/fosite/compose" foauth2 "github.com/ory/fosite/handler/oauth2" "github.com/ory/fosite/handler/openid" + "github.com/ory/fosite/handler/rfc8628" "github.com/ory/fosite/token/hmac" "github.com/ory/herodot" "github.com/ory/hydra/v2/aead" @@ -99,6 +100,7 @@ type RegistrySQL struct { ats jwk.JWTSigner hmacs foauth2.CoreStrategy enigmaHMAC *hmac.HMACStrategy + deviceHmac rfc8628.RFC8628CodeStrategy fc *fositex.Config publicCORS *cors.Cors kratos kratos.Client @@ -592,6 +594,16 @@ func (m *RegistrySQL) OAuth2HMACStrategy() foauth2.CoreStrategy { return m.hmacs } +// RFC8628HMACStrategy returns the rfc8628 strategy +func (m *RegistrySQL) RFC8628HMACStrategy() rfc8628.RFC8628CodeStrategy { + if m.deviceHmac != nil { + return m.deviceHmac + } + + m.deviceHmac = compose.NewDeviceStrategy(m.OAuth2Config()) + return m.deviceHmac +} + func (m *RegistrySQL) OAuth2Config() *fositex.Config { if m.fc != nil { return m.fc @@ -618,6 +630,7 @@ func (m *RegistrySQL) OAuth2ProviderConfig() fosite.Configurator { conf := m.OAuth2Config() hmacAtStrategy := m.OAuth2HMACStrategy() + deviceHmacAtStrategy := m.RFC8628HMACStrategy() oidcSigner := m.OpenIDJWTStrategy() atSigner := m.AccessTokenJWTStrategy() jwtAtStrategy := &foauth2.DefaultJWTStrategy{ @@ -632,6 +645,7 @@ func (m *RegistrySQL) OAuth2ProviderConfig() fosite.Configurator { HMACSHAStrategy: hmacAtStrategy, Config: conf, }), + RFC8628CodeStrategy: deviceHmacAtStrategy, OpenIDConnectTokenStrategy: &openid.DefaultStrategy{ Config: conf, Signer: oidcSigner, diff --git a/flow/.snapshots/TestOAuth2ConsentRequest_MarshalJSON.json b/flow/.snapshots/TestOAuth2ConsentRequest_MarshalJSON.json index ec893803186..5e9d62cfb1f 100644 --- a/flow/.snapshots/TestOAuth2ConsentRequest_MarshalJSON.json +++ b/flow/.snapshots/TestOAuth2ConsentRequest_MarshalJSON.json @@ -1 +1 @@ -"{\"challenge\":\"\",\"consent_request_id\":\"\",\"requested_scope\":[],\"requested_access_token_audience\":[],\"skip\":false,\"subject\":\"\",\"oidc_context\":null,\"client\":null,\"request_url\":\"\",\"login_challenge\":\"\",\"login_session_id\":\"\",\"acr\":\"\",\"amr\":[]}" +"{\"challenge\":\"\",\"consent_request_id\":\"\",\"requested_scope\":[],\"requested_access_token_audience\":[],\"skip\":false,\"subject\":\"\",\"oidc_context\":null,\"client\":null,\"request_url\":\"\",\"login_challenge\":\"\",\"login_session_id\":\"\",\"device_challenge_id\":\"\",\"acr\":\"\",\"amr\":[]}" diff --git a/flow/consent_types.go b/flow/consent_types.go index 1216760118f..64f9434629c 100644 --- a/flow/consent_types.go +++ b/flow/consent_types.go @@ -23,6 +23,7 @@ import ( ) const ( + DeviceRequestDeniedErrorName = "device request denied" ConsentRequestDeniedErrorName = "consent request denied" LoginRequestDeniedErrorName = "login request denied" ) @@ -539,6 +540,66 @@ 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"` + 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"` + + 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. +// +// swagger:model verifyUserCodeRequest +type HandledDeviceUserAuthRequest struct { + // ID is the identifier ("device challenge") of the device request. It is used to + // identify the session. + 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() +} + // Contains information on an ongoing login request. // // swagger:model oAuth2LoginRequest @@ -621,6 +682,13 @@ func (r *LoginRequest) MarshalJSON() ([]byte, error) { return json.Marshal(alias) } +// Contains information on an device verification +// +// swagger:model acceptDeviceUserCodeRequest +type AcceptDeviceUserCodeRequest struct { + UserCode string `json:"user_code"` +} + // Contains information on an ongoing consent request. // // swagger:model oAuth2ConsentRequest @@ -671,6 +739,9 @@ type OAuth2ConsentRequest struct { // channel logout. It's value can generally be used to associate consecutive login requests by a certain user. LoginSessionID sqlxx.NullString `json:"login_session_id"` + // DeviceChallenge is the device challenge this consent challenge belongs to, if this flow was initiated by a device. + DeviceChallenge sqlxx.NullString `json:"device_challenge_id" faker:"-"` + // ACR represents the Authentication AuthorizationContext Class Reference value for this authentication session. You can use it // to express that, for example, a user authenticated using two factor authentication. ACR string `json:"acr"` diff --git a/flow/flow.go b/flow/flow.go index b44e14322bb..46cb4b46675 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 @@ -53,6 +57,19 @@ const ( FlowStateConsentUnused = int16(5) FlowStateConsentUsed = int16(6) + // DeviceFlowStateLoginInitialized applies before the login app either + // accepts or rejects the login request. + DeviceFlowStateInitialized = int16(7) + + // DeviceFlowStateUnused indicates that the login has been authenticated, but + // the User Agent hasn't picked up the result yet. + DeviceFlowStateUnused = int16(8) + + // 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 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 @@ -65,6 +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: + DeviceFlowStateError = int16(127) FlowStateLoginError = int16(128) FlowStateConsentError = int16(129) ) @@ -207,10 +225,24 @@ type Flow struct { LoginError *RequestDeniedError `db:"login_error" json:"le,omitempty"` LoginAuthenticatedAt sqlxx.NullTime `db:"login_authenticated_at" json:"la,omitempty"` + // DeviceChallengeID is the device request's challenge ID + DeviceChallengeID sqlxx.NullString `db:"device_challenge_id" json:"di,omitempty"` + // DeviceCodeRequestID is the device request's ID + DeviceCodeRequestID sqlxx.NullString `db:"device_code_request_id" json:"dr,omitempty"` + // DeviceVerifier is the device request's verifier + DeviceVerifier sqlxx.NullString `db:"device_verifier" json:"dv,omitempty"` + // DeviceVerifier is the device request's CSRF + DeviceCSRF sqlxx.NullString `db:"device_csrf" json:"dc,omitempty"` + // DeviceWasUsed set to true means that the device request was already handled + DeviceWasUsed sqlxx.NullBool `db:"device_was_used" json:"du,omitempty"` + // DeviceHandledAt contains the timestamp the device user_code verification request was handled + DeviceHandledAt sqlxx.NullTime `db:"device_handled_at" json:"dh,omitempty"` + // DeviceError contains any error that happened during the handling of the device flow + DeviceError *RequestDeniedError `db:"device_error" json:"de,omitempty"` + // ConsentRequestID is the identifier of the consent request. // The database column should be named `consent_request_id`, but is not for historical reasons. ConsentRequestID sqlxx.NullString `db:"consent_challenge_id" json:"cc,omitempty"` - // ConsentSkip, if true, implies that the client has requested the same scopes from the same user previously. // If true, you must not ask the user to grant the requested scopes. You must however either allow or deny the // consent request using the usual API call. @@ -244,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, @@ -438,6 +568,7 @@ func (f *Flow) GetConsentRequest(challenge string) *OAuth2ConsentRequest { RequestURL: f.RequestURL, LoginChallenge: sqlxx.NullString(f.ID), LoginSessionID: f.SessionID, + DeviceChallenge: f.DeviceChallengeID, ACR: f.ACR, AMR: f.AMR, Context: f.Context, @@ -513,6 +644,16 @@ type CipherProvider interface { FlowCipher() *aead.XChaCha20Poly1305 } +// ToDeviceChallenge converts the flow into a device challenge. +func (f *Flow) ToDeviceChallenge(ctx context.Context, cipherProvider CipherProvider) (string, error) { + return flowctx.Encode(ctx, cipherProvider.FlowCipher(), f, flowctx.AsDeviceChallenge) +} + +// ToDeviceVerifier converts the flow into a device verifier. +func (f *Flow) ToDeviceVerifier(ctx context.Context, cipherProvider CipherProvider) (string, error) { + return flowctx.Encode(ctx, cipherProvider.FlowCipher(), f, flowctx.AsDeviceVerifier) +} + // ToLoginChallenge converts the flow into a login challenge. func (f Flow) ToLoginChallenge(ctx context.Context, cipherProvider CipherProvider) (challenge string, err error) { if f.Client != nil { diff --git a/flow/flow_test.go b/flow/flow_test.go index 1b56336b0af..91959333438 100644 --- a/flow/flow_test.go +++ b/flow/flow_test.go @@ -92,6 +92,94 @@ func (f *Flow) setHandledConsentRequest(r AcceptOAuth2ConsentRequest) { } } +func (f *Flow) setDeviceRequest(r *DeviceUserAuthRequest) { + f.DeviceChallengeID = sqlxx.NullString(r.ID) + f.DeviceCSRF = sqlxx.NullString(r.CSRF) + f.DeviceVerifier = sqlxx.NullString(r.Verifier) + f.Client = r.Client + f.RequestURL = r.RequestURL + f.RequestedAt = r.RequestedAt + f.RequestedScope = r.RequestedScope + f.RequestedAudience = r.RequestedAudience + f.DeviceWasUsed = sqlxx.NullBool{Bool: r.WasHandled, Valid: true} + f.DeviceHandledAt = r.HandledAt +} + +func (f *Flow) setHandledDeviceRequest(r *HandledDeviceUserAuthRequest) { + f.DeviceChallengeID = sqlxx.NullString(r.ID) + f.Client = r.Client + f.RequestURL = r.RequestURL + f.RequestedAt = r.RequestedAt + f.RequestedScope = r.RequestedScope + f.RequestedAudience = r.RequestedAudience + f.DeviceError = r.Error + f.RequestedAt = r.RequestedAt + f.DeviceCodeRequestID = sqlxx.NullString(r.DeviceCodeRequestID) + f.DeviceWasUsed = sqlxx.NullBool{Bool: r.WasHandled, Valid: true} + f.DeviceHandledAt = r.HandledAt +} + +func TestFlow_GetDeviceUserAuthRequest(t *testing.T) { + t.Run("GetDeviceUserAuthRequest should set all fields on its return value", func(t *testing.T) { + f := Flow{} + expected := DeviceUserAuthRequest{} + assert.NoError(t, faker.FakeData(&expected)) + f.setDeviceRequest(&expected) + actual := f.GetDeviceUserAuthRequest() + assert.Equal(t, expected, *actual) + }) +} + +func TestFlow_GetHandledDeviceUserAuthRequest(t *testing.T) { + t.Run("GetHandledDeviceUserAuthRequest should set all fields on its return value", func(t *testing.T) { + f := Flow{} + expected := HandledDeviceUserAuthRequest{} + assert.NoError(t, faker.FakeData(&expected)) + f.setHandledDeviceRequest(&expected) + actual := f.GetHandledDeviceUserAuthRequest() + assert.NotNil(t, actual.Request) + expected.Request = nil + actual.Request = nil + assert.Equal(t, expected, *actual) + }) +} + +func TestFlow_NewDeviceFlow(t *testing.T) { + t.Run("NewDeviceFlow and GetDeviceUserAuthRequest should use all DeviceUserAuthRequest fields", func(t *testing.T) { + expected := &DeviceUserAuthRequest{} + assert.NoError(t, faker.FakeData(expected)) + actual := NewDeviceFlow(expected).GetDeviceUserAuthRequest() + assert.Equal(t, expected, actual) + }) +} + +func TestFlow_HandleDeviceUserAuthRequest(t *testing.T) { + t.Run( + "HandleDeviceUserAuthRequest should ignore RequestedAt in its argument and copy the other fields", + func(t *testing.T) { + f := Flow{} + assert.NoError(t, faker.FakeData(&f)) + f.State = DeviceFlowStateInitialized + + r := HandledDeviceUserAuthRequest{} + assert.NoError(t, faker.FakeData(&r)) + r.ID = f.DeviceChallengeID.String() + f.DeviceWasUsed = sqlxx.NullBool{Bool: false, Valid: true} + f.RequestedAudience = r.RequestedAudience + f.RequestedScope = r.RequestedScope + f.RequestURL = r.RequestURL + + assert.NoError(t, f.HandleDeviceUserAuthRequest(&r)) + + actual := f.GetHandledDeviceUserAuthRequest() + assert.NotEqual(t, r.RequestedAt, actual.RequestedAt) + r.Request = f.GetDeviceUserAuthRequest() + actual.RequestedAt = r.RequestedAt + assert.Equal(t, r, *actual) + }, + ) +} + func TestFlow_GetLoginRequest(t *testing.T) { t.Run("GetLoginRequest should set all fields on its return value", func(t *testing.T) { f := Flow{} diff --git a/fositex/config.go b/fositex/config.go index 4377efb1f6d..40efcd33de3 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,9 @@ var defaultFactories = []Factory{ compose.OAuth2PKCEFactory, compose.RFC7523AssertionGrantFactory, compose.OIDCUserinfoVerifiableCredentialFactory, + compose.RFC8628DeviceFactory, + compose.RFC8628DeviceAuthorizationTokenFactory, + compose.OpenIDConnectDeviceFactory, } func NewConfig(deps configDependencies) *Config { @@ -87,6 +91,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 +121,11 @@ 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 +} + func (c *Config) GetGrantTypeJWTBearerCanSkipClientAuth(context.Context) bool { return false } @@ -206,3 +218,8 @@ func (c *Config) GetTokenURLs(ctx context.Context) []string { urlx.AppendPaths(c.deps.Config().PublicURL(ctx), oauth2.TokenPath).String(), }) } + +// GetDeviceVerificationURL returns the device verification url +func (c *Config) GetDeviceVerificationURL(ctx context.Context) string { + return urlx.AppendPaths(c.deps.Config().PublicURL(ctx), oauth2.DeviceVerificationPath).String() +} diff --git a/go.mod b/go.mod index 2a9413c0ea5..ac7c2364b51 100644 --- a/go.mod +++ b/go.mod @@ -33,7 +33,7 @@ require ( github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 github.com/oleiade/reflections v1.0.1 github.com/ory/analytics-go/v5 v5.0.1 - github.com/ory/fosite v0.49.0 + github.com/ory/fosite v0.49.1-0.20250203124447-75b904ddbee4 github.com/ory/go-acc v0.2.9-0.20230103102148-6b1c9a70dbbe github.com/ory/graceful v0.1.3 github.com/ory/herodot v0.10.3-0.20230626083119-d7e5192f0d88 @@ -232,6 +232,7 @@ require ( go.opentelemetry.io/otel/exporters/zipkin v1.32.0 // indirect go.opentelemetry.io/otel/metric v1.32.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect + go.uber.org/mock v0.5.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/mod v0.19.0 // indirect golang.org/x/net v0.33.0 // indirect diff --git a/go.sum b/go.sum index f621e7822b7..51e6007fb36 100644 --- a/go.sum +++ b/go.sum @@ -378,8 +378,8 @@ github.com/ory/analytics-go/v5 v5.0.1 h1:LX8T5B9FN8KZXOtxgN+R3I4THRRVB6+28IKgKBp github.com/ory/analytics-go/v5 v5.0.1/go.mod h1:lWCiCjAaJkKfgR/BN5DCLMol8BjKS1x+4jxBxff/FF0= github.com/ory/dockertest/v3 v3.10.1-0.20240704115616-d229e74b748d h1:By96ZSVuH5LyjXLVVMfvJoLVGHaT96LdOnwgFSLVf0E= github.com/ory/dockertest/v3 v3.10.1-0.20240704115616-d229e74b748d/go.mod h1:F2FIjwwAk6CsNAs//B8+aPFQF0t84pbM8oliyNXwQrk= -github.com/ory/fosite v0.49.0 h1:KNqO7RVt/1X8F08/UI0Y+GRvcpscCWgjqvpLBQPRovo= -github.com/ory/fosite v0.49.0/go.mod h1:FAn7IY+I6DjT1r29wMouPeRYq63DWUuBj++96uOS4mE= +github.com/ory/fosite v0.49.1-0.20250203124447-75b904ddbee4 h1:VnazT+N30kfg5TxJQ2bg4Fa1pCWT7A1i3FffmX0fhAA= +github.com/ory/fosite v0.49.1-0.20250203124447-75b904ddbee4/go.mod h1:IhAwHrxwNgB3smKB75jkMVQjFTHq9HveITItLGX8/GU= github.com/ory/go-acc v0.2.9-0.20230103102148-6b1c9a70dbbe h1:rvu4obdvqR0fkSIJ8IfgzKOWwZ5kOT2UNfLq81Qk7rc= github.com/ory/go-acc v0.2.9-0.20230103102148-6b1c9a70dbbe/go.mod h1:z4n3u6as84LbV4YmgjHhnwtccQqzf4cZlSk9f1FhygI= github.com/ory/go-convenience v0.1.0 h1:zouLKfF2GoSGnJwGq+PE/nJAE6dj2Zj5QlTgmMTsTS8= @@ -560,6 +560,8 @@ go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8= go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0= go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= diff --git a/internal/.hydra.yaml b/internal/.hydra.yaml index bb02d986ad6..9515aa64296 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,9 @@ urls: consent: https://consent logout: https://logout error: https://error + device: + verification: https://device + success: https://device/callback post_logout_redirect: https://post_logout strategies: @@ -112,12 +116,16 @@ 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 + user_code_entropy: medium pkce: enforced: true enforced_for_public_clients: true diff --git a/internal/httpclient/.openapi-generator/FILES b/internal/httpclient/.openapi-generator/FILES index d0e465ce1c3..395d900b30e 100644 --- a/internal/httpclient/.openapi-generator/FILES +++ b/internal/httpclient/.openapi-generator/FILES @@ -10,12 +10,15 @@ api_oidc.go api_wellknown.go client.go configuration.go +docs/AcceptDeviceUserCodeRequest.md docs/AcceptOAuth2ConsentRequest.md docs/AcceptOAuth2ConsentRequestSession.md docs/AcceptOAuth2LoginRequest.md docs/CreateJsonWebKeySet.md docs/CreateVerifiableCredentialRequestBody.md docs/CredentialSupportedDraft00.md +docs/DeviceAuthorization.md +docs/DeviceUserAuthRequest.md docs/ErrorOAuth2.md docs/GenericError.md docs/GetVersion200Response.md @@ -57,17 +60,21 @@ docs/TrustedOAuth2JwtGrantJsonWebKey.md docs/VerifiableCredentialPrimingResponse.md docs/VerifiableCredentialProof.md docs/VerifiableCredentialResponse.md +docs/VerifyUserCodeRequest.md docs/Version.md docs/WellknownAPI.md git_push.sh go.mod go.sum +model_accept_device_user_code_request.go model_accept_o_auth2_consent_request.go model_accept_o_auth2_consent_request_session.go model_accept_o_auth2_login_request.go model_create_json_web_key_set.go model_create_verifiable_credential_request_body.go model_credential_supported_draft00.go +model_device_authorization.go +model_device_user_auth_request.go model_error_o_auth2.go model_generic_error.go model_get_version_200_response.go @@ -105,6 +112,7 @@ model_trusted_o_auth2_jwt_grant_json_web_key.go model_verifiable_credential_priming_response.go model_verifiable_credential_proof.go model_verifiable_credential_response.go +model_verify_user_code_request.go model_version.go response.go utils.go diff --git a/internal/httpclient/README.md b/internal/httpclient/README.md index a2f17fd7fe5..f5ccc0e780c 100644 --- a/internal/httpclient/README.md +++ b/internal/httpclient/README.md @@ -92,6 +92,7 @@ Class | Method | HTTP request | Description *OAuth2API* | [**AcceptOAuth2ConsentRequest**](docs/OAuth2API.md#acceptoauth2consentrequest) | **Put** /admin/oauth2/auth/requests/consent/accept | Accept OAuth 2.0 Consent Request *OAuth2API* | [**AcceptOAuth2LoginRequest**](docs/OAuth2API.md#acceptoauth2loginrequest) | **Put** /admin/oauth2/auth/requests/login/accept | Accept OAuth 2.0 Login Request *OAuth2API* | [**AcceptOAuth2LogoutRequest**](docs/OAuth2API.md#acceptoauth2logoutrequest) | **Put** /admin/oauth2/auth/requests/logout/accept | Accept OAuth 2.0 Session Logout Request +*OAuth2API* | [**AcceptUserCodeRequest**](docs/OAuth2API.md#acceptusercoderequest) | **Put** /admin/oauth2/auth/requests/device/accept | Accepts a device grant user_code request *OAuth2API* | [**CreateOAuth2Client**](docs/OAuth2API.md#createoauth2client) | **Post** /admin/clients | Create OAuth 2.0 Client *OAuth2API* | [**DeleteOAuth2Client**](docs/OAuth2API.md#deleteoauth2client) | **Delete** /admin/clients/{id} | Delete OAuth 2.0 Client *OAuth2API* | [**DeleteOAuth2Token**](docs/OAuth2API.md#deleteoauth2token) | **Delete** /admin/oauth2/tokens | Delete OAuth 2.0 Access Tokens from specific OAuth 2.0 Client @@ -106,8 +107,10 @@ Class | Method | HTTP request | Description *OAuth2API* | [**ListOAuth2ConsentSessions**](docs/OAuth2API.md#listoauth2consentsessions) | **Get** /admin/oauth2/auth/sessions/consent | List OAuth 2.0 Consent Sessions of a Subject *OAuth2API* | [**ListTrustedOAuth2JwtGrantIssuers**](docs/OAuth2API.md#listtrustedoauth2jwtgrantissuers) | **Get** /admin/trust/grants/jwt-bearer/issuers | List Trusted OAuth2 JWT Bearer Grant Type Issuers *OAuth2API* | [**OAuth2Authorize**](docs/OAuth2API.md#oauth2authorize) | **Get** /oauth2/auth | OAuth 2.0 Authorize Endpoint +*OAuth2API* | [**OAuth2DeviceFlow**](docs/OAuth2API.md#oauth2deviceflow) | **Post** /oauth2/device/auth | The OAuth 2.0 Device Authorize Endpoint *OAuth2API* | [**Oauth2TokenExchange**](docs/OAuth2API.md#oauth2tokenexchange) | **Post** /oauth2/token | The OAuth 2.0 Token Endpoint *OAuth2API* | [**PatchOAuth2Client**](docs/OAuth2API.md#patchoauth2client) | **Patch** /admin/clients/{id} | Patch OAuth 2.0 Client +*OAuth2API* | [**PerformOAuth2DeviceVerificationFlow**](docs/OAuth2API.md#performoauth2deviceverificationflow) | **Get** /oauth2/device/verify | OAuth 2.0 Device Verification Endpoint *OAuth2API* | [**RejectOAuth2ConsentRequest**](docs/OAuth2API.md#rejectoauth2consentrequest) | **Put** /admin/oauth2/auth/requests/consent/reject | Reject OAuth 2.0 Consent Request *OAuth2API* | [**RejectOAuth2LoginRequest**](docs/OAuth2API.md#rejectoauth2loginrequest) | **Put** /admin/oauth2/auth/requests/login/reject | Reject OAuth 2.0 Login Request *OAuth2API* | [**RejectOAuth2LogoutRequest**](docs/OAuth2API.md#rejectoauth2logoutrequest) | **Put** /admin/oauth2/auth/requests/logout/reject | Reject OAuth 2.0 Session Logout Request @@ -130,12 +133,15 @@ Class | Method | HTTP request | Description ## Documentation For Models + - [AcceptDeviceUserCodeRequest](docs/AcceptDeviceUserCodeRequest.md) - [AcceptOAuth2ConsentRequest](docs/AcceptOAuth2ConsentRequest.md) - [AcceptOAuth2ConsentRequestSession](docs/AcceptOAuth2ConsentRequestSession.md) - [AcceptOAuth2LoginRequest](docs/AcceptOAuth2LoginRequest.md) - [CreateJsonWebKeySet](docs/CreateJsonWebKeySet.md) - [CreateVerifiableCredentialRequestBody](docs/CreateVerifiableCredentialRequestBody.md) - [CredentialSupportedDraft00](docs/CredentialSupportedDraft00.md) + - [DeviceAuthorization](docs/DeviceAuthorization.md) + - [DeviceUserAuthRequest](docs/DeviceUserAuthRequest.md) - [ErrorOAuth2](docs/ErrorOAuth2.md) - [GenericError](docs/GenericError.md) - [GetVersion200Response](docs/GetVersion200Response.md) @@ -173,6 +179,7 @@ Class | Method | HTTP request | Description - [VerifiableCredentialPrimingResponse](docs/VerifiableCredentialPrimingResponse.md) - [VerifiableCredentialProof](docs/VerifiableCredentialProof.md) - [VerifiableCredentialResponse](docs/VerifiableCredentialResponse.md) + - [VerifyUserCodeRequest](docs/VerifyUserCodeRequest.md) - [Version](docs/Version.md) diff --git a/internal/httpclient/api/openapi.yaml b/internal/httpclient/api/openapi.yaml index c94ecdf9f1b..36def440519 100644 --- a/internal/httpclient/api/openapi.yaml +++ b/internal/httpclient/api/openapi.yaml @@ -787,6 +787,40 @@ paths: summary: Reject OAuth 2.0 Consent Request tags: - oAuth2 + /admin/oauth2/auth/requests/device/accept: + put: + description: Accepts a device grant user_code request + operationId: acceptUserCodeRequest + parameters: + - explode: true + in: query + name: device_challenge + required: true + schema: + type: string + style: form + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/acceptDeviceUserCodeRequest' + x-originalParamName: Body + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/oAuth2RedirectTo' + description: oAuth2RedirectTo + default: + content: + application/json: + schema: + $ref: '#/components/schemas/errorOAuth2' + description: errorOAuth2 + summary: Accepts a device grant user_code request + tags: + - oAuth2 /admin/oauth2/auth/requests/login: get: description: |- @@ -1512,6 +1546,49 @@ paths: summary: OAuth 2.0 Authorize Endpoint tags: - oAuth2 + /oauth2/device/auth: + post: + description: |- + This endpoint is not documented here because you should never use your own implementation to perform OAuth2 flows. + OAuth2 is a very popular protocol and a library for your programming language will exists. + + To learn more about this flow please refer to the specification: https://tools.ietf.org/html/rfc8628 + operationId: oAuth2DeviceFlow + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/deviceAuthorization' + description: deviceAuthorization + default: + content: + application/json: + schema: + $ref: '#/components/schemas/errorOAuth2' + description: errorOAuth2 + summary: The OAuth 2.0 Device Authorize Endpoint + tags: + - oAuth2 + /oauth2/device/verify: + get: + description: This is the device user verification endpoint. The user is redirected + here when trying to login using the device flow. + operationId: performOAuth2DeviceVerificationFlow + responses: + "302": + description: |- + Empty responses are sent when, for example, resources are deleted. The HTTP status code for empty responses is + typically 204. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/errorOAuth2' + description: errorOAuth2 + summary: OAuth 2.0 Device Verification Endpoint + tags: + - oAuth2 /oauth2/register: post: description: |- @@ -1879,6 +1956,38 @@ components: a verifiable credential. type: object DefaultError: {} + DeviceUserAuthRequest: + properties: + challenge: + description: |- + ID is the identifier ("device challenge") of the device grant request. It is used to + identify the session. + type: string + client: + $ref: '#/components/schemas/oAuth2Client' + handled_at: + format: date-time + title: NullTime implements sql.NullTime functionality. + type: string + request_url: + description: RequestURL is the original Device Authorization URL requested. + type: string + requested_access_token_audience: + items: + type: string + title: "StringSliceJSONFormat represents []string{} which is encoded to/from\ + \ JSON for SQL storage." + type: array + requested_scope: + items: + type: string + title: "StringSliceJSONFormat represents []string{} which is encoded to/from\ + \ JSON for SQL storage." + type: array + required: + - challenge + title: Contains information on an ongoing device grant request. + type: object JSONRawMessage: title: "JSONRawMessage represents a json.RawMessage that works well with JSON,\ \ SQL, and Swagger." @@ -1942,6 +2051,12 @@ components: type: string title: VerifiableCredentialProof contains the proof of a verifiable credential. type: object + acceptDeviceUserCodeRequest: + description: Contains information on an device verification + properties: + user_code: + type: string + type: object acceptOAuth2ConsentRequest: properties: context: @@ -2138,6 +2253,53 @@ components: type: array title: Verifiable Credentials Metadata (Draft 00) type: object + deviceAuthorization: + description: '# Ory''s OAuth 2.0 Device Authorization API' + example: + user_code: AAAAAA + device_code: ory_dc_smldfksmdfkl.mslkmlkmlk + interval: 5 + verification_uri_complete: https://auth.ory.sh/tv?user_code=AAAAAA + verification_uri: https://auth.ory.sh/tv + expires_in: 16830 + properties: + device_code: + description: The device verification code. + example: ory_dc_smldfksmdfkl.mslkmlkmlk + type: string + expires_in: + description: The lifetime in seconds of the "device_code" and "user_code". + example: 16830 + format: int64 + type: integer + interval: + description: |- + The minimum amount of time in seconds that the client + SHOULD wait between polling requests to the token endpoint. If no + value is provided, clients MUST use 5 as the default. + example: 5 + format: int64 + type: integer + user_code: + description: The end-user verification code. + example: AAAAAA + type: string + verification_uri: + description: |- + The end-user verification URI on the authorization + server. The URI should be short and easy to remember as end users + will be asked to manually type it into their user agent. + example: https://auth.ory.sh/tv + type: string + verification_uri_complete: + description: |- + A verification URI that includes the "user_code" (or + other information with the same function as the "user_code"), + which is designed for non-textual transmission. + example: https://auth.ory.sh/tv?user_code=AAAAAA + type: string + title: OAuth2 Device Flow + type: object errorOAuth2: description: Error example: @@ -2568,47 +2730,28 @@ components: generated for applications which want to consume your OAuth 2.0 or OpenID Connect capabilities. example: metadata: "" - token_endpoint_auth_signing_alg: token_endpoint_auth_signing_alg - client_uri: client_uri - jwt_bearer_grant_access_token_lifespan: jwt_bearer_grant_access_token_lifespan - jwks: "" logo_uri: logo_uri - created_at: 2000-01-23T04:56:07.000+00:00 - registration_client_uri: registration_client_uri allowed_cors_origins: - allowed_cors_origins - allowed_cors_origins refresh_token_grant_access_token_lifespan: refresh_token_grant_access_token_lifespan - registration_access_token: registration_access_token client_id: client_id - token_endpoint_auth_method: client_secret_basic - userinfo_signed_response_alg: userinfo_signed_response_alg - authorization_code_grant_id_token_lifespan: authorization_code_grant_id_token_lifespan authorization_code_grant_refresh_token_lifespan: authorization_code_grant_refresh_token_lifespan client_credentials_grant_access_token_lifespan: client_credentials_grant_access_token_lifespan - updated_at: 2000-01-23T04:56:07.000+00:00 - scope: scope1 scope-2 scope.3 scope:4 request_uris: - request_uris - request_uris client_secret: client_secret backchannel_logout_session_required: true backchannel_logout_uri: backchannel_logout_uri - client_name: client_name - policy_uri: policy_uri - owner: owner - skip_consent: true audience: - audience - audience - authorization_code_grant_access_token_lifespan: authorization_code_grant_access_token_lifespan post_logout_redirect_uris: - post_logout_redirect_uris - post_logout_redirect_uris - grant_types: - - grant_types - - grant_types - subject_type: subject_type + device_authorization_grant_id_token_lifespan: device_authorization_grant_id_token_lifespan + device_authorization_grant_access_token_lifespan: device_authorization_grant_access_token_lifespan refresh_token_grant_refresh_token_lifespan: refresh_token_grant_refresh_token_lifespan redirect_uris: - redirect_uris @@ -2616,21 +2759,43 @@ components: sector_identifier_uri: sector_identifier_uri frontchannel_logout_session_required: true frontchannel_logout_uri: frontchannel_logout_uri - skip_logout_consent: true refresh_token_grant_id_token_lifespan: refresh_token_grant_id_token_lifespan + access_token_strategy: access_token_strategy + request_object_signing_alg: request_object_signing_alg + tos_uri: tos_uri + response_types: + - response_types + - response_types + token_endpoint_auth_signing_alg: token_endpoint_auth_signing_alg + client_uri: client_uri + jwt_bearer_grant_access_token_lifespan: jwt_bearer_grant_access_token_lifespan + jwks: "" + created_at: 2000-01-23T04:56:07.000+00:00 + registration_client_uri: registration_client_uri + registration_access_token: registration_access_token + token_endpoint_auth_method: client_secret_basic + userinfo_signed_response_alg: userinfo_signed_response_alg + authorization_code_grant_id_token_lifespan: authorization_code_grant_id_token_lifespan + updated_at: 2000-01-23T04:56:07.000+00:00 + scope: scope1 scope-2 scope.3 scope:4 + device_authorization_grant_refresh_token_lifespan: device_authorization_grant_refresh_token_lifespan + client_name: client_name + policy_uri: policy_uri + owner: owner + skip_consent: true + authorization_code_grant_access_token_lifespan: authorization_code_grant_access_token_lifespan + grant_types: + - grant_types + - grant_types + subject_type: subject_type + skip_logout_consent: true implicit_grant_id_token_lifespan: implicit_grant_id_token_lifespan client_secret_expires_at: 0 implicit_grant_access_token_lifespan: implicit_grant_access_token_lifespan - access_token_strategy: access_token_strategy jwks_uri: jwks_uri - request_object_signing_alg: request_object_signing_alg - tos_uri: tos_uri contacts: - contacts - contacts - response_types: - - response_types - - response_types properties: access_token_strategy: description: |- @@ -2738,6 +2903,24 @@ components: CreatedAt returns the timestamp of the client's creation. format: date-time type: string + device_authorization_grant_access_token_lifespan: + description: "Specify a time duration in milliseconds, seconds, minutes,\ + \ hours." + pattern: "^([0-9]+(ns|us|ms|s|m|h))*$" + title: Time duration + type: string + device_authorization_grant_id_token_lifespan: + description: "Specify a time duration in milliseconds, seconds, minutes,\ + \ hours." + pattern: "^([0-9]+(ns|us|ms|s|m|h))*$" + title: Time duration + type: string + device_authorization_grant_refresh_token_lifespan: + description: "Specify a time duration in milliseconds, seconds, minutes,\ + \ hours." + pattern: "^([0-9]+(ns|us|ms|s|m|h))*$" + title: Time duration + type: string frontchannel_logout_session_required: description: |- OpenID Connect Front-Channel Logout Session Required @@ -2992,6 +3175,24 @@ components: pattern: "^([0-9]+(ns|us|ms|s|m|h))*$" title: Time duration type: string + device_authorization_grant_access_token_lifespan: + description: "Specify a time duration in milliseconds, seconds, minutes,\ + \ hours." + pattern: "^([0-9]+(ns|us|ms|s|m|h))*$" + title: Time duration + type: string + device_authorization_grant_id_token_lifespan: + description: "Specify a time duration in milliseconds, seconds, minutes,\ + \ hours." + pattern: "^([0-9]+(ns|us|ms|s|m|h))*$" + title: Time duration + type: string + device_authorization_grant_refresh_token_lifespan: + description: "Specify a time duration in milliseconds, seconds, minutes,\ + \ hours." + pattern: "^([0-9]+(ns|us|ms|s|m|h))*$" + title: Time duration + type: string implicit_grant_access_token_lifespan: description: "Specify a time duration in milliseconds, seconds, minutes,\ \ hours." @@ -3052,6 +3253,7 @@ components: - acr_values - acr_values display: display + device_challenge_id: device_challenge_id skip: true request_url: request_url acr: acr @@ -3059,47 +3261,28 @@ components: challenge: challenge client: metadata: "" - token_endpoint_auth_signing_alg: token_endpoint_auth_signing_alg - client_uri: client_uri - jwt_bearer_grant_access_token_lifespan: jwt_bearer_grant_access_token_lifespan - jwks: "" logo_uri: logo_uri - created_at: 2000-01-23T04:56:07.000+00:00 - registration_client_uri: registration_client_uri allowed_cors_origins: - allowed_cors_origins - allowed_cors_origins refresh_token_grant_access_token_lifespan: refresh_token_grant_access_token_lifespan - registration_access_token: registration_access_token client_id: client_id - token_endpoint_auth_method: client_secret_basic - userinfo_signed_response_alg: userinfo_signed_response_alg - authorization_code_grant_id_token_lifespan: authorization_code_grant_id_token_lifespan authorization_code_grant_refresh_token_lifespan: authorization_code_grant_refresh_token_lifespan client_credentials_grant_access_token_lifespan: client_credentials_grant_access_token_lifespan - updated_at: 2000-01-23T04:56:07.000+00:00 - scope: scope1 scope-2 scope.3 scope:4 request_uris: - request_uris - request_uris client_secret: client_secret backchannel_logout_session_required: true backchannel_logout_uri: backchannel_logout_uri - client_name: client_name - policy_uri: policy_uri - owner: owner - skip_consent: true audience: - audience - audience - authorization_code_grant_access_token_lifespan: authorization_code_grant_access_token_lifespan post_logout_redirect_uris: - post_logout_redirect_uris - post_logout_redirect_uris - grant_types: - - grant_types - - grant_types - subject_type: subject_type + device_authorization_grant_id_token_lifespan: device_authorization_grant_id_token_lifespan + device_authorization_grant_access_token_lifespan: device_authorization_grant_access_token_lifespan refresh_token_grant_refresh_token_lifespan: refresh_token_grant_refresh_token_lifespan redirect_uris: - redirect_uris @@ -3107,21 +3290,43 @@ components: sector_identifier_uri: sector_identifier_uri frontchannel_logout_session_required: true frontchannel_logout_uri: frontchannel_logout_uri - skip_logout_consent: true refresh_token_grant_id_token_lifespan: refresh_token_grant_id_token_lifespan + access_token_strategy: access_token_strategy + request_object_signing_alg: request_object_signing_alg + tos_uri: tos_uri + response_types: + - response_types + - response_types + token_endpoint_auth_signing_alg: token_endpoint_auth_signing_alg + client_uri: client_uri + jwt_bearer_grant_access_token_lifespan: jwt_bearer_grant_access_token_lifespan + jwks: "" + created_at: 2000-01-23T04:56:07.000+00:00 + registration_client_uri: registration_client_uri + registration_access_token: registration_access_token + token_endpoint_auth_method: client_secret_basic + userinfo_signed_response_alg: userinfo_signed_response_alg + authorization_code_grant_id_token_lifespan: authorization_code_grant_id_token_lifespan + updated_at: 2000-01-23T04:56:07.000+00:00 + scope: scope1 scope-2 scope.3 scope:4 + device_authorization_grant_refresh_token_lifespan: device_authorization_grant_refresh_token_lifespan + client_name: client_name + policy_uri: policy_uri + owner: owner + skip_consent: true + authorization_code_grant_access_token_lifespan: authorization_code_grant_access_token_lifespan + grant_types: + - grant_types + - grant_types + subject_type: subject_type + skip_logout_consent: true implicit_grant_id_token_lifespan: implicit_grant_id_token_lifespan client_secret_expires_at: 0 implicit_grant_access_token_lifespan: implicit_grant_access_token_lifespan - access_token_strategy: access_token_strategy jwks_uri: jwks_uri - request_object_signing_alg: request_object_signing_alg - tos_uri: tos_uri contacts: - contacts - contacts - response_types: - - response_types - - response_types login_session_id: login_session_id requested_scope: - requested_scope @@ -3149,6 +3354,10 @@ components: context: title: "JSONRawMessage represents a json.RawMessage that works well with\ \ JSON, SQL, and Swagger." + device_challenge_id: + description: "DeviceChallenge is the device challenge this consent challenge\ + \ belongs to, if this flow was initiated by a device." + type: string login_challenge: description: |- LoginChallenge is the login challenge this consent challenge belongs to. It can be used to associate @@ -3285,6 +3494,7 @@ components: - acr_values - acr_values display: display + device_challenge_id: device_challenge_id skip: true request_url: request_url acr: acr @@ -3292,47 +3502,28 @@ components: challenge: challenge client: metadata: "" - token_endpoint_auth_signing_alg: token_endpoint_auth_signing_alg - client_uri: client_uri - jwt_bearer_grant_access_token_lifespan: jwt_bearer_grant_access_token_lifespan - jwks: "" logo_uri: logo_uri - created_at: 2000-01-23T04:56:07.000+00:00 - registration_client_uri: registration_client_uri allowed_cors_origins: - allowed_cors_origins - allowed_cors_origins refresh_token_grant_access_token_lifespan: refresh_token_grant_access_token_lifespan - registration_access_token: registration_access_token client_id: client_id - token_endpoint_auth_method: client_secret_basic - userinfo_signed_response_alg: userinfo_signed_response_alg - authorization_code_grant_id_token_lifespan: authorization_code_grant_id_token_lifespan authorization_code_grant_refresh_token_lifespan: authorization_code_grant_refresh_token_lifespan client_credentials_grant_access_token_lifespan: client_credentials_grant_access_token_lifespan - updated_at: 2000-01-23T04:56:07.000+00:00 - scope: scope1 scope-2 scope.3 scope:4 request_uris: - request_uris - request_uris client_secret: client_secret backchannel_logout_session_required: true backchannel_logout_uri: backchannel_logout_uri - client_name: client_name - policy_uri: policy_uri - owner: owner - skip_consent: true audience: - audience - audience - authorization_code_grant_access_token_lifespan: authorization_code_grant_access_token_lifespan post_logout_redirect_uris: - post_logout_redirect_uris - post_logout_redirect_uris - grant_types: - - grant_types - - grant_types - subject_type: subject_type + device_authorization_grant_id_token_lifespan: device_authorization_grant_id_token_lifespan + device_authorization_grant_access_token_lifespan: device_authorization_grant_access_token_lifespan refresh_token_grant_refresh_token_lifespan: refresh_token_grant_refresh_token_lifespan redirect_uris: - redirect_uris @@ -3340,21 +3531,43 @@ components: sector_identifier_uri: sector_identifier_uri frontchannel_logout_session_required: true frontchannel_logout_uri: frontchannel_logout_uri - skip_logout_consent: true refresh_token_grant_id_token_lifespan: refresh_token_grant_id_token_lifespan + access_token_strategy: access_token_strategy + request_object_signing_alg: request_object_signing_alg + tos_uri: tos_uri + response_types: + - response_types + - response_types + token_endpoint_auth_signing_alg: token_endpoint_auth_signing_alg + client_uri: client_uri + jwt_bearer_grant_access_token_lifespan: jwt_bearer_grant_access_token_lifespan + jwks: "" + created_at: 2000-01-23T04:56:07.000+00:00 + registration_client_uri: registration_client_uri + registration_access_token: registration_access_token + token_endpoint_auth_method: client_secret_basic + userinfo_signed_response_alg: userinfo_signed_response_alg + authorization_code_grant_id_token_lifespan: authorization_code_grant_id_token_lifespan + updated_at: 2000-01-23T04:56:07.000+00:00 + scope: scope1 scope-2 scope.3 scope:4 + device_authorization_grant_refresh_token_lifespan: device_authorization_grant_refresh_token_lifespan + client_name: client_name + policy_uri: policy_uri + owner: owner + skip_consent: true + authorization_code_grant_access_token_lifespan: authorization_code_grant_access_token_lifespan + grant_types: + - grant_types + - grant_types + subject_type: subject_type + skip_logout_consent: true implicit_grant_id_token_lifespan: implicit_grant_id_token_lifespan client_secret_expires_at: 0 implicit_grant_access_token_lifespan: implicit_grant_access_token_lifespan - access_token_strategy: access_token_strategy jwks_uri: jwks_uri - request_object_signing_alg: request_object_signing_alg - tos_uri: tos_uri contacts: - contacts - contacts - response_types: - - response_types - - response_types login_session_id: login_session_id requested_scope: - requested_scope @@ -3449,47 +3662,28 @@ components: challenge: challenge client: metadata: "" - token_endpoint_auth_signing_alg: token_endpoint_auth_signing_alg - client_uri: client_uri - jwt_bearer_grant_access_token_lifespan: jwt_bearer_grant_access_token_lifespan - jwks: "" logo_uri: logo_uri - created_at: 2000-01-23T04:56:07.000+00:00 - registration_client_uri: registration_client_uri allowed_cors_origins: - allowed_cors_origins - allowed_cors_origins refresh_token_grant_access_token_lifespan: refresh_token_grant_access_token_lifespan - registration_access_token: registration_access_token client_id: client_id - token_endpoint_auth_method: client_secret_basic - userinfo_signed_response_alg: userinfo_signed_response_alg - authorization_code_grant_id_token_lifespan: authorization_code_grant_id_token_lifespan authorization_code_grant_refresh_token_lifespan: authorization_code_grant_refresh_token_lifespan client_credentials_grant_access_token_lifespan: client_credentials_grant_access_token_lifespan - updated_at: 2000-01-23T04:56:07.000+00:00 - scope: scope1 scope-2 scope.3 scope:4 request_uris: - request_uris - request_uris client_secret: client_secret backchannel_logout_session_required: true backchannel_logout_uri: backchannel_logout_uri - client_name: client_name - policy_uri: policy_uri - owner: owner - skip_consent: true audience: - audience - audience - authorization_code_grant_access_token_lifespan: authorization_code_grant_access_token_lifespan post_logout_redirect_uris: - post_logout_redirect_uris - post_logout_redirect_uris - grant_types: - - grant_types - - grant_types - subject_type: subject_type + device_authorization_grant_id_token_lifespan: device_authorization_grant_id_token_lifespan + device_authorization_grant_access_token_lifespan: device_authorization_grant_access_token_lifespan refresh_token_grant_refresh_token_lifespan: refresh_token_grant_refresh_token_lifespan redirect_uris: - redirect_uris @@ -3497,21 +3691,43 @@ components: sector_identifier_uri: sector_identifier_uri frontchannel_logout_session_required: true frontchannel_logout_uri: frontchannel_logout_uri - skip_logout_consent: true refresh_token_grant_id_token_lifespan: refresh_token_grant_id_token_lifespan + access_token_strategy: access_token_strategy + request_object_signing_alg: request_object_signing_alg + tos_uri: tos_uri + response_types: + - response_types + - response_types + token_endpoint_auth_signing_alg: token_endpoint_auth_signing_alg + client_uri: client_uri + jwt_bearer_grant_access_token_lifespan: jwt_bearer_grant_access_token_lifespan + jwks: "" + created_at: 2000-01-23T04:56:07.000+00:00 + registration_client_uri: registration_client_uri + registration_access_token: registration_access_token + token_endpoint_auth_method: client_secret_basic + userinfo_signed_response_alg: userinfo_signed_response_alg + authorization_code_grant_id_token_lifespan: authorization_code_grant_id_token_lifespan + updated_at: 2000-01-23T04:56:07.000+00:00 + scope: scope1 scope-2 scope.3 scope:4 + device_authorization_grant_refresh_token_lifespan: device_authorization_grant_refresh_token_lifespan + client_name: client_name + policy_uri: policy_uri + owner: owner + skip_consent: true + authorization_code_grant_access_token_lifespan: authorization_code_grant_access_token_lifespan + grant_types: + - grant_types + - grant_types + subject_type: subject_type + skip_logout_consent: true implicit_grant_id_token_lifespan: implicit_grant_id_token_lifespan client_secret_expires_at: 0 implicit_grant_access_token_lifespan: implicit_grant_access_token_lifespan - access_token_strategy: access_token_strategy jwks_uri: jwks_uri - request_object_signing_alg: request_object_signing_alg - tos_uri: tos_uri contacts: - contacts - contacts - response_types: - - response_types - - response_types session_id: session_id skip: true request_url: request_url @@ -3579,47 +3795,28 @@ components: challenge: challenge client: metadata: "" - token_endpoint_auth_signing_alg: token_endpoint_auth_signing_alg - client_uri: client_uri - jwt_bearer_grant_access_token_lifespan: jwt_bearer_grant_access_token_lifespan - jwks: "" logo_uri: logo_uri - created_at: 2000-01-23T04:56:07.000+00:00 - registration_client_uri: registration_client_uri allowed_cors_origins: - allowed_cors_origins - allowed_cors_origins refresh_token_grant_access_token_lifespan: refresh_token_grant_access_token_lifespan - registration_access_token: registration_access_token client_id: client_id - token_endpoint_auth_method: client_secret_basic - userinfo_signed_response_alg: userinfo_signed_response_alg - authorization_code_grant_id_token_lifespan: authorization_code_grant_id_token_lifespan authorization_code_grant_refresh_token_lifespan: authorization_code_grant_refresh_token_lifespan client_credentials_grant_access_token_lifespan: client_credentials_grant_access_token_lifespan - updated_at: 2000-01-23T04:56:07.000+00:00 - scope: scope1 scope-2 scope.3 scope:4 request_uris: - request_uris - request_uris client_secret: client_secret backchannel_logout_session_required: true backchannel_logout_uri: backchannel_logout_uri - client_name: client_name - policy_uri: policy_uri - owner: owner - skip_consent: true audience: - audience - audience - authorization_code_grant_access_token_lifespan: authorization_code_grant_access_token_lifespan post_logout_redirect_uris: - post_logout_redirect_uris - post_logout_redirect_uris - grant_types: - - grant_types - - grant_types - subject_type: subject_type + device_authorization_grant_id_token_lifespan: device_authorization_grant_id_token_lifespan + device_authorization_grant_access_token_lifespan: device_authorization_grant_access_token_lifespan refresh_token_grant_refresh_token_lifespan: refresh_token_grant_refresh_token_lifespan redirect_uris: - redirect_uris @@ -3627,21 +3824,43 @@ components: sector_identifier_uri: sector_identifier_uri frontchannel_logout_session_required: true frontchannel_logout_uri: frontchannel_logout_uri - skip_logout_consent: true refresh_token_grant_id_token_lifespan: refresh_token_grant_id_token_lifespan + access_token_strategy: access_token_strategy + request_object_signing_alg: request_object_signing_alg + tos_uri: tos_uri + response_types: + - response_types + - response_types + token_endpoint_auth_signing_alg: token_endpoint_auth_signing_alg + client_uri: client_uri + jwt_bearer_grant_access_token_lifespan: jwt_bearer_grant_access_token_lifespan + jwks: "" + created_at: 2000-01-23T04:56:07.000+00:00 + registration_client_uri: registration_client_uri + registration_access_token: registration_access_token + token_endpoint_auth_method: client_secret_basic + userinfo_signed_response_alg: userinfo_signed_response_alg + authorization_code_grant_id_token_lifespan: authorization_code_grant_id_token_lifespan + updated_at: 2000-01-23T04:56:07.000+00:00 + scope: scope1 scope-2 scope.3 scope:4 + device_authorization_grant_refresh_token_lifespan: device_authorization_grant_refresh_token_lifespan + client_name: client_name + policy_uri: policy_uri + owner: owner + skip_consent: true + authorization_code_grant_access_token_lifespan: authorization_code_grant_access_token_lifespan + grant_types: + - grant_types + - grant_types + subject_type: subject_type + skip_logout_consent: true implicit_grant_id_token_lifespan: implicit_grant_id_token_lifespan client_secret_expires_at: 0 implicit_grant_access_token_lifespan: implicit_grant_access_token_lifespan - access_token_strategy: access_token_strategy jwks_uri: jwks_uri - request_object_signing_alg: request_object_signing_alg - tos_uri: tos_uri contacts: - contacts - contacts - response_types: - - response_types - - response_types rp_initiated: true request_url: request_url requested_at: 2000-01-23T04:56:07.000+00:00 @@ -3741,6 +3960,7 @@ components: - userinfo_signed_response_alg - userinfo_signed_response_alg authorization_endpoint: https://playground.ory.sh/ory-hydra/public/oauth2/auth + device_authorization_endpoint: https://playground.ory.sh/ory-hydra/public/oauth2/device/oauth claims_supported: - claims_supported - claims_supported @@ -3863,6 +4083,10 @@ components: items: $ref: '#/components/schemas/credentialSupportedDraft00' type: array + device_authorization_endpoint: + description: OAuth 2.0 Device Authorization Endpoint URL + example: https://playground.ory.sh/ory-hydra/public/oauth2/device/oauth + type: string end_session_endpoint: description: |- OpenID Connect End-Session Endpoint @@ -4042,6 +4266,7 @@ components: type: array required: - authorization_endpoint + - device_authorization_endpoint - id_token_signed_response_alg - id_token_signing_alg_values_supported - issuer @@ -4509,6 +4734,39 @@ components: type: string title: VerifiableCredentialResponse contains the verifiable credential. type: object + verifyUserCodeRequest: + properties: + challenge: + description: |- + ID is the identifier ("device challenge") of the device request. It is used to + identify the session. + type: string + client: + $ref: '#/components/schemas/oAuth2Client' + device_code_request_id: + type: string + handled_at: + format: date-time + title: NullTime implements sql.NullTime functionality. + type: string + request_url: + description: RequestURL is the original Device Authorization URL requested. + type: string + requested_access_token_audience: + items: + type: string + title: "StringSliceJSONFormat represents []string{} which is encoded to/from\ + \ JSON for SQL storage." + type: array + requested_scope: + items: + type: string + title: "StringSliceJSONFormat represents []string{} which is encoded to/from\ + \ JSON for SQL storage." + type: array + title: HandledDeviceUserAuthRequest is the request payload used to accept a + device user_code. + type: object version: properties: version: diff --git a/internal/httpclient/api_o_auth2.go b/internal/httpclient/api_o_auth2.go index 867f4a2af49..e25afb3871c 100644 --- a/internal/httpclient/api_o_auth2.go +++ b/internal/httpclient/api_o_auth2.go @@ -423,6 +423,132 @@ func (a *OAuth2APIService) AcceptOAuth2LogoutRequestExecute(r ApiAcceptOAuth2Log return localVarReturnValue, localVarHTTPResponse, nil } +type ApiAcceptUserCodeRequestRequest struct { + ctx context.Context + ApiService *OAuth2APIService + deviceChallenge *string + acceptDeviceUserCodeRequest *AcceptDeviceUserCodeRequest +} + +func (r ApiAcceptUserCodeRequestRequest) DeviceChallenge(deviceChallenge string) ApiAcceptUserCodeRequestRequest { + r.deviceChallenge = &deviceChallenge + return r +} + +func (r ApiAcceptUserCodeRequestRequest) AcceptDeviceUserCodeRequest(acceptDeviceUserCodeRequest AcceptDeviceUserCodeRequest) ApiAcceptUserCodeRequestRequest { + r.acceptDeviceUserCodeRequest = &acceptDeviceUserCodeRequest + return r +} + +func (r ApiAcceptUserCodeRequestRequest) Execute() (*OAuth2RedirectTo, *http.Response, error) { + return r.ApiService.AcceptUserCodeRequestExecute(r) +} + +/* +AcceptUserCodeRequest Accepts a device grant user_code request + +Accepts a device grant user_code request + + @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + @return ApiAcceptUserCodeRequestRequest +*/ +func (a *OAuth2APIService) AcceptUserCodeRequest(ctx context.Context) ApiAcceptUserCodeRequestRequest { + return ApiAcceptUserCodeRequestRequest{ + ApiService: a, + ctx: ctx, + } +} + +// Execute executes the request +// +// @return OAuth2RedirectTo +func (a *OAuth2APIService) AcceptUserCodeRequestExecute(r ApiAcceptUserCodeRequestRequest) (*OAuth2RedirectTo, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodPut + localVarPostBody interface{} + formFiles []formFile + localVarReturnValue *OAuth2RedirectTo + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "OAuth2APIService.AcceptUserCodeRequest") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/admin/oauth2/auth/requests/device/accept" + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + if r.deviceChallenge == nil { + return localVarReturnValue, nil, reportError("deviceChallenge is required and must be specified") + } + + parameterAddToHeaderOrQuery(localVarQueryParams, "device_challenge", r.deviceChallenge, "") + // to determine the Content-Type header + localVarHTTPContentTypes := []string{"application/json"} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + // body params + localVarPostBody = r.acceptDeviceUserCodeRequest + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + var v ErrorOAuth2 + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} + type ApiCreateOAuth2ClientRequest struct { ctx context.Context ApiService *OAuth2APIService @@ -2196,6 +2322,117 @@ func (a *OAuth2APIService) OAuth2AuthorizeExecute(r ApiOAuth2AuthorizeRequest) ( return localVarReturnValue, localVarHTTPResponse, nil } +type ApiOAuth2DeviceFlowRequest struct { + ctx context.Context + ApiService *OAuth2APIService +} + +func (r ApiOAuth2DeviceFlowRequest) Execute() (*DeviceAuthorization, *http.Response, error) { + return r.ApiService.OAuth2DeviceFlowExecute(r) +} + +/* +OAuth2DeviceFlow The OAuth 2.0 Device Authorize Endpoint + +This endpoint is not documented here because you should never use your own implementation to perform OAuth2 flows. +OAuth2 is a very popular protocol and a library for your programming language will exists. + +To learn more about this flow please refer to the specification: https://tools.ietf.org/html/rfc8628 + + @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + @return ApiOAuth2DeviceFlowRequest +*/ +func (a *OAuth2APIService) OAuth2DeviceFlow(ctx context.Context) ApiOAuth2DeviceFlowRequest { + return ApiOAuth2DeviceFlowRequest{ + ApiService: a, + ctx: ctx, + } +} + +// Execute executes the request +// +// @return DeviceAuthorization +func (a *OAuth2APIService) OAuth2DeviceFlowExecute(r ApiOAuth2DeviceFlowRequest) (*DeviceAuthorization, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodPost + localVarPostBody interface{} + formFiles []formFile + localVarReturnValue *DeviceAuthorization + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "OAuth2APIService.OAuth2DeviceFlow") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/oauth2/device/auth" + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + var v ErrorOAuth2 + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} + type ApiOauth2TokenExchangeRequest struct { ctx context.Context ApiService *OAuth2APIService @@ -2494,6 +2731,114 @@ func (a *OAuth2APIService) PatchOAuth2ClientExecute(r ApiPatchOAuth2ClientReques return localVarReturnValue, localVarHTTPResponse, nil } +type ApiPerformOAuth2DeviceVerificationFlowRequest struct { + ctx context.Context + ApiService *OAuth2APIService +} + +func (r ApiPerformOAuth2DeviceVerificationFlowRequest) Execute() (*ErrorOAuth2, *http.Response, error) { + return r.ApiService.PerformOAuth2DeviceVerificationFlowExecute(r) +} + +/* +PerformOAuth2DeviceVerificationFlow OAuth 2.0 Device Verification Endpoint + +This is the device user verification endpoint. The user is redirected here when trying to login using the device flow. + + @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + @return ApiPerformOAuth2DeviceVerificationFlowRequest +*/ +func (a *OAuth2APIService) PerformOAuth2DeviceVerificationFlow(ctx context.Context) ApiPerformOAuth2DeviceVerificationFlowRequest { + return ApiPerformOAuth2DeviceVerificationFlowRequest{ + ApiService: a, + ctx: ctx, + } +} + +// Execute executes the request +// +// @return ErrorOAuth2 +func (a *OAuth2APIService) PerformOAuth2DeviceVerificationFlowExecute(r ApiPerformOAuth2DeviceVerificationFlowRequest) (*ErrorOAuth2, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodGet + localVarPostBody interface{} + formFiles []formFile + localVarReturnValue *ErrorOAuth2 + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "OAuth2APIService.PerformOAuth2DeviceVerificationFlow") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/oauth2/device/verify" + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + var v ErrorOAuth2 + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} + type ApiRejectOAuth2ConsentRequestRequest struct { ctx context.Context ApiService *OAuth2APIService diff --git a/internal/httpclient/docs/AcceptDeviceUserCodeRequest.md b/internal/httpclient/docs/AcceptDeviceUserCodeRequest.md new file mode 100644 index 00000000000..2f892922a77 --- /dev/null +++ b/internal/httpclient/docs/AcceptDeviceUserCodeRequest.md @@ -0,0 +1,56 @@ +# AcceptDeviceUserCodeRequest + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**UserCode** | Pointer to **string** | | [optional] + +## Methods + +### NewAcceptDeviceUserCodeRequest + +`func NewAcceptDeviceUserCodeRequest() *AcceptDeviceUserCodeRequest` + +NewAcceptDeviceUserCodeRequest instantiates a new AcceptDeviceUserCodeRequest object +This constructor will assign default values to properties that have it defined, +and makes sure properties required by API are set, but the set of arguments +will change when the set of required properties is changed + +### NewAcceptDeviceUserCodeRequestWithDefaults + +`func NewAcceptDeviceUserCodeRequestWithDefaults() *AcceptDeviceUserCodeRequest` + +NewAcceptDeviceUserCodeRequestWithDefaults instantiates a new AcceptDeviceUserCodeRequest object +This constructor will only assign default values to properties that have it defined, +but it doesn't guarantee that properties required by API are set + +### GetUserCode + +`func (o *AcceptDeviceUserCodeRequest) GetUserCode() string` + +GetUserCode returns the UserCode field if non-nil, zero value otherwise. + +### GetUserCodeOk + +`func (o *AcceptDeviceUserCodeRequest) GetUserCodeOk() (*string, bool)` + +GetUserCodeOk returns a tuple with the UserCode field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetUserCode + +`func (o *AcceptDeviceUserCodeRequest) SetUserCode(v string)` + +SetUserCode sets UserCode field to given value. + +### HasUserCode + +`func (o *AcceptDeviceUserCodeRequest) HasUserCode() bool` + +HasUserCode returns a boolean if a field has been set. + + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/internal/httpclient/docs/DeviceAuthorization.md b/internal/httpclient/docs/DeviceAuthorization.md new file mode 100644 index 00000000000..4ba933a4b24 --- /dev/null +++ b/internal/httpclient/docs/DeviceAuthorization.md @@ -0,0 +1,186 @@ +# DeviceAuthorization + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**DeviceCode** | Pointer to **string** | The device verification code. | [optional] +**ExpiresIn** | Pointer to **int64** | The lifetime in seconds of the \"device_code\" and \"user_code\". | [optional] +**Interval** | Pointer to **int64** | The minimum amount of time in seconds that the client SHOULD wait between polling requests to the token endpoint. If no value is provided, clients MUST use 5 as the default. | [optional] +**UserCode** | Pointer to **string** | The end-user verification code. | [optional] +**VerificationUri** | Pointer to **string** | The end-user verification URI on the authorization server. The URI should be short and easy to remember as end users will be asked to manually type it into their user agent. | [optional] +**VerificationUriComplete** | Pointer to **string** | A verification URI that includes the \"user_code\" (or other information with the same function as the \"user_code\"), which is designed for non-textual transmission. | [optional] + +## Methods + +### NewDeviceAuthorization + +`func NewDeviceAuthorization() *DeviceAuthorization` + +NewDeviceAuthorization instantiates a new DeviceAuthorization object +This constructor will assign default values to properties that have it defined, +and makes sure properties required by API are set, but the set of arguments +will change when the set of required properties is changed + +### NewDeviceAuthorizationWithDefaults + +`func NewDeviceAuthorizationWithDefaults() *DeviceAuthorization` + +NewDeviceAuthorizationWithDefaults instantiates a new DeviceAuthorization object +This constructor will only assign default values to properties that have it defined, +but it doesn't guarantee that properties required by API are set + +### GetDeviceCode + +`func (o *DeviceAuthorization) GetDeviceCode() string` + +GetDeviceCode returns the DeviceCode field if non-nil, zero value otherwise. + +### GetDeviceCodeOk + +`func (o *DeviceAuthorization) GetDeviceCodeOk() (*string, bool)` + +GetDeviceCodeOk returns a tuple with the DeviceCode field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetDeviceCode + +`func (o *DeviceAuthorization) SetDeviceCode(v string)` + +SetDeviceCode sets DeviceCode field to given value. + +### HasDeviceCode + +`func (o *DeviceAuthorization) HasDeviceCode() bool` + +HasDeviceCode returns a boolean if a field has been set. + +### GetExpiresIn + +`func (o *DeviceAuthorization) GetExpiresIn() int64` + +GetExpiresIn returns the ExpiresIn field if non-nil, zero value otherwise. + +### GetExpiresInOk + +`func (o *DeviceAuthorization) GetExpiresInOk() (*int64, bool)` + +GetExpiresInOk returns a tuple with the ExpiresIn field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetExpiresIn + +`func (o *DeviceAuthorization) SetExpiresIn(v int64)` + +SetExpiresIn sets ExpiresIn field to given value. + +### HasExpiresIn + +`func (o *DeviceAuthorization) HasExpiresIn() bool` + +HasExpiresIn returns a boolean if a field has been set. + +### GetInterval + +`func (o *DeviceAuthorization) GetInterval() int64` + +GetInterval returns the Interval field if non-nil, zero value otherwise. + +### GetIntervalOk + +`func (o *DeviceAuthorization) GetIntervalOk() (*int64, bool)` + +GetIntervalOk returns a tuple with the Interval field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetInterval + +`func (o *DeviceAuthorization) SetInterval(v int64)` + +SetInterval sets Interval field to given value. + +### HasInterval + +`func (o *DeviceAuthorization) HasInterval() bool` + +HasInterval returns a boolean if a field has been set. + +### GetUserCode + +`func (o *DeviceAuthorization) GetUserCode() string` + +GetUserCode returns the UserCode field if non-nil, zero value otherwise. + +### GetUserCodeOk + +`func (o *DeviceAuthorization) GetUserCodeOk() (*string, bool)` + +GetUserCodeOk returns a tuple with the UserCode field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetUserCode + +`func (o *DeviceAuthorization) SetUserCode(v string)` + +SetUserCode sets UserCode field to given value. + +### HasUserCode + +`func (o *DeviceAuthorization) HasUserCode() bool` + +HasUserCode returns a boolean if a field has been set. + +### GetVerificationUri + +`func (o *DeviceAuthorization) GetVerificationUri() string` + +GetVerificationUri returns the VerificationUri field if non-nil, zero value otherwise. + +### GetVerificationUriOk + +`func (o *DeviceAuthorization) GetVerificationUriOk() (*string, bool)` + +GetVerificationUriOk returns a tuple with the VerificationUri field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetVerificationUri + +`func (o *DeviceAuthorization) SetVerificationUri(v string)` + +SetVerificationUri sets VerificationUri field to given value. + +### HasVerificationUri + +`func (o *DeviceAuthorization) HasVerificationUri() bool` + +HasVerificationUri returns a boolean if a field has been set. + +### GetVerificationUriComplete + +`func (o *DeviceAuthorization) GetVerificationUriComplete() string` + +GetVerificationUriComplete returns the VerificationUriComplete field if non-nil, zero value otherwise. + +### GetVerificationUriCompleteOk + +`func (o *DeviceAuthorization) GetVerificationUriCompleteOk() (*string, bool)` + +GetVerificationUriCompleteOk returns a tuple with the VerificationUriComplete field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetVerificationUriComplete + +`func (o *DeviceAuthorization) SetVerificationUriComplete(v string)` + +SetVerificationUriComplete sets VerificationUriComplete field to given value. + +### HasVerificationUriComplete + +`func (o *DeviceAuthorization) HasVerificationUriComplete() bool` + +HasVerificationUriComplete returns a boolean if a field has been set. + + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/internal/httpclient/docs/DeviceUserAuthRequest.md b/internal/httpclient/docs/DeviceUserAuthRequest.md new file mode 100644 index 00000000000..ae99e6223ff --- /dev/null +++ b/internal/httpclient/docs/DeviceUserAuthRequest.md @@ -0,0 +1,181 @@ +# DeviceUserAuthRequest + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**Challenge** | **string** | ID is the identifier (\"device challenge\") of the device grant request. It is used to identify the session. | +**Client** | Pointer to [**OAuth2Client**](OAuth2Client.md) | | [optional] +**HandledAt** | Pointer to **time.Time** | | [optional] +**RequestUrl** | Pointer to **string** | RequestURL is the original Device Authorization URL requested. | [optional] +**RequestedAccessTokenAudience** | Pointer to **[]string** | | [optional] +**RequestedScope** | Pointer to **[]string** | | [optional] + +## Methods + +### NewDeviceUserAuthRequest + +`func NewDeviceUserAuthRequest(challenge string, ) *DeviceUserAuthRequest` + +NewDeviceUserAuthRequest instantiates a new DeviceUserAuthRequest object +This constructor will assign default values to properties that have it defined, +and makes sure properties required by API are set, but the set of arguments +will change when the set of required properties is changed + +### NewDeviceUserAuthRequestWithDefaults + +`func NewDeviceUserAuthRequestWithDefaults() *DeviceUserAuthRequest` + +NewDeviceUserAuthRequestWithDefaults instantiates a new DeviceUserAuthRequest object +This constructor will only assign default values to properties that have it defined, +but it doesn't guarantee that properties required by API are set + +### GetChallenge + +`func (o *DeviceUserAuthRequest) GetChallenge() string` + +GetChallenge returns the Challenge field if non-nil, zero value otherwise. + +### GetChallengeOk + +`func (o *DeviceUserAuthRequest) GetChallengeOk() (*string, bool)` + +GetChallengeOk returns a tuple with the Challenge field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetChallenge + +`func (o *DeviceUserAuthRequest) SetChallenge(v string)` + +SetChallenge sets Challenge field to given value. + + +### GetClient + +`func (o *DeviceUserAuthRequest) GetClient() OAuth2Client` + +GetClient returns the Client field if non-nil, zero value otherwise. + +### GetClientOk + +`func (o *DeviceUserAuthRequest) GetClientOk() (*OAuth2Client, bool)` + +GetClientOk returns a tuple with the Client field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetClient + +`func (o *DeviceUserAuthRequest) SetClient(v OAuth2Client)` + +SetClient sets Client field to given value. + +### HasClient + +`func (o *DeviceUserAuthRequest) HasClient() bool` + +HasClient returns a boolean if a field has been set. + +### GetHandledAt + +`func (o *DeviceUserAuthRequest) GetHandledAt() time.Time` + +GetHandledAt returns the HandledAt field if non-nil, zero value otherwise. + +### GetHandledAtOk + +`func (o *DeviceUserAuthRequest) GetHandledAtOk() (*time.Time, bool)` + +GetHandledAtOk returns a tuple with the HandledAt field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetHandledAt + +`func (o *DeviceUserAuthRequest) SetHandledAt(v time.Time)` + +SetHandledAt sets HandledAt field to given value. + +### HasHandledAt + +`func (o *DeviceUserAuthRequest) HasHandledAt() bool` + +HasHandledAt returns a boolean if a field has been set. + +### GetRequestUrl + +`func (o *DeviceUserAuthRequest) GetRequestUrl() string` + +GetRequestUrl returns the RequestUrl field if non-nil, zero value otherwise. + +### GetRequestUrlOk + +`func (o *DeviceUserAuthRequest) GetRequestUrlOk() (*string, bool)` + +GetRequestUrlOk returns a tuple with the RequestUrl field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetRequestUrl + +`func (o *DeviceUserAuthRequest) SetRequestUrl(v string)` + +SetRequestUrl sets RequestUrl field to given value. + +### HasRequestUrl + +`func (o *DeviceUserAuthRequest) HasRequestUrl() bool` + +HasRequestUrl returns a boolean if a field has been set. + +### GetRequestedAccessTokenAudience + +`func (o *DeviceUserAuthRequest) GetRequestedAccessTokenAudience() []string` + +GetRequestedAccessTokenAudience returns the RequestedAccessTokenAudience field if non-nil, zero value otherwise. + +### GetRequestedAccessTokenAudienceOk + +`func (o *DeviceUserAuthRequest) GetRequestedAccessTokenAudienceOk() (*[]string, bool)` + +GetRequestedAccessTokenAudienceOk returns a tuple with the RequestedAccessTokenAudience field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetRequestedAccessTokenAudience + +`func (o *DeviceUserAuthRequest) SetRequestedAccessTokenAudience(v []string)` + +SetRequestedAccessTokenAudience sets RequestedAccessTokenAudience field to given value. + +### HasRequestedAccessTokenAudience + +`func (o *DeviceUserAuthRequest) HasRequestedAccessTokenAudience() bool` + +HasRequestedAccessTokenAudience returns a boolean if a field has been set. + +### GetRequestedScope + +`func (o *DeviceUserAuthRequest) GetRequestedScope() []string` + +GetRequestedScope returns the RequestedScope field if non-nil, zero value otherwise. + +### GetRequestedScopeOk + +`func (o *DeviceUserAuthRequest) GetRequestedScopeOk() (*[]string, bool)` + +GetRequestedScopeOk returns a tuple with the RequestedScope field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetRequestedScope + +`func (o *DeviceUserAuthRequest) SetRequestedScope(v []string)` + +SetRequestedScope sets RequestedScope field to given value. + +### HasRequestedScope + +`func (o *DeviceUserAuthRequest) HasRequestedScope() bool` + +HasRequestedScope returns a boolean if a field has been set. + + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/internal/httpclient/docs/OAuth2API.md b/internal/httpclient/docs/OAuth2API.md index a9a8f672ccb..be2d8c97467 100644 --- a/internal/httpclient/docs/OAuth2API.md +++ b/internal/httpclient/docs/OAuth2API.md @@ -7,6 +7,7 @@ Method | HTTP request | Description [**AcceptOAuth2ConsentRequest**](OAuth2API.md#AcceptOAuth2ConsentRequest) | **Put** /admin/oauth2/auth/requests/consent/accept | Accept OAuth 2.0 Consent Request [**AcceptOAuth2LoginRequest**](OAuth2API.md#AcceptOAuth2LoginRequest) | **Put** /admin/oauth2/auth/requests/login/accept | Accept OAuth 2.0 Login Request [**AcceptOAuth2LogoutRequest**](OAuth2API.md#AcceptOAuth2LogoutRequest) | **Put** /admin/oauth2/auth/requests/logout/accept | Accept OAuth 2.0 Session Logout Request +[**AcceptUserCodeRequest**](OAuth2API.md#AcceptUserCodeRequest) | **Put** /admin/oauth2/auth/requests/device/accept | Accepts a device grant user_code request [**CreateOAuth2Client**](OAuth2API.md#CreateOAuth2Client) | **Post** /admin/clients | Create OAuth 2.0 Client [**DeleteOAuth2Client**](OAuth2API.md#DeleteOAuth2Client) | **Delete** /admin/clients/{id} | Delete OAuth 2.0 Client [**DeleteOAuth2Token**](OAuth2API.md#DeleteOAuth2Token) | **Delete** /admin/oauth2/tokens | Delete OAuth 2.0 Access Tokens from specific OAuth 2.0 Client @@ -21,8 +22,10 @@ Method | HTTP request | Description [**ListOAuth2ConsentSessions**](OAuth2API.md#ListOAuth2ConsentSessions) | **Get** /admin/oauth2/auth/sessions/consent | List OAuth 2.0 Consent Sessions of a Subject [**ListTrustedOAuth2JwtGrantIssuers**](OAuth2API.md#ListTrustedOAuth2JwtGrantIssuers) | **Get** /admin/trust/grants/jwt-bearer/issuers | List Trusted OAuth2 JWT Bearer Grant Type Issuers [**OAuth2Authorize**](OAuth2API.md#OAuth2Authorize) | **Get** /oauth2/auth | OAuth 2.0 Authorize Endpoint +[**OAuth2DeviceFlow**](OAuth2API.md#OAuth2DeviceFlow) | **Post** /oauth2/device/auth | The OAuth 2.0 Device Authorize Endpoint [**Oauth2TokenExchange**](OAuth2API.md#Oauth2TokenExchange) | **Post** /oauth2/token | The OAuth 2.0 Token Endpoint [**PatchOAuth2Client**](OAuth2API.md#PatchOAuth2Client) | **Patch** /admin/clients/{id} | Patch OAuth 2.0 Client +[**PerformOAuth2DeviceVerificationFlow**](OAuth2API.md#PerformOAuth2DeviceVerificationFlow) | **Get** /oauth2/device/verify | OAuth 2.0 Device Verification Endpoint [**RejectOAuth2ConsentRequest**](OAuth2API.md#RejectOAuth2ConsentRequest) | **Put** /admin/oauth2/auth/requests/consent/reject | Reject OAuth 2.0 Consent Request [**RejectOAuth2LoginRequest**](OAuth2API.md#RejectOAuth2LoginRequest) | **Put** /admin/oauth2/auth/requests/login/reject | Reject OAuth 2.0 Login Request [**RejectOAuth2LogoutRequest**](OAuth2API.md#RejectOAuth2LogoutRequest) | **Put** /admin/oauth2/auth/requests/logout/reject | Reject OAuth 2.0 Session Logout Request @@ -237,6 +240,74 @@ No authorization required [[Back to README]](../README.md) +## AcceptUserCodeRequest + +> OAuth2RedirectTo AcceptUserCodeRequest(ctx).DeviceChallenge(deviceChallenge).AcceptDeviceUserCodeRequest(acceptDeviceUserCodeRequest).Execute() + +Accepts a device grant user_code request + + + +### Example + +```go +package main + +import ( + "context" + "fmt" + "os" + openapiclient "github.com/ory/hydra-client-go/v2" +) + +func main() { + deviceChallenge := "deviceChallenge_example" // string | + acceptDeviceUserCodeRequest := *openapiclient.NewAcceptDeviceUserCodeRequest() // AcceptDeviceUserCodeRequest | (optional) + + configuration := openapiclient.NewConfiguration() + apiClient := openapiclient.NewAPIClient(configuration) + resp, r, err := apiClient.OAuth2API.AcceptUserCodeRequest(context.Background()).DeviceChallenge(deviceChallenge).AcceptDeviceUserCodeRequest(acceptDeviceUserCodeRequest).Execute() + if err != nil { + fmt.Fprintf(os.Stderr, "Error when calling `OAuth2API.AcceptUserCodeRequest``: %v\n", err) + fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r) + } + // response from `AcceptUserCodeRequest`: OAuth2RedirectTo + fmt.Fprintf(os.Stdout, "Response from `OAuth2API.AcceptUserCodeRequest`: %v\n", resp) +} +``` + +### Path Parameters + + + +### Other Parameters + +Other parameters are passed through a pointer to a apiAcceptUserCodeRequestRequest struct via the builder pattern + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **deviceChallenge** | **string** | | + **acceptDeviceUserCodeRequest** | [**AcceptDeviceUserCodeRequest**](AcceptDeviceUserCodeRequest.md) | | + +### Return type + +[**OAuth2RedirectTo**](OAuth2RedirectTo.md) + +### Authorization + +No authorization required + +### HTTP request headers + +- **Content-Type**: application/json +- **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) +[[Back to Model list]](../README.md#documentation-for-models) +[[Back to README]](../README.md) + + ## CreateOAuth2Client > OAuth2Client CreateOAuth2Client(ctx).OAuth2Client(oAuth2Client).Execute() @@ -1184,6 +1255,67 @@ No authorization required [[Back to README]](../README.md) +## OAuth2DeviceFlow + +> DeviceAuthorization OAuth2DeviceFlow(ctx).Execute() + +The OAuth 2.0 Device Authorize Endpoint + + + +### Example + +```go +package main + +import ( + "context" + "fmt" + "os" + openapiclient "github.com/ory/hydra-client-go/v2" +) + +func main() { + + configuration := openapiclient.NewConfiguration() + apiClient := openapiclient.NewAPIClient(configuration) + resp, r, err := apiClient.OAuth2API.OAuth2DeviceFlow(context.Background()).Execute() + if err != nil { + fmt.Fprintf(os.Stderr, "Error when calling `OAuth2API.OAuth2DeviceFlow``: %v\n", err) + fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r) + } + // response from `OAuth2DeviceFlow`: DeviceAuthorization + fmt.Fprintf(os.Stdout, "Response from `OAuth2API.OAuth2DeviceFlow`: %v\n", resp) +} +``` + +### Path Parameters + +This endpoint does not need any parameter. + +### Other Parameters + +Other parameters are passed through a pointer to a apiOAuth2DeviceFlowRequest struct via the builder pattern + + +### Return type + +[**DeviceAuthorization**](DeviceAuthorization.md) + +### Authorization + +No authorization required + +### HTTP request headers + +- **Content-Type**: Not defined +- **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) +[[Back to Model list]](../README.md#documentation-for-models) +[[Back to README]](../README.md) + + ## Oauth2TokenExchange > OAuth2TokenExchange Oauth2TokenExchange(ctx).GrantType(grantType).ClientId(clientId).Code(code).RedirectUri(redirectUri).RefreshToken(refreshToken).Execute() @@ -1330,6 +1462,67 @@ No authorization required [[Back to README]](../README.md) +## PerformOAuth2DeviceVerificationFlow + +> ErrorOAuth2 PerformOAuth2DeviceVerificationFlow(ctx).Execute() + +OAuth 2.0 Device Verification Endpoint + + + +### Example + +```go +package main + +import ( + "context" + "fmt" + "os" + openapiclient "github.com/ory/hydra-client-go/v2" +) + +func main() { + + configuration := openapiclient.NewConfiguration() + apiClient := openapiclient.NewAPIClient(configuration) + resp, r, err := apiClient.OAuth2API.PerformOAuth2DeviceVerificationFlow(context.Background()).Execute() + if err != nil { + fmt.Fprintf(os.Stderr, "Error when calling `OAuth2API.PerformOAuth2DeviceVerificationFlow``: %v\n", err) + fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r) + } + // response from `PerformOAuth2DeviceVerificationFlow`: ErrorOAuth2 + fmt.Fprintf(os.Stdout, "Response from `OAuth2API.PerformOAuth2DeviceVerificationFlow`: %v\n", resp) +} +``` + +### Path Parameters + +This endpoint does not need any parameter. + +### Other Parameters + +Other parameters are passed through a pointer to a apiPerformOAuth2DeviceVerificationFlowRequest struct via the builder pattern + + +### Return type + +[**ErrorOAuth2**](ErrorOAuth2.md) + +### Authorization + +No authorization required + +### HTTP request headers + +- **Content-Type**: Not defined +- **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) +[[Back to Model list]](../README.md#documentation-for-models) +[[Back to README]](../README.md) + + ## RejectOAuth2ConsentRequest > OAuth2RedirectTo RejectOAuth2ConsentRequest(ctx).ConsentChallenge(consentChallenge).RejectOAuth2Request(rejectOAuth2Request).Execute() diff --git a/internal/httpclient/docs/OAuth2Client.md b/internal/httpclient/docs/OAuth2Client.md index 9638bd221b3..7c62a2daf50 100644 --- a/internal/httpclient/docs/OAuth2Client.md +++ b/internal/httpclient/docs/OAuth2Client.md @@ -20,6 +20,9 @@ Name | Type | Description | Notes **ClientUri** | Pointer to **string** | OAuth 2.0 Client URI ClientURI is a URL string of a web page providing information about the client. If present, the server SHOULD display this URL to the end-user in a clickable fashion. | [optional] **Contacts** | Pointer to **[]string** | | [optional] **CreatedAt** | Pointer to **time.Time** | OAuth 2.0 Client Creation Date CreatedAt returns the timestamp of the client's creation. | [optional] +**DeviceAuthorizationGrantAccessTokenLifespan** | Pointer to **string** | Specify a time duration in milliseconds, seconds, minutes, hours. | [optional] +**DeviceAuthorizationGrantIdTokenLifespan** | Pointer to **string** | Specify a time duration in milliseconds, seconds, minutes, hours. | [optional] +**DeviceAuthorizationGrantRefreshTokenLifespan** | Pointer to **string** | Specify a time duration in milliseconds, seconds, minutes, hours. | [optional] **FrontchannelLogoutSessionRequired** | Pointer to **bool** | OpenID Connect Front-Channel Logout Session Required Boolean value specifying whether the RP requires that iss (issuer) and sid (session ID) query parameters be included to identify the RP session with the OP when the frontchannel_logout_uri is used. If omitted, the default value is false. | [optional] **FrontchannelLogoutUri** | Pointer to **string** | OpenID Connect Front-Channel Logout URI RP URL that will cause the RP to log itself out when rendered in an iframe by the OP. An iss (issuer) query parameter and a sid (session ID) query parameter MAY be included by the OP to enable the RP to validate the request and to determine which of the potentially multiple sessions is to be logged out; if either is included, both MUST be. | [optional] **GrantTypes** | Pointer to **[]string** | | [optional] @@ -472,6 +475,81 @@ SetCreatedAt sets CreatedAt field to given value. HasCreatedAt returns a boolean if a field has been set. +### GetDeviceAuthorizationGrantAccessTokenLifespan + +`func (o *OAuth2Client) GetDeviceAuthorizationGrantAccessTokenLifespan() string` + +GetDeviceAuthorizationGrantAccessTokenLifespan returns the DeviceAuthorizationGrantAccessTokenLifespan field if non-nil, zero value otherwise. + +### GetDeviceAuthorizationGrantAccessTokenLifespanOk + +`func (o *OAuth2Client) GetDeviceAuthorizationGrantAccessTokenLifespanOk() (*string, bool)` + +GetDeviceAuthorizationGrantAccessTokenLifespanOk returns a tuple with the DeviceAuthorizationGrantAccessTokenLifespan field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetDeviceAuthorizationGrantAccessTokenLifespan + +`func (o *OAuth2Client) SetDeviceAuthorizationGrantAccessTokenLifespan(v string)` + +SetDeviceAuthorizationGrantAccessTokenLifespan sets DeviceAuthorizationGrantAccessTokenLifespan field to given value. + +### HasDeviceAuthorizationGrantAccessTokenLifespan + +`func (o *OAuth2Client) HasDeviceAuthorizationGrantAccessTokenLifespan() bool` + +HasDeviceAuthorizationGrantAccessTokenLifespan returns a boolean if a field has been set. + +### GetDeviceAuthorizationGrantIdTokenLifespan + +`func (o *OAuth2Client) GetDeviceAuthorizationGrantIdTokenLifespan() string` + +GetDeviceAuthorizationGrantIdTokenLifespan returns the DeviceAuthorizationGrantIdTokenLifespan field if non-nil, zero value otherwise. + +### GetDeviceAuthorizationGrantIdTokenLifespanOk + +`func (o *OAuth2Client) GetDeviceAuthorizationGrantIdTokenLifespanOk() (*string, bool)` + +GetDeviceAuthorizationGrantIdTokenLifespanOk returns a tuple with the DeviceAuthorizationGrantIdTokenLifespan field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetDeviceAuthorizationGrantIdTokenLifespan + +`func (o *OAuth2Client) SetDeviceAuthorizationGrantIdTokenLifespan(v string)` + +SetDeviceAuthorizationGrantIdTokenLifespan sets DeviceAuthorizationGrantIdTokenLifespan field to given value. + +### HasDeviceAuthorizationGrantIdTokenLifespan + +`func (o *OAuth2Client) HasDeviceAuthorizationGrantIdTokenLifespan() bool` + +HasDeviceAuthorizationGrantIdTokenLifespan returns a boolean if a field has been set. + +### GetDeviceAuthorizationGrantRefreshTokenLifespan + +`func (o *OAuth2Client) GetDeviceAuthorizationGrantRefreshTokenLifespan() string` + +GetDeviceAuthorizationGrantRefreshTokenLifespan returns the DeviceAuthorizationGrantRefreshTokenLifespan field if non-nil, zero value otherwise. + +### GetDeviceAuthorizationGrantRefreshTokenLifespanOk + +`func (o *OAuth2Client) GetDeviceAuthorizationGrantRefreshTokenLifespanOk() (*string, bool)` + +GetDeviceAuthorizationGrantRefreshTokenLifespanOk returns a tuple with the DeviceAuthorizationGrantRefreshTokenLifespan field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetDeviceAuthorizationGrantRefreshTokenLifespan + +`func (o *OAuth2Client) SetDeviceAuthorizationGrantRefreshTokenLifespan(v string)` + +SetDeviceAuthorizationGrantRefreshTokenLifespan sets DeviceAuthorizationGrantRefreshTokenLifespan field to given value. + +### HasDeviceAuthorizationGrantRefreshTokenLifespan + +`func (o *OAuth2Client) HasDeviceAuthorizationGrantRefreshTokenLifespan() bool` + +HasDeviceAuthorizationGrantRefreshTokenLifespan returns a boolean if a field has been set. + ### GetFrontchannelLogoutSessionRequired `func (o *OAuth2Client) GetFrontchannelLogoutSessionRequired() bool` diff --git a/internal/httpclient/docs/OAuth2ClientTokenLifespans.md b/internal/httpclient/docs/OAuth2ClientTokenLifespans.md index cda6ca600ca..b38aef35d74 100644 --- a/internal/httpclient/docs/OAuth2ClientTokenLifespans.md +++ b/internal/httpclient/docs/OAuth2ClientTokenLifespans.md @@ -8,6 +8,9 @@ Name | Type | Description | Notes **AuthorizationCodeGrantIdTokenLifespan** | Pointer to **string** | Specify a time duration in milliseconds, seconds, minutes, hours. | [optional] **AuthorizationCodeGrantRefreshTokenLifespan** | Pointer to **string** | Specify a time duration in milliseconds, seconds, minutes, hours. | [optional] **ClientCredentialsGrantAccessTokenLifespan** | Pointer to **string** | Specify a time duration in milliseconds, seconds, minutes, hours. | [optional] +**DeviceAuthorizationGrantAccessTokenLifespan** | Pointer to **string** | Specify a time duration in milliseconds, seconds, minutes, hours. | [optional] +**DeviceAuthorizationGrantIdTokenLifespan** | Pointer to **string** | Specify a time duration in milliseconds, seconds, minutes, hours. | [optional] +**DeviceAuthorizationGrantRefreshTokenLifespan** | Pointer to **string** | Specify a time duration in milliseconds, seconds, minutes, hours. | [optional] **ImplicitGrantAccessTokenLifespan** | Pointer to **string** | Specify a time duration in milliseconds, seconds, minutes, hours. | [optional] **ImplicitGrantIdTokenLifespan** | Pointer to **string** | Specify a time duration in milliseconds, seconds, minutes, hours. | [optional] **JwtBearerGrantAccessTokenLifespan** | Pointer to **string** | Specify a time duration in milliseconds, seconds, minutes, hours. | [optional] @@ -134,6 +137,81 @@ SetClientCredentialsGrantAccessTokenLifespan sets ClientCredentialsGrantAccessTo HasClientCredentialsGrantAccessTokenLifespan returns a boolean if a field has been set. +### GetDeviceAuthorizationGrantAccessTokenLifespan + +`func (o *OAuth2ClientTokenLifespans) GetDeviceAuthorizationGrantAccessTokenLifespan() string` + +GetDeviceAuthorizationGrantAccessTokenLifespan returns the DeviceAuthorizationGrantAccessTokenLifespan field if non-nil, zero value otherwise. + +### GetDeviceAuthorizationGrantAccessTokenLifespanOk + +`func (o *OAuth2ClientTokenLifespans) GetDeviceAuthorizationGrantAccessTokenLifespanOk() (*string, bool)` + +GetDeviceAuthorizationGrantAccessTokenLifespanOk returns a tuple with the DeviceAuthorizationGrantAccessTokenLifespan field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetDeviceAuthorizationGrantAccessTokenLifespan + +`func (o *OAuth2ClientTokenLifespans) SetDeviceAuthorizationGrantAccessTokenLifespan(v string)` + +SetDeviceAuthorizationGrantAccessTokenLifespan sets DeviceAuthorizationGrantAccessTokenLifespan field to given value. + +### HasDeviceAuthorizationGrantAccessTokenLifespan + +`func (o *OAuth2ClientTokenLifespans) HasDeviceAuthorizationGrantAccessTokenLifespan() bool` + +HasDeviceAuthorizationGrantAccessTokenLifespan returns a boolean if a field has been set. + +### GetDeviceAuthorizationGrantIdTokenLifespan + +`func (o *OAuth2ClientTokenLifespans) GetDeviceAuthorizationGrantIdTokenLifespan() string` + +GetDeviceAuthorizationGrantIdTokenLifespan returns the DeviceAuthorizationGrantIdTokenLifespan field if non-nil, zero value otherwise. + +### GetDeviceAuthorizationGrantIdTokenLifespanOk + +`func (o *OAuth2ClientTokenLifespans) GetDeviceAuthorizationGrantIdTokenLifespanOk() (*string, bool)` + +GetDeviceAuthorizationGrantIdTokenLifespanOk returns a tuple with the DeviceAuthorizationGrantIdTokenLifespan field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetDeviceAuthorizationGrantIdTokenLifespan + +`func (o *OAuth2ClientTokenLifespans) SetDeviceAuthorizationGrantIdTokenLifespan(v string)` + +SetDeviceAuthorizationGrantIdTokenLifespan sets DeviceAuthorizationGrantIdTokenLifespan field to given value. + +### HasDeviceAuthorizationGrantIdTokenLifespan + +`func (o *OAuth2ClientTokenLifespans) HasDeviceAuthorizationGrantIdTokenLifespan() bool` + +HasDeviceAuthorizationGrantIdTokenLifespan returns a boolean if a field has been set. + +### GetDeviceAuthorizationGrantRefreshTokenLifespan + +`func (o *OAuth2ClientTokenLifespans) GetDeviceAuthorizationGrantRefreshTokenLifespan() string` + +GetDeviceAuthorizationGrantRefreshTokenLifespan returns the DeviceAuthorizationGrantRefreshTokenLifespan field if non-nil, zero value otherwise. + +### GetDeviceAuthorizationGrantRefreshTokenLifespanOk + +`func (o *OAuth2ClientTokenLifespans) GetDeviceAuthorizationGrantRefreshTokenLifespanOk() (*string, bool)` + +GetDeviceAuthorizationGrantRefreshTokenLifespanOk returns a tuple with the DeviceAuthorizationGrantRefreshTokenLifespan field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetDeviceAuthorizationGrantRefreshTokenLifespan + +`func (o *OAuth2ClientTokenLifespans) SetDeviceAuthorizationGrantRefreshTokenLifespan(v string)` + +SetDeviceAuthorizationGrantRefreshTokenLifespan sets DeviceAuthorizationGrantRefreshTokenLifespan field to given value. + +### HasDeviceAuthorizationGrantRefreshTokenLifespan + +`func (o *OAuth2ClientTokenLifespans) HasDeviceAuthorizationGrantRefreshTokenLifespan() bool` + +HasDeviceAuthorizationGrantRefreshTokenLifespan returns a boolean if a field has been set. + ### GetImplicitGrantAccessTokenLifespan `func (o *OAuth2ClientTokenLifespans) GetImplicitGrantAccessTokenLifespan() string` diff --git a/internal/httpclient/docs/OAuth2ConsentRequest.md b/internal/httpclient/docs/OAuth2ConsentRequest.md index 1197410e106..c067d5f4806 100644 --- a/internal/httpclient/docs/OAuth2ConsentRequest.md +++ b/internal/httpclient/docs/OAuth2ConsentRequest.md @@ -10,6 +10,7 @@ Name | Type | Description | Notes **Client** | Pointer to [**OAuth2Client**](OAuth2Client.md) | | [optional] **ConsentRequestId** | Pointer to **string** | ConsentRequestID is the ID of the consent request. | [optional] **Context** | Pointer to **interface{}** | | [optional] +**DeviceChallengeId** | Pointer to **string** | DeviceChallenge is the device challenge this consent challenge belongs to, if this flow was initiated by a device. | [optional] **LoginChallenge** | Pointer to **string** | LoginChallenge is the login challenge this consent challenge belongs to. It can be used to associate a login and consent request in the login & consent app. | [optional] **LoginSessionId** | Pointer to **string** | LoginSessionID is the login session ID. If the user-agent reuses a login session (via cookie / remember flag) this ID will remain the same. If the user-agent did not have an existing authentication session (e.g. remember is false) this will be a new random value. This value is used as the \"sid\" parameter in the ID Token and in OIDC Front-/Back- channel logout. It's value can generally be used to associate consecutive login requests by a certain user. | [optional] **OidcContext** | Pointer to [**OAuth2ConsentRequestOpenIDConnectContext**](OAuth2ConsentRequestOpenIDConnectContext.md) | | [optional] @@ -193,6 +194,31 @@ HasContext returns a boolean if a field has been set. `func (o *OAuth2ConsentRequest) UnsetContext()` UnsetContext ensures that no value is present for Context, not even an explicit nil +### GetDeviceChallengeId + +`func (o *OAuth2ConsentRequest) GetDeviceChallengeId() string` + +GetDeviceChallengeId returns the DeviceChallengeId field if non-nil, zero value otherwise. + +### GetDeviceChallengeIdOk + +`func (o *OAuth2ConsentRequest) GetDeviceChallengeIdOk() (*string, bool)` + +GetDeviceChallengeIdOk returns a tuple with the DeviceChallengeId field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetDeviceChallengeId + +`func (o *OAuth2ConsentRequest) SetDeviceChallengeId(v string)` + +SetDeviceChallengeId sets DeviceChallengeId field to given value. + +### HasDeviceChallengeId + +`func (o *OAuth2ConsentRequest) HasDeviceChallengeId() bool` + +HasDeviceChallengeId returns a boolean if a field has been set. + ### GetLoginChallenge `func (o *OAuth2ConsentRequest) GetLoginChallenge() string` diff --git a/internal/httpclient/docs/OidcConfiguration.md b/internal/httpclient/docs/OidcConfiguration.md index 1b20c7d8733..27f0134440c 100644 --- a/internal/httpclient/docs/OidcConfiguration.md +++ b/internal/httpclient/docs/OidcConfiguration.md @@ -12,6 +12,7 @@ Name | Type | Description | Notes **CodeChallengeMethodsSupported** | Pointer to **[]string** | OAuth 2.0 PKCE Supported Code Challenge Methods JSON array containing a list of Proof Key for Code Exchange (PKCE) [RFC7636] code challenge methods supported by this authorization server. | [optional] **CredentialsEndpointDraft00** | Pointer to **string** | OpenID Connect Verifiable Credentials Endpoint Contains the URL of the Verifiable Credentials Endpoint. | [optional] **CredentialsSupportedDraft00** | Pointer to [**[]CredentialSupportedDraft00**](CredentialSupportedDraft00.md) | OpenID Connect Verifiable Credentials Supported JSON array containing a list of the Verifiable Credentials supported by this authorization server. | [optional] +**DeviceAuthorizationEndpoint** | **string** | OAuth 2.0 Device Authorization Endpoint URL | **EndSessionEndpoint** | Pointer to **string** | OpenID Connect End-Session Endpoint URL at the OP to which an RP can perform a redirect to request that the End-User be logged out at the OP. | [optional] **FrontchannelLogoutSessionSupported** | Pointer to **bool** | OpenID Connect Front-Channel Logout Session Required Boolean value specifying whether the OP can pass iss (issuer) and sid (session ID) query parameters to identify the RP session with the OP when the frontchannel_logout_uri is used. If supported, the sid Claim is also included in ID Tokens issued by the OP. | [optional] **FrontchannelLogoutSupported** | Pointer to **bool** | OpenID Connect Front-Channel Logout Supported Boolean value specifying whether the OP supports HTTP-based logout, with true indicating support. | [optional] @@ -40,7 +41,7 @@ Name | Type | Description | Notes ### NewOidcConfiguration -`func NewOidcConfiguration(authorizationEndpoint string, idTokenSignedResponseAlg []string, idTokenSigningAlgValuesSupported []string, issuer string, jwksUri string, responseTypesSupported []string, subjectTypesSupported []string, tokenEndpoint string, userinfoSignedResponseAlg []string, ) *OidcConfiguration` +`func NewOidcConfiguration(authorizationEndpoint string, deviceAuthorizationEndpoint string, idTokenSignedResponseAlg []string, idTokenSigningAlgValuesSupported []string, issuer string, jwksUri string, responseTypesSupported []string, subjectTypesSupported []string, tokenEndpoint string, userinfoSignedResponseAlg []string, ) *OidcConfiguration` NewOidcConfiguration instantiates a new OidcConfiguration object This constructor will assign default values to properties that have it defined, @@ -250,6 +251,26 @@ SetCredentialsSupportedDraft00 sets CredentialsSupportedDraft00 field to given v HasCredentialsSupportedDraft00 returns a boolean if a field has been set. +### GetDeviceAuthorizationEndpoint + +`func (o *OidcConfiguration) GetDeviceAuthorizationEndpoint() string` + +GetDeviceAuthorizationEndpoint returns the DeviceAuthorizationEndpoint field if non-nil, zero value otherwise. + +### GetDeviceAuthorizationEndpointOk + +`func (o *OidcConfiguration) GetDeviceAuthorizationEndpointOk() (*string, bool)` + +GetDeviceAuthorizationEndpointOk returns a tuple with the DeviceAuthorizationEndpoint field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetDeviceAuthorizationEndpoint + +`func (o *OidcConfiguration) SetDeviceAuthorizationEndpoint(v string)` + +SetDeviceAuthorizationEndpoint sets DeviceAuthorizationEndpoint field to given value. + + ### GetEndSessionEndpoint `func (o *OidcConfiguration) GetEndSessionEndpoint() string` diff --git a/internal/httpclient/docs/VerifyUserCodeRequest.md b/internal/httpclient/docs/VerifyUserCodeRequest.md new file mode 100644 index 00000000000..09a2270ab44 --- /dev/null +++ b/internal/httpclient/docs/VerifyUserCodeRequest.md @@ -0,0 +1,212 @@ +# VerifyUserCodeRequest + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**Challenge** | Pointer to **string** | ID is the identifier (\"device challenge\") of the device request. It is used to identify the session. | [optional] +**Client** | Pointer to [**OAuth2Client**](OAuth2Client.md) | | [optional] +**DeviceCodeRequestId** | Pointer to **string** | | [optional] +**HandledAt** | Pointer to **time.Time** | | [optional] +**RequestUrl** | Pointer to **string** | RequestURL is the original Device Authorization URL requested. | [optional] +**RequestedAccessTokenAudience** | Pointer to **[]string** | | [optional] +**RequestedScope** | Pointer to **[]string** | | [optional] + +## Methods + +### NewVerifyUserCodeRequest + +`func NewVerifyUserCodeRequest() *VerifyUserCodeRequest` + +NewVerifyUserCodeRequest instantiates a new VerifyUserCodeRequest object +This constructor will assign default values to properties that have it defined, +and makes sure properties required by API are set, but the set of arguments +will change when the set of required properties is changed + +### NewVerifyUserCodeRequestWithDefaults + +`func NewVerifyUserCodeRequestWithDefaults() *VerifyUserCodeRequest` + +NewVerifyUserCodeRequestWithDefaults instantiates a new VerifyUserCodeRequest object +This constructor will only assign default values to properties that have it defined, +but it doesn't guarantee that properties required by API are set + +### GetChallenge + +`func (o *VerifyUserCodeRequest) GetChallenge() string` + +GetChallenge returns the Challenge field if non-nil, zero value otherwise. + +### GetChallengeOk + +`func (o *VerifyUserCodeRequest) GetChallengeOk() (*string, bool)` + +GetChallengeOk returns a tuple with the Challenge field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetChallenge + +`func (o *VerifyUserCodeRequest) SetChallenge(v string)` + +SetChallenge sets Challenge field to given value. + +### HasChallenge + +`func (o *VerifyUserCodeRequest) HasChallenge() bool` + +HasChallenge returns a boolean if a field has been set. + +### GetClient + +`func (o *VerifyUserCodeRequest) GetClient() OAuth2Client` + +GetClient returns the Client field if non-nil, zero value otherwise. + +### GetClientOk + +`func (o *VerifyUserCodeRequest) GetClientOk() (*OAuth2Client, bool)` + +GetClientOk returns a tuple with the Client field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetClient + +`func (o *VerifyUserCodeRequest) SetClient(v OAuth2Client)` + +SetClient sets Client field to given value. + +### HasClient + +`func (o *VerifyUserCodeRequest) HasClient() bool` + +HasClient returns a boolean if a field has been set. + +### GetDeviceCodeRequestId + +`func (o *VerifyUserCodeRequest) GetDeviceCodeRequestId() string` + +GetDeviceCodeRequestId returns the DeviceCodeRequestId field if non-nil, zero value otherwise. + +### GetDeviceCodeRequestIdOk + +`func (o *VerifyUserCodeRequest) GetDeviceCodeRequestIdOk() (*string, bool)` + +GetDeviceCodeRequestIdOk returns a tuple with the DeviceCodeRequestId field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetDeviceCodeRequestId + +`func (o *VerifyUserCodeRequest) SetDeviceCodeRequestId(v string)` + +SetDeviceCodeRequestId sets DeviceCodeRequestId field to given value. + +### HasDeviceCodeRequestId + +`func (o *VerifyUserCodeRequest) HasDeviceCodeRequestId() bool` + +HasDeviceCodeRequestId returns a boolean if a field has been set. + +### GetHandledAt + +`func (o *VerifyUserCodeRequest) GetHandledAt() time.Time` + +GetHandledAt returns the HandledAt field if non-nil, zero value otherwise. + +### GetHandledAtOk + +`func (o *VerifyUserCodeRequest) GetHandledAtOk() (*time.Time, bool)` + +GetHandledAtOk returns a tuple with the HandledAt field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetHandledAt + +`func (o *VerifyUserCodeRequest) SetHandledAt(v time.Time)` + +SetHandledAt sets HandledAt field to given value. + +### HasHandledAt + +`func (o *VerifyUserCodeRequest) HasHandledAt() bool` + +HasHandledAt returns a boolean if a field has been set. + +### GetRequestUrl + +`func (o *VerifyUserCodeRequest) GetRequestUrl() string` + +GetRequestUrl returns the RequestUrl field if non-nil, zero value otherwise. + +### GetRequestUrlOk + +`func (o *VerifyUserCodeRequest) GetRequestUrlOk() (*string, bool)` + +GetRequestUrlOk returns a tuple with the RequestUrl field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetRequestUrl + +`func (o *VerifyUserCodeRequest) SetRequestUrl(v string)` + +SetRequestUrl sets RequestUrl field to given value. + +### HasRequestUrl + +`func (o *VerifyUserCodeRequest) HasRequestUrl() bool` + +HasRequestUrl returns a boolean if a field has been set. + +### GetRequestedAccessTokenAudience + +`func (o *VerifyUserCodeRequest) GetRequestedAccessTokenAudience() []string` + +GetRequestedAccessTokenAudience returns the RequestedAccessTokenAudience field if non-nil, zero value otherwise. + +### GetRequestedAccessTokenAudienceOk + +`func (o *VerifyUserCodeRequest) GetRequestedAccessTokenAudienceOk() (*[]string, bool)` + +GetRequestedAccessTokenAudienceOk returns a tuple with the RequestedAccessTokenAudience field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetRequestedAccessTokenAudience + +`func (o *VerifyUserCodeRequest) SetRequestedAccessTokenAudience(v []string)` + +SetRequestedAccessTokenAudience sets RequestedAccessTokenAudience field to given value. + +### HasRequestedAccessTokenAudience + +`func (o *VerifyUserCodeRequest) HasRequestedAccessTokenAudience() bool` + +HasRequestedAccessTokenAudience returns a boolean if a field has been set. + +### GetRequestedScope + +`func (o *VerifyUserCodeRequest) GetRequestedScope() []string` + +GetRequestedScope returns the RequestedScope field if non-nil, zero value otherwise. + +### GetRequestedScopeOk + +`func (o *VerifyUserCodeRequest) GetRequestedScopeOk() (*[]string, bool)` + +GetRequestedScopeOk returns a tuple with the RequestedScope field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetRequestedScope + +`func (o *VerifyUserCodeRequest) SetRequestedScope(v []string)` + +SetRequestedScope sets RequestedScope field to given value. + +### HasRequestedScope + +`func (o *VerifyUserCodeRequest) HasRequestedScope() bool` + +HasRequestedScope returns a boolean if a field has been set. + + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/internal/httpclient/go.sum b/internal/httpclient/go.sum index 734252e6815..3dee6d68163 100644 --- a/internal/httpclient/go.sum +++ b/internal/httpclient/go.sum @@ -1,13 +1,360 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/internal/httpclient/model_accept_device_user_code_request.go b/internal/httpclient/model_accept_device_user_code_request.go new file mode 100644 index 00000000000..c34d1cd5045 --- /dev/null +++ b/internal/httpclient/model_accept_device_user_code_request.go @@ -0,0 +1,125 @@ +/* +Ory Hydra API + +Documentation for all of Ory Hydra's APIs. + +API version: +Contact: hi@ory.sh +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package openapi + +import ( + "encoding/json" +) + +// checks if the AcceptDeviceUserCodeRequest type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &AcceptDeviceUserCodeRequest{} + +// AcceptDeviceUserCodeRequest Contains information on an device verification +type AcceptDeviceUserCodeRequest struct { + UserCode *string `json:"user_code,omitempty"` +} + +// NewAcceptDeviceUserCodeRequest instantiates a new AcceptDeviceUserCodeRequest object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewAcceptDeviceUserCodeRequest() *AcceptDeviceUserCodeRequest { + this := AcceptDeviceUserCodeRequest{} + return &this +} + +// NewAcceptDeviceUserCodeRequestWithDefaults instantiates a new AcceptDeviceUserCodeRequest object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewAcceptDeviceUserCodeRequestWithDefaults() *AcceptDeviceUserCodeRequest { + this := AcceptDeviceUserCodeRequest{} + return &this +} + +// GetUserCode returns the UserCode field value if set, zero value otherwise. +func (o *AcceptDeviceUserCodeRequest) GetUserCode() string { + if o == nil || IsNil(o.UserCode) { + var ret string + return ret + } + return *o.UserCode +} + +// GetUserCodeOk returns a tuple with the UserCode field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *AcceptDeviceUserCodeRequest) GetUserCodeOk() (*string, bool) { + if o == nil || IsNil(o.UserCode) { + return nil, false + } + return o.UserCode, true +} + +// HasUserCode returns a boolean if a field has been set. +func (o *AcceptDeviceUserCodeRequest) HasUserCode() bool { + if o != nil && !IsNil(o.UserCode) { + return true + } + + return false +} + +// SetUserCode gets a reference to the given string and assigns it to the UserCode field. +func (o *AcceptDeviceUserCodeRequest) SetUserCode(v string) { + o.UserCode = &v +} + +func (o AcceptDeviceUserCodeRequest) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o AcceptDeviceUserCodeRequest) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.UserCode) { + toSerialize["user_code"] = o.UserCode + } + return toSerialize, nil +} + +type NullableAcceptDeviceUserCodeRequest struct { + value *AcceptDeviceUserCodeRequest + isSet bool +} + +func (v NullableAcceptDeviceUserCodeRequest) Get() *AcceptDeviceUserCodeRequest { + return v.value +} + +func (v *NullableAcceptDeviceUserCodeRequest) Set(val *AcceptDeviceUserCodeRequest) { + v.value = val + v.isSet = true +} + +func (v NullableAcceptDeviceUserCodeRequest) IsSet() bool { + return v.isSet +} + +func (v *NullableAcceptDeviceUserCodeRequest) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableAcceptDeviceUserCodeRequest(val *AcceptDeviceUserCodeRequest) *NullableAcceptDeviceUserCodeRequest { + return &NullableAcceptDeviceUserCodeRequest{value: val, isSet: true} +} + +func (v NullableAcceptDeviceUserCodeRequest) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableAcceptDeviceUserCodeRequest) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/model_device_authorization.go b/internal/httpclient/model_device_authorization.go new file mode 100644 index 00000000000..975972a8532 --- /dev/null +++ b/internal/httpclient/model_device_authorization.go @@ -0,0 +1,311 @@ +/* +Ory Hydra API + +Documentation for all of Ory Hydra's APIs. + +API version: +Contact: hi@ory.sh +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package openapi + +import ( + "encoding/json" +) + +// checks if the DeviceAuthorization type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &DeviceAuthorization{} + +// DeviceAuthorization # Ory's OAuth 2.0 Device Authorization API +type DeviceAuthorization struct { + // The device verification code. + DeviceCode *string `json:"device_code,omitempty"` + // The lifetime in seconds of the \"device_code\" and \"user_code\". + ExpiresIn *int64 `json:"expires_in,omitempty"` + // The minimum amount of time in seconds that the client SHOULD wait between polling requests to the token endpoint. If no value is provided, clients MUST use 5 as the default. + Interval *int64 `json:"interval,omitempty"` + // The end-user verification code. + UserCode *string `json:"user_code,omitempty"` + // The end-user verification URI on the authorization server. The URI should be short and easy to remember as end users will be asked to manually type it into their user agent. + VerificationUri *string `json:"verification_uri,omitempty"` + // A verification URI that includes the \"user_code\" (or other information with the same function as the \"user_code\"), which is designed for non-textual transmission. + VerificationUriComplete *string `json:"verification_uri_complete,omitempty"` +} + +// NewDeviceAuthorization instantiates a new DeviceAuthorization object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewDeviceAuthorization() *DeviceAuthorization { + this := DeviceAuthorization{} + return &this +} + +// NewDeviceAuthorizationWithDefaults instantiates a new DeviceAuthorization object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewDeviceAuthorizationWithDefaults() *DeviceAuthorization { + this := DeviceAuthorization{} + return &this +} + +// GetDeviceCode returns the DeviceCode field value if set, zero value otherwise. +func (o *DeviceAuthorization) GetDeviceCode() string { + if o == nil || IsNil(o.DeviceCode) { + var ret string + return ret + } + return *o.DeviceCode +} + +// GetDeviceCodeOk returns a tuple with the DeviceCode field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthorization) GetDeviceCodeOk() (*string, bool) { + if o == nil || IsNil(o.DeviceCode) { + return nil, false + } + return o.DeviceCode, true +} + +// HasDeviceCode returns a boolean if a field has been set. +func (o *DeviceAuthorization) HasDeviceCode() bool { + if o != nil && !IsNil(o.DeviceCode) { + return true + } + + return false +} + +// SetDeviceCode gets a reference to the given string and assigns it to the DeviceCode field. +func (o *DeviceAuthorization) SetDeviceCode(v string) { + o.DeviceCode = &v +} + +// GetExpiresIn returns the ExpiresIn field value if set, zero value otherwise. +func (o *DeviceAuthorization) GetExpiresIn() int64 { + if o == nil || IsNil(o.ExpiresIn) { + var ret int64 + return ret + } + return *o.ExpiresIn +} + +// GetExpiresInOk returns a tuple with the ExpiresIn field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthorization) GetExpiresInOk() (*int64, bool) { + if o == nil || IsNil(o.ExpiresIn) { + return nil, false + } + return o.ExpiresIn, true +} + +// HasExpiresIn returns a boolean if a field has been set. +func (o *DeviceAuthorization) HasExpiresIn() bool { + if o != nil && !IsNil(o.ExpiresIn) { + return true + } + + return false +} + +// SetExpiresIn gets a reference to the given int64 and assigns it to the ExpiresIn field. +func (o *DeviceAuthorization) SetExpiresIn(v int64) { + o.ExpiresIn = &v +} + +// GetInterval returns the Interval field value if set, zero value otherwise. +func (o *DeviceAuthorization) GetInterval() int64 { + if o == nil || IsNil(o.Interval) { + var ret int64 + return ret + } + return *o.Interval +} + +// GetIntervalOk returns a tuple with the Interval field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthorization) GetIntervalOk() (*int64, bool) { + if o == nil || IsNil(o.Interval) { + return nil, false + } + return o.Interval, true +} + +// HasInterval returns a boolean if a field has been set. +func (o *DeviceAuthorization) HasInterval() bool { + if o != nil && !IsNil(o.Interval) { + return true + } + + return false +} + +// SetInterval gets a reference to the given int64 and assigns it to the Interval field. +func (o *DeviceAuthorization) SetInterval(v int64) { + o.Interval = &v +} + +// GetUserCode returns the UserCode field value if set, zero value otherwise. +func (o *DeviceAuthorization) GetUserCode() string { + if o == nil || IsNil(o.UserCode) { + var ret string + return ret + } + return *o.UserCode +} + +// GetUserCodeOk returns a tuple with the UserCode field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthorization) GetUserCodeOk() (*string, bool) { + if o == nil || IsNil(o.UserCode) { + return nil, false + } + return o.UserCode, true +} + +// HasUserCode returns a boolean if a field has been set. +func (o *DeviceAuthorization) HasUserCode() bool { + if o != nil && !IsNil(o.UserCode) { + return true + } + + return false +} + +// SetUserCode gets a reference to the given string and assigns it to the UserCode field. +func (o *DeviceAuthorization) SetUserCode(v string) { + o.UserCode = &v +} + +// GetVerificationUri returns the VerificationUri field value if set, zero value otherwise. +func (o *DeviceAuthorization) GetVerificationUri() string { + if o == nil || IsNil(o.VerificationUri) { + var ret string + return ret + } + return *o.VerificationUri +} + +// GetVerificationUriOk returns a tuple with the VerificationUri field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthorization) GetVerificationUriOk() (*string, bool) { + if o == nil || IsNil(o.VerificationUri) { + return nil, false + } + return o.VerificationUri, true +} + +// HasVerificationUri returns a boolean if a field has been set. +func (o *DeviceAuthorization) HasVerificationUri() bool { + if o != nil && !IsNil(o.VerificationUri) { + return true + } + + return false +} + +// SetVerificationUri gets a reference to the given string and assigns it to the VerificationUri field. +func (o *DeviceAuthorization) SetVerificationUri(v string) { + o.VerificationUri = &v +} + +// GetVerificationUriComplete returns the VerificationUriComplete field value if set, zero value otherwise. +func (o *DeviceAuthorization) GetVerificationUriComplete() string { + if o == nil || IsNil(o.VerificationUriComplete) { + var ret string + return ret + } + return *o.VerificationUriComplete +} + +// GetVerificationUriCompleteOk returns a tuple with the VerificationUriComplete field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceAuthorization) GetVerificationUriCompleteOk() (*string, bool) { + if o == nil || IsNil(o.VerificationUriComplete) { + return nil, false + } + return o.VerificationUriComplete, true +} + +// HasVerificationUriComplete returns a boolean if a field has been set. +func (o *DeviceAuthorization) HasVerificationUriComplete() bool { + if o != nil && !IsNil(o.VerificationUriComplete) { + return true + } + + return false +} + +// SetVerificationUriComplete gets a reference to the given string and assigns it to the VerificationUriComplete field. +func (o *DeviceAuthorization) SetVerificationUriComplete(v string) { + o.VerificationUriComplete = &v +} + +func (o DeviceAuthorization) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o DeviceAuthorization) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.DeviceCode) { + toSerialize["device_code"] = o.DeviceCode + } + if !IsNil(o.ExpiresIn) { + toSerialize["expires_in"] = o.ExpiresIn + } + if !IsNil(o.Interval) { + toSerialize["interval"] = o.Interval + } + if !IsNil(o.UserCode) { + toSerialize["user_code"] = o.UserCode + } + if !IsNil(o.VerificationUri) { + toSerialize["verification_uri"] = o.VerificationUri + } + if !IsNil(o.VerificationUriComplete) { + toSerialize["verification_uri_complete"] = o.VerificationUriComplete + } + return toSerialize, nil +} + +type NullableDeviceAuthorization struct { + value *DeviceAuthorization + isSet bool +} + +func (v NullableDeviceAuthorization) Get() *DeviceAuthorization { + return v.value +} + +func (v *NullableDeviceAuthorization) Set(val *DeviceAuthorization) { + v.value = val + v.isSet = true +} + +func (v NullableDeviceAuthorization) IsSet() bool { + return v.isSet +} + +func (v *NullableDeviceAuthorization) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableDeviceAuthorization(val *DeviceAuthorization) *NullableDeviceAuthorization { + return &NullableDeviceAuthorization{value: val, isSet: true} +} + +func (v NullableDeviceAuthorization) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableDeviceAuthorization) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/model_device_user_auth_request.go b/internal/httpclient/model_device_user_auth_request.go new file mode 100644 index 00000000000..a101144b4a1 --- /dev/null +++ b/internal/httpclient/model_device_user_auth_request.go @@ -0,0 +1,340 @@ +/* +Ory Hydra API + +Documentation for all of Ory Hydra's APIs. + +API version: +Contact: hi@ory.sh +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package openapi + +import ( + "bytes" + "encoding/json" + "fmt" + "time" +) + +// checks if the DeviceUserAuthRequest type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &DeviceUserAuthRequest{} + +// DeviceUserAuthRequest struct for DeviceUserAuthRequest +type DeviceUserAuthRequest struct { + // ID is the identifier (\"device challenge\") of the device grant request. It is used to identify the session. + Challenge string `json:"challenge"` + Client *OAuth2Client `json:"client,omitempty"` + HandledAt *time.Time `json:"handled_at,omitempty"` + // RequestURL is the original Device Authorization URL requested. + RequestUrl *string `json:"request_url,omitempty"` + RequestedAccessTokenAudience []string `json:"requested_access_token_audience,omitempty"` + RequestedScope []string `json:"requested_scope,omitempty"` +} + +type _DeviceUserAuthRequest DeviceUserAuthRequest + +// NewDeviceUserAuthRequest instantiates a new DeviceUserAuthRequest object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewDeviceUserAuthRequest(challenge string) *DeviceUserAuthRequest { + this := DeviceUserAuthRequest{} + this.Challenge = challenge + return &this +} + +// NewDeviceUserAuthRequestWithDefaults instantiates a new DeviceUserAuthRequest object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewDeviceUserAuthRequestWithDefaults() *DeviceUserAuthRequest { + this := DeviceUserAuthRequest{} + return &this +} + +// GetChallenge returns the Challenge field value +func (o *DeviceUserAuthRequest) GetChallenge() string { + if o == nil { + var ret string + return ret + } + + return o.Challenge +} + +// GetChallengeOk returns a tuple with the Challenge field value +// and a boolean to check if the value has been set. +func (o *DeviceUserAuthRequest) GetChallengeOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Challenge, true +} + +// SetChallenge sets field value +func (o *DeviceUserAuthRequest) SetChallenge(v string) { + o.Challenge = v +} + +// GetClient returns the Client field value if set, zero value otherwise. +func (o *DeviceUserAuthRequest) GetClient() OAuth2Client { + if o == nil || IsNil(o.Client) { + var ret OAuth2Client + return ret + } + return *o.Client +} + +// GetClientOk returns a tuple with the Client field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceUserAuthRequest) GetClientOk() (*OAuth2Client, bool) { + if o == nil || IsNil(o.Client) { + return nil, false + } + return o.Client, true +} + +// HasClient returns a boolean if a field has been set. +func (o *DeviceUserAuthRequest) HasClient() bool { + if o != nil && !IsNil(o.Client) { + return true + } + + return false +} + +// SetClient gets a reference to the given OAuth2Client and assigns it to the Client field. +func (o *DeviceUserAuthRequest) SetClient(v OAuth2Client) { + o.Client = &v +} + +// GetHandledAt returns the HandledAt field value if set, zero value otherwise. +func (o *DeviceUserAuthRequest) GetHandledAt() time.Time { + if o == nil || IsNil(o.HandledAt) { + var ret time.Time + return ret + } + return *o.HandledAt +} + +// GetHandledAtOk returns a tuple with the HandledAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceUserAuthRequest) GetHandledAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.HandledAt) { + return nil, false + } + return o.HandledAt, true +} + +// HasHandledAt returns a boolean if a field has been set. +func (o *DeviceUserAuthRequest) HasHandledAt() bool { + if o != nil && !IsNil(o.HandledAt) { + return true + } + + return false +} + +// SetHandledAt gets a reference to the given time.Time and assigns it to the HandledAt field. +func (o *DeviceUserAuthRequest) SetHandledAt(v time.Time) { + o.HandledAt = &v +} + +// GetRequestUrl returns the RequestUrl field value if set, zero value otherwise. +func (o *DeviceUserAuthRequest) GetRequestUrl() string { + if o == nil || IsNil(o.RequestUrl) { + var ret string + return ret + } + return *o.RequestUrl +} + +// GetRequestUrlOk returns a tuple with the RequestUrl field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceUserAuthRequest) GetRequestUrlOk() (*string, bool) { + if o == nil || IsNil(o.RequestUrl) { + return nil, false + } + return o.RequestUrl, true +} + +// HasRequestUrl returns a boolean if a field has been set. +func (o *DeviceUserAuthRequest) HasRequestUrl() bool { + if o != nil && !IsNil(o.RequestUrl) { + return true + } + + return false +} + +// SetRequestUrl gets a reference to the given string and assigns it to the RequestUrl field. +func (o *DeviceUserAuthRequest) SetRequestUrl(v string) { + o.RequestUrl = &v +} + +// GetRequestedAccessTokenAudience returns the RequestedAccessTokenAudience field value if set, zero value otherwise. +func (o *DeviceUserAuthRequest) GetRequestedAccessTokenAudience() []string { + if o == nil || IsNil(o.RequestedAccessTokenAudience) { + var ret []string + return ret + } + return o.RequestedAccessTokenAudience +} + +// GetRequestedAccessTokenAudienceOk returns a tuple with the RequestedAccessTokenAudience field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceUserAuthRequest) GetRequestedAccessTokenAudienceOk() ([]string, bool) { + if o == nil || IsNil(o.RequestedAccessTokenAudience) { + return nil, false + } + return o.RequestedAccessTokenAudience, true +} + +// HasRequestedAccessTokenAudience returns a boolean if a field has been set. +func (o *DeviceUserAuthRequest) HasRequestedAccessTokenAudience() bool { + if o != nil && !IsNil(o.RequestedAccessTokenAudience) { + return true + } + + return false +} + +// SetRequestedAccessTokenAudience gets a reference to the given []string and assigns it to the RequestedAccessTokenAudience field. +func (o *DeviceUserAuthRequest) SetRequestedAccessTokenAudience(v []string) { + o.RequestedAccessTokenAudience = v +} + +// GetRequestedScope returns the RequestedScope field value if set, zero value otherwise. +func (o *DeviceUserAuthRequest) GetRequestedScope() []string { + if o == nil || IsNil(o.RequestedScope) { + var ret []string + return ret + } + return o.RequestedScope +} + +// GetRequestedScopeOk returns a tuple with the RequestedScope field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DeviceUserAuthRequest) GetRequestedScopeOk() ([]string, bool) { + if o == nil || IsNil(o.RequestedScope) { + return nil, false + } + return o.RequestedScope, true +} + +// HasRequestedScope returns a boolean if a field has been set. +func (o *DeviceUserAuthRequest) HasRequestedScope() bool { + if o != nil && !IsNil(o.RequestedScope) { + return true + } + + return false +} + +// SetRequestedScope gets a reference to the given []string and assigns it to the RequestedScope field. +func (o *DeviceUserAuthRequest) SetRequestedScope(v []string) { + o.RequestedScope = v +} + +func (o DeviceUserAuthRequest) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o DeviceUserAuthRequest) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["challenge"] = o.Challenge + if !IsNil(o.Client) { + toSerialize["client"] = o.Client + } + if !IsNil(o.HandledAt) { + toSerialize["handled_at"] = o.HandledAt + } + if !IsNil(o.RequestUrl) { + toSerialize["request_url"] = o.RequestUrl + } + if !IsNil(o.RequestedAccessTokenAudience) { + toSerialize["requested_access_token_audience"] = o.RequestedAccessTokenAudience + } + if !IsNil(o.RequestedScope) { + toSerialize["requested_scope"] = o.RequestedScope + } + return toSerialize, nil +} + +func (o *DeviceUserAuthRequest) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "challenge", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varDeviceUserAuthRequest := _DeviceUserAuthRequest{} + + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.DisallowUnknownFields() + err = decoder.Decode(&varDeviceUserAuthRequest) + + if err != nil { + return err + } + + *o = DeviceUserAuthRequest(varDeviceUserAuthRequest) + + return err +} + +type NullableDeviceUserAuthRequest struct { + value *DeviceUserAuthRequest + isSet bool +} + +func (v NullableDeviceUserAuthRequest) Get() *DeviceUserAuthRequest { + return v.value +} + +func (v *NullableDeviceUserAuthRequest) Set(val *DeviceUserAuthRequest) { + v.value = val + v.isSet = true +} + +func (v NullableDeviceUserAuthRequest) IsSet() bool { + return v.isSet +} + +func (v *NullableDeviceUserAuthRequest) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableDeviceUserAuthRequest(val *DeviceUserAuthRequest) *NullableDeviceUserAuthRequest { + return &NullableDeviceUserAuthRequest{value: val, isSet: true} +} + +func (v NullableDeviceUserAuthRequest) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableDeviceUserAuthRequest) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/model_o_auth2_client.go b/internal/httpclient/model_o_auth2_client.go index 0be981e6017..ddf0d733545 100644 --- a/internal/httpclient/model_o_auth2_client.go +++ b/internal/httpclient/model_o_auth2_client.go @@ -50,6 +50,12 @@ type OAuth2Client struct { Contacts []string `json:"contacts,omitempty"` // OAuth 2.0 Client Creation Date CreatedAt returns the timestamp of the client's creation. CreatedAt *time.Time `json:"created_at,omitempty"` + // Specify a time duration in milliseconds, seconds, minutes, hours. + DeviceAuthorizationGrantAccessTokenLifespan *string `json:"device_authorization_grant_access_token_lifespan,omitempty"` + // Specify a time duration in milliseconds, seconds, minutes, hours. + DeviceAuthorizationGrantIdTokenLifespan *string `json:"device_authorization_grant_id_token_lifespan,omitempty"` + // Specify a time duration in milliseconds, seconds, minutes, hours. + DeviceAuthorizationGrantRefreshTokenLifespan *string `json:"device_authorization_grant_refresh_token_lifespan,omitempty"` // OpenID Connect Front-Channel Logout Session Required Boolean value specifying whether the RP requires that iss (issuer) and sid (session ID) query parameters be included to identify the RP session with the OP when the frontchannel_logout_uri is used. If omitted, the default value is false. FrontchannelLogoutSessionRequired *bool `json:"frontchannel_logout_session_required,omitempty"` // OpenID Connect Front-Channel Logout URI RP URL that will cause the RP to log itself out when rendered in an iframe by the OP. An iss (issuer) query parameter and a sid (session ID) query parameter MAY be included by the OP to enable the RP to validate the request and to determine which of the potentially multiple sessions is to be logged out; if either is included, both MUST be. @@ -643,6 +649,102 @@ func (o *OAuth2Client) SetCreatedAt(v time.Time) { o.CreatedAt = &v } +// GetDeviceAuthorizationGrantAccessTokenLifespan returns the DeviceAuthorizationGrantAccessTokenLifespan field value if set, zero value otherwise. +func (o *OAuth2Client) GetDeviceAuthorizationGrantAccessTokenLifespan() string { + if o == nil || IsNil(o.DeviceAuthorizationGrantAccessTokenLifespan) { + var ret string + return ret + } + return *o.DeviceAuthorizationGrantAccessTokenLifespan +} + +// GetDeviceAuthorizationGrantAccessTokenLifespanOk returns a tuple with the DeviceAuthorizationGrantAccessTokenLifespan field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetDeviceAuthorizationGrantAccessTokenLifespanOk() (*string, bool) { + if o == nil || IsNil(o.DeviceAuthorizationGrantAccessTokenLifespan) { + return nil, false + } + return o.DeviceAuthorizationGrantAccessTokenLifespan, true +} + +// HasDeviceAuthorizationGrantAccessTokenLifespan returns a boolean if a field has been set. +func (o *OAuth2Client) HasDeviceAuthorizationGrantAccessTokenLifespan() bool { + if o != nil && !IsNil(o.DeviceAuthorizationGrantAccessTokenLifespan) { + return true + } + + return false +} + +// SetDeviceAuthorizationGrantAccessTokenLifespan gets a reference to the given string and assigns it to the DeviceAuthorizationGrantAccessTokenLifespan field. +func (o *OAuth2Client) SetDeviceAuthorizationGrantAccessTokenLifespan(v string) { + o.DeviceAuthorizationGrantAccessTokenLifespan = &v +} + +// GetDeviceAuthorizationGrantIdTokenLifespan returns the DeviceAuthorizationGrantIdTokenLifespan field value if set, zero value otherwise. +func (o *OAuth2Client) GetDeviceAuthorizationGrantIdTokenLifespan() string { + if o == nil || IsNil(o.DeviceAuthorizationGrantIdTokenLifespan) { + var ret string + return ret + } + return *o.DeviceAuthorizationGrantIdTokenLifespan +} + +// GetDeviceAuthorizationGrantIdTokenLifespanOk returns a tuple with the DeviceAuthorizationGrantIdTokenLifespan field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetDeviceAuthorizationGrantIdTokenLifespanOk() (*string, bool) { + if o == nil || IsNil(o.DeviceAuthorizationGrantIdTokenLifespan) { + return nil, false + } + return o.DeviceAuthorizationGrantIdTokenLifespan, true +} + +// HasDeviceAuthorizationGrantIdTokenLifespan returns a boolean if a field has been set. +func (o *OAuth2Client) HasDeviceAuthorizationGrantIdTokenLifespan() bool { + if o != nil && !IsNil(o.DeviceAuthorizationGrantIdTokenLifespan) { + return true + } + + return false +} + +// SetDeviceAuthorizationGrantIdTokenLifespan gets a reference to the given string and assigns it to the DeviceAuthorizationGrantIdTokenLifespan field. +func (o *OAuth2Client) SetDeviceAuthorizationGrantIdTokenLifespan(v string) { + o.DeviceAuthorizationGrantIdTokenLifespan = &v +} + +// GetDeviceAuthorizationGrantRefreshTokenLifespan returns the DeviceAuthorizationGrantRefreshTokenLifespan field value if set, zero value otherwise. +func (o *OAuth2Client) GetDeviceAuthorizationGrantRefreshTokenLifespan() string { + if o == nil || IsNil(o.DeviceAuthorizationGrantRefreshTokenLifespan) { + var ret string + return ret + } + return *o.DeviceAuthorizationGrantRefreshTokenLifespan +} + +// GetDeviceAuthorizationGrantRefreshTokenLifespanOk returns a tuple with the DeviceAuthorizationGrantRefreshTokenLifespan field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetDeviceAuthorizationGrantRefreshTokenLifespanOk() (*string, bool) { + if o == nil || IsNil(o.DeviceAuthorizationGrantRefreshTokenLifespan) { + return nil, false + } + return o.DeviceAuthorizationGrantRefreshTokenLifespan, true +} + +// HasDeviceAuthorizationGrantRefreshTokenLifespan returns a boolean if a field has been set. +func (o *OAuth2Client) HasDeviceAuthorizationGrantRefreshTokenLifespan() bool { + if o != nil && !IsNil(o.DeviceAuthorizationGrantRefreshTokenLifespan) { + return true + } + + return false +} + +// SetDeviceAuthorizationGrantRefreshTokenLifespan gets a reference to the given string and assigns it to the DeviceAuthorizationGrantRefreshTokenLifespan field. +func (o *OAuth2Client) SetDeviceAuthorizationGrantRefreshTokenLifespan(v string) { + o.DeviceAuthorizationGrantRefreshTokenLifespan = &v +} + // GetFrontchannelLogoutSessionRequired returns the FrontchannelLogoutSessionRequired field value if set, zero value otherwise. func (o *OAuth2Client) GetFrontchannelLogoutSessionRequired() bool { if o == nil || IsNil(o.FrontchannelLogoutSessionRequired) { @@ -1727,6 +1829,15 @@ func (o OAuth2Client) ToMap() (map[string]interface{}, error) { if !IsNil(o.CreatedAt) { toSerialize["created_at"] = o.CreatedAt } + if !IsNil(o.DeviceAuthorizationGrantAccessTokenLifespan) { + toSerialize["device_authorization_grant_access_token_lifespan"] = o.DeviceAuthorizationGrantAccessTokenLifespan + } + if !IsNil(o.DeviceAuthorizationGrantIdTokenLifespan) { + toSerialize["device_authorization_grant_id_token_lifespan"] = o.DeviceAuthorizationGrantIdTokenLifespan + } + if !IsNil(o.DeviceAuthorizationGrantRefreshTokenLifespan) { + toSerialize["device_authorization_grant_refresh_token_lifespan"] = o.DeviceAuthorizationGrantRefreshTokenLifespan + } if !IsNil(o.FrontchannelLogoutSessionRequired) { toSerialize["frontchannel_logout_session_required"] = o.FrontchannelLogoutSessionRequired } diff --git a/internal/httpclient/model_o_auth2_client_token_lifespans.go b/internal/httpclient/model_o_auth2_client_token_lifespans.go index 2ed10b8508c..16e925f679c 100644 --- a/internal/httpclient/model_o_auth2_client_token_lifespans.go +++ b/internal/httpclient/model_o_auth2_client_token_lifespans.go @@ -29,6 +29,12 @@ type OAuth2ClientTokenLifespans struct { // Specify a time duration in milliseconds, seconds, minutes, hours. ClientCredentialsGrantAccessTokenLifespan *string `json:"client_credentials_grant_access_token_lifespan,omitempty"` // Specify a time duration in milliseconds, seconds, minutes, hours. + DeviceAuthorizationGrantAccessTokenLifespan *string `json:"device_authorization_grant_access_token_lifespan,omitempty"` + // Specify a time duration in milliseconds, seconds, minutes, hours. + DeviceAuthorizationGrantIdTokenLifespan *string `json:"device_authorization_grant_id_token_lifespan,omitempty"` + // Specify a time duration in milliseconds, seconds, minutes, hours. + DeviceAuthorizationGrantRefreshTokenLifespan *string `json:"device_authorization_grant_refresh_token_lifespan,omitempty"` + // Specify a time duration in milliseconds, seconds, minutes, hours. ImplicitGrantAccessTokenLifespan *string `json:"implicit_grant_access_token_lifespan,omitempty"` // Specify a time duration in milliseconds, seconds, minutes, hours. ImplicitGrantIdTokenLifespan *string `json:"implicit_grant_id_token_lifespan,omitempty"` @@ -187,6 +193,102 @@ func (o *OAuth2ClientTokenLifespans) SetClientCredentialsGrantAccessTokenLifespa o.ClientCredentialsGrantAccessTokenLifespan = &v } +// GetDeviceAuthorizationGrantAccessTokenLifespan returns the DeviceAuthorizationGrantAccessTokenLifespan field value if set, zero value otherwise. +func (o *OAuth2ClientTokenLifespans) GetDeviceAuthorizationGrantAccessTokenLifespan() string { + if o == nil || IsNil(o.DeviceAuthorizationGrantAccessTokenLifespan) { + var ret string + return ret + } + return *o.DeviceAuthorizationGrantAccessTokenLifespan +} + +// GetDeviceAuthorizationGrantAccessTokenLifespanOk returns a tuple with the DeviceAuthorizationGrantAccessTokenLifespan field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2ClientTokenLifespans) GetDeviceAuthorizationGrantAccessTokenLifespanOk() (*string, bool) { + if o == nil || IsNil(o.DeviceAuthorizationGrantAccessTokenLifespan) { + return nil, false + } + return o.DeviceAuthorizationGrantAccessTokenLifespan, true +} + +// HasDeviceAuthorizationGrantAccessTokenLifespan returns a boolean if a field has been set. +func (o *OAuth2ClientTokenLifespans) HasDeviceAuthorizationGrantAccessTokenLifespan() bool { + if o != nil && !IsNil(o.DeviceAuthorizationGrantAccessTokenLifespan) { + return true + } + + return false +} + +// SetDeviceAuthorizationGrantAccessTokenLifespan gets a reference to the given string and assigns it to the DeviceAuthorizationGrantAccessTokenLifespan field. +func (o *OAuth2ClientTokenLifespans) SetDeviceAuthorizationGrantAccessTokenLifespan(v string) { + o.DeviceAuthorizationGrantAccessTokenLifespan = &v +} + +// GetDeviceAuthorizationGrantIdTokenLifespan returns the DeviceAuthorizationGrantIdTokenLifespan field value if set, zero value otherwise. +func (o *OAuth2ClientTokenLifespans) GetDeviceAuthorizationGrantIdTokenLifespan() string { + if o == nil || IsNil(o.DeviceAuthorizationGrantIdTokenLifespan) { + var ret string + return ret + } + return *o.DeviceAuthorizationGrantIdTokenLifespan +} + +// GetDeviceAuthorizationGrantIdTokenLifespanOk returns a tuple with the DeviceAuthorizationGrantIdTokenLifespan field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2ClientTokenLifespans) GetDeviceAuthorizationGrantIdTokenLifespanOk() (*string, bool) { + if o == nil || IsNil(o.DeviceAuthorizationGrantIdTokenLifespan) { + return nil, false + } + return o.DeviceAuthorizationGrantIdTokenLifespan, true +} + +// HasDeviceAuthorizationGrantIdTokenLifespan returns a boolean if a field has been set. +func (o *OAuth2ClientTokenLifespans) HasDeviceAuthorizationGrantIdTokenLifespan() bool { + if o != nil && !IsNil(o.DeviceAuthorizationGrantIdTokenLifespan) { + return true + } + + return false +} + +// SetDeviceAuthorizationGrantIdTokenLifespan gets a reference to the given string and assigns it to the DeviceAuthorizationGrantIdTokenLifespan field. +func (o *OAuth2ClientTokenLifespans) SetDeviceAuthorizationGrantIdTokenLifespan(v string) { + o.DeviceAuthorizationGrantIdTokenLifespan = &v +} + +// GetDeviceAuthorizationGrantRefreshTokenLifespan returns the DeviceAuthorizationGrantRefreshTokenLifespan field value if set, zero value otherwise. +func (o *OAuth2ClientTokenLifespans) GetDeviceAuthorizationGrantRefreshTokenLifespan() string { + if o == nil || IsNil(o.DeviceAuthorizationGrantRefreshTokenLifespan) { + var ret string + return ret + } + return *o.DeviceAuthorizationGrantRefreshTokenLifespan +} + +// GetDeviceAuthorizationGrantRefreshTokenLifespanOk returns a tuple with the DeviceAuthorizationGrantRefreshTokenLifespan field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2ClientTokenLifespans) GetDeviceAuthorizationGrantRefreshTokenLifespanOk() (*string, bool) { + if o == nil || IsNil(o.DeviceAuthorizationGrantRefreshTokenLifespan) { + return nil, false + } + return o.DeviceAuthorizationGrantRefreshTokenLifespan, true +} + +// HasDeviceAuthorizationGrantRefreshTokenLifespan returns a boolean if a field has been set. +func (o *OAuth2ClientTokenLifespans) HasDeviceAuthorizationGrantRefreshTokenLifespan() bool { + if o != nil && !IsNil(o.DeviceAuthorizationGrantRefreshTokenLifespan) { + return true + } + + return false +} + +// SetDeviceAuthorizationGrantRefreshTokenLifespan gets a reference to the given string and assigns it to the DeviceAuthorizationGrantRefreshTokenLifespan field. +func (o *OAuth2ClientTokenLifespans) SetDeviceAuthorizationGrantRefreshTokenLifespan(v string) { + o.DeviceAuthorizationGrantRefreshTokenLifespan = &v +} + // GetImplicitGrantAccessTokenLifespan returns the ImplicitGrantAccessTokenLifespan field value if set, zero value otherwise. func (o *OAuth2ClientTokenLifespans) GetImplicitGrantAccessTokenLifespan() string { if o == nil || IsNil(o.ImplicitGrantAccessTokenLifespan) { @@ -401,6 +503,15 @@ func (o OAuth2ClientTokenLifespans) ToMap() (map[string]interface{}, error) { if !IsNil(o.ClientCredentialsGrantAccessTokenLifespan) { toSerialize["client_credentials_grant_access_token_lifespan"] = o.ClientCredentialsGrantAccessTokenLifespan } + if !IsNil(o.DeviceAuthorizationGrantAccessTokenLifespan) { + toSerialize["device_authorization_grant_access_token_lifespan"] = o.DeviceAuthorizationGrantAccessTokenLifespan + } + if !IsNil(o.DeviceAuthorizationGrantIdTokenLifespan) { + toSerialize["device_authorization_grant_id_token_lifespan"] = o.DeviceAuthorizationGrantIdTokenLifespan + } + if !IsNil(o.DeviceAuthorizationGrantRefreshTokenLifespan) { + toSerialize["device_authorization_grant_refresh_token_lifespan"] = o.DeviceAuthorizationGrantRefreshTokenLifespan + } if !IsNil(o.ImplicitGrantAccessTokenLifespan) { toSerialize["implicit_grant_access_token_lifespan"] = o.ImplicitGrantAccessTokenLifespan } diff --git a/internal/httpclient/model_o_auth2_consent_request.go b/internal/httpclient/model_o_auth2_consent_request.go index 552c5c1e50f..8ff08a8c15e 100644 --- a/internal/httpclient/model_o_auth2_consent_request.go +++ b/internal/httpclient/model_o_auth2_consent_request.go @@ -31,6 +31,8 @@ type OAuth2ConsentRequest struct { // ConsentRequestID is the ID of the consent request. ConsentRequestId *string `json:"consent_request_id,omitempty"` Context interface{} `json:"context,omitempty"` + // DeviceChallenge is the device challenge this consent challenge belongs to, if this flow was initiated by a device. + DeviceChallengeId *string `json:"device_challenge_id,omitempty"` // LoginChallenge is the login challenge this consent challenge belongs to. It can be used to associate a login and consent request in the login & consent app. LoginChallenge *string `json:"login_challenge,omitempty"` // LoginSessionID is the login session ID. If the user-agent reuses a login session (via cookie / remember flag) this ID will remain the same. If the user-agent did not have an existing authentication session (e.g. remember is false) this will be a new random value. This value is used as the \"sid\" parameter in the ID Token and in OIDC Front-/Back- channel logout. It's value can generally be used to associate consecutive login requests by a certain user. @@ -251,6 +253,38 @@ func (o *OAuth2ConsentRequest) SetContext(v interface{}) { o.Context = v } +// GetDeviceChallengeId returns the DeviceChallengeId field value if set, zero value otherwise. +func (o *OAuth2ConsentRequest) GetDeviceChallengeId() string { + if o == nil || IsNil(o.DeviceChallengeId) { + var ret string + return ret + } + return *o.DeviceChallengeId +} + +// GetDeviceChallengeIdOk returns a tuple with the DeviceChallengeId field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2ConsentRequest) GetDeviceChallengeIdOk() (*string, bool) { + if o == nil || IsNil(o.DeviceChallengeId) { + return nil, false + } + return o.DeviceChallengeId, true +} + +// HasDeviceChallengeId returns a boolean if a field has been set. +func (o *OAuth2ConsentRequest) HasDeviceChallengeId() bool { + if o != nil && !IsNil(o.DeviceChallengeId) { + return true + } + + return false +} + +// SetDeviceChallengeId gets a reference to the given string and assigns it to the DeviceChallengeId field. +func (o *OAuth2ConsentRequest) SetDeviceChallengeId(v string) { + o.DeviceChallengeId = &v +} + // GetLoginChallenge returns the LoginChallenge field value if set, zero value otherwise. func (o *OAuth2ConsentRequest) GetLoginChallenge() string { if o == nil || IsNil(o.LoginChallenge) { @@ -533,6 +567,9 @@ func (o OAuth2ConsentRequest) ToMap() (map[string]interface{}, error) { if o.Context != nil { toSerialize["context"] = o.Context } + if !IsNil(o.DeviceChallengeId) { + toSerialize["device_challenge_id"] = o.DeviceChallengeId + } if !IsNil(o.LoginChallenge) { toSerialize["login_challenge"] = o.LoginChallenge } diff --git a/internal/httpclient/model_oidc_configuration.go b/internal/httpclient/model_oidc_configuration.go index 240e40b307f..465fa997f3f 100644 --- a/internal/httpclient/model_oidc_configuration.go +++ b/internal/httpclient/model_oidc_configuration.go @@ -38,6 +38,8 @@ type OidcConfiguration struct { CredentialsEndpointDraft00 *string `json:"credentials_endpoint_draft_00,omitempty"` // OpenID Connect Verifiable Credentials Supported JSON array containing a list of the Verifiable Credentials supported by this authorization server. CredentialsSupportedDraft00 []CredentialSupportedDraft00 `json:"credentials_supported_draft_00,omitempty"` + // OAuth 2.0 Device Authorization Endpoint URL + DeviceAuthorizationEndpoint string `json:"device_authorization_endpoint"` // OpenID Connect End-Session Endpoint URL at the OP to which an RP can perform a redirect to request that the End-User be logged out at the OP. EndSessionEndpoint *string `json:"end_session_endpoint,omitempty"` // OpenID Connect Front-Channel Logout Session Required Boolean value specifying whether the OP can pass iss (issuer) and sid (session ID) query parameters to identify the RP session with the OP when the frontchannel_logout_uri is used. If supported, the sid Claim is also included in ID Tokens issued by the OP. @@ -92,9 +94,10 @@ type _OidcConfiguration OidcConfiguration // This constructor will assign default values to properties that have it defined, // and makes sure properties required by API are set, but the set of arguments // will change when the set of required properties is changed -func NewOidcConfiguration(authorizationEndpoint string, idTokenSignedResponseAlg []string, idTokenSigningAlgValuesSupported []string, issuer string, jwksUri string, responseTypesSupported []string, subjectTypesSupported []string, tokenEndpoint string, userinfoSignedResponseAlg []string) *OidcConfiguration { +func NewOidcConfiguration(authorizationEndpoint string, deviceAuthorizationEndpoint string, idTokenSignedResponseAlg []string, idTokenSigningAlgValuesSupported []string, issuer string, jwksUri string, responseTypesSupported []string, subjectTypesSupported []string, tokenEndpoint string, userinfoSignedResponseAlg []string) *OidcConfiguration { this := OidcConfiguration{} this.AuthorizationEndpoint = authorizationEndpoint + this.DeviceAuthorizationEndpoint = deviceAuthorizationEndpoint this.IdTokenSignedResponseAlg = idTokenSignedResponseAlg this.IdTokenSigningAlgValuesSupported = idTokenSigningAlgValuesSupported this.Issuer = issuer @@ -362,6 +365,30 @@ func (o *OidcConfiguration) SetCredentialsSupportedDraft00(v []CredentialSupport o.CredentialsSupportedDraft00 = v } +// GetDeviceAuthorizationEndpoint returns the DeviceAuthorizationEndpoint field value +func (o *OidcConfiguration) GetDeviceAuthorizationEndpoint() string { + if o == nil { + var ret string + return ret + } + + return o.DeviceAuthorizationEndpoint +} + +// GetDeviceAuthorizationEndpointOk returns a tuple with the DeviceAuthorizationEndpoint field value +// and a boolean to check if the value has been set. +func (o *OidcConfiguration) GetDeviceAuthorizationEndpointOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.DeviceAuthorizationEndpoint, true +} + +// SetDeviceAuthorizationEndpoint sets field value +func (o *OidcConfiguration) SetDeviceAuthorizationEndpoint(v string) { + o.DeviceAuthorizationEndpoint = v +} + // GetEndSessionEndpoint returns the EndSessionEndpoint field value if set, zero value otherwise. func (o *OidcConfiguration) GetEndSessionEndpoint() string { if o == nil || IsNil(o.EndSessionEndpoint) { @@ -1066,6 +1093,7 @@ func (o OidcConfiguration) ToMap() (map[string]interface{}, error) { if !IsNil(o.CredentialsSupportedDraft00) { toSerialize["credentials_supported_draft_00"] = o.CredentialsSupportedDraft00 } + toSerialize["device_authorization_endpoint"] = o.DeviceAuthorizationEndpoint if !IsNil(o.EndSessionEndpoint) { toSerialize["end_session_endpoint"] = o.EndSessionEndpoint } @@ -1128,6 +1156,7 @@ func (o *OidcConfiguration) UnmarshalJSON(data []byte) (err error) { // that every required field exists as a key in the generic map. requiredProperties := []string{ "authorization_endpoint", + "device_authorization_endpoint", "id_token_signed_response_alg", "id_token_signing_alg_values_supported", "issuer", diff --git a/internal/httpclient/model_verify_user_code_request.go b/internal/httpclient/model_verify_user_code_request.go new file mode 100644 index 00000000000..692694e9040 --- /dev/null +++ b/internal/httpclient/model_verify_user_code_request.go @@ -0,0 +1,344 @@ +/* +Ory Hydra API + +Documentation for all of Ory Hydra's APIs. + +API version: +Contact: hi@ory.sh +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package openapi + +import ( + "encoding/json" + "time" +) + +// checks if the VerifyUserCodeRequest type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &VerifyUserCodeRequest{} + +// VerifyUserCodeRequest struct for VerifyUserCodeRequest +type VerifyUserCodeRequest struct { + // ID is the identifier (\"device challenge\") of the device request. It is used to identify the session. + Challenge *string `json:"challenge,omitempty"` + Client *OAuth2Client `json:"client,omitempty"` + DeviceCodeRequestId *string `json:"device_code_request_id,omitempty"` + HandledAt *time.Time `json:"handled_at,omitempty"` + // RequestURL is the original Device Authorization URL requested. + RequestUrl *string `json:"request_url,omitempty"` + RequestedAccessTokenAudience []string `json:"requested_access_token_audience,omitempty"` + RequestedScope []string `json:"requested_scope,omitempty"` +} + +// NewVerifyUserCodeRequest instantiates a new VerifyUserCodeRequest object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewVerifyUserCodeRequest() *VerifyUserCodeRequest { + this := VerifyUserCodeRequest{} + return &this +} + +// NewVerifyUserCodeRequestWithDefaults instantiates a new VerifyUserCodeRequest object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewVerifyUserCodeRequestWithDefaults() *VerifyUserCodeRequest { + this := VerifyUserCodeRequest{} + return &this +} + +// GetChallenge returns the Challenge field value if set, zero value otherwise. +func (o *VerifyUserCodeRequest) GetChallenge() string { + if o == nil || IsNil(o.Challenge) { + var ret string + return ret + } + return *o.Challenge +} + +// GetChallengeOk returns a tuple with the Challenge field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *VerifyUserCodeRequest) GetChallengeOk() (*string, bool) { + if o == nil || IsNil(o.Challenge) { + return nil, false + } + return o.Challenge, true +} + +// HasChallenge returns a boolean if a field has been set. +func (o *VerifyUserCodeRequest) HasChallenge() bool { + if o != nil && !IsNil(o.Challenge) { + return true + } + + return false +} + +// SetChallenge gets a reference to the given string and assigns it to the Challenge field. +func (o *VerifyUserCodeRequest) SetChallenge(v string) { + o.Challenge = &v +} + +// GetClient returns the Client field value if set, zero value otherwise. +func (o *VerifyUserCodeRequest) GetClient() OAuth2Client { + if o == nil || IsNil(o.Client) { + var ret OAuth2Client + return ret + } + return *o.Client +} + +// GetClientOk returns a tuple with the Client field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *VerifyUserCodeRequest) GetClientOk() (*OAuth2Client, bool) { + if o == nil || IsNil(o.Client) { + return nil, false + } + return o.Client, true +} + +// HasClient returns a boolean if a field has been set. +func (o *VerifyUserCodeRequest) HasClient() bool { + if o != nil && !IsNil(o.Client) { + return true + } + + return false +} + +// SetClient gets a reference to the given OAuth2Client and assigns it to the Client field. +func (o *VerifyUserCodeRequest) SetClient(v OAuth2Client) { + o.Client = &v +} + +// GetDeviceCodeRequestId returns the DeviceCodeRequestId field value if set, zero value otherwise. +func (o *VerifyUserCodeRequest) GetDeviceCodeRequestId() string { + if o == nil || IsNil(o.DeviceCodeRequestId) { + var ret string + return ret + } + return *o.DeviceCodeRequestId +} + +// GetDeviceCodeRequestIdOk returns a tuple with the DeviceCodeRequestId field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *VerifyUserCodeRequest) GetDeviceCodeRequestIdOk() (*string, bool) { + if o == nil || IsNil(o.DeviceCodeRequestId) { + return nil, false + } + return o.DeviceCodeRequestId, true +} + +// HasDeviceCodeRequestId returns a boolean if a field has been set. +func (o *VerifyUserCodeRequest) HasDeviceCodeRequestId() bool { + if o != nil && !IsNil(o.DeviceCodeRequestId) { + return true + } + + return false +} + +// SetDeviceCodeRequestId gets a reference to the given string and assigns it to the DeviceCodeRequestId field. +func (o *VerifyUserCodeRequest) SetDeviceCodeRequestId(v string) { + o.DeviceCodeRequestId = &v +} + +// GetHandledAt returns the HandledAt field value if set, zero value otherwise. +func (o *VerifyUserCodeRequest) GetHandledAt() time.Time { + if o == nil || IsNil(o.HandledAt) { + var ret time.Time + return ret + } + return *o.HandledAt +} + +// GetHandledAtOk returns a tuple with the HandledAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *VerifyUserCodeRequest) GetHandledAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.HandledAt) { + return nil, false + } + return o.HandledAt, true +} + +// HasHandledAt returns a boolean if a field has been set. +func (o *VerifyUserCodeRequest) HasHandledAt() bool { + if o != nil && !IsNil(o.HandledAt) { + return true + } + + return false +} + +// SetHandledAt gets a reference to the given time.Time and assigns it to the HandledAt field. +func (o *VerifyUserCodeRequest) SetHandledAt(v time.Time) { + o.HandledAt = &v +} + +// GetRequestUrl returns the RequestUrl field value if set, zero value otherwise. +func (o *VerifyUserCodeRequest) GetRequestUrl() string { + if o == nil || IsNil(o.RequestUrl) { + var ret string + return ret + } + return *o.RequestUrl +} + +// GetRequestUrlOk returns a tuple with the RequestUrl field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *VerifyUserCodeRequest) GetRequestUrlOk() (*string, bool) { + if o == nil || IsNil(o.RequestUrl) { + return nil, false + } + return o.RequestUrl, true +} + +// HasRequestUrl returns a boolean if a field has been set. +func (o *VerifyUserCodeRequest) HasRequestUrl() bool { + if o != nil && !IsNil(o.RequestUrl) { + return true + } + + return false +} + +// SetRequestUrl gets a reference to the given string and assigns it to the RequestUrl field. +func (o *VerifyUserCodeRequest) SetRequestUrl(v string) { + o.RequestUrl = &v +} + +// GetRequestedAccessTokenAudience returns the RequestedAccessTokenAudience field value if set, zero value otherwise. +func (o *VerifyUserCodeRequest) GetRequestedAccessTokenAudience() []string { + if o == nil || IsNil(o.RequestedAccessTokenAudience) { + var ret []string + return ret + } + return o.RequestedAccessTokenAudience +} + +// GetRequestedAccessTokenAudienceOk returns a tuple with the RequestedAccessTokenAudience field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *VerifyUserCodeRequest) GetRequestedAccessTokenAudienceOk() ([]string, bool) { + if o == nil || IsNil(o.RequestedAccessTokenAudience) { + return nil, false + } + return o.RequestedAccessTokenAudience, true +} + +// HasRequestedAccessTokenAudience returns a boolean if a field has been set. +func (o *VerifyUserCodeRequest) HasRequestedAccessTokenAudience() bool { + if o != nil && !IsNil(o.RequestedAccessTokenAudience) { + return true + } + + return false +} + +// SetRequestedAccessTokenAudience gets a reference to the given []string and assigns it to the RequestedAccessTokenAudience field. +func (o *VerifyUserCodeRequest) SetRequestedAccessTokenAudience(v []string) { + o.RequestedAccessTokenAudience = v +} + +// GetRequestedScope returns the RequestedScope field value if set, zero value otherwise. +func (o *VerifyUserCodeRequest) GetRequestedScope() []string { + if o == nil || IsNil(o.RequestedScope) { + var ret []string + return ret + } + return o.RequestedScope +} + +// GetRequestedScopeOk returns a tuple with the RequestedScope field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *VerifyUserCodeRequest) GetRequestedScopeOk() ([]string, bool) { + if o == nil || IsNil(o.RequestedScope) { + return nil, false + } + return o.RequestedScope, true +} + +// HasRequestedScope returns a boolean if a field has been set. +func (o *VerifyUserCodeRequest) HasRequestedScope() bool { + if o != nil && !IsNil(o.RequestedScope) { + return true + } + + return false +} + +// SetRequestedScope gets a reference to the given []string and assigns it to the RequestedScope field. +func (o *VerifyUserCodeRequest) SetRequestedScope(v []string) { + o.RequestedScope = v +} + +func (o VerifyUserCodeRequest) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o VerifyUserCodeRequest) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Challenge) { + toSerialize["challenge"] = o.Challenge + } + if !IsNil(o.Client) { + toSerialize["client"] = o.Client + } + if !IsNil(o.DeviceCodeRequestId) { + toSerialize["device_code_request_id"] = o.DeviceCodeRequestId + } + if !IsNil(o.HandledAt) { + toSerialize["handled_at"] = o.HandledAt + } + if !IsNil(o.RequestUrl) { + toSerialize["request_url"] = o.RequestUrl + } + if !IsNil(o.RequestedAccessTokenAudience) { + toSerialize["requested_access_token_audience"] = o.RequestedAccessTokenAudience + } + if !IsNil(o.RequestedScope) { + toSerialize["requested_scope"] = o.RequestedScope + } + return toSerialize, nil +} + +type NullableVerifyUserCodeRequest struct { + value *VerifyUserCodeRequest + isSet bool +} + +func (v NullableVerifyUserCodeRequest) Get() *VerifyUserCodeRequest { + return v.value +} + +func (v *NullableVerifyUserCodeRequest) Set(val *VerifyUserCodeRequest) { + v.value = val + v.isSet = true +} + +func (v NullableVerifyUserCodeRequest) IsSet() bool { + return v.isSet +} + +func (v *NullableVerifyUserCodeRequest) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableVerifyUserCodeRequest(val *VerifyUserCodeRequest) *NullableVerifyUserCodeRequest { + return &NullableVerifyUserCodeRequest{value: val, isSet: true} +} + +func (v NullableVerifyUserCodeRequest) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableVerifyUserCodeRequest) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/mock/config_cookie.go b/internal/mock/config_cookie.go index 5fab6d1d7dc..b326baec6b4 100644 --- a/internal/mock/config_cookie.go +++ b/internal/mock/config_cookie.go @@ -1,8 +1,8 @@ -// Copyright © 2022 Ory Corp +// Copyright © 2025 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/internal/testhelpers/lifespans.go b/internal/testhelpers/lifespans.go index 86477c90b09..e2ba8a218c4 100644 --- a/internal/testhelpers/lifespans.go +++ b/internal/testhelpers/lifespans.go @@ -11,16 +11,19 @@ import ( ) var TestLifespans = client.Lifespans{ - AuthorizationCodeGrantAccessTokenLifespan: x.NullDuration{Duration: 31 * time.Hour, Valid: true}, - AuthorizationCodeGrantIDTokenLifespan: x.NullDuration{Duration: 32 * time.Hour, Valid: true}, - AuthorizationCodeGrantRefreshTokenLifespan: x.NullDuration{Duration: 33 * time.Hour, Valid: true}, - ClientCredentialsGrantAccessTokenLifespan: x.NullDuration{Duration: 34 * time.Hour, Valid: true}, - ImplicitGrantAccessTokenLifespan: x.NullDuration{Duration: 35 * time.Hour, Valid: true}, - ImplicitGrantIDTokenLifespan: x.NullDuration{Duration: 36 * time.Hour, Valid: true}, - JwtBearerGrantAccessTokenLifespan: x.NullDuration{Duration: 37 * time.Hour, Valid: true}, - PasswordGrantAccessTokenLifespan: x.NullDuration{Duration: 38 * time.Hour, Valid: true}, - PasswordGrantRefreshTokenLifespan: x.NullDuration{Duration: 39 * time.Hour, Valid: true}, - RefreshTokenGrantIDTokenLifespan: x.NullDuration{Duration: 40 * time.Hour, Valid: true}, - RefreshTokenGrantAccessTokenLifespan: x.NullDuration{Duration: 41 * time.Hour, Valid: true}, - RefreshTokenGrantRefreshTokenLifespan: x.NullDuration{Duration: 42 * time.Hour, Valid: true}, + AuthorizationCodeGrantAccessTokenLifespan: x.NullDuration{Duration: 31 * time.Hour, Valid: true}, + AuthorizationCodeGrantIDTokenLifespan: x.NullDuration{Duration: 32 * time.Hour, Valid: true}, + AuthorizationCodeGrantRefreshTokenLifespan: x.NullDuration{Duration: 33 * time.Hour, Valid: true}, + ClientCredentialsGrantAccessTokenLifespan: x.NullDuration{Duration: 34 * time.Hour, Valid: true}, + ImplicitGrantAccessTokenLifespan: x.NullDuration{Duration: 35 * time.Hour, Valid: true}, + ImplicitGrantIDTokenLifespan: x.NullDuration{Duration: 36 * time.Hour, Valid: true}, + JwtBearerGrantAccessTokenLifespan: x.NullDuration{Duration: 37 * time.Hour, Valid: true}, + PasswordGrantAccessTokenLifespan: x.NullDuration{Duration: 38 * time.Hour, Valid: true}, + PasswordGrantRefreshTokenLifespan: x.NullDuration{Duration: 39 * time.Hour, Valid: true}, + RefreshTokenGrantIDTokenLifespan: x.NullDuration{Duration: 40 * time.Hour, Valid: true}, + RefreshTokenGrantAccessTokenLifespan: x.NullDuration{Duration: 41 * time.Hour, Valid: true}, + RefreshTokenGrantRefreshTokenLifespan: x.NullDuration{Duration: 42 * time.Hour, Valid: true}, + DeviceAuthorizationGrantIDTokenLifespan: x.NullDuration{Duration: 45 * time.Hour, Valid: true}, + DeviceAuthorizationGrantAccessTokenLifespan: x.NullDuration{Duration: 46 * time.Hour, Valid: true}, + DeviceAuthorizationGrantRefreshTokenLifespan: x.NullDuration{Duration: 47 * time.Hour, Valid: true}, } diff --git a/internal/testhelpers/oauth2.go b/internal/testhelpers/oauth2.go index 4a7b5bc696e..bdee8493cb5 100644 --- a/internal/testhelpers/oauth2.go +++ b/internal/testhelpers/oauth2.go @@ -182,6 +182,17 @@ func NewLoginConsentUI(t testing.TB, c *config.DefaultProvider, login, consent h c.MustSet(context.Background(), config.KeyConsentURL, ct.URL) } +func NewDeviceLoginConsentUI(t testing.TB, c *config.DefaultProvider, device, login, consent http.HandlerFunc) { + if device == nil { + device = HTTPServerNotImplementedHandler + } + dt := httptest.NewServer(device) + t.Cleanup(dt.Close) + c.MustSet(context.Background(), config.KeyDeviceVerificationURL, dt.URL) + + NewLoginConsentUI(t, c, login, consent) +} + func NewCallbackURL(t testing.TB, prefix string, h http.HandlerFunc) string { if h == nil { h = HTTPServerNotImplementedHandler diff --git a/jwk/registry_mock_test.go b/jwk/registry_mock_test.go index c305fd18167..f9624dc2b75 100644 --- a/jwk/registry_mock_test.go +++ b/jwk/registry_mock_test.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Ory Corp +// Copyright © 2024 Ory Corp // SPDX-License-Identifier: Apache-2.0 // Code generated by MockGen. DO NOT EDIT. @@ -13,7 +13,7 @@ import ( 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/.snapshots/TestAuthCodeWithMockStrategy-strategy=jwt-case=0-description=should_pass_request_if_strategy_passes-should_call_refresh_token_hook_if_configured-hook=new.json b/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=jwt-case=0-description=should_pass_request_if_strategy_passes-should_call_refresh_token_hook_if_configured-hook=new.json index c9ca2a2ca07..85da3a9fb18 100644 --- a/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=jwt-case=0-description=should_pass_request_if_strategy_passes-should_call_refresh_token_hook_if_configured-hook=new.json +++ b/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=jwt-case=0-description=should_pass_request_if_strategy_passes-should_call_refresh_token_hook_if_configured-hook=new.json @@ -36,9 +36,9 @@ "request": { "client_id": "app-client", "requested_scopes": [ - "hydra.*", "offline", - "openid" + "openid", + "hydra.*" ], "granted_scopes": [ "offline", diff --git a/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=jwt-case=2-description=should_pass_because_prompt=none_and_max_age_is_less_than_auth_time-should_call_refresh_token_hook_if_configured-hook=new.json b/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=jwt-case=2-description=should_pass_because_prompt=none_and_max_age_is_less_than_auth_time-should_call_refresh_token_hook_if_configured-hook=new.json index c9ca2a2ca07..85da3a9fb18 100644 --- a/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=jwt-case=2-description=should_pass_because_prompt=none_and_max_age_is_less_than_auth_time-should_call_refresh_token_hook_if_configured-hook=new.json +++ b/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=jwt-case=2-description=should_pass_because_prompt=none_and_max_age_is_less_than_auth_time-should_call_refresh_token_hook_if_configured-hook=new.json @@ -36,9 +36,9 @@ "request": { "client_id": "app-client", "requested_scopes": [ - "hydra.*", "offline", - "openid" + "openid", + "hydra.*" ], "granted_scopes": [ "offline", diff --git a/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=jwt-case=5-description=should_pass_with_prompt=login_when_authentication_time_is_recent-should_call_refresh_token_hook_if_configured-hook=new.json b/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=jwt-case=5-description=should_pass_with_prompt=login_when_authentication_time_is_recent-should_call_refresh_token_hook_if_configured-hook=new.json index c9ca2a2ca07..85da3a9fb18 100644 --- a/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=jwt-case=5-description=should_pass_with_prompt=login_when_authentication_time_is_recent-should_call_refresh_token_hook_if_configured-hook=new.json +++ b/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=jwt-case=5-description=should_pass_with_prompt=login_when_authentication_time_is_recent-should_call_refresh_token_hook_if_configured-hook=new.json @@ -36,9 +36,9 @@ "request": { "client_id": "app-client", "requested_scopes": [ - "hydra.*", "offline", - "openid" + "openid", + "hydra.*" ], "granted_scopes": [ "offline", diff --git a/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=opaque-case=0-description=should_pass_request_if_strategy_passes-should_call_refresh_token_hook_if_configured-hook=new.json b/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=opaque-case=0-description=should_pass_request_if_strategy_passes-should_call_refresh_token_hook_if_configured-hook=new.json index c9ca2a2ca07..85da3a9fb18 100644 --- a/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=opaque-case=0-description=should_pass_request_if_strategy_passes-should_call_refresh_token_hook_if_configured-hook=new.json +++ b/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=opaque-case=0-description=should_pass_request_if_strategy_passes-should_call_refresh_token_hook_if_configured-hook=new.json @@ -36,9 +36,9 @@ "request": { "client_id": "app-client", "requested_scopes": [ - "hydra.*", "offline", - "openid" + "openid", + "hydra.*" ], "granted_scopes": [ "offline", diff --git a/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=opaque-case=2-description=should_pass_because_prompt=none_and_max_age_is_less_than_auth_time-should_call_refresh_token_hook_if_configured-hook=new.json b/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=opaque-case=2-description=should_pass_because_prompt=none_and_max_age_is_less_than_auth_time-should_call_refresh_token_hook_if_configured-hook=new.json index c9ca2a2ca07..85da3a9fb18 100644 --- a/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=opaque-case=2-description=should_pass_because_prompt=none_and_max_age_is_less_than_auth_time-should_call_refresh_token_hook_if_configured-hook=new.json +++ b/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=opaque-case=2-description=should_pass_because_prompt=none_and_max_age_is_less_than_auth_time-should_call_refresh_token_hook_if_configured-hook=new.json @@ -36,9 +36,9 @@ "request": { "client_id": "app-client", "requested_scopes": [ - "hydra.*", "offline", - "openid" + "openid", + "hydra.*" ], "granted_scopes": [ "offline", diff --git a/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=opaque-case=5-description=should_pass_with_prompt=login_when_authentication_time_is_recent-should_call_refresh_token_hook_if_configured-hook=new.json b/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=opaque-case=5-description=should_pass_with_prompt=login_when_authentication_time_is_recent-should_call_refresh_token_hook_if_configured-hook=new.json index c9ca2a2ca07..85da3a9fb18 100644 --- a/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=opaque-case=5-description=should_pass_with_prompt=login_when_authentication_time_is_recent-should_call_refresh_token_hook_if_configured-hook=new.json +++ b/oauth2/.snapshots/TestAuthCodeWithMockStrategy-strategy=opaque-case=5-description=should_pass_with_prompt=login_when_authentication_time_is_recent-should_call_refresh_token_hook_if_configured-hook=new.json @@ -36,9 +36,9 @@ "request": { "client_id": "app-client", "requested_scopes": [ - "hydra.*", "offline", - "openid" + "openid", + "hydra.*" ], "granted_scopes": [ "offline", diff --git a/oauth2/.snapshots/TestHandlerWellKnown-hsm_enabled=false.json b/oauth2/.snapshots/TestHandlerWellKnown-hsm_enabled=false.json index 5bc92ec79a5..177300163a0 100644 --- a/oauth2/.snapshots/TestHandlerWellKnown-hsm_enabled=false.json +++ b/oauth2/.snapshots/TestHandlerWellKnown-hsm_enabled=false.json @@ -35,6 +35,7 @@ ] } ], + "device_authorization_endpoint": "http://hydra.localhost/oauth2/device/auth", "end_session_endpoint": "http://hydra.localhost/oauth2/sessions/logout", "frontchannel_logout_session_supported": true, "frontchannel_logout_supported": true, @@ -42,7 +43,8 @@ "authorization_code", "implicit", "client_credentials", - "refresh_token" + "refresh_token", + "urn:ietf:params:oauth:grant-type:device_code" ], "id_token_signed_response_alg": [ "RS256" diff --git a/oauth2/.snapshots/TestHandlerWellKnown-hsm_enabled=true.json b/oauth2/.snapshots/TestHandlerWellKnown-hsm_enabled=true.json index 5bc92ec79a5..177300163a0 100644 --- a/oauth2/.snapshots/TestHandlerWellKnown-hsm_enabled=true.json +++ b/oauth2/.snapshots/TestHandlerWellKnown-hsm_enabled=true.json @@ -35,6 +35,7 @@ ] } ], + "device_authorization_endpoint": "http://hydra.localhost/oauth2/device/auth", "end_session_endpoint": "http://hydra.localhost/oauth2/sessions/logout", "frontchannel_logout_session_supported": true, "frontchannel_logout_supported": true, @@ -42,7 +43,8 @@ "authorization_code", "implicit", "client_credentials", - "refresh_token" + "refresh_token", + "urn:ietf:params:oauth:grant-type:device_code" ], "id_token_signed_response_alg": [ "RS256" diff --git a/oauth2/flowctx/encoding.go b/oauth2/flowctx/encoding.go index 8a1f8cbf270..8c659ad724e 100644 --- a/oauth2/flowctx/encoding.go +++ b/oauth2/flowctx/encoding.go @@ -25,6 +25,8 @@ type ( const ( loginChallenge purpose = iota loginVerifier + deviceChallenge + deviceVerifier consentChallenge consentVerifier ) @@ -34,6 +36,8 @@ func withPurpose(purpose purpose) CodecOption { return func(ad *data) { ad.Purpo var ( AsLoginChallenge = withPurpose(loginChallenge) AsLoginVerifier = withPurpose(loginVerifier) + AsDeviceChallenge = withPurpose(deviceChallenge) + AsDeviceVerifier = withPurpose(deviceVerifier) AsConsentChallenge = withPurpose(consentChallenge) AsConsentVerifier = withPurpose(consentVerifier) ) diff --git a/oauth2/handler.go b/oauth2/handler.go index 61c6ae4ddd1..ea76f10b4d4 100644 --- a/oauth2/handler.go +++ b/oauth2/handler.go @@ -10,15 +10,19 @@ import ( "fmt" "html/template" "net/http" + "net/url" "reflect" "strings" "time" + "github.com/ory/x/otelx" + "github.com/gobuffalo/pop/v6" "github.com/tidwall/gjson" "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" @@ -43,14 +47,16 @@ import ( ) const ( - DefaultLoginPath = "/oauth2/fallbacks/login" - DefaultConsentPath = "/oauth2/fallbacks/consent" - DefaultPostLogoutPath = "/oauth2/fallbacks/logout/callback" - DefaultLogoutPath = "/oauth2/fallbacks/logout" - DefaultErrorPath = "/oauth2/fallbacks/error" - TokenPath = "/oauth2/token" // #nosec G101 - AuthPath = "/oauth2/auth" - LogoutPath = "/oauth2/sessions/logout" + DefaultLoginPath = "/oauth2/fallbacks/login" + DefaultConsentPath = "/oauth2/fallbacks/consent" + DefaultPostLogoutPath = "/oauth2/fallbacks/logout/callback" + DefaultDeviceVerificationPath = "/oauth2/fallbacks/device" + DefaultPostDevicePath = "/oauth2/fallbacks/device/done" + DefaultLogoutPath = "/oauth2/fallbacks/logout" + DefaultErrorPath = "/oauth2/fallbacks/error" + TokenPath = "/oauth2/token" // #nosec G101 + AuthPath = "/oauth2/auth" + LogoutPath = "/oauth2/sessions/logout" VerifiableCredentialsPath = "/credentials" UserinfoPath = "/userinfo" @@ -61,8 +67,21 @@ const ( IntrospectPath = "/oauth2/introspect" RevocationPath = "/oauth2/revoke" DeleteTokensPath = "/oauth2/tokens" // #nosec G101 + + // Device authorization endpoint + DeviceAuthPath = "/oauth2/device/auth" + DeviceVerificationPath = "/oauth2/device/verify" ) +// Taken from https://github.com/ory/fosite/blob/049ed1924cd0b41f12357b0fe617530c264421ac/handler/openid/flow_explicit_auth.go#L29 +var oidcParameters = []string{"grant_type", + "max_age", + "prompt", + "acr_values", + "id_token_hint", + "nonce", +} + type Handler struct { r InternalRegistry c *config.DefaultProvider @@ -93,6 +112,13 @@ func (h *Handler) SetRoutes(admin *httprouterx.RouterAdmin, public *httprouterx. http.StatusOK, config.KeyLogoutRedirectURL, )) + public.GET(DefaultDeviceVerificationPath, h.fallbackHandler("", "", http.StatusOK, config.KeyDeviceVerificationURL)) + public.GET(DefaultPostDevicePath, h.fallbackHandler( + "You successfully authenticated on your device!", + "The Default Post Device URL is not set which is why you are seeing this fallback page. Your device login request however succeeded.", + http.StatusOK, + config.KeyDeviceDoneURL, + )) public.GET(DefaultErrorPath, h.DefaultErrorHandler) public.Handler("OPTIONS", RevocationPath, corsMiddleware(http.HandlerFunc(h.handleOptions))) @@ -106,6 +132,9 @@ 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.oAuth2DeviceFlow)) + public.GET(DeviceVerificationPath, h.performOAuth2DeviceVerificationFlow) + admin.POST(IntrospectPath, h.introspectOAuth2Token) admin.DELETE(DeleteTokensPath, h.deleteOAuth2Token) } @@ -246,6 +275,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 @@ -483,6 +518,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(), @@ -496,7 +532,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, @@ -689,6 +725,182 @@ func (h *Handler) getOidcUserInfo(w http.ResponseWriter, r *http.Request) { } } +// swagger:route GET /oauth2/device/verify oAuth2 performOAuth2DeviceVerificationFlow +// +// # OAuth 2.0 Device Verification Endpoint +// +// This is the device user verification endpoint. The user is redirected here when trying to login using the device flow. +// +// Consumes: +// - application/x-www-form-urlencoded +// +// Schemes: http, https +// +// Responses: +// 302: emptyResponse +// default: errorOAuth2 +func (h *Handler) performOAuth2DeviceVerificationFlow(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + var ( + ctx = r.Context() + err error + ) + + ctx, span := h.r.Tracer(ctx).Tracer().Start(ctx, "oauth2.handler.performOAuth2DeviceVerificationFlow") + defer otelx.End(span, &err) + + // When this endpoint is called with a valid consent_verifier (meaning that the login flow completed successfully) + // there are 3 writes happening to the database: + // - The flow is created + // - The device auth session is updated (user_code is marked as accepted) + // - The OpenID session is created + // If there were multiple flows created for the same user_code then we may end up with multiple flow objects + // persisted to the database, while only one of them was actually used to validate the user_code + // (see https://github.com/ory/hydra/pull/3851#discussion_r1843678761) + consentSession, f, err := h.r.ConsentStrategy().HandleOAuth2DeviceAuthorizationRequest(ctx, w, r) + if errors.Is(err, consent.ErrAbortOAuth2Request) { + x.LogAudit(r, nil, h.r.AuditLogger()) + 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 + } + + req, sig, err := h.r.OAuth2Storage().GetDeviceCodeSessionByRequestID(ctx, f.DeviceCodeRequestID.String(), &Session{}) + if err != nil { + x.LogError(r, err, h.r.Logger()) + h.r.Writer().WriteError(w, r, err) + return + } + + req.SetUserCodeState(fosite.UserCodeAccepted) + session, err := h.updateSessionWithRequest(ctx, consentSession, f, r, req, req.GetSession().(*Session)) + if err != nil { + x.LogError(r, err, h.r.Logger()) + h.r.Writer().WriteError(w, r, err) + return + } + + req.SetSession(session) + if err := h.r.Persister().Transaction(ctx, func(ctx context.Context, _ *pop.Connection) error { + // Update the device code session with + // - the claims for which the user gave consent + // - the granted scopes + // - the granted audiences + // - the user_code_state set to `accepted` + // This marks it as ready to be used for the token exchange endpoint. + if err = h.r.OAuth2Storage().UpdateDeviceCodeSessionBySignature(ctx, sig, req); err != nil { + return err + } + + // Update the OpenID Connect session if "openid" scope is granted + if req.GetGrantedScopes().Has("openid") { + if err := h.r.OAuth2Storage().CreateOpenIDConnectSession(ctx, sig, req.Sanitize(oidcParameters)); err != nil { + return err + } + } + + return nil + }); err != nil { + x.LogError(r, err, h.r.Logger()) + h.r.Writer().WriteError(w, r, err) + return + } + + redirectURL := urlx.SetQuery(h.c.DeviceDoneURL(ctx), url.Values{"client_id": {f.Client.GetID()}}).String() + http.Redirect(w, r, redirectURL, http.StatusFound) +} + +// OAuth2 Device Flow +// +// # 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. + // + // 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 oAuth2 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 +// +// Consumes: +// - application/x-www-form-urlencoded +// +// Schemes: http, https +// +// Responses: +// 200: deviceAuthorization +// 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) + return + } + + 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 @@ -1074,7 +1286,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 @@ -1089,83 +1301,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.ConsentRequestID) + session, err := h.updateSessionWithRequest(ctx, acceptConsentSession, flow, r, authorizeRequest, nil) if err != nil { - x.LogError(r, err, h.r.Logger()) h.writeAuthorizeError(w, r, authorizeRequest, err) return } - - var accessTokenKeyID string - if h.c.AccessTokenStrategy(ctx, client.AccessTokenStrategySource(authorizeRequest.GetClient())) == "jwt" { - accessTokenKeyID, err = h.r.AccessTokenJWTStrategy().GetPublicKeyID(ctx) - if err != nil { - x.LogError(r, err, h.r.Logger()) - h.writeAuthorizeError(w, r, authorizeRequest, err) - return - } - } - - obfuscatedSubject, err := h.r.ConsentStrategy().ObfuscateSubjectIdentifier(ctx, authorizeRequest.GetClient(), session.ConsentRequest.Subject, session.ConsentRequest.ForceSubjectIdentifier) - if e := &(fosite.RFC6749Error{}); errors.As(err, &e) { - x.LogAudit(r, err, h.r.AuditLogger()) - h.writeAuthorizeError(w, r, authorizeRequest, err) - return - } else if err != nil { - x.LogError(r, err, h.r.Logger()) - h.writeAuthorizeError(w, r, authorizeRequest, err) - return - } - - authorizeRequest.SetID(session.ConsentRequestID) - claims := &jwt.IDTokenClaims{ - Subject: obfuscatedSubject, - Issuer: h.c.IssuerURL(ctx).String(), - AuthTime: time.Time(session.AuthenticatedAt), - RequestedAt: session.RequestedAt, - Extra: session.Session.IDToken, - AuthenticationContextClassReference: session.ConsentRequest.ACR, - AuthenticationMethodsReferences: session.ConsentRequest.AMR, - - // These are required for work around https://github.com/ory/fosite/issues/530 - Nonce: authorizeRequest.GetRequestForm().Get("nonce"), - Audience: []string{authorizeRequest.GetClient().GetID()}, - IssuedAt: time.Now().Truncate(time.Second).UTC(), - - // This is set by the fosite strategy - // ExpiresAt: time.Now().Add(h.IDTokenLifespan).UTC(), - } - claims.Add("sid", session.ConsentRequest.LoginSessionID) - - // done var response fosite.AuthorizeResponder if err := h.r.Persister().Transaction(ctx, func(ctx context.Context, _ *pop.Connection) (err error) { - response, err = h.r.OAuth2Provider().NewAuthorizeResponse(ctx, authorizeRequest, &Session{ - DefaultSession: &openid.DefaultSession{ - Claims: claims, - Headers: &jwt.Headers{Extra: map[string]interface{}{ - // required for lookup on jwk endpoint - "kid": openIDKeyID, - }}, - Subject: session.ConsentRequest.Subject, - }, - Extra: session.Session.AccessToken, - KID: accessTokenKeyID, - ClientID: authorizeRequest.GetClient().GetID(), - ConsentChallenge: session.ConsentRequestID, - ExcludeNotBeforeClaim: h.c.ExcludeNotBeforeClaim(ctx), - AllowedTopLevelClaims: h.c.AllowedTopLevelClaims(ctx), - MirrorTopLevelClaims: h.c.MirrorTopLevelClaims(ctx), - Flow: flow, - }) + response, err = h.r.OAuth2Provider().NewAuthorizeResponse(ctx, authorizeRequest, session) return err }); err != nil { x.LogError(r, err, h.r.Logger()) @@ -1237,6 +1381,93 @@ 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, + 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 consent.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(request.GetClient())) == "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, 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 + } else if err != nil { + x.LogError(r, err, h.r.Logger()) + return nil, err + } + + request.SetID(consent.ConsentRequestID) + claims := &jwt.IDTokenClaims{ + Subject: obfuscatedSubject, + Issuer: h.c.IssuerURL(ctx).String(), + 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"), + Audience: []string{request.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", consent.ConsentRequest.LoginSessionID) + + if session == nil { + session = &Session{} + } + + if session.DefaultSession == nil { + session.DefaultSession = &openid.DefaultSession{} + } + session.DefaultSession.Claims = claims + session.DefaultSession.Headers = &jwt.Headers{Extra: map[string]interface{}{ + // required for lookup on jwk endpoint + "kid": openIDKeyID, + }} + session.DefaultSession.Subject = consent.ConsentRequest.Subject + session.Extra = consent.Session.AccessToken + session.KID = accessTokenKeyID + session.ClientID = request.GetClient().GetID() + session.ConsentChallenge = consent.ConsentRequestID + session.ExcludeNotBeforeClaim = h.c.ExcludeNotBeforeClaim(ctx) + session.AllowedTopLevelClaims = h.c.AllowedTopLevelClaims(ctx) + session.MirrorTopLevelClaims = h.c.MirrorTopLevelClaims(ctx) + session.Flow = flow + + return session, 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()) diff --git a/oauth2/oauth2_auth_code_test.go b/oauth2/oauth2_auth_code_test.go index fdd4ba94121..797fd1144f8 100644 --- a/oauth2/oauth2_auth_code_test.go +++ b/oauth2/oauth2_auth_code_test.go @@ -1804,7 +1804,7 @@ func TestAuthCodeWithMockStrategy(t *testing.T) { TokenURL: ts.URL + "/oauth2/token", }, RedirectURL: ts.URL + "/callback", - Scopes: []string{"hydra.*", "offline", "openid"}, + Scopes: []string{"offline", "openid", "hydra.*"}, } var code string diff --git a/oauth2/oauth2_device_code_test.go b/oauth2/oauth2_device_code_test.go new file mode 100644 index 00000000000..6fd21bca9fc --- /dev/null +++ b/oauth2/oauth2_device_code_test.go @@ -0,0 +1,882 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oauth2_test + +import ( + "context" + "net/http" + "strconv" + "strings" + "testing" + "time" + + "github.com/pborman/uuid" + + "github.com/ory/fosite/token/jwt" + + "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/testhelpers" + hydraoauth2 "github.com/ory/hydra/v2/oauth2" + "github.com/ory/hydra/v2/x" + "github.com/ory/x/contextx" + "github.com/ory/x/pointerx" + "github.com/ory/x/requirex" +) + +func TestDeviceAuthRequest(t *testing.T) { + ctx := context.Background() + reg := testhelpers.NewMockedRegistry(t, &contextx.Default{}) + testhelpers.NewOAuth2Server(ctx, t, reg) + + secret := uuid.New() + c := &client.Client{ + 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", + } + require.NoError(t, reg.ClientManager().CreateClient(ctx, c)) + + oauthClient := &oauth2.Config{ + 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, " "), + } + + 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(), []oauth2.AuthCodeOption{oauth2.SetAuthURLParam("client_secret", secret)}...) + + if testCase.check != nil { + testCase.check(t, resp, err) + } + + if testCase.cleanUp != nil { + testCase.cleanUp() + } + }) + } +} + +func TestDeviceTokenRequest(t *testing.T) { + ctx := context.Background() + reg := testhelpers.NewMockedRegistry(t, &contextx.Default{}) + testhelpers.NewOAuth2Server(ctx, t, reg) + + secret := uuid.New() + c := &client.Client{ + ID: "device-client", + Secret: secret, + GrantTypes: []string{ + string(fosite.GrantTypeDeviceCode), + string(fosite.GrantTypeRefreshToken), + }, + Scope: "hydra offline openid", + Audience: []string{"https://api.ory.sh/"}, + } + require.NoError(t, reg.ClientManager().CreateClient(ctx, c)) + + oauthClient := &oauth2.Config{ + 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, " "), + } + + testCases := []struct { + description string + setUp func(signature, userCodeSignature string) + check func(t *testing.T, token *oauth2.Token, err error) + cleanUp func() + }{ + { + description: "should pass with refresh token", + setUp: func(signature, userCodeSignature string) { + authreq := &fosite.DeviceRequest{ + UserCodeState: fosite.UserCodeAccepted, + 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(), + }, + }, + }, + RequestedAt: time.Now(), + }, + } + + require.NoError(t, reg.OAuth2Storage().CreateDeviceAuthSession(context.TODO(), signature, userCodeSignature, authreq)) + }, + check: func(t *testing.T, token *oauth2.Token, err error) { + assert.NotEmpty(t, token.AccessToken) + assert.NotEmpty(t, token.RefreshToken) + }, + }, + { + description: "should pass with ID token", + setUp: func(signature, userCodeSignature string) { + authreq := &fosite.DeviceRequest{ + UserCodeState: fosite.UserCodeAccepted, + Request: fosite.Request{ + 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(), + }, + }, + }, + RequestedAt: time.Now(), + }, + } + + require.NoError(t, reg.OAuth2Storage().CreateDeviceAuthSession(context.TODO(), signature, userCodeSignature, authreq)) + require.NoError(t, reg.OAuth2Storage().CreateOpenIDConnectSession(context.TODO(), signature, authreq)) + }, + check: func(t *testing.T, token *oauth2.Token, err error) { + 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()) + _, userCodeSignature, err := reg.RFC8628HMACStrategy().GenerateUserCode(context.TODO()) + require.NoError(t, err) + + if testCase.setUp != nil { + testCase.setUp(signature, userCodeSignature) + } + + 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() + } + }) + } +} + +func TestDeviceCodeWithDefaultStrategy(t *testing.T) { + ctx := context.Background() + reg := testhelpers.NewMockedRegistry(t, &contextx.Default{}) + reg.Config().MustSet(ctx, config.KeyAccessTokenStrategy, "opaque") + reg.Config().MustSet(ctx, config.KeyRefreshTokenHook, "") + publicTS, adminTS := testhelpers.NewOAuth2Server(ctx, t, reg) + + publicClient := hydra.NewAPIClient(hydra.NewConfiguration()) + publicClient.GetConfig().Servers = hydra.ServerConfigurations{{URL: publicTS.URL}} + adminClient := hydra.NewAPIClient(hydra.NewConfiguration()) + adminClient.GetConfig().Servers = hydra.ServerConfigurations{{URL: adminTS.URL}} + + getDeviceCode := func(t *testing.T, conf *oauth2.Config, c *http.Client, params ...oauth2.AuthCodeOption) (*oauth2.DeviceAuthResponse, error) { + if c == nil { + c = testhelpers.NewEmptyJarClient(t) + } + + return conf.DeviceAuth(ctx, params...) + } + + acceptUserCode := func(t *testing.T, conf *oauth2.Config, c *http.Client, devResp *oauth2.DeviceAuthResponse) *http.Response { + if c == nil { + c = testhelpers.NewEmptyJarClient(t) + } + + resp, err := c.Get(devResp.VerificationURIComplete) + require.NoError(t, err) + require.Contains(t, reg.Config().DeviceDoneURL(ctx).String(), resp.Request.URL.Path, "did not end up in post device URL") + require.Equal(t, resp.Request.URL.Query().Get("client_id"), conf.ClientID) + + return resp + } + + acceptDeviceHandler := func(t *testing.T, c *client.Client) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + userCode := r.URL.Query().Get("user_code") + payload := hydra.AcceptDeviceUserCodeRequest{ + UserCode: &userCode, + } + + v, _, err := adminClient.OAuth2API.AcceptUserCodeRequest(context.Background()). + DeviceChallenge(r.URL.Query().Get("device_challenge")). + AcceptDeviceUserCodeRequest(payload). + Execute() + require.NoError(t, err) + require.NotEmpty(t, v.RedirectTo) + http.Redirect(w, r, v.RedirectTo, http.StatusFound) + } + } + + acceptLoginHandler := func(t *testing.T, c *client.Client, subject string, scopes []string, checkRequestPayload func(request *hydra.OAuth2LoginRequest) *hydra.AcceptOAuth2LoginRequest) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + rr, _, err := adminClient.OAuth2API.GetOAuth2LoginRequest(context.Background()).LoginChallenge(r.URL.Query().Get("login_challenge")).Execute() + require.NoError(t, err) + + 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, scopes, 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, scopes []string, checkRequestPayload func(*hydra.OAuth2ConsentRequest)) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + rr, _, err := adminClient.OAuth2API.GetOAuth2ConsentRequest(context.Background()).ConsentChallenge(r.URL.Query().Get("consent_challenge")).Execute() + require.NoError(t, err) + + 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, scopes, rr.RequestedScope) + 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: scopes, 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 without ID and refresh tokens", func(t *testing.T) { + + c, conf := newDeviceClient(t, reg) + conf.Scopes = []string{"hydra"} + testhelpers.NewDeviceLoginConsentUI(t, reg.Config(), + acceptDeviceHandler(t, c), + acceptLoginHandler(t, c, subject, conf.Scopes, nil), + acceptConsentHandler(t, c, subject, conf.Scopes, nil), + ) + + resp, err := getDeviceCode(t, conf, nil) + require.NoError(t, err) + require.NotEmpty(t, resp.DeviceCode) + require.NotEmpty(t, resp.UserCode) + loginFlowResp := acceptUserCode(t, conf, nil, resp) + require.NotNil(t, loginFlowResp) + token, err := conf.DeviceAccessToken(context.Background(), resp) + require.NoError(t, err) + + assert.Empty(t, token.Extra("c_nonce_draft_00"), "should not be set if not requested") + assert.Empty(t, token.Extra("c_nonce_expires_in_draft_00"), "should not be set if not requested") + introspectAccessToken(t, conf, token, subject) + assert.Empty(t, token.Extra("id_token")) + assert.Empty(t, token.RefreshToken) + }) + t.Run("case=perform device flow with ID token", func(t *testing.T) { + + c, conf := newDeviceClient(t, reg) + conf.Scopes = []string{"openid", "hydra"} + testhelpers.NewDeviceLoginConsentUI(t, reg.Config(), + acceptDeviceHandler(t, c), + acceptLoginHandler(t, c, subject, conf.Scopes, nil), + acceptConsentHandler(t, c, subject, conf.Scopes, nil), + ) + + resp, err := getDeviceCode(t, conf, nil) + require.NoError(t, err) + require.NotEmpty(t, resp.DeviceCode) + require.NotEmpty(t, resp.UserCode) + loginFlowResp := acceptUserCode(t, conf, nil, resp) + require.NotNil(t, loginFlowResp) + token, err := conf.DeviceAccessToken(context.Background(), resp) + iat := time.Now() + require.NoError(t, err) + + assert.Empty(t, token.Extra("c_nonce_draft_00"), "should not be set if not requested") + assert.Empty(t, token.Extra("c_nonce_expires_in_draft_00"), "should not be set if not requested") + introspectAccessToken(t, conf, token, subject) + assertIDToken(t, token, conf, subject, nonce, iat.Add(reg.Config().GetIDTokenLifespan(ctx))) + assert.Empty(t, token.RefreshToken) + }) + t.Run("case=perform device flow with refresh token", func(t *testing.T) { + + c, conf := newDeviceClient(t, reg) + conf.Scopes = []string{"hydra", "offline"} + testhelpers.NewDeviceLoginConsentUI(t, reg.Config(), + acceptDeviceHandler(t, c), + acceptLoginHandler(t, c, subject, conf.Scopes, nil), + acceptConsentHandler(t, c, subject, conf.Scopes, nil), + ) + + resp, err := getDeviceCode(t, conf, nil) + require.NoError(t, err) + require.NotEmpty(t, resp.DeviceCode) + require.NotEmpty(t, resp.UserCode) + loginFlowResp := acceptUserCode(t, conf, nil, resp) + require.NotNil(t, loginFlowResp) + token, err := conf.DeviceAccessToken(context.Background(), resp) + iat := time.Now() + require.NoError(t, err) + + assert.Empty(t, token.Extra("c_nonce_draft_00"), "should not be set if not requested") + assert.Empty(t, token.Extra("c_nonce_expires_in_draft_00"), "should not be set if not requested") + introspectAccessToken(t, conf, token, subject) + assert.Empty(t, token.Extra("id_token")) + assertRefreshToken(t, token, conf, iat.Add(reg.Config().GetRefreshTokenLifespan(ctx))) + }) + t.Run("case=perform device flow with ID token and refresh tokens", func(t *testing.T) { + run := func(t *testing.T, strategy string) { + c, conf := newDeviceClient(t, reg) + testhelpers.NewDeviceLoginConsentUI(t, reg.Config(), + acceptDeviceHandler(t, c), + acceptLoginHandler(t, c, subject, conf.Scopes, nil), + acceptConsentHandler(t, c, subject, conf.Scopes, nil), + ) + + resp, err := getDeviceCode(t, conf, nil) + require.NoError(t, err) + require.NotEmpty(t, resp.DeviceCode) + require.NotEmpty(t, resp.UserCode) + loginFlowResp := acceptUserCode(t, conf, nil, resp) + require.NotNil(t, loginFlowResp) + token, err := conf.DeviceAccessToken(context.Background(), resp) + iat := time.Now() + require.NoError(t, err) + + assert.Empty(t, token.Extra("c_nonce_draft_00"), "should not be set if not requested") + assert.Empty(t, token.Extra("c_nonce_expires_in_draft_00"), "should not be set if not requested") + introspectAccessToken(t, conf, token, subject) + 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, conf.Scopes, func(r *hydra.OAuth2LoginRequest) *hydra.AcceptOAuth2LoginRequest { + assert.False(t, r.Skip) + assert.EqualValues(t, []string{expectAud}, r.RequestedAccessTokenAudience) + return nil + }), + acceptConsentHandler(t, c, subject, conf.Scopes, 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, conf.Scopes, nil), + acceptConsentHandler(t, c, subject, conf.Scopes, nil), + ) + + resp, err := getDeviceCode(t, conf, nil) + require.NoError(t, err) + require.NotEmpty(t, resp.DeviceCode) + require.NotEmpty(t, resp.UserCode) + loginFlowResp := acceptUserCode(t, conf, nil, resp) + require.NotNil(t, loginFlowResp) + + token, err := conf.DeviceAccessToken(context.Background(), resp) + iat := time.Now() + require.NoError(t, err) + + 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) + }) + }) + t.Run("case=cannot reuse user_code", func(t *testing.T) { + c, conf := newDeviceClient(t, reg) + testhelpers.NewDeviceLoginConsentUI(t, reg.Config(), + func(w http.ResponseWriter, r *http.Request) { + userCode := r.URL.Query().Get("user_code") + payload := hydra.AcceptDeviceUserCodeRequest{ + UserCode: &userCode, + } + + v, _, err := adminClient.OAuth2API.AcceptUserCodeRequest(context.Background()). + DeviceChallenge(r.URL.Query().Get("device_challenge")). + AcceptDeviceUserCodeRequest(payload). + Execute() + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + require.NotEmpty(t, v.RedirectTo) + http.Redirect(w, r, v.RedirectTo, http.StatusFound) + }, + acceptLoginHandler(t, c, subject, conf.Scopes, nil), + acceptConsentHandler(t, c, subject, conf.Scopes, nil), + ) + + resp, err := getDeviceCode(t, conf, nil) + require.NoError(t, err) + require.NotEmpty(t, resp.DeviceCode) + require.NotEmpty(t, resp.UserCode) + loginFlowResp := acceptUserCode(t, conf, nil, resp) + require.NotNil(t, loginFlowResp) + token, err := conf.DeviceAccessToken(context.Background(), resp) + iat := time.Now() + require.NoError(t, err) + + introspectAccessToken(t, conf, token, subject) + assertIDToken(t, token, conf, subject, nonce, iat.Add(reg.Config().GetIDTokenLifespan(ctx))) + assertRefreshToken(t, token, conf, iat.Add(reg.Config().GetRefreshTokenLifespan(ctx))) + + hc := testhelpers.NewEmptyJarClient(t) + + loginFlowResp2, err := hc.Get(resp.VerificationURIComplete) + require.NoError(t, err) + require.Equal(t, loginFlowResp2.StatusCode, http.StatusBadRequest) + }) + t.Run("case=cannot reuse device_challenge", func(t *testing.T) { + var deviceChallenge string + c, conf := newDeviceClient(t, reg) + testhelpers.NewDeviceLoginConsentUI(t, reg.Config(), + func(w http.ResponseWriter, r *http.Request) { + userCode := r.URL.Query().Get("user_code") + payload := hydra.AcceptDeviceUserCodeRequest{ + UserCode: &userCode, + } + + if deviceChallenge == "" { + deviceChallenge = r.URL.Query().Get("device_challenge") + } + v, _, err := adminClient.OAuth2API.AcceptUserCodeRequest(context.Background()). + DeviceChallenge(deviceChallenge). + AcceptDeviceUserCodeRequest(payload). + Execute() + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + require.NoError(t, err) + require.NotEmpty(t, v.RedirectTo) + http.Redirect(w, r, v.RedirectTo, http.StatusFound) + }, + acceptLoginHandler(t, c, subject, conf.Scopes, nil), + acceptConsentHandler(t, c, subject, conf.Scopes, nil), + ) + + resp, err := getDeviceCode(t, conf, nil) + require.NoError(t, err) + require.NotEmpty(t, resp.DeviceCode) + require.NotEmpty(t, resp.UserCode) + + hc := testhelpers.NewEmptyJarClient(t) + loginFlowResp := acceptUserCode(t, conf, hc, resp) + require.NoError(t, err) + require.Contains(t, reg.Config().DeviceDoneURL(ctx).String(), loginFlowResp.Request.URL.Path, "did not end up in post device URL") + require.Equal(t, loginFlowResp.Request.URL.Query().Get("client_id"), conf.ClientID) + + require.NotNil(t, loginFlowResp) + token, err := conf.DeviceAccessToken(context.Background(), resp) + iat := time.Now() + require.NoError(t, err) + + introspectAccessToken(t, conf, token, subject) + assertIDToken(t, token, conf, subject, nonce, iat.Add(reg.Config().GetIDTokenLifespan(ctx))) + assertRefreshToken(t, token, conf, iat.Add(reg.Config().GetRefreshTokenLifespan(ctx))) + + resp2, err := getDeviceCode(t, conf, nil) + require.NoError(t, err) + require.NotEmpty(t, resp2.DeviceCode) + require.NotEmpty(t, resp2.UserCode) + + payload := hydra.AcceptDeviceUserCodeRequest{ + UserCode: &resp2.UserCode, + } + + acceptResp, _, err := adminClient.OAuth2API.AcceptUserCodeRequest(context.Background()). + DeviceChallenge(deviceChallenge). + AcceptDeviceUserCodeRequest(payload). + Execute() + + loginFlowResp2, err := hc.Get(acceptResp.RedirectTo) + require.NoError(t, err) + require.Equal(t, http.StatusForbidden, loginFlowResp2.StatusCode) + }) +} + +func newDeviceClient( + 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, " "), + } +} diff --git a/oauth2/oauth2_helper_test.go b/oauth2/oauth2_helper_test.go index 52a30e5975e..769679ec17e 100644 --- a/oauth2/oauth2_helper_test.go +++ b/oauth2/oauth2_helper_test.go @@ -46,6 +46,27 @@ func (c *consentMock) HandleOAuth2AuthorizationRequest(ctx context.Context, w ht }, nil, nil } +func (c *consentMock) HandleOAuth2DeviceAuthorizationRequest(ctx context.Context, w http.ResponseWriter, r *http.Request) (*flow.AcceptOAuth2ConsentRequest, *flow.Flow, error) { + if c.deny { + return nil, nil, fosite.ErrRequestForbidden + } + + return &flow.AcceptOAuth2ConsentRequest{ + ConsentRequest: &flow.OAuth2ConsentRequest{ + Subject: "foo", + ACR: "1", + DeviceChallenge: "12345", + }, + AuthenticatedAt: sqlxx.NullTime(c.authTime), + GrantedScope: []string{"offline", "openid", "hydra.*"}, + Session: &flow.AcceptOAuth2ConsentRequestSession{ + AccessToken: map[string]interface{}{}, + IDToken: map[string]interface{}{}, + }, + RequestedAt: c.requestTime, + }, nil, nil +} + func (c *consentMock) HandleOpenIDConnectLogout(ctx context.Context, w http.ResponseWriter, r *http.Request) (*flow.LogoutResult, error) { panic("not implemented") } diff --git a/oauth2/oauth2_provider_mock_test.go b/oauth2/oauth2_provider_mock_test.go index 83d584eb12f..7dd35e6a157 100644 --- a/oauth2/oauth2_provider_mock_test.go +++ b/oauth2/oauth2_provider_mock_test.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Ory Corp +// Copyright © 2024 Ory Corp // SPDX-License-Identifier: Apache-2.0 // Code generated by MockGen. DO NOT EDIT. @@ -121,6 +121,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, arg2 fosite.Session) (fosite.DeviceResponder, error) { + m.ctrl.T.Helper() + 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, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewDeviceResponse", reflect.TypeOf((*MockOAuth2Provider)(nil).NewDeviceResponse), arg0, arg1, arg2) +} + // NewIntrospectionRequest mocks base method. func (m *MockOAuth2Provider) NewIntrospectionRequest(arg0 context.Context, arg1 *http.Request, arg2 fosite.Session) (fosite.IntrospectionResponder, error) { m.ctrl.T.Helper() @@ -181,7 +211,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) } @@ -228,6 +258,18 @@ func (mr *MockOAuth2ProviderMockRecorder) WriteAuthorizeResponse(arg0, arg1, arg return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WriteAuthorizeResponse", reflect.TypeOf((*MockOAuth2Provider)(nil).WriteAuthorizeResponse), arg0, arg1, arg2, arg3) } +// WriteDeviceResponse mocks base method. +func (m *MockOAuth2Provider) WriteDeviceResponse(arg0 context.Context, arg1 http.ResponseWriter, arg2 fosite.DeviceRequester, arg3 fosite.DeviceResponder) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "WriteDeviceResponse", arg0, arg1, arg2, arg3) +} + +// WriteDeviceResponse indicates an expected call of WriteDeviceResponse. +func (mr *MockOAuth2ProviderMockRecorder) WriteDeviceResponse(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WriteDeviceResponse", reflect.TypeOf((*MockOAuth2Provider)(nil).WriteDeviceResponse), arg0, arg1, arg2, arg3) +} + // WriteIntrospectionError mocks base method. func (m *MockOAuth2Provider) WriteIntrospectionError(arg0 context.Context, arg1 http.ResponseWriter, arg2 error) { m.ctrl.T.Helper() diff --git a/oauth2/registry.go b/oauth2/registry.go index 0e7cdfae81a..ec0fb806354 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" @@ -25,6 +26,7 @@ type InternalRegistry interface { persistence.Provider Registry FlowCipher() *aead.XChaCha20Poly1305 + x.TracingProvider } type Registry interface { @@ -35,4 +37,5 @@ type Registry interface { OpenIDConnectRequestValidator() *openid.OpenIDConnectRequestValidator AccessRequestHooks() []AccessRequestHook OAuth2ProviderConfig() fosite.Configurator + RFC8628HMACStrategy() rfc8628.RFC8628CodeStrategy } diff --git a/persistence/sql/migratest/fixtures/hydra_client/client-0001.json b/persistence/sql/migratest/fixtures/hydra_client/client-0001.json index eb65327c43f..92a6eb6b00f 100644 --- a/persistence/sql/migratest/fixtures/hydra_client/client-0001.json +++ b/persistence/sql/migratest/fixtures/hydra_client/client-0001.json @@ -36,6 +36,18 @@ "Duration": 0, "Valid": false }, + "DeviceAuthorizationGrantAccessTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantIDTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantRefreshTokenLifespan": { + "Duration": 0, + "Valid": false + }, "ImplicitGrantAccessTokenLifespan": { "Duration": 0, "Valid": false diff --git a/persistence/sql/migratest/fixtures/hydra_client/client-0002.json b/persistence/sql/migratest/fixtures/hydra_client/client-0002.json index d58301981be..1cb9ff6e769 100644 --- a/persistence/sql/migratest/fixtures/hydra_client/client-0002.json +++ b/persistence/sql/migratest/fixtures/hydra_client/client-0002.json @@ -36,6 +36,18 @@ "Duration": 0, "Valid": false }, + "DeviceAuthorizationGrantAccessTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantIDTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantRefreshTokenLifespan": { + "Duration": 0, + "Valid": false + }, "ImplicitGrantAccessTokenLifespan": { "Duration": 0, "Valid": false diff --git a/persistence/sql/migratest/fixtures/hydra_client/client-0003.json b/persistence/sql/migratest/fixtures/hydra_client/client-0003.json index b0a9c4116b0..b2d8a612220 100644 --- a/persistence/sql/migratest/fixtures/hydra_client/client-0003.json +++ b/persistence/sql/migratest/fixtures/hydra_client/client-0003.json @@ -36,6 +36,18 @@ "Duration": 0, "Valid": false }, + "DeviceAuthorizationGrantAccessTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantIDTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantRefreshTokenLifespan": { + "Duration": 0, + "Valid": false + }, "ImplicitGrantAccessTokenLifespan": { "Duration": 0, "Valid": false diff --git a/persistence/sql/migratest/fixtures/hydra_client/client-0004.json b/persistence/sql/migratest/fixtures/hydra_client/client-0004.json index ad8ddb8fa58..10e001ac97e 100644 --- a/persistence/sql/migratest/fixtures/hydra_client/client-0004.json +++ b/persistence/sql/migratest/fixtures/hydra_client/client-0004.json @@ -36,6 +36,18 @@ "Duration": 0, "Valid": false }, + "DeviceAuthorizationGrantAccessTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantIDTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantRefreshTokenLifespan": { + "Duration": 0, + "Valid": false + }, "ImplicitGrantAccessTokenLifespan": { "Duration": 0, "Valid": false diff --git a/persistence/sql/migratest/fixtures/hydra_client/client-0005.json b/persistence/sql/migratest/fixtures/hydra_client/client-0005.json index 295a11833a1..c51c01b13e7 100644 --- a/persistence/sql/migratest/fixtures/hydra_client/client-0005.json +++ b/persistence/sql/migratest/fixtures/hydra_client/client-0005.json @@ -36,6 +36,18 @@ "Duration": 0, "Valid": false }, + "DeviceAuthorizationGrantAccessTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantIDTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantRefreshTokenLifespan": { + "Duration": 0, + "Valid": false + }, "ImplicitGrantAccessTokenLifespan": { "Duration": 0, "Valid": false diff --git a/persistence/sql/migratest/fixtures/hydra_client/client-0006.json b/persistence/sql/migratest/fixtures/hydra_client/client-0006.json index 9864869db0a..f87065ee097 100644 --- a/persistence/sql/migratest/fixtures/hydra_client/client-0006.json +++ b/persistence/sql/migratest/fixtures/hydra_client/client-0006.json @@ -36,6 +36,18 @@ "Duration": 0, "Valid": false }, + "DeviceAuthorizationGrantAccessTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantIDTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantRefreshTokenLifespan": { + "Duration": 0, + "Valid": false + }, "ImplicitGrantAccessTokenLifespan": { "Duration": 0, "Valid": false diff --git a/persistence/sql/migratest/fixtures/hydra_client/client-0007.json b/persistence/sql/migratest/fixtures/hydra_client/client-0007.json index 8186c89de28..6bf27b0d29b 100644 --- a/persistence/sql/migratest/fixtures/hydra_client/client-0007.json +++ b/persistence/sql/migratest/fixtures/hydra_client/client-0007.json @@ -36,6 +36,18 @@ "Duration": 0, "Valid": false }, + "DeviceAuthorizationGrantAccessTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantIDTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantRefreshTokenLifespan": { + "Duration": 0, + "Valid": false + }, "ImplicitGrantAccessTokenLifespan": { "Duration": 0, "Valid": false diff --git a/persistence/sql/migratest/fixtures/hydra_client/client-0008.json b/persistence/sql/migratest/fixtures/hydra_client/client-0008.json index 84bf09f3572..51cbcaf1c58 100644 --- a/persistence/sql/migratest/fixtures/hydra_client/client-0008.json +++ b/persistence/sql/migratest/fixtures/hydra_client/client-0008.json @@ -38,6 +38,18 @@ "Duration": 0, "Valid": false }, + "DeviceAuthorizationGrantAccessTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantIDTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantRefreshTokenLifespan": { + "Duration": 0, + "Valid": false + }, "ImplicitGrantAccessTokenLifespan": { "Duration": 0, "Valid": false diff --git a/persistence/sql/migratest/fixtures/hydra_client/client-0009.json b/persistence/sql/migratest/fixtures/hydra_client/client-0009.json index afae63b8668..ffe308afe0a 100644 --- a/persistence/sql/migratest/fixtures/hydra_client/client-0009.json +++ b/persistence/sql/migratest/fixtures/hydra_client/client-0009.json @@ -38,6 +38,18 @@ "Duration": 0, "Valid": false }, + "DeviceAuthorizationGrantAccessTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantIDTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantRefreshTokenLifespan": { + "Duration": 0, + "Valid": false + }, "ImplicitGrantAccessTokenLifespan": { "Duration": 0, "Valid": false diff --git a/persistence/sql/migratest/fixtures/hydra_client/client-0010.json b/persistence/sql/migratest/fixtures/hydra_client/client-0010.json index 5385cde2a58..573049c6c96 100644 --- a/persistence/sql/migratest/fixtures/hydra_client/client-0010.json +++ b/persistence/sql/migratest/fixtures/hydra_client/client-0010.json @@ -38,6 +38,18 @@ "Duration": 0, "Valid": false }, + "DeviceAuthorizationGrantAccessTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantIDTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantRefreshTokenLifespan": { + "Duration": 0, + "Valid": false + }, "ImplicitGrantAccessTokenLifespan": { "Duration": 0, "Valid": false diff --git a/persistence/sql/migratest/fixtures/hydra_client/client-0011.json b/persistence/sql/migratest/fixtures/hydra_client/client-0011.json index 7e3e68023fc..a49000472ae 100644 --- a/persistence/sql/migratest/fixtures/hydra_client/client-0011.json +++ b/persistence/sql/migratest/fixtures/hydra_client/client-0011.json @@ -40,6 +40,18 @@ "Duration": 0, "Valid": false }, + "DeviceAuthorizationGrantAccessTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantIDTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantRefreshTokenLifespan": { + "Duration": 0, + "Valid": false + }, "ImplicitGrantAccessTokenLifespan": { "Duration": 0, "Valid": false diff --git a/persistence/sql/migratest/fixtures/hydra_client/client-0012.json b/persistence/sql/migratest/fixtures/hydra_client/client-0012.json index cd61f01cbbe..1877d4b298f 100644 --- a/persistence/sql/migratest/fixtures/hydra_client/client-0012.json +++ b/persistence/sql/migratest/fixtures/hydra_client/client-0012.json @@ -40,6 +40,18 @@ "Duration": 0, "Valid": false }, + "DeviceAuthorizationGrantAccessTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantIDTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantRefreshTokenLifespan": { + "Duration": 0, + "Valid": false + }, "ImplicitGrantAccessTokenLifespan": { "Duration": 0, "Valid": false diff --git a/persistence/sql/migratest/fixtures/hydra_client/client-0013.json b/persistence/sql/migratest/fixtures/hydra_client/client-0013.json index 7eaff4d8137..fb67f9202b9 100644 --- a/persistence/sql/migratest/fixtures/hydra_client/client-0013.json +++ b/persistence/sql/migratest/fixtures/hydra_client/client-0013.json @@ -40,6 +40,18 @@ "Duration": 0, "Valid": false }, + "DeviceAuthorizationGrantAccessTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantIDTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantRefreshTokenLifespan": { + "Duration": 0, + "Valid": false + }, "ImplicitGrantAccessTokenLifespan": { "Duration": 0, "Valid": false diff --git a/persistence/sql/migratest/fixtures/hydra_client/client-0014.json b/persistence/sql/migratest/fixtures/hydra_client/client-0014.json index 7571ef23533..1bc2ef1ea63 100644 --- a/persistence/sql/migratest/fixtures/hydra_client/client-0014.json +++ b/persistence/sql/migratest/fixtures/hydra_client/client-0014.json @@ -40,6 +40,18 @@ "Duration": 0, "Valid": false }, + "DeviceAuthorizationGrantAccessTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantIDTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantRefreshTokenLifespan": { + "Duration": 0, + "Valid": false + }, "ImplicitGrantAccessTokenLifespan": { "Duration": 0, "Valid": false diff --git a/persistence/sql/migratest/fixtures/hydra_client/client-0015.json b/persistence/sql/migratest/fixtures/hydra_client/client-0015.json index ab4ee61170d..42b12e6b492 100644 --- a/persistence/sql/migratest/fixtures/hydra_client/client-0015.json +++ b/persistence/sql/migratest/fixtures/hydra_client/client-0015.json @@ -40,6 +40,18 @@ "Duration": 154000000000, "Valid": true }, + "DeviceAuthorizationGrantAccessTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantIDTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantRefreshTokenLifespan": { + "Duration": 0, + "Valid": false + }, "ImplicitGrantAccessTokenLifespan": { "Duration": 155000000000, "Valid": true diff --git a/persistence/sql/migratest/fixtures/hydra_client/client-20.json b/persistence/sql/migratest/fixtures/hydra_client/client-20.json index 63339ce7da9..fbc35aedfc4 100644 --- a/persistence/sql/migratest/fixtures/hydra_client/client-20.json +++ b/persistence/sql/migratest/fixtures/hydra_client/client-20.json @@ -40,6 +40,18 @@ "Duration": 0, "Valid": false }, + "DeviceAuthorizationGrantAccessTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantIDTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantRefreshTokenLifespan": { + "Duration": 0, + "Valid": false + }, "ImplicitGrantAccessTokenLifespan": { "Duration": 0, "Valid": false diff --git a/persistence/sql/migratest/fixtures/hydra_client/client-2005.json b/persistence/sql/migratest/fixtures/hydra_client/client-2005.json index 140d8a42021..40470238a5b 100644 --- a/persistence/sql/migratest/fixtures/hydra_client/client-2005.json +++ b/persistence/sql/migratest/fixtures/hydra_client/client-2005.json @@ -40,6 +40,18 @@ "Duration": 0, "Valid": false }, + "DeviceAuthorizationGrantAccessTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantIDTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantRefreshTokenLifespan": { + "Duration": 0, + "Valid": false + }, "ImplicitGrantAccessTokenLifespan": { "Duration": 0, "Valid": false diff --git a/persistence/sql/migratest/fixtures/hydra_client/client-21.json b/persistence/sql/migratest/fixtures/hydra_client/client-21.json index 6bc3911af94..7b3e67c7770 100644 --- a/persistence/sql/migratest/fixtures/hydra_client/client-21.json +++ b/persistence/sql/migratest/fixtures/hydra_client/client-21.json @@ -44,6 +44,18 @@ "Duration": 0, "Valid": false }, + "DeviceAuthorizationGrantAccessTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantIDTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantRefreshTokenLifespan": { + "Duration": 0, + "Valid": false + }, "ImplicitGrantAccessTokenLifespan": { "Duration": 0, "Valid": false diff --git a/persistence/sql/migratest/fixtures/hydra_client/client-22.json b/persistence/sql/migratest/fixtures/hydra_client/client-22.json index 13a940c8416..49c9fb5ea91 100644 --- a/persistence/sql/migratest/fixtures/hydra_client/client-22.json +++ b/persistence/sql/migratest/fixtures/hydra_client/client-22.json @@ -44,6 +44,18 @@ "Duration": 0, "Valid": false }, + "DeviceAuthorizationGrantAccessTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantIDTokenLifespan": { + "Duration": 0, + "Valid": false + }, + "DeviceAuthorizationGrantRefreshTokenLifespan": { + "Duration": 0, + "Valid": false + }, "ImplicitGrantAccessTokenLifespan": { "Duration": 0, "Valid": false diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0001.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0001.json index fae8513d60a..1611399b0d1 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0001.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0001.json @@ -29,6 +29,8 @@ "valid": false }, "la": null, + "du": null, + "dh": null, "cc": "challenge-0001", "cs": true, "cv": "verifier-0001", diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0002.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0002.json index bc73e23fc21..6689bda2fb5 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0002.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0002.json @@ -30,6 +30,8 @@ "valid": false }, "la": null, + "du": null, + "dh": null, "cc": "challenge-0002", "cs": true, "cv": "verifier-0002", diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0003.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0003.json index f04dee37267..8214625e79d 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0003.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0003.json @@ -31,6 +31,8 @@ "valid": false }, "la": null, + "du": null, + "dh": null, "cc": "challenge-0003", "cs": true, "cv": "verifier-0003", diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0004.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0004.json index e3b5d630dd2..2b401d6f02e 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0004.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0004.json @@ -34,6 +34,8 @@ "valid": false }, "la": null, + "du": null, + "dh": null, "cc": "challenge-0004", "cs": true, "cv": "verifier-0004", diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0005.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0005.json index db4e0787291..d7c7c9eeb2e 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0005.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0005.json @@ -34,6 +34,8 @@ "valid": false }, "la": null, + "du": null, + "dh": null, "cc": "challenge-0005", "cs": true, "cv": "verifier-0005", diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0006.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0006.json index 7a8b9fd8890..6c6f67e1925 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0006.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0006.json @@ -34,6 +34,8 @@ "valid": false }, "la": null, + "du": null, + "dh": null, "cc": "challenge-0006", "cs": true, "cv": "verifier-0006", diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0007.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0007.json index b5f6814ea47..927c4395df1 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0007.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0007.json @@ -34,6 +34,8 @@ "valid": false }, "la": null, + "du": null, + "dh": null, "cc": "challenge-0007", "cs": true, "cv": "verifier-0007", diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0008.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0008.json index e821518707f..4eafe51c612 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0008.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0008.json @@ -36,6 +36,8 @@ "valid": false }, "la": null, + "du": null, + "dh": null, "cc": "challenge-0008", "cs": true, "cv": "verifier-0008", diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0009.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0009.json index be51195ca6a..ba284a35de7 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0009.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0009.json @@ -36,6 +36,8 @@ "valid": false }, "la": null, + "du": null, + "dh": null, "cc": "challenge-0009", "cs": true, "cv": "verifier-0009", diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0010.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0010.json index 353ed37ffe5..2c620ceb353 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0010.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0010.json @@ -36,6 +36,8 @@ "valid": false }, "la": null, + "du": null, + "dh": null, "cc": "challenge-0010", "cs": true, "cv": "verifier-0010", diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0011.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0011.json index ed92bbce294..fe02c17b38c 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0011.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0011.json @@ -36,6 +36,8 @@ "valid": false }, "la": null, + "du": null, + "dh": null, "cc": "challenge-0011", "cs": true, "cv": "verifier-0011", diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0012.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0012.json index 6375e369280..908b8d6d6f5 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0012.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0012.json @@ -36,6 +36,8 @@ "valid": false }, "la": null, + "du": null, + "dh": null, "cc": "challenge-0012", "cs": true, "cv": "verifier-0012", diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0013.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0013.json index 3939f00e959..367e83e24ed 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0013.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0013.json @@ -36,6 +36,8 @@ "valid": false }, "la": null, + "du": null, + "dh": null, "cc": "challenge-0013", "cs": true, "cv": "verifier-0013", diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0014.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0014.json index 38e0af54056..55ab3eef0df 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0014.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0014.json @@ -36,6 +36,8 @@ "valid": false }, "la": null, + "du": null, + "dh": null, "cc": "challenge-0014", "cs": true, "cv": "verifier-0014", diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0015.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0015.json index f55d9d59c0a..433ce5c8763 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0015.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0015.json @@ -42,6 +42,8 @@ "valid": false }, "la": null, + "du": null, + "dh": null, "cc": "challenge-0015", "cs": true, "cv": "verifier-0015", diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0016.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0016.json index be6ca67a2d1..0c576c04a34 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0016.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0016.json @@ -43,6 +43,8 @@ "valid": false }, "la": null, + "du": null, + "dh": null, "cc": "challenge-0016", "cs": true, "cv": "verifier-0016", diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0017.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0017.json index e8f9235696b..bf289f469cc 100644 --- a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0017.json +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0017.json @@ -44,6 +44,8 @@ "valid": false }, "la": null, + "du": null, + "dh": null, "cc": "challenge-0017", "cs": true, "cv": "verifier-0017", diff --git a/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0018.json b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0018.json new file mode 100644 index 00000000000..210d63747c2 --- /dev/null +++ b/persistence/sql/migratest/fixtures/hydra_oauth2_flow/challenge-0018.json @@ -0,0 +1,78 @@ +{ + "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", + "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.down.sql b/persistence/sql/migrations/20241609000001000000_device_flow.down.sql new file mode 100644 index 00000000000..d7a243c77e6 --- /dev/null +++ b/persistence/sql/migrations/20241609000001000000_device_flow.down.sql @@ -0,0 +1,17 @@ +ALTER TABLE hydra_oauth2_device_auth_codes DROP FOREIGN KEY IF EXISTS hydra_oauth2_device_auth_codes_challenge_id_fk; +ALTER TABLE hydra_oauth2_device_auth_codes DROP FOREIGN KEY IF EXISTS hydra_oauth2_device_auth_codes_client_id_fk; +ALTER TABLE hydra_oauth2_device_auth_codes DROP FOREIGN KEY IF EXISTS hydra_oauth2_device_auth_codes_nid_fk_idx; + +DROP TABLE IF EXISTS hydra_oauth2_device_auth_codes; + +ALTER TABLE hydra_oauth2_flow DROP COLUMN IF EXISTS device_challenge_id; +ALTER TABLE hydra_oauth2_flow DROP COLUMN IF EXISTS device_code_request_id; +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_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; diff --git a/persistence/sql/migrations/20241609000001000000_device_flow.mysql.up.sql b/persistence/sql/migrations/20241609000001000000_device_flow.mysql.up.sql new file mode 100644 index 00000000000..89ae76d125f --- /dev/null +++ b/persistence/sql/migrations/20241609000001000000_device_flow.mysql.up.sql @@ -0,0 +1,54 @@ +CREATE TABLE IF NOT EXISTS hydra_oauth2_device_auth_codes +( + device_code_signature VARCHAR(255) NOT NULL, + user_code_signature VARCHAR(255) NOT NULL, + request_id VARCHAR(40) NOT NULL, + requested_at TIMESTAMP NOT NULL DEFAULT NOW(), + client_id VARCHAR(255) NOT NULL, + scope VARCHAR(1024) NOT NULL, + granted_scope VARCHAR(1024) NOT NULL, + form_data VARCHAR(4096) NOT NULL, + session_data TEXT NOT NULL, + subject VARCHAR(255) NOT NULL DEFAULT '', + device_code_active BOOL NOT NULL DEFAULT true, + user_code_state SMALLINT NOT NULL DEFAULT 0, + requested_audience VARCHAR(1024) NOT NULL, + granted_audience VARCHAR(1024) NOT NULL, + challenge_id VARCHAR(40) NULL, + expires_at TIMESTAMP NULL, + nid CHAR(36) NOT NULL, + + FOREIGN KEY (client_id, nid) REFERENCES hydra_client (id, nid) ON DELETE CASCADE, + FOREIGN KEY (nid) REFERENCES networks (id) ON UPDATE RESTRICT ON DELETE CASCADE, + FOREIGN KEY (challenge_id) REFERENCES hydra_oauth2_flow (consent_challenge_id) ON DELETE CASCADE, + PRIMARY KEY (device_code_signature, nid) +); + +CREATE INDEX hydra_oauth2_device_auth_codes_request_id_idx ON hydra_oauth2_device_auth_codes (request_id, nid); +CREATE INDEX hydra_oauth2_device_auth_codes_client_id_idx ON hydra_oauth2_device_auth_codes (client_id, nid); +CREATE INDEX hydra_oauth2_device_auth_codes_challenge_id_idx ON hydra_oauth2_device_auth_codes (challenge_id); +CREATE UNIQUE INDEX hydra_oauth2_device_auth_codes_user_code_signature_idx ON hydra_oauth2_device_auth_codes (nid, user_code_signature); + +ALTER TABLE hydra_oauth2_flow + ADD COLUMN device_challenge_id VARCHAR(255) NULL; +ALTER TABLE hydra_oauth2_flow + ADD COLUMN device_code_request_id VARCHAR(255) NULL; +ALTER TABLE hydra_oauth2_flow + ADD COLUMN device_verifier VARCHAR(40) NULL; +ALTER TABLE hydra_oauth2_flow + ADD COLUMN device_csrf VARCHAR(40) NULL; +ALTER TABLE hydra_oauth2_flow + ADD COLUMN device_was_used BOOL NULL; +ALTER TABLE hydra_oauth2_flow + ADD COLUMN device_handled_at TIMESTAMP NULL; +ALTER TABLE hydra_oauth2_flow + ADD COLUMN device_error VARCHAR(2048) NULL; + +CREATE UNIQUE INDEX hydra_oauth2_flow_device_challenge_idx ON hydra_oauth2_flow (device_challenge_id); + +ALTER TABLE hydra_client + ADD COLUMN device_authorization_grant_id_token_lifespan BIGINT NULL DEFAULT NULL; +ALTER TABLE hydra_client + ADD COLUMN device_authorization_grant_access_token_lifespan BIGINT NULL DEFAULT NULL; +ALTER TABLE hydra_client + ADD COLUMN device_authorization_grant_refresh_token_lifespan BIGINT NULL DEFAULT NULL; diff --git a/persistence/sql/migrations/20241609000001000000_device_flow.sqlite.up.sql b/persistence/sql/migrations/20241609000001000000_device_flow.sqlite.up.sql new file mode 100644 index 00000000000..d6fe883f137 --- /dev/null +++ b/persistence/sql/migrations/20241609000001000000_device_flow.sqlite.up.sql @@ -0,0 +1,54 @@ +CREATE TABLE IF NOT EXISTS hydra_oauth2_device_auth_codes +( + device_code_signature VARCHAR(255) NOT NULL, + user_code_signature VARCHAR(255) NOT NULL, + request_id VARCHAR(40) NOT NULL, + requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + client_id VARCHAR(255) NOT NULL, + scope VARCHAR(1024) NOT NULL, + granted_scope VARCHAR(1024) NOT NULL, + form_data VARCHAR(4096) NOT NULL, + session_data TEXT NOT NULL, + subject VARCHAR(255) NOT NULL DEFAULT '', + device_code_active BOOL NOT NULL DEFAULT true, + user_code_state SMALLINT NOT NULL DEFAULT 0, + requested_audience VARCHAR(1024) NOT NULL, + granted_audience VARCHAR(1024) NOT NULL, + challenge_id VARCHAR(40) NULL, + expires_at TIMESTAMP NULL, + nid UUID NOT NULL, + + FOREIGN KEY (client_id, nid) REFERENCES hydra_client (id, nid) ON DELETE CASCADE, + FOREIGN KEY (nid) REFERENCES networks (id) ON UPDATE RESTRICT ON DELETE CASCADE, + FOREIGN KEY (challenge_id) REFERENCES hydra_oauth2_flow (consent_challenge_id) ON DELETE CASCADE, + PRIMARY KEY (device_code_signature, nid) +); + +CREATE INDEX hydra_oauth2_device_auth_codes_request_id_idx ON hydra_oauth2_device_auth_codes (request_id, nid); +CREATE INDEX hydra_oauth2_device_auth_codes_client_id_idx ON hydra_oauth2_device_auth_codes (client_id, nid); +CREATE INDEX hydra_oauth2_device_auth_codes_challenge_id_idx ON hydra_oauth2_device_auth_codes (challenge_id); +CREATE UNIQUE INDEX hydra_oauth2_device_auth_codes_user_code_signature_idx ON hydra_oauth2_device_auth_codes (nid, user_code_signature); + +ALTER TABLE hydra_oauth2_flow + ADD COLUMN device_challenge_id VARCHAR(255) NULL; +ALTER TABLE hydra_oauth2_flow + ADD COLUMN device_code_request_id VARCHAR(255) NULL; +ALTER TABLE hydra_oauth2_flow + ADD COLUMN device_verifier VARCHAR(40) NULL; +ALTER TABLE hydra_oauth2_flow + ADD COLUMN device_csrf VARCHAR(40) NULL; +ALTER TABLE hydra_oauth2_flow + ADD COLUMN device_was_used BOOLEAN NULL; +ALTER TABLE hydra_oauth2_flow + ADD COLUMN device_handled_at TIMESTAMP NULL; +ALTER TABLE hydra_oauth2_flow + ADD COLUMN device_error VARCHAR(2048) NULL; + +CREATE UNIQUE INDEX hydra_oauth2_flow_device_challenge_idx ON hydra_oauth2_flow (device_challenge_id); + +ALTER TABLE hydra_client + ADD COLUMN device_authorization_grant_id_token_lifespan BIGINT NULL DEFAULT NULL; +ALTER TABLE hydra_client + ADD COLUMN device_authorization_grant_access_token_lifespan BIGINT NULL DEFAULT NULL; +ALTER TABLE hydra_client + ADD COLUMN device_authorization_grant_refresh_token_lifespan BIGINT NULL DEFAULT NULL; diff --git a/persistence/sql/migrations/20241609000001000000_device_flow.up.sql b/persistence/sql/migrations/20241609000001000000_device_flow.up.sql new file mode 100644 index 00000000000..0e946b5d0d2 --- /dev/null +++ b/persistence/sql/migrations/20241609000001000000_device_flow.up.sql @@ -0,0 +1,54 @@ +CREATE TABLE IF NOT EXISTS hydra_oauth2_device_auth_codes +( + device_code_signature VARCHAR(255) NOT NULL, + user_code_signature VARCHAR(255) NOT NULL, + request_id VARCHAR(40) NOT NULL, + requested_at TIMESTAMP NOT NULL DEFAULT NOW(), + client_id VARCHAR(255) NOT NULL, + scope VARCHAR(1024) NOT NULL, + granted_scope VARCHAR(1024) NOT NULL, + form_data VARCHAR(4096) NOT NULL, + session_data TEXT NOT NULL, + subject VARCHAR(255) NOT NULL DEFAULT '', + device_code_active BOOL NOT NULL DEFAULT true, + user_code_state SMALLINT NOT NULL DEFAULT 0, + requested_audience VARCHAR(1024) NOT NULL, + granted_audience VARCHAR(1024) NOT NULL, + challenge_id VARCHAR(40) NULL, + expires_at TIMESTAMP NULL, + nid UUID NOT NULL, + + FOREIGN KEY (client_id, nid) REFERENCES hydra_client (id, nid) ON DELETE CASCADE, + FOREIGN KEY (nid) REFERENCES networks (id) ON UPDATE RESTRICT ON DELETE CASCADE, + FOREIGN KEY (challenge_id) REFERENCES hydra_oauth2_flow (consent_challenge_id) ON DELETE CASCADE, + PRIMARY KEY (device_code_signature, nid) +); + +CREATE INDEX hydra_oauth2_device_auth_codes_request_id_idx ON hydra_oauth2_device_auth_codes (request_id, nid); +CREATE INDEX hydra_oauth2_device_auth_codes_client_id_idx ON hydra_oauth2_device_auth_codes (client_id, nid); +CREATE INDEX hydra_oauth2_device_auth_codes_challenge_id_idx ON hydra_oauth2_device_auth_codes (challenge_id); +CREATE UNIQUE INDEX hydra_oauth2_device_auth_codes_user_code_signature_idx ON hydra_oauth2_device_auth_codes (nid, user_code_signature); + +ALTER TABLE hydra_oauth2_flow + ADD COLUMN device_challenge_id VARCHAR(255) NULL; +ALTER TABLE hydra_oauth2_flow + ADD COLUMN device_code_request_id VARCHAR(255) NULL; +ALTER TABLE hydra_oauth2_flow + ADD COLUMN device_verifier VARCHAR(40) NULL; +ALTER TABLE hydra_oauth2_flow + ADD COLUMN device_csrf VARCHAR(40) NULL; +ALTER TABLE hydra_oauth2_flow + ADD COLUMN device_was_used BOOLEAN NULL; +ALTER TABLE hydra_oauth2_flow + ADD COLUMN device_handled_at TIMESTAMP NULL; +ALTER TABLE hydra_oauth2_flow + ADD COLUMN device_error VARCHAR(2048) NULL; + +CREATE UNIQUE INDEX hydra_oauth2_flow_device_challenge_idx ON hydra_oauth2_flow (device_challenge_id); + +ALTER TABLE hydra_client + ADD COLUMN device_authorization_grant_id_token_lifespan BIGINT NULL DEFAULT NULL; +ALTER TABLE hydra_client + ADD COLUMN device_authorization_grant_access_token_lifespan BIGINT NULL DEFAULT NULL; +ALTER TABLE hydra_client + ADD COLUMN device_authorization_grant_refresh_token_lifespan BIGINT NULL DEFAULT NULL; diff --git a/persistence/sql/persister_consent.go b/persistence/sql/persister_consent.go index 7b314f275f0..76ef0c9096c 100644 --- a/persistence/sql/persister_consent.go +++ b/persistence/sql/persister_consent.go @@ -224,11 +224,107 @@ func (p *Persister) GetConsentRequest(ctx context.Context, challenge string) (_ return f.GetConsentRequest(challenge), nil } +// CreateDeviceUserAuthRequest creates a new flow from a DeviceUserAuthRequest. +func (p *Persister) CreateDeviceUserAuthRequest(ctx context.Context, req *flow.DeviceUserAuthRequest) (_ *flow.Flow, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateDeviceUserAuthRequest") + defer otelx.End(span, &err) + + nid := p.NetworkID(ctx) + 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, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.GetDeviceUserAuthRequest") + defer otelx.End(span, &err) + + 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.")) + } + + return f.GetDeviceUserAuthRequest(), 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, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.HandleDeviceUserAuthRequest") + defer otelx.End(span, &err) + + 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, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.VerifyAndInvalidateDeviceUserAuthRequest") + defer otelx.End(span, &err) + + 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) CreateLoginRequestFromDeviceRequest(ctx context.Context, f *flow.Flow, req *flow.LoginRequest) (_ *flow.Flow, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateLoginRequestFromDeviceRequest") + defer otelx.End(span, &err) + + f.ID = req.ID + f.LoginSkip = req.Skip + f.Subject = req.Subject + f.SessionID = req.SessionID + f.LoginWasUsed = req.WasHandled + f.ForceSubjectIdentifier = req.ForceSubjectIdentifier + f.LoginVerifier = req.Verifier + f.LoginCSRF = req.CSRF + f.LoginAuthenticatedAt = req.AuthenticatedAt + f.RequestedAt = req.RequestedAt + f.State = flow.FlowStateLoginInitialized + + nid := p.NetworkID(ctx) + if nid == uuid.Nil { + return nil, errorsx.WithStack(x.ErrNotFound) + } + f.NID = nid + + return f, nil +} + func (p *Persister) CreateLoginRequest(ctx context.Context, req *flow.LoginRequest) (_ *flow.Flow, err error) { ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateLoginRequest") defer otelx.End(span, &err) f := flow.NewFlow(req) + nid := p.NetworkID(ctx) if nid == uuid.Nil { return nil, errorsx.WithStack(x.ErrNotFound) diff --git a/persistence/sql/persister_device.go b/persistence/sql/persister_device.go new file mode 100644 index 00000000000..0bf9d8ef74d --- /dev/null +++ b/persistence/sql/persister_device.go @@ -0,0 +1,291 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sql + +import ( + "context" + "database/sql" + "encoding/json" + "fmt" + "net/url" + "strings" + "time" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + "github.com/tidwall/gjson" + + "github.com/ory/fosite" + "github.com/ory/hydra/v2/oauth2" + "github.com/ory/x/errorsx" + "github.com/ory/x/otelx" + "github.com/ory/x/sqlcon" + "github.com/ory/x/sqlxx" + "github.com/ory/x/stringsx" +) + +const ( + sqlTableDeviceAuthCodes tableName = "hydra_oauth2_device_auth_codes" +) + +type DeviceRequestSQL struct { + ID string `db:"device_code_signature"` + UserCodeID string `db:"user_code_signature"` + NID uuid.UUID `db:"nid"` + Request string `db:"request_id"` + ConsentChallenge sql.NullString `db:"challenge_id"` + RequestedAt time.Time `db:"requested_at"` + Client string `db:"client_id"` + Scopes string `db:"scope"` + GrantedScope string `db:"granted_scope"` + RequestedAudience string `db:"requested_audience"` + GrantedAudience string `db:"granted_audience"` + Form string `db:"form_data"` + Subject string `db:"subject"` + DeviceCodeActive bool `db:"device_code_active"` + UserCodeState fosite.UserCodeState `db:"user_code_state"` + Session []byte `db:"session_data"` + // InternalExpiresAt denormalizes the expiry from the session to additionally store it as a row. + InternalExpiresAt sqlxx.NullTime `db:"expires_at" json:"-"` +} + +func (r DeviceRequestSQL) TableName() string { + return string(sqlTableDeviceAuthCodes) +} + +func (r *DeviceRequestSQL) toRequest(ctx context.Context, session fosite.Session, p *Persister) (_ *fosite.DeviceRequest, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.DeviceRequestSQL.toRequest") + defer otelx.End(span, &err) + + sess := r.Session + if !gjson.ValidBytes(sess) { + var err error + sess, err = p.r.KeyCipher().Decrypt(ctx, string(sess), nil) + if err != nil { + return nil, errorsx.WithStack(err) + } + } + + if session != nil { + if err := json.Unmarshal(sess, session); err != nil { + return nil, errorsx.WithStack(err) + } + } else { + p.l.Debugf("Got an empty session in toRequest") + } + + c, err := p.GetClient(ctx, r.Client) + if err != nil { + return nil, err + } + + val, err := url.ParseQuery(r.Form) + if err != nil { + return nil, errorsx.WithStack(err) + } + + return &fosite.DeviceRequest{ + UserCodeState: fosite.UserCodeState(r.UserCodeState), + Request: fosite.Request{ + ID: r.Request, + RequestedAt: r.RequestedAt, + // ExpiresAt does not need to be populated as we get the expiry time from the session. + Client: c, + RequestedScope: stringsx.Splitx(r.Scopes, "|"), + GrantedScope: stringsx.Splitx(r.GrantedScope, "|"), + RequestedAudience: stringsx.Splitx(r.RequestedAudience, "|"), + GrantedAudience: stringsx.Splitx(r.GrantedAudience, "|"), + Form: val, + Session: session, + }, + }, nil +} + +func (p *Persister) sqlDeviceSchemaFromRequest(ctx context.Context, deviceCodeSignature, userCodeSignature string, r fosite.DeviceRequester, expiresAt time.Time) (*DeviceRequestSQL, error) { + subject := "" + if r.GetSession() == nil { + p.l.Debugf("Got an empty session in sqlSchemaFromRequest") + } else { + subject = r.GetSession().GetSubject() + } + + session, err := json.Marshal(r.GetSession()) + if err != nil { + return nil, errorsx.WithStack(err) + } + + if p.config.EncryptSessionData(ctx) { + ciphertext, err := p.r.KeyCipher().Encrypt(ctx, session, nil) + if err != nil { + return nil, errorsx.WithStack(err) + } + session = []byte(ciphertext) + } + + var challenge sql.NullString + rr, ok := r.GetSession().(*oauth2.Session) + if !ok && r.GetSession() != nil { + return nil, errors.Errorf("Expected request to be of type *Session, but got: %T", r.GetSession()) + } else if ok { + if len(rr.ConsentChallenge) > 0 { + challenge = sql.NullString{Valid: true, String: rr.ConsentChallenge} + } + } + + return &DeviceRequestSQL{ + Request: r.GetID(), + ConsentChallenge: challenge, + ID: deviceCodeSignature, + UserCodeID: userCodeSignature, + RequestedAt: r.GetRequestedAt(), + InternalExpiresAt: sqlxx.NullTime(expiresAt), + Client: r.GetClient().GetID(), + Scopes: strings.Join(r.GetRequestedScopes(), "|"), + GrantedScope: strings.Join(r.GetGrantedScopes(), "|"), + GrantedAudience: strings.Join(r.GetGrantedAudience(), "|"), + RequestedAudience: strings.Join(r.GetRequestedAudience(), "|"), + Form: r.GetRequestForm().Encode(), + Session: session, + Subject: subject, + DeviceCodeActive: true, + UserCodeState: r.GetUserCodeState(), + }, nil +} + +// CreateDeviceCodeSession creates a new device code session and stores it in the database +func (p *Persister) CreateDeviceAuthSession(ctx context.Context, deviceCodeSignature, userCodeSignature string, requester fosite.DeviceRequester) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateDeviceCodeSession") + defer otelx.End(span, &err) + + req, err := p.sqlDeviceSchemaFromRequest(ctx, deviceCodeSignature, userCodeSignature, requester, requester.GetSession().GetExpiresAt(fosite.DeviceCode).UTC()) + if err != nil { + return err + } + + if err := sqlcon.HandleError(p.CreateWithNetwork(ctx, req)); errors.Is(err, sqlcon.ErrConcurrentUpdate) { + return errors.Wrap(fosite.ErrSerializationFailure, err.Error()) + } else if errors.Is(err, sqlcon.ErrUniqueViolation) { + return errors.Wrap(fosite.ErrExistingUserCodeSignature, err.Error()) + } else if err != nil { + return err + } + + return nil +} + +// UpdateDeviceCodeSessionBySignature updates a device code session by the device_code signature +func (p *Persister) UpdateDeviceCodeSessionBySignature(ctx context.Context, signature string, requester fosite.DeviceRequester) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.UpdateDeviceCodeSessionBySignature") + defer otelx.End(span, &err) + + req, err := p.sqlDeviceSchemaFromRequest(ctx, signature, "", requester, requester.GetSession().GetExpiresAt(fosite.DeviceCode).UTC()) + if err != nil { + return err + } + + stmt := fmt.Sprintf( + "UPDATE %s SET granted_scope=?, granted_audience=?, session_data=?, user_code_state=?, subject=?, challenge_id=? WHERE device_code_signature=? AND nid = ?", + sqlTableDeviceAuthCodes, + ) + + /* #nosec G201 table is static */ + return sqlcon.HandleError( + p.Connection(ctx).RawQuery(stmt, + req.GrantedScope, req.GrantedAudience, + req.Session, req.UserCodeState, + req.Subject, req.ConsentChallenge, + signature, p.NetworkID(ctx), + ).Exec(), + ) +} + +// GetDeviceCodeSession returns a device code session from the database +func (p *Persister) GetDeviceCodeSession(ctx context.Context, signature string, session fosite.Session) (_ fosite.DeviceRequester, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.GetDeviceCodeSession") + defer otelx.End(span, &err) + + r := DeviceRequestSQL{} + if err = p.QueryWithNetwork(ctx).Where("device_code_signature = ?", signature).First(&r); errors.Is(err, sql.ErrNoRows) { + return nil, errorsx.WithStack(fosite.ErrNotFound) + } else if err != nil { + return nil, sqlcon.HandleError(err) + } + + if !r.DeviceCodeActive { + fr, err := r.toRequest(ctx, session, p) + if err != nil { + return nil, err + } + return fr, errorsx.WithStack(fosite.ErrInactiveToken) + } + + return r.toRequest(ctx, session, p) +} + +// GetDeviceCodeSessionByRequestID returns a device code session from the database +func (p *Persister) GetDeviceCodeSessionByRequestID(ctx context.Context, requestID string, session fosite.Session) (_ fosite.DeviceRequester, deviceCodeSignature string, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.GetDeviceCodeSessionByRequestID") + defer otelx.End(span, &err) + + r := DeviceRequestSQL{} + if err = p.QueryWithNetwork(ctx).Where("request_id = ?", requestID).First(&r); errors.Is(err, sql.ErrNoRows) { + return nil, "", errorsx.WithStack(fosite.ErrNotFound) + } else if err != nil { + return nil, "", sqlcon.HandleError(err) + } + + if !r.DeviceCodeActive { + fr, err := r.toRequest(ctx, session, p) + if err != nil { + return nil, "", err + } + return fr, r.ID, errorsx.WithStack(fosite.ErrInactiveToken) + } + + fr, err := r.toRequest(ctx, session, p) + if err != nil { + return nil, "", err + } + return fr, r.ID, nil +} + +// InvalidateDeviceCodeSession invalidates a device code session +func (p *Persister) InvalidateDeviceCodeSession(ctx context.Context, signature string) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.InvalidateDeviceCodeSession") + defer otelx.End(span, &err) + + /* #nosec G201 table is static */ + return sqlcon.HandleError( + p.QueryWithNetwork(ctx). + Where("device_code_signature = ?", signature). + Delete(DeviceRequestSQL{})) +} + +// GetUserCodeSession returns a user code session from the database +func (p *Persister) GetUserCodeSession(ctx context.Context, signature string, session fosite.Session) (_ fosite.DeviceRequester, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.GetUserCodeSession") + defer otelx.End(span, &err) + + r := DeviceRequestSQL{} + if session == nil { + session = oauth2.NewSession("") + } + + if err = p.QueryWithNetwork(ctx).Where("user_code_signature = ?", signature).First(&r); errors.Is(err, sql.ErrNoRows) { + return nil, errorsx.WithStack(fosite.ErrNotFound) + } else if err != nil { + return nil, sqlcon.HandleError(err) + } + + fr, err := r.toRequest(ctx, session, p) + if err != nil { + return nil, err + } + + if r.UserCodeState != fosite.UserCodeUnused { + return fr, errorsx.WithStack(fosite.ErrInactiveToken) + } + + return fr, err +} diff --git a/persistence/sql/persister_nid_test.go b/persistence/sql/persister_nid_test.go index 99090a47690..108ae42e7cc 100644 --- a/persistence/sql/persister_nid_test.go +++ b/persistence/sql/persister_nid_test.go @@ -1365,7 +1365,7 @@ func (s *PersisterTestSuite) TestGetPublicKeys() { ks := newKeySet(issuer, "use") grant := trust.Grant{ ID: uuid.Must(uuid.NewV4()).String(), - ExpiresAt: time.Now().Add(time.Hour), + ExpiresAt: time.Now().UTC().Add(time.Hour), Issuer: issuer, PublicKey: trust.PublicKey{Set: issuer, KeyID: ks.Keys[0].KeyID}, } @@ -2176,7 +2176,7 @@ func (s *PersisterTestSuite) TestVerifyAndInvalidateLogoutRequest() { t.Run("case=logout request that expired returns error", func(t *testing.T) { lr := newLogoutRequest() - lr.ExpiresAt = sqlxx.NullTime(time.Now().Add(-time.Hour)) + lr.ExpiresAt = sqlxx.NullTime(time.Now().UTC().Add(-time.Hour)) lr.Verifier = uuid.Must(uuid.NewV4()).String() lr.Accepted = true lr.Rejected = false diff --git a/spec/api.json b/spec/api.json index c1ae0ecf1dd..772573fdb61 100644 --- a/spec/api.json +++ b/spec/api.json @@ -68,6 +68,35 @@ "type": "object" }, "DefaultError": {}, + "DeviceUserAuthRequest": { + "properties": { + "challenge": { + "description": "ID is the identifier (\"device challenge\") of the device grant request. It is used to\nidentify the session.", + "type": "string" + }, + "client": { + "$ref": "#/components/schemas/oAuth2Client" + }, + "handled_at": { + "$ref": "#/components/schemas/nullTime" + }, + "request_url": { + "description": "RequestURL is the original Device Authorization URL requested.", + "type": "string" + }, + "requested_access_token_audience": { + "$ref": "#/components/schemas/StringSliceJSONFormat" + }, + "requested_scope": { + "$ref": "#/components/schemas/StringSliceJSONFormat" + } + }, + "required": [ + "challenge" + ], + "title": "Contains information on an ongoing device grant request.", + "type": "object" + }, "JSONRawMessage": { "title": "JSONRawMessage represents a json.RawMessage that works well with JSON, SQL, and Swagger." }, @@ -148,6 +177,15 @@ "title": "VerifiableCredentialProof contains the proof of a verifiable credential.", "type": "object" }, + "acceptDeviceUserCodeRequest": { + "description": "Contains information on an device verification", + "properties": { + "user_code": { + "type": "string" + } + }, + "type": "object" + }, "acceptOAuth2ConsentRequest": { "properties": { "context": { @@ -289,6 +327,45 @@ "title": "Verifiable Credentials Metadata (Draft 00)", "type": "object" }, + "deviceAuthorization": { + "description": "# Ory's OAuth 2.0 Device Authorization API", + "properties": { + "device_code": { + "description": "The device verification code.", + "example": "ory_dc_smldfksmdfkl.mslkmlkmlk", + "type": "string" + }, + "expires_in": { + "description": "The lifetime in seconds of the \"device_code\" and \"user_code\".", + "example": 16830, + "format": "int64", + "type": "integer" + }, + "interval": { + "description": "The minimum amount of time in seconds that the client\nSHOULD wait between polling requests to the token endpoint. If no\nvalue is provided, clients MUST use 5 as the default.", + "example": 5, + "format": "int64", + "type": "integer" + }, + "user_code": { + "description": "The end-user verification code.", + "example": "AAAAAA", + "type": "string" + }, + "verification_uri": { + "description": "The end-user verification URI on the authorization\nserver. The URI should be short and easy to remember as end users\nwill be asked to manually type it into their user agent.", + "example": "https://auth.ory.sh/tv", + "type": "string" + }, + "verification_uri_complete": { + "description": "A verification URI that includes the \"user_code\" (or\nother information with the same function as the \"user_code\"),\nwhich is designed for non-textual transmission.", + "example": "https://auth.ory.sh/tv?user_code=AAAAAA", + "type": "string" + } + }, + "title": "OAuth2 Device Flow", + "type": "object" + }, "errorOAuth2": { "description": "Error", "properties": { @@ -670,6 +747,15 @@ "format": "date-time", "type": "string" }, + "device_authorization_grant_access_token_lifespan": { + "$ref": "#/components/schemas/NullDuration" + }, + "device_authorization_grant_id_token_lifespan": { + "$ref": "#/components/schemas/NullDuration" + }, + "device_authorization_grant_refresh_token_lifespan": { + "$ref": "#/components/schemas/NullDuration" + }, "frontchannel_logout_session_required": { "description": "OpenID Connect Front-Channel Logout Session Required\n\nBoolean value specifying whether the RP requires that iss (issuer) and sid (session ID) query parameters be\nincluded to identify the RP session with the OP when the frontchannel_logout_uri is used.\nIf omitted, the default value is false.", "type": "boolean" @@ -807,6 +893,15 @@ "client_credentials_grant_access_token_lifespan": { "$ref": "#/components/schemas/NullDuration" }, + "device_authorization_grant_access_token_lifespan": { + "$ref": "#/components/schemas/NullDuration" + }, + "device_authorization_grant_id_token_lifespan": { + "$ref": "#/components/schemas/NullDuration" + }, + "device_authorization_grant_refresh_token_lifespan": { + "$ref": "#/components/schemas/NullDuration" + }, "implicit_grant_access_token_lifespan": { "$ref": "#/components/schemas/NullDuration" }, @@ -852,6 +947,10 @@ "context": { "$ref": "#/components/schemas/JSONRawMessage" }, + "device_challenge_id": { + "description": "DeviceChallenge is the device challenge this consent challenge belongs to, if this flow was initiated by a device.", + "type": "string" + }, "login_challenge": { "description": "LoginChallenge is the login challenge this consent challenge belongs to. It can be used to associate\na login and consent request in the login \u0026 consent app.", "type": "string" @@ -1161,6 +1260,11 @@ }, "type": "array" }, + "device_authorization_endpoint": { + "description": "OAuth 2.0 Device Authorization Endpoint URL", + "example": "https://playground.ory.sh/ory-hydra/public/oauth2/device/oauth", + "type": "string" + }, "end_session_endpoint": { "description": "OpenID Connect End-Session Endpoint\n\nURL at the OP to which an RP can perform a redirect to request that the End-User be logged out at the OP.", "type": "string" @@ -1294,6 +1398,7 @@ "required": [ "issuer", "authorization_endpoint", + "device_authorization_endpoint", "token_endpoint", "jwks_uri", "subject_types_supported", @@ -1679,6 +1784,35 @@ "title": "VerifiableCredentialResponse contains the verifiable credential.", "type": "object" }, + "verifyUserCodeRequest": { + "properties": { + "challenge": { + "description": "ID is the identifier (\"device challenge\") of the device request. It is used to\nidentify the session.", + "type": "string" + }, + "client": { + "$ref": "#/components/schemas/oAuth2Client" + }, + "device_code_request_id": { + "type": "string" + }, + "handled_at": { + "$ref": "#/components/schemas/nullTime" + }, + "request_url": { + "description": "RequestURL is the original Device Authorization URL requested.", + "type": "string" + }, + "requested_access_token_audience": { + "$ref": "#/components/schemas/StringSliceJSONFormat" + }, + "requested_scope": { + "$ref": "#/components/schemas/StringSliceJSONFormat" + } + }, + "title": "HandledDeviceUserAuthRequest is the request payload used to accept a device user_code.", + "type": "object" + }, "version": { "properties": { "version": { @@ -2605,6 +2739,58 @@ ] } }, + "/admin/oauth2/auth/requests/device/accept": { + "put": { + "description": "Accepts a device grant user_code request", + "operationId": "acceptUserCodeRequest", + "parameters": [ + { + "in": "query", + "name": "device_challenge", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/acceptDeviceUserCodeRequest" + } + } + }, + "x-originalParamName": "Body" + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/oAuth2RedirectTo" + } + } + }, + "description": "oAuth2RedirectTo" + }, + "default": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorOAuth2" + } + } + }, + "description": "errorOAuth2" + } + }, + "summary": "Accepts a device grant user_code request", + "tags": [ + "oAuth2" + ] + } + }, "/admin/oauth2/auth/requests/login": { "get": { "description": "When an authorization code, hybrid, or implicit OAuth 2.0 Flow is initiated, Ory asks the login provider\nto authenticate the subject and then tell the Ory OAuth2 Service about it.\n\nPer default, the login provider is Ory itself. You may use a different login provider which needs to be a web-app\nyou write and host, and it must be able to authenticate (\"show the subject a login screen\")\na subject (in OAuth2 the proper name for subject is \"resource owner\").\n\nThe authentication challenge is appended to the login provider URL to which the subject's user-agent (browser) is redirected to. The login\nprovider uses that challenge to fetch information on the OAuth2 request and then accept or reject the requested authentication process.", @@ -3493,6 +3679,63 @@ ] } }, + "/oauth2/device/auth": { + "post": { + "description": "This endpoint is not documented here because you should never use your own implementation to perform OAuth2 flows.\nOAuth2 is a very popular protocol and a library for your programming language will exists.\n\nTo learn more about this flow please refer to the specification: https://tools.ietf.org/html/rfc8628", + "operationId": "oAuth2DeviceFlow", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/deviceAuthorization" + } + } + }, + "description": "deviceAuthorization" + }, + "default": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorOAuth2" + } + } + }, + "description": "errorOAuth2" + } + }, + "summary": "The OAuth 2.0 Device Authorize Endpoint", + "tags": [ + "oAuth2" + ] + } + }, + "/oauth2/device/verify": { + "get": { + "description": "This is the device user verification endpoint. The user is redirected here when trying to login using the device flow.", + "operationId": "performOAuth2DeviceVerificationFlow", + "responses": { + "302": { + "$ref": "#/components/responses/emptyResponse" + }, + "default": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorOAuth2" + } + } + }, + "description": "errorOAuth2" + } + }, + "summary": "OAuth 2.0 Device Verification Endpoint", + "tags": [ + "oAuth2" + ] + } + }, "/oauth2/register": { "post": { "description": "This endpoint behaves like the administrative counterpart (`createOAuth2Client`) but is capable of facing the\npublic internet directly and can be used in self-service. It implements the OpenID Connect\nDynamic Client Registration Protocol. This feature needs to be enabled in the configuration. This endpoint\nis disabled by default. It can be enabled by an administrator.\n\nPlease note that using this endpoint you are not able to choose the `client_secret` nor the `client_id` as those\nvalues will be server generated when specifying `token_endpoint_auth_method` as `client_secret_basic` or\n`client_secret_post`.\n\nThe `client_secret` will be returned in the response and you will not be able to retrieve it later on.\nWrite the secret down and keep it somewhere safe.", diff --git a/spec/config.json b/spec/config.json index f4f5dcbd60a..275e1a1110a 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,30 @@ "/ui/logout" ] }, + "device": { + "type": "object", + "description": "Configure URLs for the OAuth 2.0 Device Code Flow.", + "properties": { + "verification": { + "type": "string", + "description": "Sets the device user code verification endpoint. Defaults to an internal fallback URL showing an error.", + "format": "uri-reference", + "examples": [ + "https://my-logout.app/device_verification", + "/ui/device_verification" + ] + }, + "success": { + "type": "string", + "description": "Sets the post device authentication endpoint. Defaults to an internal fallback URL showing an error.", + "format": "uri-reference", + "examples": [ + "https://my-logout.app/device_done", + "/ui/device_done" + ] + } + } + }, "error": { "type": "string", "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 +984,15 @@ "$ref": "#/definitions/duration" } ] + }, + "device_user_code": { + "description": "Configures how long device & user codes are valid.", + "default": "10m", + "allOf": [ + { + "$ref": "#/definitions/duration" + } + ] } } }, @@ -1124,6 +1170,28 @@ } ] }, + "device_authorization": { + "type": "object", + "additionalProperties": false, + "properties": { + "token_polling_interval": { + "allOf": [ + { + "$ref": "#/definitions/duration" + } + ], + "default": "5s", + "description": "Configures how often a non-interactive device should poll the device token endpoint, this is a purely informational configuration and does not enforce rate-limiting.", + "examples": ["5s", "15s", "1m"] + }, + "user_code_entropy": { + "type": "string", + "description": "Sets the entropy for the user codes.", + "default": "medium", + "enum": ["high", "medium", "low"] + } + } + }, "token_hook": { "description": "Sets the token hook endpoint for all grant types. If set it will be called while providing token to customize claims.", "examples": ["https://my-example.app/token-hook"], diff --git a/spec/swagger.json b/spec/swagger.json index 14758f6e806..0db4dac74e7 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -891,6 +891,55 @@ } } }, + "/admin/oauth2/auth/requests/device/accept": { + "put": { + "description": "Accepts a device grant user_code request", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "schemes": [ + "http", + "https" + ], + "tags": [ + "oAuth2" + ], + "summary": "Accepts a device grant user_code request", + "operationId": "acceptUserCodeRequest", + "parameters": [ + { + "type": "string", + "name": "device_challenge", + "in": "query", + "required": true + }, + { + "name": "Body", + "in": "body", + "schema": { + "$ref": "#/definitions/acceptDeviceUserCodeRequest" + } + } + ], + "responses": { + "200": { + "description": "oAuth2RedirectTo", + "schema": { + "$ref": "#/definitions/oAuth2RedirectTo" + } + }, + "default": { + "description": "errorOAuth2", + "schema": { + "$ref": "#/definitions/errorOAuth2" + } + } + } + } + }, "/admin/oauth2/auth/requests/login": { "get": { "description": "When an authorization code, hybrid, or implicit OAuth 2.0 Flow is initiated, Ory asks the login provider\nto authenticate the subject and then tell the Ory OAuth2 Service about it.\n\nPer default, the login provider is Ory itself. You may use a different login provider which needs to be a web-app\nyou write and host, and it must be able to authenticate (\"show the subject a login screen\")\na subject (in OAuth2 the proper name for subject is \"resource owner\").\n\nThe authentication challenge is appended to the login provider URL to which the subject's user-agent (browser) is redirected to. The login\nprovider uses that challenge to fetch information on the OAuth2 request and then accept or reject the requested authentication process.", @@ -1718,6 +1767,65 @@ } } }, + "/oauth2/device/auth": { + "post": { + "description": "This endpoint is not documented here because you should never use your own implementation to perform OAuth2 flows.\nOAuth2 is a very popular protocol and a library for your programming language will exists.\n\nTo learn more about this flow please refer to the specification: https://tools.ietf.org/html/rfc8628", + "consumes": [ + "application/x-www-form-urlencoded" + ], + "schemes": [ + "http", + "https" + ], + "tags": [ + "oAuth2" + ], + "summary": "The OAuth 2.0 Device Authorize Endpoint", + "operationId": "oAuth2DeviceFlow", + "responses": { + "200": { + "description": "deviceAuthorization", + "schema": { + "$ref": "#/definitions/deviceAuthorization" + } + }, + "default": { + "description": "errorOAuth2", + "schema": { + "$ref": "#/definitions/errorOAuth2" + } + } + } + } + }, + "/oauth2/device/verify": { + "get": { + "description": "This is the device user verification endpoint. The user is redirected here when trying to login using the device flow.", + "consumes": [ + "application/x-www-form-urlencoded" + ], + "schemes": [ + "http", + "https" + ], + "tags": [ + "oAuth2" + ], + "summary": "OAuth 2.0 Device Verification Endpoint", + "operationId": "performOAuth2DeviceVerificationFlow", + "responses": { + "302": { + "$ref": "#/responses/emptyResponse" + }, + "default": { + "description": "errorOAuth2", + "schema": { + "$ref": "#/definitions/errorOAuth2" + } + } + } + } + }, "/oauth2/register": { "post": { "description": "This endpoint behaves like the administrative counterpart (`createOAuth2Client`) but is capable of facing the\npublic internet directly and can be used in self-service. It implements the OpenID Connect\nDynamic Client Registration Protocol. This feature needs to be enabled in the configuration. This endpoint\nis disabled by default. It can be enabled by an administrator.\n\nPlease note that using this endpoint you are not able to choose the `client_secret` nor the `client_id` as those\nvalues will be server generated when specifying `token_endpoint_auth_method` as `client_secret_basic` or\n`client_secret_post`.\n\nThe `client_secret` will be returned in the response and you will not be able to retrieve it later on.\nWrite the secret down and keep it somewhere safe.", @@ -2124,6 +2232,35 @@ } }, "DefaultError": {}, + "DeviceUserAuthRequest": { + "type": "object", + "title": "Contains information on an ongoing device grant request.", + "required": [ + "challenge" + ], + "properties": { + "challenge": { + "description": "ID is the identifier (\"device challenge\") of the device grant request. It is used to\nidentify the session.", + "type": "string" + }, + "client": { + "$ref": "#/definitions/oAuth2Client" + }, + "handled_at": { + "$ref": "#/definitions/nullTime" + }, + "request_url": { + "description": "RequestURL is the original Device Authorization URL requested.", + "type": "string" + }, + "requested_access_token_audience": { + "$ref": "#/definitions/StringSliceJSONFormat" + }, + "requested_scope": { + "$ref": "#/definitions/StringSliceJSONFormat" + } + } + }, "JSONRawMessage": { "type": "object", "title": "JSONRawMessage represents a json.RawMessage that works well with JSON, SQL, and Swagger." @@ -2174,6 +2311,15 @@ } } }, + "acceptDeviceUserCodeRequest": { + "description": "Contains information on an device verification", + "type": "object", + "properties": { + "user_code": { + "type": "string" + } + } + }, "acceptOAuth2ConsentRequest": { "type": "object", "title": "The request payload used to accept a consent request.", @@ -2319,6 +2465,45 @@ } } }, + "deviceAuthorization": { + "description": "# Ory's OAuth 2.0 Device Authorization API", + "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", @@ -2695,6 +2880,15 @@ "type": "string", "format": "date-time" }, + "device_authorization_grant_access_token_lifespan": { + "$ref": "#/definitions/NullDuration" + }, + "device_authorization_grant_id_token_lifespan": { + "$ref": "#/definitions/NullDuration" + }, + "device_authorization_grant_refresh_token_lifespan": { + "$ref": "#/definitions/NullDuration" + }, "frontchannel_logout_session_required": { "description": "OpenID Connect Front-Channel Logout Session Required\n\nBoolean value specifying whether the RP requires that iss (issuer) and sid (session ID) query parameters be\nincluded to identify the RP session with the OP when the frontchannel_logout_uri is used.\nIf omitted, the default value is false.", "type": "boolean" @@ -2832,6 +3026,15 @@ "client_credentials_grant_access_token_lifespan": { "$ref": "#/definitions/NullDuration" }, + "device_authorization_grant_access_token_lifespan": { + "$ref": "#/definitions/NullDuration" + }, + "device_authorization_grant_id_token_lifespan": { + "$ref": "#/definitions/NullDuration" + }, + "device_authorization_grant_refresh_token_lifespan": { + "$ref": "#/definitions/NullDuration" + }, "implicit_grant_access_token_lifespan": { "$ref": "#/definitions/NullDuration" }, @@ -2880,6 +3083,10 @@ "context": { "$ref": "#/definitions/JSONRawMessage" }, + "device_challenge_id": { + "description": "DeviceChallenge is the device challenge this consent challenge belongs to, if this flow was initiated by a device.", + "type": "string" + }, "login_challenge": { "description": "LoginChallenge is the login challenge this consent challenge belongs to. It can be used to associate\na login and consent request in the login \u0026 consent app.", "type": "string" @@ -3121,6 +3328,7 @@ "required": [ "issuer", "authorization_endpoint", + "device_authorization_endpoint", "token_endpoint", "jwks_uri", "subject_types_supported", @@ -3172,6 +3380,11 @@ "$ref": "#/definitions/credentialSupportedDraft00" } }, + "device_authorization_endpoint": { + "description": "OAuth 2.0 Device Authorization Endpoint URL", + "type": "string", + "example": "https://playground.ory.sh/ory-hydra/public/oauth2/device/oauth" + }, "end_session_endpoint": { "description": "OpenID Connect End-Session Endpoint\n\nURL at the OP to which an RP can perform a redirect to request that the End-User be logged out at the OP.", "type": "string" @@ -3677,6 +3890,35 @@ } } }, + "verifyUserCodeRequest": { + "type": "object", + "title": "HandledDeviceUserAuthRequest is the request payload used to accept a device user_code.", + "properties": { + "challenge": { + "description": "ID is the identifier (\"device challenge\") of the device request. It is used to\nidentify the session.", + "type": "string" + }, + "client": { + "$ref": "#/definitions/oAuth2Client" + }, + "device_code_request_id": { + "type": "string" + }, + "handled_at": { + "$ref": "#/definitions/nullTime" + }, + "request_url": { + "description": "RequestURL is the original Device Authorization URL requested.", + "type": "string" + }, + "requested_access_token_audience": { + "$ref": "#/definitions/StringSliceJSONFormat" + }, + "requested_scope": { + "$ref": "#/definitions/StringSliceJSONFormat" + } + } + }, "version": { "type": "object", "properties": { diff --git a/test/e2e/circle-ci.bash b/test/e2e/circle-ci.bash index 03b8f70ac79..bad46d4adcf 100755 --- a/test/e2e/circle-ci.bash +++ b/test/e2e/circle-ci.bash @@ -38,6 +38,8 @@ fi (cd oauth2-client; PORT=5002 HYDRA_ADMIN_URL=http://127.0.0.1:5001 npm run consent > ../login-consent-logout.e2e.log 2>&1 &) export URLS_SELF_ISSUER=http://127.0.0.1:5004/ +export URLS_DEVICE_VERIFICATION=http://127.0.0.1:5002/device/verify +export URLS_DEVICE_SUCCESS=http://127.0.0.1:5002/oauth2/device/success export URLS_CONSENT=http://127.0.0.1:5002/consent export URLS_LOGIN=http://127.0.0.1:5002/login export URLS_LOGOUT=http://127.0.0.1:5002/logout diff --git a/test/e2e/oauth2-client/package-lock.json b/test/e2e/oauth2-client/package-lock.json index ffbd7675356..b208455fa0a 100644 --- a/test/e2e/oauth2-client/package-lock.json +++ b/test/e2e/oauth2-client/package-lock.json @@ -13,7 +13,7 @@ "express": "^4.21.2", "express-session": "^1.17.0", "express-winston": "^3.4.0", - "hydra-login-consent-logout": "2.0.4-pre.2", + "hydra-login-consent-logout": "2.4.0-pre.3", "jsonwebtoken": "^8.5.1", "jwks-rsa": "^2.1.4", "node-fetch": "^2.6.0", @@ -27,6 +27,52 @@ "nodemon": "^2.0.22" } }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.9.tgz", + "integrity": "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.9" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.9.tgz", + "integrity": "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@hapi/address": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz", @@ -66,13 +112,11 @@ "@hapi/hoek": "^8.3.0" } }, - "node_modules/@ory/client": { - "version": "0.2.0-alpha.60", - "resolved": "https://registry.npmjs.org/@ory/client/-/client-0.2.0-alpha.60.tgz", - "integrity": "sha512-fGovJ/xIl7dvJJP9/IL4Xu1yiOCy9pvmkfj2xnHZbPrIbL9c9tqVcC3CSlzBq6zJQZMC3XI7VmZ8uEQ+cF4suw==", - "dependencies": { - "axios": "^0.21.4" - } + "node_modules/@ory/hydra-client-fetch": { + "version": "2.4.0-alpha.1", + "resolved": "https://registry.npmjs.org/@ory/hydra-client-fetch/-/hydra-client-fetch-2.4.0-alpha.1.tgz", + "integrity": "sha512-TTuw+1DdIFskz4JU7yP2OSOHP3pVr7HjLnr8YI4S1pQJ91JaGwBQxCPQLrqdNuffs7d8JU1VRkcgdSWOo+89eA==", + "license": "Apache-2.0" }, "node_modules/@panva/asn1.js": { "version": "1.0.0", @@ -90,19 +134,6 @@ "node": ">=4" } }, - "node_modules/@types/babel-types": { - "version": "7.0.7", - "resolved": "https://registry.npmjs.org/@types/babel-types/-/babel-types-7.0.7.tgz", - "integrity": "sha512-dBtBbrc+qTHy1WdfHYjBwRln4+LWqASWakLHsWHR2NWHIFkv4W3O070IGoGLEBrJBvct3r0L1BUPuvURi7kYUQ==" - }, - "node_modules/@types/babylon": { - "version": "6.16.5", - "resolved": "https://registry.npmjs.org/@types/babylon/-/babylon-6.16.5.tgz", - "integrity": "sha512-xH2e58elpj1X4ynnKp9qSnWlsRTIs6n3tgLGNfwAGHwePw0mulHQllV34n0T25uYSu1k0hRKkWXF890B1yS47w==", - "dependencies": { - "@types/babel-types": "*" - } - }, "node_modules/@types/body-parser": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", @@ -226,28 +257,10 @@ } }, "node_modules/acorn": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", - "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-globals": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-3.1.0.tgz", - "integrity": "sha1-/YJw9x+7SZawBPqIDuXUZXOnMb8=", - "dependencies": { - "acorn": "^4.0.4" - } - }, - "node_modules/acorn-globals/node_modules/acorn": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", - "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=", + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -267,30 +280,6 @@ "node": ">=4" } }, - "node_modules/align-text": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", - "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", - "dependencies": { - "kind-of": "^3.0.2", - "longest": "^1.0.1", - "repeat-string": "^1.5.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/align-text/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", @@ -323,7 +312,14 @@ "node_modules/asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "license": "MIT" + }, + "node_modules/assert-never": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.4.0.tgz", + "integrity": "sha512-5oJg84os6NMQNl27T9LnZkvvqzvAnHu03ShCnoj6bsJwS7L8AO4lf+C/XjK/nvzEqQB744moC6V128RucQd1jA==", + "license": "MIT" }, "node_modules/async": { "version": "2.6.4", @@ -333,40 +329,16 @@ "lodash": "^4.17.14" } }, - "node_modules/axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", - "dependencies": { - "follow-redirects": "^1.14.0" - } - }, - "node_modules/babel-runtime": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", - "dependencies": { - "core-js": "^2.4.0", - "regenerator-runtime": "^0.11.0" - } - }, - "node_modules/babel-types": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", - "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "node_modules/babel-walk": { + "version": "3.0.0-canary-5", + "resolved": "https://registry.npmjs.org/babel-walk/-/babel-walk-3.0.0-canary-5.tgz", + "integrity": "sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==", + "license": "MIT", "dependencies": { - "babel-runtime": "^6.26.0", - "esutils": "^2.0.2", - "lodash": "^4.17.4", - "to-fast-properties": "^1.0.3" - } - }, - "node_modules/babylon": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", - "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", - "bin": { - "babylon": "bin/babylon.js" + "@babel/types": "^7.9.6" + }, + "engines": { + "node": ">= 10.0.0" } }, "node_modules/balanced-match": { @@ -557,16 +529,33 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/center-align": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", - "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", "dependencies": { - "align-text": "^0.1.3", - "lazy-cache": "^1.0.3" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", + "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/chalk": { @@ -585,7 +574,8 @@ "node_modules/character-parser": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz", - "integrity": "sha1-x84o821LzZdE5f/CxfzeHHMmH8A=", + "integrity": "sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==", + "license": "MIT", "dependencies": { "is-regex": "^1.0.3" } @@ -617,25 +607,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/clean-css": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz", - "integrity": "sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA==", - "dependencies": { - "source-map": "~0.6.0" - }, - "engines": { - "node": ">= 4.0" - } - }, - "node_modules/clean-css/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/clean-stack": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-1.3.0.tgz", @@ -644,16 +615,6 @@ "node": ">=4" } }, - "node_modules/cliui": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", - "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", - "dependencies": { - "center-align": "^0.1.1", - "right-align": "^0.1.1", - "wordwrap": "0.0.2" - } - }, "node_modules/clone-response": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", @@ -722,14 +683,13 @@ "dev": true }, "node_modules/constantinople": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-3.1.2.tgz", - "integrity": "sha512-yePcBqEFhLOqSBtwYOGGS1exHo/s1xjekXiinh4itpNQGCu4KA1euPh1fg07N2wMITZXQkBz75Ntdt1ctGZouw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-4.0.1.tgz", + "integrity": "sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==", + "license": "MIT", "dependencies": { - "@types/babel-types": "^7.0.0", - "@types/babylon": "^6.16.2", - "babel-types": "^6.26.0", - "babylon": "^6.18.0" + "@babel/parser": "^7.6.0", + "@babel/types": "^7.6.1" } }, "node_modules/content-disposition": { @@ -779,29 +739,32 @@ } }, "node_modules/cookie-parser": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.5.tgz", - "integrity": "sha512-f13bPUj/gG/5mDr+xLmSxxDsB9DQiTIfhJS/sqjrmfAWiAN+x2O4i/XguTL9yDZ+/IFDanJ+5x7hC4CXT9Tdzw==", + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", "dependencies": { - "cookie": "0.4.0", + "cookie": "0.7.2", "cookie-signature": "1.0.6" }, "engines": { "node": ">= 0.8.0" } }, + "node_modules/cookie-parser/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" }, - "node_modules/core-js": { - "version": "2.6.11", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz", - "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==", - "deprecated": "core-js@<3.4 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Please, upgrade your dependencies to the actual version of core-js.", - "hasInstallScript": true - }, "node_modules/core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -898,14 +861,6 @@ "ms": "2.0.0" } }, - "node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/decode-uri-component": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", @@ -971,7 +926,8 @@ "node_modules/doctypes": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz", - "integrity": "sha1-6oCxBqh1OHdOijpKWv4pPeSJ4Kk=" + "integrity": "sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==", + "license": "MIT" }, "node_modules/dotenv": { "version": "7.0.0", @@ -981,6 +937,20 @@ "node": ">=6" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/duplexer3": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", @@ -1021,12 +991,10 @@ "integrity": "sha512-zoB603vQReOFvTg5xMl9I1P2PnHsHQQKTEowsKKD7nseUfJq6UWzK+4YtlWUO1nhiQUxe6XMkk+JleSZD1NZFA==" }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -1039,6 +1007,18 @@ "node": ">= 0.4" } }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es6-promise": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", @@ -1057,14 +1037,6 @@ "node": ">=0.8.0" } }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -1281,25 +1253,6 @@ "node": ">= 0.8" } }, - "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -1370,15 +1323,21 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -1387,6 +1346,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", @@ -1408,27 +1380,17 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dependencies": { - "get-intrinsic": "^1.1.3" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -1448,17 +1410,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/has-symbol-support-x": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz", @@ -1468,9 +1419,10 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -1489,6 +1441,21 @@ "node": "*" } }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -1556,27 +1523,27 @@ } }, "node_modules/hydra-login-consent-logout": { - "version": "2.0.4-pre.2", - "resolved": "https://registry.npmjs.org/hydra-login-consent-logout/-/hydra-login-consent-logout-2.0.4-pre.2.tgz", - "integrity": "sha512-nB3JKffjiTyQZzr0DPdkdoUAg7mPlNTv7c/jZrC5IrIyodc3X4s16LzcZJcs/e2U3pZyu3CoWGUrnF//wPzmqQ==", + "version": "2.4.0-pre.3", + "resolved": "https://registry.npmjs.org/hydra-login-consent-logout/-/hydra-login-consent-logout-2.4.0-pre.3.tgz", + "integrity": "sha512-Dtoop55BOQ/z+DQunVblxcFC5IMLw9NOw4vqTm2WmvMn7TjWwCfq+gA03ifGPAiFuqbyprxUnUaILCSJn9Hqdg==", "dependencies": { - "@ory/client": "^0.2.0-alpha.24", + "@ory/hydra-client-fetch": "^2.4.0-alpha.1", "@types/cookie-parser": "^1.4.2", "@types/csurf": "^1.9.36", "@types/express": "^4.17.7", "@types/morgan": "^1.9.1", "@types/url-join": "^4.0.0", - "body-parser": "^1.19.0", - "cookie-parser": "^1.4.5", + "body-parser": "^1.20.3", + "cookie-parser": "^1.4.7", "csurf": "^1.11.0", "debug": "^4.1.1", - "express": "^4.17.1", + "express": "^4.21.2", "morgan": "^1.10.0", "node-fetch": "^2.6.7", - "pug": "^2.0.4", + "pug": "^3.0.3", "querystring": "^0.2.0", "serve-favicon": "^2.5.0", - "typescript": "^3.7.5", + "typescript": "^5.7.3", "url-join": "^4.0.1" }, "bin": { @@ -1669,29 +1636,29 @@ "node": ">=8" } }, - "node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" - }, - "node_modules/is-expression": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-3.0.0.tgz", - "integrity": "sha1-Oayqa+f9HzRx3ELHQW5hwkMXrJ8=", + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", "dependencies": { - "acorn": "~4.0.2", - "object-assign": "^4.0.1" - } - }, - "node_modules/is-expression/node_modules/acorn": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", - "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=", - "bin": { - "acorn": "bin/acorn" + "hasown": "^2.0.2" }, "engines": { - "node": ">=0.4.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-expression": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-4.0.0.tgz", + "integrity": "sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==", + "license": "MIT", + "dependencies": { + "acorn": "^7.1.1", + "object-assign": "^4.1.1" } }, "node_modules/is-extglob": { @@ -1738,16 +1705,21 @@ } }, "node_modules/is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", - "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "license": "MIT" }, "node_modules/is-regex": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", - "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "license": "MIT", "dependencies": { - "has": "^1.0.3" + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -1812,7 +1784,8 @@ "node_modules/js-stringify": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz", - "integrity": "sha1-Fzb939lyTyijaCrcYjCufk6Weds=" + "integrity": "sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==", + "license": "MIT" }, "node_modules/json-buffer": { "version": "3.0.0", @@ -1848,7 +1821,8 @@ "node_modules/jstransformer": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-1.0.0.tgz", - "integrity": "sha1-7Yvwkh4vPx7U1cGkT2hwntJHIsM=", + "integrity": "sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==", + "license": "MIT", "dependencies": { "is-promise": "^2.0.0", "promise": "^7.0.1" @@ -1926,14 +1900,6 @@ "colornames": "^1.1.1" } }, - "node_modules/lazy-cache": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", - "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/limiter": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", @@ -2006,14 +1972,6 @@ "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" }, - "node_modules/longest": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", - "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/lowercase-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", @@ -2040,6 +1998,15 @@ "lru-cache": "~4.0.0" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -2527,9 +2494,10 @@ } }, "node_modules/path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" }, "node_modules/path-to-regexp": { "version": "0.1.12", @@ -2573,6 +2541,7 @@ "version": "7.3.1", "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "license": "MIT", "dependencies": { "asap": "~2.0.3" } @@ -2601,118 +2570,128 @@ "dev": true }, "node_modules/pug": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pug/-/pug-2.0.4.tgz", - "integrity": "sha512-XhoaDlvi6NIzL49nu094R2NA6P37ijtgMDuWE+ofekDChvfKnzFal60bhSdiy8y2PBO6fmz3oMEIcfpBVRUdvw==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pug/-/pug-3.0.3.tgz", + "integrity": "sha512-uBi6kmc9f3SZ3PXxqcHiUZLmIXgfgWooKWXcwSGwQd2Zi5Rb0bT14+8CJjJgI8AB+nndLaNgHGrcc6bPIB665g==", + "license": "MIT", "dependencies": { - "pug-code-gen": "^2.0.2", - "pug-filters": "^3.1.1", - "pug-lexer": "^4.1.0", - "pug-linker": "^3.0.6", - "pug-load": "^2.0.12", - "pug-parser": "^5.0.1", - "pug-runtime": "^2.0.5", - "pug-strip-comments": "^1.0.4" + "pug-code-gen": "^3.0.3", + "pug-filters": "^4.0.0", + "pug-lexer": "^5.0.1", + "pug-linker": "^4.0.0", + "pug-load": "^3.0.0", + "pug-parser": "^6.0.0", + "pug-runtime": "^3.0.1", + "pug-strip-comments": "^2.0.0" } }, "node_modules/pug-attrs": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pug-attrs/-/pug-attrs-2.0.4.tgz", - "integrity": "sha512-TaZ4Z2TWUPDJcV3wjU3RtUXMrd3kM4Wzjbe3EWnSsZPsJ3LDI0F3yCnf2/W7PPFF+edUFQ0HgDL1IoxSz5K8EQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pug-attrs/-/pug-attrs-3.0.0.tgz", + "integrity": "sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==", + "license": "MIT", "dependencies": { - "constantinople": "^3.0.1", - "js-stringify": "^1.0.1", - "pug-runtime": "^2.0.5" + "constantinople": "^4.0.1", + "js-stringify": "^1.0.2", + "pug-runtime": "^3.0.0" } }, "node_modules/pug-code-gen": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-2.0.3.tgz", - "integrity": "sha512-r9sezXdDuZJfW9J91TN/2LFbiqDhmltTFmGpHTsGdrNGp3p4SxAjjXEfnuK2e4ywYsRIVP0NeLbSAMHUcaX1EA==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-3.0.3.tgz", + "integrity": "sha512-cYQg0JW0w32Ux+XTeZnBEeuWrAY7/HNE6TWnhiHGnnRYlCgyAUPoyh9KzCMa9WhcJlJ1AtQqpEYHc+vbCzA+Aw==", + "license": "MIT", "dependencies": { - "constantinople": "^3.1.2", + "constantinople": "^4.0.1", "doctypes": "^1.1.0", - "js-stringify": "^1.0.1", - "pug-attrs": "^2.0.4", - "pug-error": "^1.3.3", - "pug-runtime": "^2.0.5", - "void-elements": "^2.0.1", - "with": "^5.0.0" + "js-stringify": "^1.0.2", + "pug-attrs": "^3.0.0", + "pug-error": "^2.1.0", + "pug-runtime": "^3.0.1", + "void-elements": "^3.1.0", + "with": "^7.0.0" } }, "node_modules/pug-error": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-1.3.3.tgz", - "integrity": "sha512-qE3YhESP2mRAWMFJgKdtT5D7ckThRScXRwkfo+Erqga7dyJdY3ZquspprMCj/9sJ2ijm5hXFWQE/A3l4poMWiQ==" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-2.1.0.tgz", + "integrity": "sha512-lv7sU9e5Jk8IeUheHata6/UThZ7RK2jnaaNztxfPYUY+VxZyk/ePVaNZ/vwmH8WqGvDz3LrNYt/+gA55NDg6Pg==", + "license": "MIT" }, "node_modules/pug-filters": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-3.1.1.tgz", - "integrity": "sha512-lFfjNyGEyVWC4BwX0WyvkoWLapI5xHSM3xZJFUhx4JM4XyyRdO8Aucc6pCygnqV2uSgJFaJWW3Ft1wCWSoQkQg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-4.0.0.tgz", + "integrity": "sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==", + "license": "MIT", "dependencies": { - "clean-css": "^4.1.11", - "constantinople": "^3.0.1", + "constantinople": "^4.0.1", "jstransformer": "1.0.0", - "pug-error": "^1.3.3", - "pug-walk": "^1.1.8", - "resolve": "^1.1.6", - "uglify-js": "^2.6.1" + "pug-error": "^2.0.0", + "pug-walk": "^2.0.0", + "resolve": "^1.15.1" } }, "node_modules/pug-lexer": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-4.1.0.tgz", - "integrity": "sha512-i55yzEBtjm0mlplW4LoANq7k3S8gDdfC6+LThGEvsK4FuobcKfDAwt6V4jKPH9RtiE3a2Akfg5UpafZ1OksaPA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-5.0.1.tgz", + "integrity": "sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w==", + "license": "MIT", "dependencies": { - "character-parser": "^2.1.1", - "is-expression": "^3.0.0", - "pug-error": "^1.3.3" + "character-parser": "^2.2.0", + "is-expression": "^4.0.0", + "pug-error": "^2.0.0" } }, "node_modules/pug-linker": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-3.0.6.tgz", - "integrity": "sha512-bagfuHttfQOpANGy1Y6NJ+0mNb7dD2MswFG2ZKj22s8g0wVsojpRlqveEQHmgXXcfROB2RT6oqbPYr9EN2ZWzg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-4.0.0.tgz", + "integrity": "sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==", + "license": "MIT", "dependencies": { - "pug-error": "^1.3.3", - "pug-walk": "^1.1.8" + "pug-error": "^2.0.0", + "pug-walk": "^2.0.0" } }, "node_modules/pug-load": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/pug-load/-/pug-load-2.0.12.tgz", - "integrity": "sha512-UqpgGpyyXRYgJs/X60sE6SIf8UBsmcHYKNaOccyVLEuT6OPBIMo6xMPhoJnqtB3Q3BbO4Z3Bjz5qDsUWh4rXsg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pug-load/-/pug-load-3.0.0.tgz", + "integrity": "sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==", + "license": "MIT", "dependencies": { - "object-assign": "^4.1.0", - "pug-walk": "^1.1.8" + "object-assign": "^4.1.1", + "pug-walk": "^2.0.0" } }, "node_modules/pug-parser": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-5.0.1.tgz", - "integrity": "sha512-nGHqK+w07p5/PsPIyzkTQfzlYfuqoiGjaoqHv1LjOv2ZLXmGX1O+4Vcvps+P4LhxZ3drYSljjq4b+Naid126wA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-6.0.0.tgz", + "integrity": "sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==", + "license": "MIT", "dependencies": { - "pug-error": "^1.3.3", - "token-stream": "0.0.1" + "pug-error": "^2.0.0", + "token-stream": "1.0.0" } }, "node_modules/pug-runtime": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-2.0.5.tgz", - "integrity": "sha512-P+rXKn9un4fQY77wtpcuFyvFaBww7/91f3jHa154qU26qFAnOe6SW1CbIDcxiG5lLK9HazYrMCCuDvNgDQNptw==" + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-3.0.1.tgz", + "integrity": "sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg==", + "license": "MIT" }, "node_modules/pug-strip-comments": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-1.0.4.tgz", - "integrity": "sha512-i5j/9CS4yFhSxHp5iKPHwigaig/VV9g+FgReLJWWHEHbvKsbqL0oP/K5ubuLco6Wu3Kan5p7u7qk8A4oLLh6vw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-2.0.0.tgz", + "integrity": "sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==", + "license": "MIT", "dependencies": { - "pug-error": "^1.3.3" + "pug-error": "^2.0.0" } }, "node_modules/pug-walk": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-1.1.8.tgz", - "integrity": "sha512-GMu3M5nUL3fju4/egXwZO0XLi6fW/K3T3VTgFQ14GxNi8btlxgT5qZL//JwZFm/2Fa64J/PNS8AZeys3wiMkVA==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-2.0.0.tgz", + "integrity": "sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==", + "license": "MIT" }, "node_modules/qs": { "version": "6.13.0", @@ -2810,25 +2789,21 @@ "node": ">=8.10.0" } }, - "node_modules/regenerator-runtime": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" - }, - "node_modules/repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", - "engines": { - "node": ">=0.10" - } - }, "node_modules/resolve": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.16.0.tgz", - "integrity": "sha512-LarL/PIKJvc09k1jaeT4kQb/8/7P+qV4qSnN2K80AES+OHdfZELAKVOBjxsvtToT/uLOfFbvYvKfZmV8cee7nA==", + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "license": "MIT", "dependencies": { - "path-parse": "^1.0.6" + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2842,17 +2817,6 @@ "lowercase-keys": "^1.0.0" } }, - "node_modules/right-align": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", - "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", - "dependencies": { - "align-text": "^0.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/rndm": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz", @@ -3103,14 +3067,6 @@ "node": ">=4" } }, - "node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/stack-trace": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", @@ -3154,6 +3110,18 @@ "node": ">=4" } }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/text-hex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", @@ -3167,14 +3135,6 @@ "node": ">=0.10.0" } }, - "node_modules/to-fast-properties": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", - "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -3196,9 +3156,10 @@ } }, "node_modules/token-stream": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-0.0.1.tgz", - "integrity": "sha1-zu78cXp2xDFvEm0LnbqlXX598Bo=" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-1.0.0.tgz", + "integrity": "sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==", + "license": "MIT" }, "node_modules/touch": { "version": "3.1.0", @@ -3243,41 +3204,18 @@ } }, "node_modules/typescript": { - "version": "3.9.10", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz", - "integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==", + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" - } - }, - "node_modules/uglify-js": { - "version": "2.8.29", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", - "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", - "dependencies": { - "source-map": "~0.5.1", - "yargs": "~3.10.0" - }, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - }, - "optionalDependencies": { - "uglify-to-browserify": "~1.0.0" + "node": ">=14.17" } }, - "node_modules/uglify-to-browserify": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", - "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", - "optional": true - }, "node_modules/uid-safe": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", @@ -3338,9 +3276,10 @@ } }, "node_modules/void-elements": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", - "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -3371,14 +3310,6 @@ "which": "bin/which" } }, - "node_modules/window-size": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", - "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/winston": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/winston/-/winston-3.2.1.tgz", @@ -3433,20 +3364,18 @@ } }, "node_modules/with": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/with/-/with-5.1.1.tgz", - "integrity": "sha1-+k2qktrzLE6pTtRTyB8EaGtXXf4=", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/with/-/with-7.0.2.tgz", + "integrity": "sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==", + "license": "MIT", "dependencies": { - "acorn": "^3.1.0", - "acorn-globals": "^3.0.0" - } - }, - "node_modules/wordwrap": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", - "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", + "@babel/parser": "^7.9.6", + "@babel/types": "^7.9.6", + "assert-never": "^1.2.1", + "babel-walk": "3.0.0-canary-5" + }, "engines": { - "node": ">=0.4.0" + "node": ">= 10.0.0" } }, "node_modules/wreck": { @@ -3464,28 +3393,36 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" + } + }, + "dependencies": { + "@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==" }, - "node_modules/yargs": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", - "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", - "dependencies": { - "camelcase": "^1.0.2", - "cliui": "^2.1.0", - "decamelize": "^1.0.0", - "window-size": "0.1.0" + "@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==" + }, + "@babel/parser": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.9.tgz", + "integrity": "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==", + "requires": { + "@babel/types": "^7.26.9" } }, - "node_modules/yargs/node_modules/camelcase": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", - "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", - "engines": { - "node": ">=0.10.0" + "@babel/types": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.9.tgz", + "integrity": "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==", + "requires": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" } - } - }, - "dependencies": { + }, "@hapi/address": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz", @@ -3520,13 +3457,10 @@ "@hapi/hoek": "^8.3.0" } }, - "@ory/client": { - "version": "0.2.0-alpha.60", - "resolved": "https://registry.npmjs.org/@ory/client/-/client-0.2.0-alpha.60.tgz", - "integrity": "sha512-fGovJ/xIl7dvJJP9/IL4Xu1yiOCy9pvmkfj2xnHZbPrIbL9c9tqVcC3CSlzBq6zJQZMC3XI7VmZ8uEQ+cF4suw==", - "requires": { - "axios": "^0.21.4" - } + "@ory/hydra-client-fetch": { + "version": "2.4.0-alpha.1", + "resolved": "https://registry.npmjs.org/@ory/hydra-client-fetch/-/hydra-client-fetch-2.4.0-alpha.1.tgz", + "integrity": "sha512-TTuw+1DdIFskz4JU7yP2OSOHP3pVr7HjLnr8YI4S1pQJ91JaGwBQxCPQLrqdNuffs7d8JU1VRkcgdSWOo+89eA==" }, "@panva/asn1.js": { "version": "1.0.0", @@ -3538,19 +3472,6 @@ "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.7.0.tgz", "integrity": "sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==" }, - "@types/babel-types": { - "version": "7.0.7", - "resolved": "https://registry.npmjs.org/@types/babel-types/-/babel-types-7.0.7.tgz", - "integrity": "sha512-dBtBbrc+qTHy1WdfHYjBwRln4+LWqASWakLHsWHR2NWHIFkv4W3O070IGoGLEBrJBvct3r0L1BUPuvURi7kYUQ==" - }, - "@types/babylon": { - "version": "6.16.5", - "resolved": "https://registry.npmjs.org/@types/babylon/-/babylon-6.16.5.tgz", - "integrity": "sha512-xH2e58elpj1X4ynnKp9qSnWlsRTIs6n3tgLGNfwAGHwePw0mulHQllV34n0T25uYSu1k0hRKkWXF890B1yS47w==", - "requires": { - "@types/babel-types": "*" - } - }, "@types/body-parser": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", @@ -3671,24 +3592,9 @@ } }, "acorn": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", - "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=" - }, - "acorn-globals": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-3.1.0.tgz", - "integrity": "sha1-/YJw9x+7SZawBPqIDuXUZXOnMb8=", - "requires": { - "acorn": "^4.0.4" - }, - "dependencies": { - "acorn": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", - "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=" - } - } + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==" }, "aggregate-error": { "version": "1.0.0", @@ -3699,26 +3605,6 @@ "indent-string": "^3.0.0" } }, - "align-text": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", - "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", - "requires": { - "kind-of": "^3.0.2", - "longest": "^1.0.1", - "repeat-string": "^1.5.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, "ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", @@ -3745,7 +3631,12 @@ "asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" + }, + "assert-never": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.4.0.tgz", + "integrity": "sha512-5oJg84os6NMQNl27T9LnZkvvqzvAnHu03ShCnoj6bsJwS7L8AO4lf+C/XjK/nvzEqQB744moC6V128RucQd1jA==" }, "async": { "version": "2.6.4", @@ -3755,39 +3646,14 @@ "lodash": "^4.17.14" } }, - "axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", - "requires": { - "follow-redirects": "^1.14.0" - } - }, - "babel-runtime": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", - "requires": { - "core-js": "^2.4.0", - "regenerator-runtime": "^0.11.0" - } - }, - "babel-types": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", - "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "babel-walk": { + "version": "3.0.0-canary-5", + "resolved": "https://registry.npmjs.org/babel-walk/-/babel-walk-3.0.0-canary-5.tgz", + "integrity": "sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==", "requires": { - "babel-runtime": "^6.26.0", - "esutils": "^2.0.2", - "lodash": "^4.17.4", - "to-fast-properties": "^1.0.3" + "@babel/types": "^7.9.6" } }, - "babylon": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", - "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==" - }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -3944,13 +3810,22 @@ "set-function-length": "^1.2.1" } }, - "center-align": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", - "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", + "call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "requires": { - "align-text": "^0.1.3", - "lazy-cache": "^1.0.3" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + } + }, + "call-bound": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", + "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "requires": { + "call-bind-apply-helpers": "^1.0.1", + "get-intrinsic": "^1.2.6" } }, "chalk": { @@ -3966,7 +3841,7 @@ "character-parser": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz", - "integrity": "sha1-x84o821LzZdE5f/CxfzeHHMmH8A=", + "integrity": "sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==", "requires": { "is-regex": "^1.0.3" } @@ -3987,36 +3862,11 @@ "readdirp": "~3.6.0" } }, - "clean-css": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz", - "integrity": "sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA==", - "requires": { - "source-map": "~0.6.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - } - } - }, "clean-stack": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-1.3.0.tgz", "integrity": "sha1-noIVAa6XmYbEax1m0tQy2y/UrjE=" }, - "cliui": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", - "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", - "requires": { - "center-align": "^0.1.1", - "right-align": "^0.1.1", - "wordwrap": "0.0.2" - } - }, "clone-response": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", @@ -4082,14 +3932,12 @@ "dev": true }, "constantinople": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-3.1.2.tgz", - "integrity": "sha512-yePcBqEFhLOqSBtwYOGGS1exHo/s1xjekXiinh4itpNQGCu4KA1euPh1fg07N2wMITZXQkBz75Ntdt1ctGZouw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-4.0.1.tgz", + "integrity": "sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==", "requires": { - "@types/babel-types": "^7.0.0", - "@types/babylon": "^6.16.2", - "babel-types": "^6.26.0", - "babylon": "^6.18.0" + "@babel/parser": "^7.6.0", + "@babel/types": "^7.6.1" } }, "content-disposition": { @@ -4118,12 +3966,19 @@ "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" }, "cookie-parser": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.5.tgz", - "integrity": "sha512-f13bPUj/gG/5mDr+xLmSxxDsB9DQiTIfhJS/sqjrmfAWiAN+x2O4i/XguTL9yDZ+/IFDanJ+5x7hC4CXT9Tdzw==", + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", "requires": { - "cookie": "0.4.0", + "cookie": "0.7.2", "cookie-signature": "1.0.6" + }, + "dependencies": { + "cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==" + } } }, "cookie-signature": { @@ -4131,11 +3986,6 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" }, - "core-js": { - "version": "2.6.11", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz", - "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==" - }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -4211,11 +4061,6 @@ "ms": "2.0.0" } }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" - }, "decode-uri-component": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", @@ -4262,13 +4107,23 @@ "doctypes": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz", - "integrity": "sha1-6oCxBqh1OHdOijpKWv4pPeSJ4Kk=" + "integrity": "sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==" }, "dotenv": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-7.0.0.tgz", "integrity": "sha512-M3NhsLbV1i6HuGzBUH8vXrtxOk+tWmzWKDMbAVSUp3Zsjm7ywFeuwrUXhmhQyRK1q5B5GGy7hcXPbj3bnfZg2g==" }, + "dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "requires": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + } + }, "duplexer3": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", @@ -4306,18 +4161,23 @@ "integrity": "sha512-zoB603vQReOFvTg5xMl9I1P2PnHsHQQKTEowsKKD7nseUfJq6UWzK+4YtlWUO1nhiQUxe6XMkk+JleSZD1NZFA==" }, "es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "requires": { - "get-intrinsic": "^1.2.4" - } + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" }, "es-errors": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" }, + "es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "requires": { + "es-errors": "^1.3.0" + } + }, "es6-promise": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", @@ -4333,11 +4193,6 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, - "esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" - }, "etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -4500,11 +4355,6 @@ } } }, - "follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==" - }, "forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -4561,15 +4411,29 @@ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" }, "get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "requires": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + } + }, + "get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "requires": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" } }, "get-stream": { @@ -4587,20 +4451,9 @@ } }, "gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "requires": { - "get-intrinsic": "^1.1.3" - } - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "requires": { - "function-bind": "^1.1.1" - } + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" }, "has-flag": { "version": "3.0.0", @@ -4615,20 +4468,15 @@ "es-define-property": "^1.0.0" } }, - "has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==" - }, "has-symbol-support-x": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz", "integrity": "sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw==" }, "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" }, "has-to-string-tag-x": { "version": "1.4.1", @@ -4638,6 +4486,14 @@ "has-symbol-support-x": "^1.4.1" } }, + "has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "requires": { + "has-symbols": "^1.0.3" + } + }, "hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -4691,27 +4547,27 @@ } }, "hydra-login-consent-logout": { - "version": "2.0.4-pre.2", - "resolved": "https://registry.npmjs.org/hydra-login-consent-logout/-/hydra-login-consent-logout-2.0.4-pre.2.tgz", - "integrity": "sha512-nB3JKffjiTyQZzr0DPdkdoUAg7mPlNTv7c/jZrC5IrIyodc3X4s16LzcZJcs/e2U3pZyu3CoWGUrnF//wPzmqQ==", + "version": "2.4.0-pre.3", + "resolved": "https://registry.npmjs.org/hydra-login-consent-logout/-/hydra-login-consent-logout-2.4.0-pre.3.tgz", + "integrity": "sha512-Dtoop55BOQ/z+DQunVblxcFC5IMLw9NOw4vqTm2WmvMn7TjWwCfq+gA03ifGPAiFuqbyprxUnUaILCSJn9Hqdg==", "requires": { - "@ory/client": "^0.2.0-alpha.24", + "@ory/hydra-client-fetch": "^2.4.0-alpha.1", "@types/cookie-parser": "^1.4.2", "@types/csurf": "^1.9.36", "@types/express": "^4.17.7", "@types/morgan": "^1.9.1", "@types/url-join": "^4.0.0", - "body-parser": "^1.19.0", - "cookie-parser": "^1.4.5", + "body-parser": "^1.20.3", + "cookie-parser": "^1.4.7", "csurf": "^1.11.0", "debug": "^4.1.1", - "express": "^4.17.1", + "express": "^4.21.2", "morgan": "^1.10.0", "node-fetch": "^2.6.7", - "pug": "^2.0.4", + "pug": "^3.0.3", "querystring": "^0.2.0", "serve-favicon": "^2.5.0", - "typescript": "^3.7.5", + "typescript": "^5.7.3", "url-join": "^4.0.1" }, "dependencies": { @@ -4789,25 +4645,21 @@ "binary-extensions": "^2.0.0" } }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + "is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "requires": { + "hasown": "^2.0.2" + } }, "is-expression": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-3.0.0.tgz", - "integrity": "sha1-Oayqa+f9HzRx3ELHQW5hwkMXrJ8=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-4.0.0.tgz", + "integrity": "sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==", "requires": { - "acorn": "~4.0.2", - "object-assign": "^4.0.1" - }, - "dependencies": { - "acorn": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", - "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=" - } + "acorn": "^7.1.1", + "object-assign": "^4.1.1" } }, "is-extglob": { @@ -4842,16 +4694,19 @@ "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=" }, "is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", - "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==" }, "is-regex": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", - "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", "requires": { - "has": "^1.0.3" + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" } }, "is-retry-allowed": { @@ -4895,7 +4750,7 @@ "js-stringify": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz", - "integrity": "sha1-Fzb939lyTyijaCrcYjCufk6Weds=" + "integrity": "sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==" }, "json-buffer": { "version": "3.0.0", @@ -4929,7 +4784,7 @@ "jstransformer": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-1.0.0.tgz", - "integrity": "sha1-7Yvwkh4vPx7U1cGkT2hwntJHIsM=", + "integrity": "sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==", "requires": { "is-promise": "^2.0.0", "promise": "^7.0.1" @@ -4998,11 +4853,6 @@ "colornames": "^1.1.1" } }, - "lazy-cache": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", - "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=" - }, "limiter": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", @@ -5077,11 +4927,6 @@ "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" }, - "longest": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", - "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=" - }, "lowercase-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", @@ -5105,6 +4950,11 @@ "lru-cache": "~4.0.0" } }, + "math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" + }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -5462,9 +5312,9 @@ "dev": true }, "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "path-to-regexp": { "version": "0.1.12", @@ -5521,118 +5371,116 @@ "dev": true }, "pug": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pug/-/pug-2.0.4.tgz", - "integrity": "sha512-XhoaDlvi6NIzL49nu094R2NA6P37ijtgMDuWE+ofekDChvfKnzFal60bhSdiy8y2PBO6fmz3oMEIcfpBVRUdvw==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pug/-/pug-3.0.3.tgz", + "integrity": "sha512-uBi6kmc9f3SZ3PXxqcHiUZLmIXgfgWooKWXcwSGwQd2Zi5Rb0bT14+8CJjJgI8AB+nndLaNgHGrcc6bPIB665g==", "requires": { - "pug-code-gen": "^2.0.2", - "pug-filters": "^3.1.1", - "pug-lexer": "^4.1.0", - "pug-linker": "^3.0.6", - "pug-load": "^2.0.12", - "pug-parser": "^5.0.1", - "pug-runtime": "^2.0.5", - "pug-strip-comments": "^1.0.4" + "pug-code-gen": "^3.0.3", + "pug-filters": "^4.0.0", + "pug-lexer": "^5.0.1", + "pug-linker": "^4.0.0", + "pug-load": "^3.0.0", + "pug-parser": "^6.0.0", + "pug-runtime": "^3.0.1", + "pug-strip-comments": "^2.0.0" } }, "pug-attrs": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pug-attrs/-/pug-attrs-2.0.4.tgz", - "integrity": "sha512-TaZ4Z2TWUPDJcV3wjU3RtUXMrd3kM4Wzjbe3EWnSsZPsJ3LDI0F3yCnf2/W7PPFF+edUFQ0HgDL1IoxSz5K8EQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pug-attrs/-/pug-attrs-3.0.0.tgz", + "integrity": "sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==", "requires": { - "constantinople": "^3.0.1", - "js-stringify": "^1.0.1", - "pug-runtime": "^2.0.5" + "constantinople": "^4.0.1", + "js-stringify": "^1.0.2", + "pug-runtime": "^3.0.0" } }, "pug-code-gen": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-2.0.3.tgz", - "integrity": "sha512-r9sezXdDuZJfW9J91TN/2LFbiqDhmltTFmGpHTsGdrNGp3p4SxAjjXEfnuK2e4ywYsRIVP0NeLbSAMHUcaX1EA==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-3.0.3.tgz", + "integrity": "sha512-cYQg0JW0w32Ux+XTeZnBEeuWrAY7/HNE6TWnhiHGnnRYlCgyAUPoyh9KzCMa9WhcJlJ1AtQqpEYHc+vbCzA+Aw==", "requires": { - "constantinople": "^3.1.2", + "constantinople": "^4.0.1", "doctypes": "^1.1.0", - "js-stringify": "^1.0.1", - "pug-attrs": "^2.0.4", - "pug-error": "^1.3.3", - "pug-runtime": "^2.0.5", - "void-elements": "^2.0.1", - "with": "^5.0.0" + "js-stringify": "^1.0.2", + "pug-attrs": "^3.0.0", + "pug-error": "^2.1.0", + "pug-runtime": "^3.0.1", + "void-elements": "^3.1.0", + "with": "^7.0.0" } }, "pug-error": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-1.3.3.tgz", - "integrity": "sha512-qE3YhESP2mRAWMFJgKdtT5D7ckThRScXRwkfo+Erqga7dyJdY3ZquspprMCj/9sJ2ijm5hXFWQE/A3l4poMWiQ==" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-2.1.0.tgz", + "integrity": "sha512-lv7sU9e5Jk8IeUheHata6/UThZ7RK2jnaaNztxfPYUY+VxZyk/ePVaNZ/vwmH8WqGvDz3LrNYt/+gA55NDg6Pg==" }, "pug-filters": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-3.1.1.tgz", - "integrity": "sha512-lFfjNyGEyVWC4BwX0WyvkoWLapI5xHSM3xZJFUhx4JM4XyyRdO8Aucc6pCygnqV2uSgJFaJWW3Ft1wCWSoQkQg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-4.0.0.tgz", + "integrity": "sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==", "requires": { - "clean-css": "^4.1.11", - "constantinople": "^3.0.1", + "constantinople": "^4.0.1", "jstransformer": "1.0.0", - "pug-error": "^1.3.3", - "pug-walk": "^1.1.8", - "resolve": "^1.1.6", - "uglify-js": "^2.6.1" + "pug-error": "^2.0.0", + "pug-walk": "^2.0.0", + "resolve": "^1.15.1" } }, "pug-lexer": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-4.1.0.tgz", - "integrity": "sha512-i55yzEBtjm0mlplW4LoANq7k3S8gDdfC6+LThGEvsK4FuobcKfDAwt6V4jKPH9RtiE3a2Akfg5UpafZ1OksaPA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-5.0.1.tgz", + "integrity": "sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w==", "requires": { - "character-parser": "^2.1.1", - "is-expression": "^3.0.0", - "pug-error": "^1.3.3" + "character-parser": "^2.2.0", + "is-expression": "^4.0.0", + "pug-error": "^2.0.0" } }, "pug-linker": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-3.0.6.tgz", - "integrity": "sha512-bagfuHttfQOpANGy1Y6NJ+0mNb7dD2MswFG2ZKj22s8g0wVsojpRlqveEQHmgXXcfROB2RT6oqbPYr9EN2ZWzg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-4.0.0.tgz", + "integrity": "sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==", "requires": { - "pug-error": "^1.3.3", - "pug-walk": "^1.1.8" + "pug-error": "^2.0.0", + "pug-walk": "^2.0.0" } }, "pug-load": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/pug-load/-/pug-load-2.0.12.tgz", - "integrity": "sha512-UqpgGpyyXRYgJs/X60sE6SIf8UBsmcHYKNaOccyVLEuT6OPBIMo6xMPhoJnqtB3Q3BbO4Z3Bjz5qDsUWh4rXsg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pug-load/-/pug-load-3.0.0.tgz", + "integrity": "sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==", "requires": { - "object-assign": "^4.1.0", - "pug-walk": "^1.1.8" + "object-assign": "^4.1.1", + "pug-walk": "^2.0.0" } }, "pug-parser": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-5.0.1.tgz", - "integrity": "sha512-nGHqK+w07p5/PsPIyzkTQfzlYfuqoiGjaoqHv1LjOv2ZLXmGX1O+4Vcvps+P4LhxZ3drYSljjq4b+Naid126wA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-6.0.0.tgz", + "integrity": "sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==", "requires": { - "pug-error": "^1.3.3", - "token-stream": "0.0.1" + "pug-error": "^2.0.0", + "token-stream": "1.0.0" } }, "pug-runtime": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-2.0.5.tgz", - "integrity": "sha512-P+rXKn9un4fQY77wtpcuFyvFaBww7/91f3jHa154qU26qFAnOe6SW1CbIDcxiG5lLK9HazYrMCCuDvNgDQNptw==" + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-3.0.1.tgz", + "integrity": "sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg==" }, "pug-strip-comments": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-1.0.4.tgz", - "integrity": "sha512-i5j/9CS4yFhSxHp5iKPHwigaig/VV9g+FgReLJWWHEHbvKsbqL0oP/K5ubuLco6Wu3Kan5p7u7qk8A4oLLh6vw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-2.0.0.tgz", + "integrity": "sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==", "requires": { - "pug-error": "^1.3.3" + "pug-error": "^2.0.0" } }, "pug-walk": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-1.1.8.tgz", - "integrity": "sha512-GMu3M5nUL3fju4/egXwZO0XLi6fW/K3T3VTgFQ14GxNi8btlxgT5qZL//JwZFm/2Fa64J/PNS8AZeys3wiMkVA==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-2.0.0.tgz", + "integrity": "sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==" }, "qs": { "version": "6.13.0", @@ -5702,22 +5550,14 @@ "picomatch": "^2.2.1" } }, - "regenerator-runtime": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" - }, "resolve": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.16.0.tgz", - "integrity": "sha512-LarL/PIKJvc09k1jaeT4kQb/8/7P+qV4qSnN2K80AES+OHdfZELAKVOBjxsvtToT/uLOfFbvYvKfZmV8cee7nA==", + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", "requires": { - "path-parse": "^1.0.6" + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" } }, "responselike": { @@ -5728,14 +5568,6 @@ "lowercase-keys": "^1.0.0" } }, - "right-align": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", - "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", - "requires": { - "align-text": "^0.1.1" - } - }, "rndm": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz", @@ -5944,11 +5776,6 @@ "is-plain-obj": "^1.0.0" } }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" - }, "stack-trace": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", @@ -5980,6 +5807,11 @@ "has-flag": "^3.0.0" } }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" + }, "text-hex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", @@ -5990,11 +5822,6 @@ "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=" }, - "to-fast-properties": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", - "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=" - }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -6010,9 +5837,9 @@ "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" }, "token-stream": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-0.0.1.tgz", - "integrity": "sha1-zu78cXp2xDFvEm0LnbqlXX598Bo=" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-1.0.0.tgz", + "integrity": "sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==" }, "touch": { "version": "3.1.0", @@ -6048,25 +5875,9 @@ } }, "typescript": { - "version": "3.9.10", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz", - "integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==" - }, - "uglify-js": { - "version": "2.8.29", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", - "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", - "requires": { - "source-map": "~0.5.1", - "uglify-to-browserify": "~1.0.0", - "yargs": "~3.10.0" - } - }, - "uglify-to-browserify": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", - "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", - "optional": true + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==" }, "uid-safe": { "version": "2.1.5", @@ -6113,9 +5924,9 @@ "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" }, "void-elements": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", - "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=" + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==" }, "webidl-conversions": { "version": "3.0.1", @@ -6140,11 +5951,6 @@ "isexe": "^2.0.0" } }, - "window-size": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", - "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=" - }, "winston": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/winston/-/winston-3.2.1.tgz", @@ -6195,19 +6001,16 @@ } }, "with": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/with/-/with-5.1.1.tgz", - "integrity": "sha1-+k2qktrzLE6pTtRTyB8EaGtXXf4=", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/with/-/with-7.0.2.tgz", + "integrity": "sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==", "requires": { - "acorn": "^3.1.0", - "acorn-globals": "^3.0.0" + "@babel/parser": "^7.9.6", + "@babel/types": "^7.9.6", + "assert-never": "^1.2.1", + "babel-walk": "3.0.0-canary-5" } }, - "wordwrap": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", - "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=" - }, "wreck": { "version": "14.2.0", "resolved": "https://registry.npmjs.org/wreck/-/wreck-14.2.0.tgz", @@ -6222,24 +6025,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" - }, - "yargs": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", - "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", - "requires": { - "camelcase": "^1.0.2", - "cliui": "^2.1.0", - "decamelize": "^1.0.0", - "window-size": "0.1.0" - }, - "dependencies": { - "camelcase": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", - "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=" - } - } } } } diff --git a/test/e2e/oauth2-client/package.json b/test/e2e/oauth2-client/package.json index 90412a46396..0ddd96f7002 100644 --- a/test/e2e/oauth2-client/package.json +++ b/test/e2e/oauth2-client/package.json @@ -14,7 +14,7 @@ "express": "^4.21.2", "express-session": "^1.17.0", "express-winston": "^3.4.0", - "hydra-login-consent-logout": "2.0.4-pre.2", + "hydra-login-consent-logout": "2.4.0-pre.3", "jsonwebtoken": "^8.5.1", "jwks-rsa": "^2.1.4", "node-fetch": "^2.6.0", diff --git a/test/e2e/oauth2-client/src/index.js b/test/e2e/oauth2-client/src/index.js index b27512bedeb..d868fbc0d84 100644 --- a/test/e2e/oauth2-client/src/index.js +++ b/test/e2e/oauth2-client/src/index.js @@ -152,6 +152,87 @@ app.get("/oauth2/callback", async (req, res) => { }) }) +app.get("/oauth2/device", async (req, res) => { + const client = { + id: req.query.client_id, + secret: req.query.client_secret, + } + + const state = uuid.v4() + const scope = req.query.scope || "" + + req.session.client = client + req.session.scope = scope.split(" ") + + const params = new URLSearchParams() + params.append("client_id", req.query.client_id) + params.append("scope", scope) + + let headers = new Headers() + headers.set( + "Authorization", + "Basic " + + Buffer.from(req.query.client_id + ":" + req.query.client_secret).toString( + "base64", + ), + ) + + fetch(new URL("/oauth2/device/auth", config.public).toString(), { + method: "POST", + body: params, + headers: headers, + }) + .then(isStatusOk) + .then((res) => res.json()) + .then((body) => { + // Store the device_code to use after authentication to get the tokens + req.session.device_code = body?.device_code + res.redirect(body?.verification_uri_complete) + }) + .catch((err) => { + res.send(JSON.stringify({ error: err.toString() })) + }) +}) + +app.get("/oauth2/device/success", async (req, res) => { + const clientId = req.session?.client?.id + const clientSecret = req.session?.client?.secret + + if (clientId === undefined || clientSecret === undefined) { + res.send( + JSON.stringify({ + result: "error", + error: "no client credentials in session", + }), + ) + return + } + + const params = new URLSearchParams() + params.append("client_id", clientId) + params.append("device_code", req.session?.device_code) + params.append("grant_type", "urn:ietf:params:oauth:grant-type:device_code") + let headers = new Headers() + headers.set( + "Authorization", + "Basic " + Buffer.from(clientId + ":" + clientSecret).toString("base64"), + ) + + fetch(new URL("/oauth2/token", config.public).toString(), { + method: "POST", + body: params, + headers: headers, + }) + .then(isStatusOk) + .then((resp) => resp.json()) + .then((data) => { + res.send({ result: "success", token: data }) + }) + .catch((err) => { + res.send(JSON.stringify({ error: err.toString() })) + }) +}) + app.get("/oauth2/refresh", function (req, res) { oauth2 .create(req.session.credentials) diff --git a/x/clean_sql.go b/x/clean_sql.go index a02a9a054ce..2b51ec2cde3 100644 --- a/x/clean_sql.go +++ b/x/clean_sql.go @@ -16,6 +16,7 @@ func DeleteHydraRows(t *testing.T, c *pop.Connection) { "hydra_oauth2_code", "hydra_oauth2_oidc", "hydra_oauth2_pkce", + "hydra_oauth2_device_auth_codes", "hydra_oauth2_flow", "hydra_oauth2_authentication_session", "hydra_oauth2_obfuscated_authentication_session", @@ -39,6 +40,7 @@ func CleanSQLPop(t *testing.T, c *pop.Connection) { "hydra_oauth2_code", "hydra_oauth2_oidc", "hydra_oauth2_pkce", + "hydra_oauth2_device_auth_codes", "hydra_oauth2_flow", "hydra_oauth2_authentication_session", "hydra_oauth2_obfuscated_authentication_session", diff --git a/x/events/events.go b/x/events/events.go index afc8eb4ae81..065a8b1ea46 100644 --- a/x/events/events.go +++ b/x/events/events.go @@ -20,6 +20,8 @@ const ( // LoginRejected will be emitted when the login UI rejects a login request. LoginRejected semconv.Event = "OAuth2LoginRejected" + DeviceUserCodeAccepted semconv.Event = "OAuth2DeviceUserCodeAccepted" + // ConsentAccepted will be emitted when the consent UI accepts a consent request. ConsentAccepted semconv.Event = "OAuth2ConsentAccepted" diff --git a/x/fosite_storer.go b/x/fosite_storer.go index 546cfc98870..2313ca199d8 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,8 @@ 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 + + GetUserCodeSession(context.Context, string, fosite.Session) (fosite.DeviceRequester, error) + GetDeviceCodeSessionByRequestID(ctx context.Context, requestID string, requester fosite.Session) (fosite.DeviceRequester, string, error) + UpdateDeviceCodeSessionBySignature(ctx context.Context, requestID string, requester fosite.DeviceRequester) error }