From ff6be40f5e5497da1f312d2896e210201a24b048 Mon Sep 17 00:00:00 2001 From: James Elliott Date: Mon, 6 Mar 2023 14:58:50 +1100 Subject: [PATCH] feat(oidc): pushed authorization requests (#4546) This implements RFC9126 OAuth 2.0 Pushed Authorization Requests. See https://datatracker.ietf.org/doc/html/rfc9126 for the specification details. --- .../identity-providers/open-id-connect.md | 25 ++ .../en/configuration/storage/migrations.md | 1 + .../openid-connect/introduction.md | 88 ++++++- docs/data/configkeys.json | 2 +- .../schema/identity_providers.go | 8 + internal/configuration/schema/keys.go | 3 + internal/configuration/validator/const.go | 2 +- .../validator/identity_providers_test.go | 2 +- .../handlers/handler_oidc_authorization.go | 79 +++++- internal/mocks/notifier.go | 3 +- internal/mocks/storage.go | 49 +++- internal/mocks/totp.go | 3 +- internal/mocks/user_provider.go | 3 +- internal/model/oidc.go | 113 ++++++++- internal/oidc/client.go | 37 ++- internal/oidc/client_test.go | 6 +- internal/oidc/config.go | 30 ++- internal/oidc/const.go | 23 +- .../oidc/{hmac.go => core_strategy_hmac.go} | 13 +- internal/oidc/core_strategy_hmac_test.go | 56 +++++ internal/oidc/discovery.go | 11 +- internal/oidc/discovery_test.go | 12 +- internal/oidc/errors.go | 28 ++- internal/oidc/hasher.go | 13 +- internal/oidc/hasher_test.go | 8 +- internal/oidc/provider.go | 20 +- internal/oidc/store.go | 42 +++- internal/oidc/types.go | 2 + internal/server/handlers.go | 9 + internal/storage/const.go | 48 ++-- internal/storage/migrations.go | 18 +- .../V0008.OpenIDConnectPAR.all.down.sql | 1 + .../V0008.OpenIDConnectPAR.mysql.up.sql | 17 ++ .../V0008.OpenIDConnectPAR.postgres.up.sql | 17 ++ .../V0008.OpenIDConnectPAR.sqlite.up.sql | 17 ++ internal/storage/migrations_test.go | 2 +- internal/storage/provider.go | 8 +- internal/storage/sql_provider.go | 230 +++++++++++------- .../storage/sql_provider_backend_postgres.go | 42 ++-- internal/storage/sql_provider_queries.go | 13 + internal/suites/suite_cli_test.go | 2 + internal/utils/strings.go | 7 +- 42 files changed, 872 insertions(+), 241 deletions(-) rename internal/oidc/{hmac.go => core_strategy_hmac.go} (92%) create mode 100644 internal/oidc/core_strategy_hmac_test.go create mode 100644 internal/storage/migrations/V0008.OpenIDConnectPAR.all.down.sql create mode 100644 internal/storage/migrations/V0008.OpenIDConnectPAR.mysql.up.sql create mode 100644 internal/storage/migrations/V0008.OpenIDConnectPAR.postgres.up.sql create mode 100644 internal/storage/migrations/V0008.OpenIDConnectPAR.sqlite.up.sql diff --git a/docs/content/en/configuration/identity-providers/open-id-connect.md b/docs/content/en/configuration/identity-providers/open-id-connect.md index f0a30ba90..89d35e63a 100644 --- a/docs/content/en/configuration/identity-providers/open-id-connect.md +++ b/docs/content/en/configuration/identity-providers/open-id-connect.md @@ -272,6 +272,23 @@ Allows [PKCE] `plain` challenges when set to `true`. *__Security Notice:__* Changing this value is generally discouraged. Applications should use the `S256` [PKCE] challenge method instead. +### pushed_authorizations + +Controls the behaviour of [Pushed Authorization Requests]. + +#### enforce + +{{< confkey type="boolean" default="false" required="no" >}} + +When enabled all authorization requests must use the [Pushed Authorization Requests] flow. + +#### context_lifespan + +{{< confkey type="duration" default="5m" required="no" >}} + +The maximum amount of time between the [Pushed Authorization Requests] flow being initiated and the generated +`request_uri` being utilized by a client. + ### cors Some [OpenID Connect 1.0] Endpoints need to allow cross-origin resource sharing, however some are optional. This section allows @@ -285,6 +302,7 @@ A list of endpoints to configure with cross-origin resource sharing headers. It option is at least in this list. The potential endpoints which this can be enabled on are as follows: * authorization +* pushed-authorization-request * token * revocation * introspection @@ -472,6 +490,12 @@ See the [Response Modes](../../integration/openid-connect/introduction.md#respon The authorization policy for this client: either `one_factor` or `two_factor`. +#### enforce_par + +{{< confkey type="boolean" default="false" required="no" >}} + +Enforces the use of a [Pushed Authorization Requests] flow for this client. + #### enforce_pkce {{< confkey type="bool" default="false" required="no" >}} @@ -550,3 +574,4 @@ To integrate Authelia's [OpenID Connect 1.0] implementation with a relying party [Authorization Code Flow]: https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth [Subject Identifier Type]: https://openid.net/specs/openid-connect-core-1_0.html#SubjectIDTypes [Pairwise Identifier Algorithm]: https://openid.net/specs/openid-connect-core-1_0.html#PairwiseAlg +[Pushed Authorization Requests]: https://datatracker.ietf.org/doc/html/rfc9126 diff --git a/docs/content/en/configuration/storage/migrations.md b/docs/content/en/configuration/storage/migrations.md index 4d987ce92..263f659de 100644 --- a/docs/content/en/configuration/storage/migrations.md +++ b/docs/content/en/configuration/storage/migrations.md @@ -36,3 +36,4 @@ this instance if you wanted to downgrade to pre1 you would need to use an Authel | 5 | 4.35.1 | Fixed the oauth2_consent_session table to accept NULL subjects for users who are not yet signed in | | 6 | 4.37.0 | Adjusted the OpenID Connect tables to allow pre-configured consent improvements | | 7 | 4.37.3 | Fixed some schema inconsistencies most notably the MySQL/MariaDB Engine and Collation | +| 8 | 4.38.0 | OpenID Connect 1.0 Pushed Authorization Requests | diff --git a/docs/content/en/integration/openid-connect/introduction.md b/docs/content/en/integration/openid-connect/introduction.md index 4b06ae438..6ab7ba46c 100644 --- a/docs/content/en/integration/openid-connect/introduction.md +++ b/docs/content/en/integration/openid-connect/introduction.md @@ -209,14 +209,71 @@ These endpoints can be utilized to discover other endpoints and metadata about t These endpoints implement OpenID Connect elements. -| Endpoint | Path | Discovery Attribute | -|:-------------------:|:-----------------------------------------------:|:----------------------:| -| [JSON Web Key Sets] | https://auth.example.com/jwks.json | jwks_uri | -| [Authorization] | https://auth.example.com/api/oidc/authorization | authorization_endpoint | -| [Token] | https://auth.example.com/api/oidc/token | token_endpoint | -| [UserInfo] | https://auth.example.com/api/oidc/userinfo | userinfo_endpoint | -| [Introspection] | https://auth.example.com/api/oidc/introspection | introspection_endpoint | -| [Revocation] | https://auth.example.com/api/oidc/revocation | revocation_endpoint | +| Endpoint | Path | Discovery Attribute | +|:-------------------------------:|:--------------------------------------------------------------:|:-------------------------------------:| +| [JSON Web Key Set] | https://auth.example.com/jwks.json | jwks_uri | +| [Authorization] | https://auth.example.com/api/oidc/authorization | authorization_endpoint | +| [Pushed Authorization Requests] | https://auth.example.com/api/oidc/pushed-authorization-request | pushed_authorization_request_endpoint | +| [Token] | https://auth.example.com/api/oidc/token | token_endpoint | +| [UserInfo] | https://auth.example.com/api/oidc/userinfo | userinfo_endpoint | +| [Introspection] | https://auth.example.com/api/oidc/introspection | introspection_endpoint | +| [Revocation] | https://auth.example.com/api/oidc/revocation | revocation_endpoint | + +## Security + +The following information covers some security topics some users may wish to be familiar with. + +#### Pushed Authorization Requests Endpoint + +The [Pushed Authorization Requests] endpoint is discussed in depth in [RFC9126] as well as in the +[OAuth 2.0 Pushed Authorization Requests](https://oauth.net/2/pushed-authorization-requests/) documentation. + +Essentially it's a special endpoint that takes the same parameters as the [Authorization] endpoint (including +[Proof Key Code Exchange](#proof-key-code-exchange)) with a few caveats: + +1. The same [Client Authentication] mechanism required by the [Token] endpoint **MUST** be used. +2. The request **MUST** use the [HTTP POST method]. +3. The request **MUST** use the `application/x-www-form-urlencoded` content type (i.e. the parameters **MUST** be in the + body, not the URI). +4. The request **MUST** occur over the back-channel. + +The response of this endpoint is a JSON Object with two key-value pairs: +- `request_uri` +- `expires_in` + +The `expires_in` indicates how long the `request_uri` is valid for. The `request_uri` is used as a parameter to the +[Authorization] endpoint instead of the standard parameters (as the `request_uri` parameter). + +The advantages of this approach are as follows: + +1. [Pushed Authorization Requests] cannot be created or influenced by any party other than the Relying Party (client). +2. Since you can force all [Authorization] requests to be initiated via [Pushed Authorization Requests] you drastically + improve the authorization flows resistance to phishing attacks (this can be done globally or on a per-client basis). +3. Since the [Pushed Authorization Requests] endpoint requires all of the same [Client Authentication] mechanisms as the + [Token] endpoint: + 1. Clients using the confidential [Client Type] can't have [Pushed Authorization Requests] generated by parties who do not + have the credentials. + 2. Clients using the public [Client Type] and utilizing [Proof Key Code Exchange](#proof-key-code-exchange) never + transmit the verifier over any front-channel making even the `plain` challenge method relatively secure. + +#### Proof Key Code Exchange + +The [Proof Key Code Exchange] mechanism is discussed in depth in [RFC7636] as well as in the +[OAuth 2.0 Proof Key Code Exchange](https://oauth.net/2/pkce/) documentation. + +Essentially a random opaque value is generated by the Relying Party and optionally (but recommended) passed through a +SHA256 hash. The original value is saved by the Relying Party, and the hashed value is sent in the [Authorization] +request in the `code_verifier` parameter with the `code_challenge_method` set to `S256` (or `plain` using a bad practice +of not hashing the opaque value). + +When the Relying Party requests the token from the [Token] endpoint, they must include the `code_verifier` parameter +again (in the body), but this time they send the value without it being hashed. + +The advantages of this approach are as follows: + +1. Provided the value was hashed it's certain that the Relying Party which generated the authorization request is the + same party as the one requesting the token or is permitted by the Relying Party to make this request. +2. Even when using the public [Client Type] there is a form of authentication on the [Token] endpoint. [ID Token]: https://openid.net/specs/openid-connect-core-1_0.html#IDToken [Access Token]: https://datatracker.ietf.org/doc/html/rfc6749#section-1.4 @@ -230,14 +287,23 @@ These endpoints implement OpenID Connect elements. [OpenID Connect Discovery]: https://openid.net/specs/openid-connect-discovery-1_0.html [OAuth 2.0 Authorization Server Metadata]: https://datatracker.ietf.org/doc/html/rfc8414 -[JSON Web Key Sets]: https://datatracker.ietf.org/doc/html/rfc7517#section-5 +[JSON Web Key Set]: https://datatracker.ietf.org/doc/html/rfc7517#section-5 [Authorization]: https://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint +[Pushed Authorization Requests]: https://datatracker.ietf.org/doc/html/rfc9126 [Token]: https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint [UserInfo]: https://openid.net/specs/openid-connect-core-1_0.html#UserInfo [Introspection]: https://datatracker.ietf.org/doc/html/rfc7662 [Revocation]: https://datatracker.ietf.org/doc/html/rfc7009 +[Proof Key Code Exchange]: https://www.rfc-editor.org/rfc/rfc7636.html -[RFC8176]: https://datatracker.ietf.org/doc/html/rfc8176 -[RFC4122]: https://datatracker.ietf.org/doc/html/rfc4122 [Subject Identifier Types]: https://openid.net/specs/openid-connect-core-1_0.html#SubjectIDTypes +[Client Authentication]: https://datatracker.ietf.org/doc/html/rfc6749#section-2.3 +[Client Type]: https://oauth.net/2/client-types/ +[HTTP POST method]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST +[Proof Key Code Exchange]: #proof-key-code-exchange + +[RFC4122]: https://datatracker.ietf.org/doc/html/rfc4122 +[RFC7636]: https://datatracker.ietf.org/doc/html/rfc7636 +[RFC8176]: https://datatracker.ietf.org/doc/html/rfc8176 +[RFC9126]: https://datatracker.ietf.org/doc/html/rfc9126 diff --git a/docs/data/configkeys.json b/docs/data/configkeys.json index 0f6c42911..bf06efb15 100644 --- a/docs/data/configkeys.json +++ b/docs/data/configkeys.json @@ -1 +1 @@ -[{"path":"theme","secret":false,"env":"AUTHELIA_THEME"},{"path":"certificates_directory","secret":false,"env":"AUTHELIA_CERTIFICATES_DIRECTORY"},{"path":"jwt_secret","secret":true,"env":"AUTHELIA_JWT_SECRET_FILE"},{"path":"default_redirection_url","secret":false,"env":"AUTHELIA_DEFAULT_REDIRECTION_URL"},{"path":"default_2fa_method","secret":false,"env":"AUTHELIA_DEFAULT_2FA_METHOD"},{"path":"log.level","secret":false,"env":"AUTHELIA_LOG_LEVEL"},{"path":"log.format","secret":false,"env":"AUTHELIA_LOG_FORMAT"},{"path":"log.file_path","secret":false,"env":"AUTHELIA_LOG_FILE_PATH"},{"path":"log.keep_stdout","secret":false,"env":"AUTHELIA_LOG_KEEP_STDOUT"},{"path":"identity_providers.oidc.hmac_secret","secret":true,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_HMAC_SECRET_FILE"},{"path":"identity_providers.oidc.issuer_certificate_chain","secret":true,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ISSUER_CERTIFICATE_CHAIN_FILE"},{"path":"identity_providers.oidc.issuer_private_key","secret":true,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ISSUER_PRIVATE_KEY_FILE"},{"path":"identity_providers.oidc.access_token_lifespan","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ACCESS_TOKEN_LIFESPAN"},{"path":"identity_providers.oidc.authorize_code_lifespan","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_AUTHORIZE_CODE_LIFESPAN"},{"path":"identity_providers.oidc.id_token_lifespan","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ID_TOKEN_LIFESPAN"},{"path":"identity_providers.oidc.refresh_token_lifespan","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_REFRESH_TOKEN_LIFESPAN"},{"path":"identity_providers.oidc.enable_client_debug_messages","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ENABLE_CLIENT_DEBUG_MESSAGES"},{"path":"identity_providers.oidc.minimum_parameter_entropy","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_MINIMUM_PARAMETER_ENTROPY"},{"path":"identity_providers.oidc.enforce_pkce","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ENFORCE_PKCE"},{"path":"identity_providers.oidc.enable_pkce_plain_challenge","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ENABLE_PKCE_PLAIN_CHALLENGE"},{"path":"identity_providers.oidc.cors.allowed_origins_from_client_redirect_uris","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_CORS_ALLOWED_ORIGINS_FROM_CLIENT_REDIRECT_URIS"},{"path":"authentication_backend.password_reset.disable","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_PASSWORD_RESET_DISABLE"},{"path":"authentication_backend.password_reset.custom_url","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_PASSWORD_RESET_CUSTOM_URL"},{"path":"authentication_backend.refresh_interval","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_REFRESH_INTERVAL"},{"path":"authentication_backend.file.path","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PATH"},{"path":"authentication_backend.file.watch","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_WATCH"},{"path":"authentication_backend.file.password.algorithm","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ALGORITHM"},{"path":"authentication_backend.file.password.argon2.variant","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ARGON2_VARIANT"},{"path":"authentication_backend.file.password.argon2.iterations","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ARGON2_ITERATIONS"},{"path":"authentication_backend.file.password.argon2.memory","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ARGON2_MEMORY"},{"path":"authentication_backend.file.password.argon2.parallelism","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ARGON2_PARALLELISM"},{"path":"authentication_backend.file.password.argon2.key_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ARGON2_KEY_LENGTH"},{"path":"authentication_backend.file.password.argon2.salt_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ARGON2_SALT_LENGTH"},{"path":"authentication_backend.file.password.sha2crypt.variant","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SHA2CRYPT_VARIANT"},{"path":"authentication_backend.file.password.sha2crypt.iterations","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SHA2CRYPT_ITERATIONS"},{"path":"authentication_backend.file.password.sha2crypt.salt_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SHA2CRYPT_SALT_LENGTH"},{"path":"authentication_backend.file.password.pbkdf2.variant","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_PBKDF2_VARIANT"},{"path":"authentication_backend.file.password.pbkdf2.iterations","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_PBKDF2_ITERATIONS"},{"path":"authentication_backend.file.password.pbkdf2.salt_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_PBKDF2_SALT_LENGTH"},{"path":"authentication_backend.file.password.bcrypt.variant","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_BCRYPT_VARIANT"},{"path":"authentication_backend.file.password.bcrypt.cost","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_BCRYPT_COST"},{"path":"authentication_backend.file.password.scrypt.iterations","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SCRYPT_ITERATIONS"},{"path":"authentication_backend.file.password.scrypt.block_size","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SCRYPT_BLOCK_SIZE"},{"path":"authentication_backend.file.password.scrypt.parallelism","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SCRYPT_PARALLELISM"},{"path":"authentication_backend.file.password.scrypt.key_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SCRYPT_KEY_LENGTH"},{"path":"authentication_backend.file.password.scrypt.salt_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SCRYPT_SALT_LENGTH"},{"path":"authentication_backend.file.password.iterations","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ITERATIONS"},{"path":"authentication_backend.file.password.memory","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_MEMORY"},{"path":"authentication_backend.file.password.parallelism","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_PARALLELISM"},{"path":"authentication_backend.file.password.key_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_KEY_LENGTH"},{"path":"authentication_backend.file.password.salt_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SALT_LENGTH"},{"path":"authentication_backend.file.search.email","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_SEARCH_EMAIL"},{"path":"authentication_backend.file.search.case_insensitive","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_SEARCH_CASE_INSENSITIVE"},{"path":"authentication_backend.ldap.implementation","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_IMPLEMENTATION"},{"path":"authentication_backend.ldap.url","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_URL"},{"path":"authentication_backend.ldap.timeout","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TIMEOUT"},{"path":"authentication_backend.ldap.start_tls","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_START_TLS"},{"path":"authentication_backend.ldap.tls.minimum_version","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TLS_MINIMUM_VERSION"},{"path":"authentication_backend.ldap.tls.maximum_version","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TLS_MAXIMUM_VERSION"},{"path":"authentication_backend.ldap.tls.skip_verify","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TLS_SKIP_VERIFY"},{"path":"authentication_backend.ldap.tls.server_name","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TLS_SERVER_NAME"},{"path":"authentication_backend.ldap.tls.private_key","secret":true,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TLS_PRIVATE_KEY_FILE"},{"path":"authentication_backend.ldap.tls.certificate_chain","secret":true,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TLS_CERTIFICATE_CHAIN_FILE"},{"path":"authentication_backend.ldap.base_dn","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_BASE_DN"},{"path":"authentication_backend.ldap.additional_users_dn","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_ADDITIONAL_USERS_DN"},{"path":"authentication_backend.ldap.users_filter","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_USERS_FILTER"},{"path":"authentication_backend.ldap.additional_groups_dn","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_ADDITIONAL_GROUPS_DN"},{"path":"authentication_backend.ldap.groups_filter","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_GROUPS_FILTER"},{"path":"authentication_backend.ldap.group_name_attribute","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_GROUP_NAME_ATTRIBUTE"},{"path":"authentication_backend.ldap.username_attribute","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_USERNAME_ATTRIBUTE"},{"path":"authentication_backend.ldap.mail_attribute","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_MAIL_ATTRIBUTE"},{"path":"authentication_backend.ldap.display_name_attribute","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_DISPLAY_NAME_ATTRIBUTE"},{"path":"authentication_backend.ldap.permit_referrals","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PERMIT_REFERRALS"},{"path":"authentication_backend.ldap.permit_unauthenticated_bind","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PERMIT_UNAUTHENTICATED_BIND"},{"path":"authentication_backend.ldap.permit_feature_detection_failure","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PERMIT_FEATURE_DETECTION_FAILURE"},{"path":"authentication_backend.ldap.user","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_USER"},{"path":"authentication_backend.ldap.password","secret":true,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE"},{"path":"session.secret","secret":true,"env":"AUTHELIA_SESSION_SECRET_FILE"},{"path":"session.name","secret":false,"env":"AUTHELIA_SESSION_NAME"},{"path":"session.domain","secret":false,"env":"AUTHELIA_SESSION_DOMAIN"},{"path":"session.same_site","secret":false,"env":"AUTHELIA_SESSION_SAME_SITE"},{"path":"session.expiration","secret":false,"env":"AUTHELIA_SESSION_EXPIRATION"},{"path":"session.inactivity","secret":false,"env":"AUTHELIA_SESSION_INACTIVITY"},{"path":"session.remember_me","secret":false,"env":"AUTHELIA_SESSION_REMEMBER_ME"},{"path":"session","secret":false,"env":"AUTHELIA_SESSION"},{"path":"session.redis.host","secret":false,"env":"AUTHELIA_SESSION_REDIS_HOST"},{"path":"session.redis.port","secret":false,"env":"AUTHELIA_SESSION_REDIS_PORT"},{"path":"session.redis.username","secret":false,"env":"AUTHELIA_SESSION_REDIS_USERNAME"},{"path":"session.redis.password","secret":true,"env":"AUTHELIA_SESSION_REDIS_PASSWORD_FILE"},{"path":"session.redis.database_index","secret":false,"env":"AUTHELIA_SESSION_REDIS_DATABASE_INDEX"},{"path":"session.redis.maximum_active_connections","secret":false,"env":"AUTHELIA_SESSION_REDIS_MAXIMUM_ACTIVE_CONNECTIONS"},{"path":"session.redis.minimum_idle_connections","secret":false,"env":"AUTHELIA_SESSION_REDIS_MINIMUM_IDLE_CONNECTIONS"},{"path":"session.redis.tls.minimum_version","secret":false,"env":"AUTHELIA_SESSION_REDIS_TLS_MINIMUM_VERSION"},{"path":"session.redis.tls.maximum_version","secret":false,"env":"AUTHELIA_SESSION_REDIS_TLS_MAXIMUM_VERSION"},{"path":"session.redis.tls.skip_verify","secret":false,"env":"AUTHELIA_SESSION_REDIS_TLS_SKIP_VERIFY"},{"path":"session.redis.tls.server_name","secret":false,"env":"AUTHELIA_SESSION_REDIS_TLS_SERVER_NAME"},{"path":"session.redis.tls.private_key","secret":true,"env":"AUTHELIA_SESSION_REDIS_TLS_PRIVATE_KEY_FILE"},{"path":"session.redis.tls.certificate_chain","secret":true,"env":"AUTHELIA_SESSION_REDIS_TLS_CERTIFICATE_CHAIN_FILE"},{"path":"session.redis.high_availability.sentinel_name","secret":false,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_SENTINEL_NAME"},{"path":"session.redis.high_availability.sentinel_username","secret":false,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_SENTINEL_USERNAME"},{"path":"session.redis.high_availability.sentinel_password","secret":true,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_SENTINEL_PASSWORD_FILE"},{"path":"session.redis.high_availability.route_by_latency","secret":false,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_ROUTE_BY_LATENCY"},{"path":"session.redis.high_availability.route_randomly","secret":false,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_ROUTE_RANDOMLY"},{"path":"totp.disable","secret":false,"env":"AUTHELIA_TOTP_DISABLE"},{"path":"totp.issuer","secret":false,"env":"AUTHELIA_TOTP_ISSUER"},{"path":"totp.algorithm","secret":false,"env":"AUTHELIA_TOTP_ALGORITHM"},{"path":"totp.digits","secret":false,"env":"AUTHELIA_TOTP_DIGITS"},{"path":"totp.period","secret":false,"env":"AUTHELIA_TOTP_PERIOD"},{"path":"totp.skew","secret":false,"env":"AUTHELIA_TOTP_SKEW"},{"path":"totp.secret_size","secret":false,"env":"AUTHELIA_TOTP_SECRET_SIZE"},{"path":"duo_api.disable","secret":false,"env":"AUTHELIA_DUO_API_DISABLE"},{"path":"duo_api.hostname","secret":false,"env":"AUTHELIA_DUO_API_HOSTNAME"},{"path":"duo_api.integration_key","secret":true,"env":"AUTHELIA_DUO_API_INTEGRATION_KEY_FILE"},{"path":"duo_api.secret_key","secret":true,"env":"AUTHELIA_DUO_API_SECRET_KEY_FILE"},{"path":"duo_api.enable_self_enrollment","secret":false,"env":"AUTHELIA_DUO_API_ENABLE_SELF_ENROLLMENT"},{"path":"access_control.default_policy","secret":false,"env":"AUTHELIA_ACCESS_CONTROL_DEFAULT_POLICY"},{"path":"ntp.address","secret":false,"env":"AUTHELIA_NTP_ADDRESS"},{"path":"ntp.version","secret":false,"env":"AUTHELIA_NTP_VERSION"},{"path":"ntp.max_desync","secret":false,"env":"AUTHELIA_NTP_MAX_DESYNC"},{"path":"ntp.disable_startup_check","secret":false,"env":"AUTHELIA_NTP_DISABLE_STARTUP_CHECK"},{"path":"ntp.disable_failure","secret":false,"env":"AUTHELIA_NTP_DISABLE_FAILURE"},{"path":"regulation.max_retries","secret":false,"env":"AUTHELIA_REGULATION_MAX_RETRIES"},{"path":"regulation.find_time","secret":false,"env":"AUTHELIA_REGULATION_FIND_TIME"},{"path":"regulation.ban_time","secret":false,"env":"AUTHELIA_REGULATION_BAN_TIME"},{"path":"storage.local.path","secret":false,"env":"AUTHELIA_STORAGE_LOCAL_PATH"},{"path":"storage.mysql.host","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_HOST"},{"path":"storage.mysql.port","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_PORT"},{"path":"storage.mysql.database","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_DATABASE"},{"path":"storage.mysql.username","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_USERNAME"},{"path":"storage.mysql.password","secret":true,"env":"AUTHELIA_STORAGE_MYSQL_PASSWORD_FILE"},{"path":"storage.mysql.timeout","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_TIMEOUT"},{"path":"storage.mysql.tls.minimum_version","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_TLS_MINIMUM_VERSION"},{"path":"storage.mysql.tls.maximum_version","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_TLS_MAXIMUM_VERSION"},{"path":"storage.mysql.tls.skip_verify","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_TLS_SKIP_VERIFY"},{"path":"storage.mysql.tls.server_name","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_TLS_SERVER_NAME"},{"path":"storage.mysql.tls.private_key","secret":true,"env":"AUTHELIA_STORAGE_MYSQL_TLS_PRIVATE_KEY_FILE"},{"path":"storage.mysql.tls.certificate_chain","secret":true,"env":"AUTHELIA_STORAGE_MYSQL_TLS_CERTIFICATE_CHAIN_FILE"},{"path":"storage.postgres.host","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_HOST"},{"path":"storage.postgres.port","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_PORT"},{"path":"storage.postgres.database","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_DATABASE"},{"path":"storage.postgres.username","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_USERNAME"},{"path":"storage.postgres.password","secret":true,"env":"AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE"},{"path":"storage.postgres.timeout","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_TIMEOUT"},{"path":"storage.postgres.schema","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_SCHEMA"},{"path":"storage.postgres.tls.minimum_version","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_TLS_MINIMUM_VERSION"},{"path":"storage.postgres.tls.maximum_version","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_TLS_MAXIMUM_VERSION"},{"path":"storage.postgres.tls.skip_verify","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_TLS_SKIP_VERIFY"},{"path":"storage.postgres.tls.server_name","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_TLS_SERVER_NAME"},{"path":"storage.postgres.tls.private_key","secret":true,"env":"AUTHELIA_STORAGE_POSTGRES_TLS_PRIVATE_KEY_FILE"},{"path":"storage.postgres.tls.certificate_chain","secret":true,"env":"AUTHELIA_STORAGE_POSTGRES_TLS_CERTIFICATE_CHAIN_FILE"},{"path":"storage.postgres.ssl.mode","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_SSL_MODE"},{"path":"storage.postgres.ssl.root_certificate","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_SSL_ROOT_CERTIFICATE"},{"path":"storage.postgres.ssl.certificate","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_SSL_CERTIFICATE"},{"path":"storage.postgres.ssl.key","secret":true,"env":"AUTHELIA_STORAGE_POSTGRES_SSL_KEY_FILE"},{"path":"storage.encryption_key","secret":true,"env":"AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE"},{"path":"notifier.disable_startup_check","secret":false,"env":"AUTHELIA_NOTIFIER_DISABLE_STARTUP_CHECK"},{"path":"notifier.filesystem.filename","secret":false,"env":"AUTHELIA_NOTIFIER_FILESYSTEM_FILENAME"},{"path":"notifier.smtp.host","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_HOST"},{"path":"notifier.smtp.port","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_PORT"},{"path":"notifier.smtp.timeout","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_TIMEOUT"},{"path":"notifier.smtp.username","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_USERNAME"},{"path":"notifier.smtp.password","secret":true,"env":"AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE"},{"path":"notifier.smtp.identifier","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_IDENTIFIER"},{"path":"notifier.smtp.sender","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_SENDER"},{"path":"notifier.smtp.subject","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_SUBJECT"},{"path":"notifier.smtp.startup_check_address","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_STARTUP_CHECK_ADDRESS"},{"path":"notifier.smtp.disable_require_tls","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_DISABLE_REQUIRE_TLS"},{"path":"notifier.smtp.disable_html_emails","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_DISABLE_HTML_EMAILS"},{"path":"notifier.smtp.disable_starttls","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_DISABLE_STARTTLS"},{"path":"notifier.smtp.tls.minimum_version","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_TLS_MINIMUM_VERSION"},{"path":"notifier.smtp.tls.maximum_version","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_TLS_MAXIMUM_VERSION"},{"path":"notifier.smtp.tls.skip_verify","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_TLS_SKIP_VERIFY"},{"path":"notifier.smtp.tls.server_name","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_TLS_SERVER_NAME"},{"path":"notifier.smtp.tls.private_key","secret":true,"env":"AUTHELIA_NOTIFIER_SMTP_TLS_PRIVATE_KEY_FILE"},{"path":"notifier.smtp.tls.certificate_chain","secret":true,"env":"AUTHELIA_NOTIFIER_SMTP_TLS_CERTIFICATE_CHAIN_FILE"},{"path":"notifier.template_path","secret":false,"env":"AUTHELIA_NOTIFIER_TEMPLATE_PATH"},{"path":"server.host","secret":false,"env":"AUTHELIA_SERVER_HOST"},{"path":"server.port","secret":false,"env":"AUTHELIA_SERVER_PORT"},{"path":"server.path","secret":false,"env":"AUTHELIA_SERVER_PATH"},{"path":"server.asset_path","secret":false,"env":"AUTHELIA_SERVER_ASSET_PATH"},{"path":"server.disable_healthcheck","secret":false,"env":"AUTHELIA_SERVER_DISABLE_HEALTHCHECK"},{"path":"server.tls.certificate","secret":false,"env":"AUTHELIA_SERVER_TLS_CERTIFICATE"},{"path":"server.tls.key","secret":true,"env":"AUTHELIA_SERVER_TLS_KEY_FILE"},{"path":"server.headers.csp_template","secret":false,"env":"AUTHELIA_SERVER_HEADERS_CSP_TEMPLATE"},{"path":"server.endpoints.enable_pprof","secret":false,"env":"AUTHELIA_SERVER_ENDPOINTS_ENABLE_PPROF"},{"path":"server.endpoints.enable_expvars","secret":false,"env":"AUTHELIA_SERVER_ENDPOINTS_ENABLE_EXPVARS"},{"path":"server.buffers.read","secret":false,"env":"AUTHELIA_SERVER_BUFFERS_READ"},{"path":"server.buffers.write","secret":false,"env":"AUTHELIA_SERVER_BUFFERS_WRITE"},{"path":"server.timeouts.read","secret":false,"env":"AUTHELIA_SERVER_TIMEOUTS_READ"},{"path":"server.timeouts.write","secret":false,"env":"AUTHELIA_SERVER_TIMEOUTS_WRITE"},{"path":"server.timeouts.idle","secret":false,"env":"AUTHELIA_SERVER_TIMEOUTS_IDLE"},{"path":"telemetry.metrics.enabled","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_ENABLED"},{"path":"telemetry.metrics.address","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_ADDRESS"},{"path":"telemetry.metrics.buffers.read","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_BUFFERS_READ"},{"path":"telemetry.metrics.buffers.write","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_BUFFERS_WRITE"},{"path":"telemetry.metrics.timeouts.read","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_TIMEOUTS_READ"},{"path":"telemetry.metrics.timeouts.write","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_TIMEOUTS_WRITE"},{"path":"telemetry.metrics.timeouts.idle","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_TIMEOUTS_IDLE"},{"path":"webauthn.disable","secret":false,"env":"AUTHELIA_WEBAUTHN_DISABLE"},{"path":"webauthn.display_name","secret":false,"env":"AUTHELIA_WEBAUTHN_DISPLAY_NAME"},{"path":"webauthn.attestation_conveyance_preference","secret":false,"env":"AUTHELIA_WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE"},{"path":"webauthn.user_verification","secret":false,"env":"AUTHELIA_WEBAUTHN_USER_VERIFICATION"},{"path":"webauthn.timeout","secret":false,"env":"AUTHELIA_WEBAUTHN_TIMEOUT"},{"path":"password_policy.standard.enabled","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_ENABLED"},{"path":"password_policy.standard.min_length","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_MIN_LENGTH"},{"path":"password_policy.standard.max_length","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_MAX_LENGTH"},{"path":"password_policy.standard.require_uppercase","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_REQUIRE_UPPERCASE"},{"path":"password_policy.standard.require_lowercase","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_REQUIRE_LOWERCASE"},{"path":"password_policy.standard.require_number","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_REQUIRE_NUMBER"},{"path":"password_policy.standard.require_special","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_REQUIRE_SPECIAL"},{"path":"password_policy.zxcvbn.enabled","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_ZXCVBN_ENABLED"},{"path":"password_policy.zxcvbn.min_score","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_ZXCVBN_MIN_SCORE"},{"path":"privacy_policy.enabled","secret":false,"env":"AUTHELIA_PRIVACY_POLICY_ENABLED"},{"path":"privacy_policy.require_user_acceptance","secret":false,"env":"AUTHELIA_PRIVACY_POLICY_REQUIRE_USER_ACCEPTANCE"},{"path":"privacy_policy.policy_url","secret":false,"env":"AUTHELIA_PRIVACY_POLICY_POLICY_URL"}] \ No newline at end of file +[{"path":"theme","secret":false,"env":"AUTHELIA_THEME"},{"path":"certificates_directory","secret":false,"env":"AUTHELIA_CERTIFICATES_DIRECTORY"},{"path":"jwt_secret","secret":true,"env":"AUTHELIA_JWT_SECRET_FILE"},{"path":"default_redirection_url","secret":false,"env":"AUTHELIA_DEFAULT_REDIRECTION_URL"},{"path":"default_2fa_method","secret":false,"env":"AUTHELIA_DEFAULT_2FA_METHOD"},{"path":"log.level","secret":false,"env":"AUTHELIA_LOG_LEVEL"},{"path":"log.format","secret":false,"env":"AUTHELIA_LOG_FORMAT"},{"path":"log.file_path","secret":false,"env":"AUTHELIA_LOG_FILE_PATH"},{"path":"log.keep_stdout","secret":false,"env":"AUTHELIA_LOG_KEEP_STDOUT"},{"path":"identity_providers.oidc.hmac_secret","secret":true,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_HMAC_SECRET_FILE"},{"path":"identity_providers.oidc.issuer_certificate_chain","secret":true,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ISSUER_CERTIFICATE_CHAIN_FILE"},{"path":"identity_providers.oidc.issuer_private_key","secret":true,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ISSUER_PRIVATE_KEY_FILE"},{"path":"identity_providers.oidc.access_token_lifespan","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ACCESS_TOKEN_LIFESPAN"},{"path":"identity_providers.oidc.authorize_code_lifespan","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_AUTHORIZE_CODE_LIFESPAN"},{"path":"identity_providers.oidc.id_token_lifespan","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ID_TOKEN_LIFESPAN"},{"path":"identity_providers.oidc.refresh_token_lifespan","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_REFRESH_TOKEN_LIFESPAN"},{"path":"identity_providers.oidc.enable_client_debug_messages","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ENABLE_CLIENT_DEBUG_MESSAGES"},{"path":"identity_providers.oidc.minimum_parameter_entropy","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_MINIMUM_PARAMETER_ENTROPY"},{"path":"identity_providers.oidc.enforce_pkce","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ENFORCE_PKCE"},{"path":"identity_providers.oidc.enable_pkce_plain_challenge","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ENABLE_PKCE_PLAIN_CHALLENGE"},{"path":"identity_providers.oidc.cors.allowed_origins_from_client_redirect_uris","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_CORS_ALLOWED_ORIGINS_FROM_CLIENT_REDIRECT_URIS"},{"path":"identity_providers.oidc.pushed_authorizations.enforce","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_PUSHED_AUTHORIZATIONS_ENFORCE"},{"path":"identity_providers.oidc.pushed_authorizations.context_lifespan","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_PUSHED_AUTHORIZATIONS_CONTEXT_LIFESPAN"},{"path":"authentication_backend.password_reset.disable","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_PASSWORD_RESET_DISABLE"},{"path":"authentication_backend.password_reset.custom_url","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_PASSWORD_RESET_CUSTOM_URL"},{"path":"authentication_backend.refresh_interval","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_REFRESH_INTERVAL"},{"path":"authentication_backend.file.path","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PATH"},{"path":"authentication_backend.file.watch","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_WATCH"},{"path":"authentication_backend.file.password.algorithm","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ALGORITHM"},{"path":"authentication_backend.file.password.argon2.variant","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ARGON2_VARIANT"},{"path":"authentication_backend.file.password.argon2.iterations","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ARGON2_ITERATIONS"},{"path":"authentication_backend.file.password.argon2.memory","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ARGON2_MEMORY"},{"path":"authentication_backend.file.password.argon2.parallelism","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ARGON2_PARALLELISM"},{"path":"authentication_backend.file.password.argon2.key_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ARGON2_KEY_LENGTH"},{"path":"authentication_backend.file.password.argon2.salt_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ARGON2_SALT_LENGTH"},{"path":"authentication_backend.file.password.sha2crypt.variant","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SHA2CRYPT_VARIANT"},{"path":"authentication_backend.file.password.sha2crypt.iterations","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SHA2CRYPT_ITERATIONS"},{"path":"authentication_backend.file.password.sha2crypt.salt_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SHA2CRYPT_SALT_LENGTH"},{"path":"authentication_backend.file.password.pbkdf2.variant","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_PBKDF2_VARIANT"},{"path":"authentication_backend.file.password.pbkdf2.iterations","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_PBKDF2_ITERATIONS"},{"path":"authentication_backend.file.password.pbkdf2.salt_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_PBKDF2_SALT_LENGTH"},{"path":"authentication_backend.file.password.bcrypt.variant","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_BCRYPT_VARIANT"},{"path":"authentication_backend.file.password.bcrypt.cost","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_BCRYPT_COST"},{"path":"authentication_backend.file.password.scrypt.iterations","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SCRYPT_ITERATIONS"},{"path":"authentication_backend.file.password.scrypt.block_size","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SCRYPT_BLOCK_SIZE"},{"path":"authentication_backend.file.password.scrypt.parallelism","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SCRYPT_PARALLELISM"},{"path":"authentication_backend.file.password.scrypt.key_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SCRYPT_KEY_LENGTH"},{"path":"authentication_backend.file.password.scrypt.salt_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SCRYPT_SALT_LENGTH"},{"path":"authentication_backend.file.password.iterations","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ITERATIONS"},{"path":"authentication_backend.file.password.memory","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_MEMORY"},{"path":"authentication_backend.file.password.parallelism","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_PARALLELISM"},{"path":"authentication_backend.file.password.key_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_KEY_LENGTH"},{"path":"authentication_backend.file.password.salt_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SALT_LENGTH"},{"path":"authentication_backend.file.search.email","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_SEARCH_EMAIL"},{"path":"authentication_backend.file.search.case_insensitive","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_SEARCH_CASE_INSENSITIVE"},{"path":"authentication_backend.ldap.implementation","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_IMPLEMENTATION"},{"path":"authentication_backend.ldap.url","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_URL"},{"path":"authentication_backend.ldap.timeout","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TIMEOUT"},{"path":"authentication_backend.ldap.start_tls","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_START_TLS"},{"path":"authentication_backend.ldap.tls.minimum_version","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TLS_MINIMUM_VERSION"},{"path":"authentication_backend.ldap.tls.maximum_version","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TLS_MAXIMUM_VERSION"},{"path":"authentication_backend.ldap.tls.skip_verify","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TLS_SKIP_VERIFY"},{"path":"authentication_backend.ldap.tls.server_name","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TLS_SERVER_NAME"},{"path":"authentication_backend.ldap.tls.private_key","secret":true,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TLS_PRIVATE_KEY_FILE"},{"path":"authentication_backend.ldap.tls.certificate_chain","secret":true,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TLS_CERTIFICATE_CHAIN_FILE"},{"path":"authentication_backend.ldap.base_dn","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_BASE_DN"},{"path":"authentication_backend.ldap.additional_users_dn","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_ADDITIONAL_USERS_DN"},{"path":"authentication_backend.ldap.users_filter","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_USERS_FILTER"},{"path":"authentication_backend.ldap.additional_groups_dn","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_ADDITIONAL_GROUPS_DN"},{"path":"authentication_backend.ldap.groups_filter","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_GROUPS_FILTER"},{"path":"authentication_backend.ldap.group_name_attribute","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_GROUP_NAME_ATTRIBUTE"},{"path":"authentication_backend.ldap.username_attribute","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_USERNAME_ATTRIBUTE"},{"path":"authentication_backend.ldap.mail_attribute","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_MAIL_ATTRIBUTE"},{"path":"authentication_backend.ldap.display_name_attribute","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_DISPLAY_NAME_ATTRIBUTE"},{"path":"authentication_backend.ldap.permit_referrals","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PERMIT_REFERRALS"},{"path":"authentication_backend.ldap.permit_unauthenticated_bind","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PERMIT_UNAUTHENTICATED_BIND"},{"path":"authentication_backend.ldap.permit_feature_detection_failure","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PERMIT_FEATURE_DETECTION_FAILURE"},{"path":"authentication_backend.ldap.user","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_USER"},{"path":"authentication_backend.ldap.password","secret":true,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE"},{"path":"session.secret","secret":true,"env":"AUTHELIA_SESSION_SECRET_FILE"},{"path":"session.name","secret":false,"env":"AUTHELIA_SESSION_NAME"},{"path":"session.domain","secret":false,"env":"AUTHELIA_SESSION_DOMAIN"},{"path":"session.same_site","secret":false,"env":"AUTHELIA_SESSION_SAME_SITE"},{"path":"session.expiration","secret":false,"env":"AUTHELIA_SESSION_EXPIRATION"},{"path":"session.inactivity","secret":false,"env":"AUTHELIA_SESSION_INACTIVITY"},{"path":"session.remember_me","secret":false,"env":"AUTHELIA_SESSION_REMEMBER_ME"},{"path":"session","secret":false,"env":"AUTHELIA_SESSION"},{"path":"session.redis.host","secret":false,"env":"AUTHELIA_SESSION_REDIS_HOST"},{"path":"session.redis.port","secret":false,"env":"AUTHELIA_SESSION_REDIS_PORT"},{"path":"session.redis.username","secret":false,"env":"AUTHELIA_SESSION_REDIS_USERNAME"},{"path":"session.redis.password","secret":true,"env":"AUTHELIA_SESSION_REDIS_PASSWORD_FILE"},{"path":"session.redis.database_index","secret":false,"env":"AUTHELIA_SESSION_REDIS_DATABASE_INDEX"},{"path":"session.redis.maximum_active_connections","secret":false,"env":"AUTHELIA_SESSION_REDIS_MAXIMUM_ACTIVE_CONNECTIONS"},{"path":"session.redis.minimum_idle_connections","secret":false,"env":"AUTHELIA_SESSION_REDIS_MINIMUM_IDLE_CONNECTIONS"},{"path":"session.redis.tls.minimum_version","secret":false,"env":"AUTHELIA_SESSION_REDIS_TLS_MINIMUM_VERSION"},{"path":"session.redis.tls.maximum_version","secret":false,"env":"AUTHELIA_SESSION_REDIS_TLS_MAXIMUM_VERSION"},{"path":"session.redis.tls.skip_verify","secret":false,"env":"AUTHELIA_SESSION_REDIS_TLS_SKIP_VERIFY"},{"path":"session.redis.tls.server_name","secret":false,"env":"AUTHELIA_SESSION_REDIS_TLS_SERVER_NAME"},{"path":"session.redis.tls.private_key","secret":true,"env":"AUTHELIA_SESSION_REDIS_TLS_PRIVATE_KEY_FILE"},{"path":"session.redis.tls.certificate_chain","secret":true,"env":"AUTHELIA_SESSION_REDIS_TLS_CERTIFICATE_CHAIN_FILE"},{"path":"session.redis.high_availability.sentinel_name","secret":false,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_SENTINEL_NAME"},{"path":"session.redis.high_availability.sentinel_username","secret":false,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_SENTINEL_USERNAME"},{"path":"session.redis.high_availability.sentinel_password","secret":true,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_SENTINEL_PASSWORD_FILE"},{"path":"session.redis.high_availability.route_by_latency","secret":false,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_ROUTE_BY_LATENCY"},{"path":"session.redis.high_availability.route_randomly","secret":false,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_ROUTE_RANDOMLY"},{"path":"totp.disable","secret":false,"env":"AUTHELIA_TOTP_DISABLE"},{"path":"totp.issuer","secret":false,"env":"AUTHELIA_TOTP_ISSUER"},{"path":"totp.algorithm","secret":false,"env":"AUTHELIA_TOTP_ALGORITHM"},{"path":"totp.digits","secret":false,"env":"AUTHELIA_TOTP_DIGITS"},{"path":"totp.period","secret":false,"env":"AUTHELIA_TOTP_PERIOD"},{"path":"totp.skew","secret":false,"env":"AUTHELIA_TOTP_SKEW"},{"path":"totp.secret_size","secret":false,"env":"AUTHELIA_TOTP_SECRET_SIZE"},{"path":"duo_api.disable","secret":false,"env":"AUTHELIA_DUO_API_DISABLE"},{"path":"duo_api.hostname","secret":false,"env":"AUTHELIA_DUO_API_HOSTNAME"},{"path":"duo_api.integration_key","secret":true,"env":"AUTHELIA_DUO_API_INTEGRATION_KEY_FILE"},{"path":"duo_api.secret_key","secret":true,"env":"AUTHELIA_DUO_API_SECRET_KEY_FILE"},{"path":"duo_api.enable_self_enrollment","secret":false,"env":"AUTHELIA_DUO_API_ENABLE_SELF_ENROLLMENT"},{"path":"access_control.default_policy","secret":false,"env":"AUTHELIA_ACCESS_CONTROL_DEFAULT_POLICY"},{"path":"ntp.address","secret":false,"env":"AUTHELIA_NTP_ADDRESS"},{"path":"ntp.version","secret":false,"env":"AUTHELIA_NTP_VERSION"},{"path":"ntp.max_desync","secret":false,"env":"AUTHELIA_NTP_MAX_DESYNC"},{"path":"ntp.disable_startup_check","secret":false,"env":"AUTHELIA_NTP_DISABLE_STARTUP_CHECK"},{"path":"ntp.disable_failure","secret":false,"env":"AUTHELIA_NTP_DISABLE_FAILURE"},{"path":"regulation.max_retries","secret":false,"env":"AUTHELIA_REGULATION_MAX_RETRIES"},{"path":"regulation.find_time","secret":false,"env":"AUTHELIA_REGULATION_FIND_TIME"},{"path":"regulation.ban_time","secret":false,"env":"AUTHELIA_REGULATION_BAN_TIME"},{"path":"storage.local.path","secret":false,"env":"AUTHELIA_STORAGE_LOCAL_PATH"},{"path":"storage.mysql.host","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_HOST"},{"path":"storage.mysql.port","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_PORT"},{"path":"storage.mysql.database","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_DATABASE"},{"path":"storage.mysql.username","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_USERNAME"},{"path":"storage.mysql.password","secret":true,"env":"AUTHELIA_STORAGE_MYSQL_PASSWORD_FILE"},{"path":"storage.mysql.timeout","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_TIMEOUT"},{"path":"storage.mysql.tls.minimum_version","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_TLS_MINIMUM_VERSION"},{"path":"storage.mysql.tls.maximum_version","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_TLS_MAXIMUM_VERSION"},{"path":"storage.mysql.tls.skip_verify","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_TLS_SKIP_VERIFY"},{"path":"storage.mysql.tls.server_name","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_TLS_SERVER_NAME"},{"path":"storage.mysql.tls.private_key","secret":true,"env":"AUTHELIA_STORAGE_MYSQL_TLS_PRIVATE_KEY_FILE"},{"path":"storage.mysql.tls.certificate_chain","secret":true,"env":"AUTHELIA_STORAGE_MYSQL_TLS_CERTIFICATE_CHAIN_FILE"},{"path":"storage.postgres.host","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_HOST"},{"path":"storage.postgres.port","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_PORT"},{"path":"storage.postgres.database","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_DATABASE"},{"path":"storage.postgres.username","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_USERNAME"},{"path":"storage.postgres.password","secret":true,"env":"AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE"},{"path":"storage.postgres.timeout","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_TIMEOUT"},{"path":"storage.postgres.schema","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_SCHEMA"},{"path":"storage.postgres.tls.minimum_version","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_TLS_MINIMUM_VERSION"},{"path":"storage.postgres.tls.maximum_version","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_TLS_MAXIMUM_VERSION"},{"path":"storage.postgres.tls.skip_verify","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_TLS_SKIP_VERIFY"},{"path":"storage.postgres.tls.server_name","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_TLS_SERVER_NAME"},{"path":"storage.postgres.tls.private_key","secret":true,"env":"AUTHELIA_STORAGE_POSTGRES_TLS_PRIVATE_KEY_FILE"},{"path":"storage.postgres.tls.certificate_chain","secret":true,"env":"AUTHELIA_STORAGE_POSTGRES_TLS_CERTIFICATE_CHAIN_FILE"},{"path":"storage.postgres.ssl.mode","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_SSL_MODE"},{"path":"storage.postgres.ssl.root_certificate","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_SSL_ROOT_CERTIFICATE"},{"path":"storage.postgres.ssl.certificate","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_SSL_CERTIFICATE"},{"path":"storage.postgres.ssl.key","secret":true,"env":"AUTHELIA_STORAGE_POSTGRES_SSL_KEY_FILE"},{"path":"storage.encryption_key","secret":true,"env":"AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE"},{"path":"notifier.disable_startup_check","secret":false,"env":"AUTHELIA_NOTIFIER_DISABLE_STARTUP_CHECK"},{"path":"notifier.filesystem.filename","secret":false,"env":"AUTHELIA_NOTIFIER_FILESYSTEM_FILENAME"},{"path":"notifier.smtp.host","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_HOST"},{"path":"notifier.smtp.port","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_PORT"},{"path":"notifier.smtp.timeout","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_TIMEOUT"},{"path":"notifier.smtp.username","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_USERNAME"},{"path":"notifier.smtp.password","secret":true,"env":"AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE"},{"path":"notifier.smtp.identifier","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_IDENTIFIER"},{"path":"notifier.smtp.sender","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_SENDER"},{"path":"notifier.smtp.subject","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_SUBJECT"},{"path":"notifier.smtp.startup_check_address","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_STARTUP_CHECK_ADDRESS"},{"path":"notifier.smtp.disable_require_tls","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_DISABLE_REQUIRE_TLS"},{"path":"notifier.smtp.disable_html_emails","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_DISABLE_HTML_EMAILS"},{"path":"notifier.smtp.disable_starttls","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_DISABLE_STARTTLS"},{"path":"notifier.smtp.tls.minimum_version","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_TLS_MINIMUM_VERSION"},{"path":"notifier.smtp.tls.maximum_version","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_TLS_MAXIMUM_VERSION"},{"path":"notifier.smtp.tls.skip_verify","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_TLS_SKIP_VERIFY"},{"path":"notifier.smtp.tls.server_name","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_TLS_SERVER_NAME"},{"path":"notifier.smtp.tls.private_key","secret":true,"env":"AUTHELIA_NOTIFIER_SMTP_TLS_PRIVATE_KEY_FILE"},{"path":"notifier.smtp.tls.certificate_chain","secret":true,"env":"AUTHELIA_NOTIFIER_SMTP_TLS_CERTIFICATE_CHAIN_FILE"},{"path":"notifier.template_path","secret":false,"env":"AUTHELIA_NOTIFIER_TEMPLATE_PATH"},{"path":"server.host","secret":false,"env":"AUTHELIA_SERVER_HOST"},{"path":"server.port","secret":false,"env":"AUTHELIA_SERVER_PORT"},{"path":"server.path","secret":false,"env":"AUTHELIA_SERVER_PATH"},{"path":"server.asset_path","secret":false,"env":"AUTHELIA_SERVER_ASSET_PATH"},{"path":"server.disable_healthcheck","secret":false,"env":"AUTHELIA_SERVER_DISABLE_HEALTHCHECK"},{"path":"server.tls.certificate","secret":false,"env":"AUTHELIA_SERVER_TLS_CERTIFICATE"},{"path":"server.tls.key","secret":true,"env":"AUTHELIA_SERVER_TLS_KEY_FILE"},{"path":"server.headers.csp_template","secret":false,"env":"AUTHELIA_SERVER_HEADERS_CSP_TEMPLATE"},{"path":"server.endpoints.enable_pprof","secret":false,"env":"AUTHELIA_SERVER_ENDPOINTS_ENABLE_PPROF"},{"path":"server.endpoints.enable_expvars","secret":false,"env":"AUTHELIA_SERVER_ENDPOINTS_ENABLE_EXPVARS"},{"path":"server.buffers.read","secret":false,"env":"AUTHELIA_SERVER_BUFFERS_READ"},{"path":"server.buffers.write","secret":false,"env":"AUTHELIA_SERVER_BUFFERS_WRITE"},{"path":"server.timeouts.read","secret":false,"env":"AUTHELIA_SERVER_TIMEOUTS_READ"},{"path":"server.timeouts.write","secret":false,"env":"AUTHELIA_SERVER_TIMEOUTS_WRITE"},{"path":"server.timeouts.idle","secret":false,"env":"AUTHELIA_SERVER_TIMEOUTS_IDLE"},{"path":"telemetry.metrics.enabled","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_ENABLED"},{"path":"telemetry.metrics.address","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_ADDRESS"},{"path":"telemetry.metrics.buffers.read","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_BUFFERS_READ"},{"path":"telemetry.metrics.buffers.write","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_BUFFERS_WRITE"},{"path":"telemetry.metrics.timeouts.read","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_TIMEOUTS_READ"},{"path":"telemetry.metrics.timeouts.write","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_TIMEOUTS_WRITE"},{"path":"telemetry.metrics.timeouts.idle","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_TIMEOUTS_IDLE"},{"path":"webauthn.disable","secret":false,"env":"AUTHELIA_WEBAUTHN_DISABLE"},{"path":"webauthn.display_name","secret":false,"env":"AUTHELIA_WEBAUTHN_DISPLAY_NAME"},{"path":"webauthn.attestation_conveyance_preference","secret":false,"env":"AUTHELIA_WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE"},{"path":"webauthn.user_verification","secret":false,"env":"AUTHELIA_WEBAUTHN_USER_VERIFICATION"},{"path":"webauthn.timeout","secret":false,"env":"AUTHELIA_WEBAUTHN_TIMEOUT"},{"path":"password_policy.standard.enabled","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_ENABLED"},{"path":"password_policy.standard.min_length","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_MIN_LENGTH"},{"path":"password_policy.standard.max_length","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_MAX_LENGTH"},{"path":"password_policy.standard.require_uppercase","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_REQUIRE_UPPERCASE"},{"path":"password_policy.standard.require_lowercase","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_REQUIRE_LOWERCASE"},{"path":"password_policy.standard.require_number","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_REQUIRE_NUMBER"},{"path":"password_policy.standard.require_special","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_REQUIRE_SPECIAL"},{"path":"password_policy.zxcvbn.enabled","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_ZXCVBN_ENABLED"},{"path":"password_policy.zxcvbn.min_score","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_ZXCVBN_MIN_SCORE"},{"path":"privacy_policy.enabled","secret":false,"env":"AUTHELIA_PRIVACY_POLICY_ENABLED"},{"path":"privacy_policy.require_user_acceptance","secret":false,"env":"AUTHELIA_PRIVACY_POLICY_REQUIRE_USER_ACCEPTANCE"},{"path":"privacy_policy.policy_url","secret":false,"env":"AUTHELIA_PRIVACY_POLICY_POLICY_URL"}] \ No newline at end of file diff --git a/internal/configuration/schema/identity_providers.go b/internal/configuration/schema/identity_providers.go index d56d79f57..57376dc87 100644 --- a/internal/configuration/schema/identity_providers.go +++ b/internal/configuration/schema/identity_providers.go @@ -29,10 +29,17 @@ type OpenIDConnectConfiguration struct { EnablePKCEPlainChallenge bool `koanf:"enable_pkce_plain_challenge"` CORS OpenIDConnectCORSConfiguration `koanf:"cors"` + PAR OpenIDConnectPARConfiguration `koanf:"pushed_authorizations"` Clients []OpenIDConnectClientConfiguration `koanf:"clients"` } +// OpenIDConnectPARConfiguration represents an OpenID Connect PAR config. +type OpenIDConnectPARConfiguration struct { + Enforce bool `koanf:"enforce"` + ContextLifespan time.Duration `koanf:"context_lifespan"` +} + // OpenIDConnectCORSConfiguration represents an OpenID Connect CORS config. type OpenIDConnectCORSConfiguration struct { Endpoints []string `koanf:"endpoints"` @@ -59,6 +66,7 @@ type OpenIDConnectClientConfiguration struct { Policy string `koanf:"authorization_policy"` + EnforcePAR bool `koanf:"enforce_par"` EnforcePKCE bool `koanf:"enforce_pkce"` PKCEChallengeMethod string `koanf:"pkce_challenge_method"` diff --git a/internal/configuration/schema/keys.go b/internal/configuration/schema/keys.go index a5122ee82..526913918 100644 --- a/internal/configuration/schema/keys.go +++ b/internal/configuration/schema/keys.go @@ -31,6 +31,8 @@ var Keys = []string{ "identity_providers.oidc.cors.endpoints", "identity_providers.oidc.cors.allowed_origins", "identity_providers.oidc.cors.allowed_origins_from_client_redirect_uris", + "identity_providers.oidc.pushed_authorizations.enforce", + "identity_providers.oidc.pushed_authorizations.context_lifespan", "identity_providers.oidc.clients", "identity_providers.oidc.clients[].id", "identity_providers.oidc.clients[].description", @@ -44,6 +46,7 @@ var Keys = []string{ "identity_providers.oidc.clients[].response_types", "identity_providers.oidc.clients[].response_modes", "identity_providers.oidc.clients[].authorization_policy", + "identity_providers.oidc.clients[].enforce_par", "identity_providers.oidc.clients[].enforce_pkce", "identity_providers.oidc.clients[].pkce_challenge_method", "identity_providers.oidc.clients[].userinfo_signing_algorithm", diff --git a/internal/configuration/validator/const.go b/internal/configuration/validator/const.go index 8730518de..0f4b26a0c 100644 --- a/internal/configuration/validator/const.go +++ b/internal/configuration/validator/const.go @@ -392,7 +392,7 @@ var ( validOIDCGrantTypes = []string{oidc.GrantTypeImplicit, oidc.GrantTypeRefreshToken, oidc.GrantTypeAuthorizationCode, oidc.GrantTypePassword, oidc.GrantTypeClientCredentials} validOIDCResponseModes = []string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery, oidc.ResponseModeFragment} validOIDCUserinfoAlgorithms = []string{oidc.SigningAlgorithmNone, oidc.SigningAlgorithmRSAWithSHA256} - validOIDCCORSEndpoints = []string{oidc.EndpointAuthorization, oidc.EndpointToken, oidc.EndpointIntrospection, oidc.EndpointRevocation, oidc.EndpointUserinfo} + validOIDCCORSEndpoints = []string{oidc.EndpointAuthorization, oidc.EndpointPushedAuthorizationRequest, oidc.EndpointToken, oidc.EndpointIntrospection, oidc.EndpointRevocation, oidc.EndpointUserinfo} validOIDCClientConsentModes = []string{"auto", oidc.ClientConsentModeImplicit.String(), oidc.ClientConsentModeExplicit.String(), oidc.ClientConsentModePreConfigured.String()} ) diff --git a/internal/configuration/validator/identity_providers_test.go b/internal/configuration/validator/identity_providers_test.go index d0e345e97..8113e857e 100644 --- a/internal/configuration/validator/identity_providers_test.go +++ b/internal/configuration/validator/identity_providers_test.go @@ -80,7 +80,7 @@ func TestShouldRaiseErrorWhenCORSEndpointsNotValid(t *testing.T) { require.Len(t, validator.Errors(), 1) - assert.EqualError(t, validator.Errors()[0], "identity_providers: oidc: cors: option 'endpoints' contains an invalid value 'invalid_endpoint': must be one of 'authorization', 'token', 'introspection', 'revocation', 'userinfo'") + assert.EqualError(t, validator.Errors()[0], "identity_providers: oidc: cors: option 'endpoints' contains an invalid value 'invalid_endpoint': must be one of 'authorization', 'pushed-authorization-request', 'token', 'introspection', 'revocation', 'userinfo'") } func TestShouldRaiseErrorWhenOIDCPKCEEnforceValueInvalid(t *testing.T) { diff --git a/internal/handlers/handler_oidc_authorization.go b/internal/handlers/handler_oidc_authorization.go index 57f686068..29147a0de 100644 --- a/internal/handlers/handler_oidc_authorization.go +++ b/internal/handlers/handler_oidc_authorization.go @@ -53,10 +53,20 @@ func OpenIDConnectAuthorization(ctx *middlewares.AutheliaCtx, rw http.ResponseWr return } - if err = client.ValidateAuthorizationPolicy(requester); err != nil { + if err = client.ValidatePARPolicy(requester, ctx.Providers.OpenIDConnect.GetPushedAuthorizeRequestURIPrefix(ctx)); err != nil { rfc := fosite.ErrorToRFC6749Error(err) - ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' failed to validate the authorization policy: %s", requester.GetID(), clientID, rfc.WithExposeDebug(true).GetDescription()) + ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' failed to validate the PAR policy: %s", requester.GetID(), clientID, rfc.WithExposeDebug(true).GetDescription()) + + ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, err) + + return + } + + if err = client.ValidatePKCEPolicy(requester); err != nil { + rfc := fosite.ErrorToRFC6749Error(err) + + ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' failed to validate the PKCE policy: %s", requester.GetID(), clientID, rfc.WithExposeDebug(true).GetDescription()) ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, err) @@ -95,13 +105,13 @@ func OpenIDConnectAuthorization(ctx *middlewares.AutheliaCtx, rw http.ResponseWr ctx.Logger.Debugf("Authorization Request with id '%s' on client with id '%s' was successfully processed, proceeding to build Authorization Response", requester.GetID(), clientID) - oidcSession := oidc.NewSessionWithAuthorizeRequest(issuer, ctx.Providers.OpenIDConnect.KeyManager.GetActiveKeyID(), + session := oidc.NewSessionWithAuthorizeRequest(issuer, ctx.Providers.OpenIDConnect.KeyManager.GetActiveKeyID(), userSession.Username, userSession.AuthenticationMethodRefs.MarshalRFC8176(), extraClaims, authTime, consent, requester) ctx.Logger.Tracef("Authorization Request with id '%s' on client with id '%s' creating session for Authorization Response for subject '%s' with username '%s' with claims: %+v", - requester.GetID(), oidcSession.ClientID, oidcSession.Subject, oidcSession.Username, oidcSession.Claims) + requester.GetID(), session.ClientID, session.Subject, session.Username, session.Claims) - if responder, err = ctx.Providers.OpenIDConnect.NewAuthorizeResponse(ctx, requester, oidcSession); err != nil { + if responder, err = ctx.Providers.OpenIDConnect.NewAuthorizeResponse(ctx, requester, session); err != nil { rfc := fosite.ErrorToRFC6749Error(err) ctx.Logger.Errorf("Authorization Response for Request with id '%s' on client with id '%s' could not be created: %s", requester.GetID(), clientID, rfc.WithExposeDebug(true).GetDescription()) @@ -125,3 +135,62 @@ func OpenIDConnectAuthorization(ctx *middlewares.AutheliaCtx, rw http.ResponseWr ctx.Providers.OpenIDConnect.WriteAuthorizeResponse(ctx, rw, requester, responder) } + +// OpenIDConnectPushedAuthorizationRequest handles POST requests to the OAuth 2.0 Pushed Authorization Requests endpoint. +// +// RFC9126 https://www.rfc-editor.org/rfc/rfc9126.html +func OpenIDConnectPushedAuthorizationRequest(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter, r *http.Request) { + var ( + requester fosite.AuthorizeRequester + responder fosite.PushedAuthorizeResponder + err error + ) + + if requester, err = ctx.Providers.OpenIDConnect.NewPushedAuthorizeRequest(ctx, r); err != nil { + rfc := fosite.ErrorToRFC6749Error(err) + + ctx.Logger.Errorf("Pushed Authorization Request failed with error: %s", rfc.WithExposeDebug(true).GetDescription()) + + ctx.Providers.OpenIDConnect.WritePushedAuthorizeError(ctx, rw, requester, err) + + return + } + + var client *oidc.Client + + clientID := requester.GetClient().GetID() + + if client, err = ctx.Providers.OpenIDConnect.GetFullClient(clientID); err != nil { + if errors.Is(err, fosite.ErrNotFound) { + ctx.Logger.Errorf("Pushed Authorization Request with id '%s' on client with id '%s' could not be processed: client was not found", requester.GetID(), clientID) + } else { + ctx.Logger.Errorf("Pushed Authorization Request with id '%s' on client with id '%s' could not be processed: failed to find client: %+v", requester.GetID(), clientID, err) + } + + ctx.Providers.OpenIDConnect.WritePushedAuthorizeError(ctx, rw, requester, err) + + return + } + + if err = client.ValidatePKCEPolicy(requester); err != nil { + rfc := fosite.ErrorToRFC6749Error(err) + + ctx.Logger.Errorf("Pushed Authorization Request with id '%s' on client with id '%s' failed to validate the PKCE policy: %s", requester.GetID(), clientID, rfc.WithExposeDebug(true).GetDescription()) + + ctx.Providers.OpenIDConnect.WritePushedAuthorizeError(ctx, rw, requester, err) + + return + } + + if responder, err = ctx.Providers.OpenIDConnect.NewPushedAuthorizeResponse(ctx, requester, oidc.NewSession()); err != nil { + rfc := fosite.ErrorToRFC6749Error(err) + + ctx.Logger.Errorf("Pushed Authorization Request failed with error: %s", rfc.WithExposeDebug(true).GetDescription()) + + ctx.Providers.OpenIDConnect.WritePushedAuthorizeError(ctx, rw, requester, err) + + return + } + + ctx.Providers.OpenIDConnect.WritePushedAuthorizeResponse(ctx, rw, requester, responder) +} diff --git a/internal/mocks/notifier.go b/internal/mocks/notifier.go index 36ef6f94c..b568e808c 100644 --- a/internal/mocks/notifier.go +++ b/internal/mocks/notifier.go @@ -9,9 +9,8 @@ import ( mail "net/mail" reflect "reflect" - gomock "github.com/golang/mock/gomock" - templates "github.com/authelia/authelia/v4/internal/templates" + gomock "github.com/golang/mock/gomock" ) // MockNotifier is a mock of Notifier interface. diff --git a/internal/mocks/storage.go b/internal/mocks/storage.go index 11c5aea49..2c74f695e 100644 --- a/internal/mocks/storage.go +++ b/internal/mocks/storage.go @@ -10,11 +10,10 @@ import ( reflect "reflect" time "time" - gomock "github.com/golang/mock/gomock" - uuid "github.com/google/uuid" - model "github.com/authelia/authelia/v4/internal/model" storage "github.com/authelia/authelia/v4/internal/storage" + gomock "github.com/golang/mock/gomock" + uuid "github.com/google/uuid" ) // MockStorage is a mock of Provider interface. @@ -40,6 +39,7 @@ func (m *MockStorage) EXPECT() *MockStorageMockRecorder { return m.recorder } + // AppendAuthenticationLog mocks base method. func (m *MockStorage) AppendAuthenticationLog(arg0 context.Context, arg1 model.AuthenticationAttempt) error { m.ctrl.T.Helper() @@ -270,6 +270,21 @@ func (mr *MockStorageMockRecorder) LoadOAuth2ConsentSessionByChallengeID(arg0, a return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadOAuth2ConsentSessionByChallengeID", reflect.TypeOf((*MockStorage)(nil).LoadOAuth2ConsentSessionByChallengeID), arg0, arg1) } +// LoadOAuth2PARContext mocks base method. +func (m *MockStorage) LoadOAuth2PARContext(arg0 context.Context, arg1 string) (*model.OAuth2PARContext, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LoadOAuth2PARContext", arg0, arg1) + ret0, _ := ret[0].(*model.OAuth2PARContext) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// LoadOAuth2PARContext indicates an expected call of LoadOAuth2PARContext. +func (mr *MockStorageMockRecorder) LoadOAuth2PARContext(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadOAuth2PARContext", reflect.TypeOf((*MockStorage)(nil).LoadOAuth2PARContext), arg0, arg1) +} + // LoadOAuth2Session mocks base method. func (m *MockStorage) LoadOAuth2Session(arg0 context.Context, arg1 storage.OAuth2SessionType, arg2 string) (*model.OAuth2Session, error) { m.ctrl.T.Helper() @@ -435,6 +450,20 @@ func (mr *MockStorageMockRecorder) LoadWebauthnDevicesByUsername(arg0, arg1 inte return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadWebauthnDevicesByUsername", reflect.TypeOf((*MockStorage)(nil).LoadWebauthnDevicesByUsername), arg0, arg1) } +// RevokeOAuth2PARContext mocks base method. +func (m *MockStorage) RevokeOAuth2PARContext(arg0 context.Context, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RevokeOAuth2PARContext", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// RevokeOAuth2PARContext indicates an expected call of RevokeOAuth2PARContext. +func (mr *MockStorageMockRecorder) RevokeOAuth2PARContext(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RevokeOAuth2PARContext", reflect.TypeOf((*MockStorage)(nil).RevokeOAuth2PARContext), arg0, arg1) +} + // RevokeOAuth2Session mocks base method. func (m *MockStorage) RevokeOAuth2Session(arg0 context.Context, arg1 storage.OAuth2SessionType, arg2 string) error { m.ctrl.T.Helper() @@ -576,6 +605,20 @@ func (mr *MockStorageMockRecorder) SaveOAuth2ConsentSessionSubject(arg0, arg1 in return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveOAuth2ConsentSessionSubject", reflect.TypeOf((*MockStorage)(nil).SaveOAuth2ConsentSessionSubject), arg0, arg1) } +// SaveOAuth2PARContext mocks base method. +func (m *MockStorage) SaveOAuth2PARContext(arg0 context.Context, arg1 model.OAuth2PARContext) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SaveOAuth2PARContext", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// SaveOAuth2PARContext indicates an expected call of SaveOAuth2PARContext. +func (mr *MockStorageMockRecorder) SaveOAuth2PARContext(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveOAuth2PARContext", reflect.TypeOf((*MockStorage)(nil).SaveOAuth2PARContext), arg0, arg1) +} + // SaveOAuth2Session mocks base method. func (m *MockStorage) SaveOAuth2Session(arg0 context.Context, arg1 storage.OAuth2SessionType, arg2 model.OAuth2Session) error { m.ctrl.T.Helper() diff --git a/internal/mocks/totp.go b/internal/mocks/totp.go index 673cdb8c0..8b029b6ea 100644 --- a/internal/mocks/totp.go +++ b/internal/mocks/totp.go @@ -7,9 +7,8 @@ package mocks import ( reflect "reflect" - gomock "github.com/golang/mock/gomock" - model "github.com/authelia/authelia/v4/internal/model" + gomock "github.com/golang/mock/gomock" ) // MockTOTP is a mock of Provider interface. diff --git a/internal/mocks/user_provider.go b/internal/mocks/user_provider.go index 25d0e2aec..4cc592dc9 100644 --- a/internal/mocks/user_provider.go +++ b/internal/mocks/user_provider.go @@ -7,9 +7,8 @@ package mocks import ( reflect "reflect" - gomock "github.com/golang/mock/gomock" - authentication "github.com/authelia/authelia/v4/internal/authentication" + gomock "github.com/golang/mock/gomock" ) // MockUserProvider is a mock of UserProvider interface. diff --git a/internal/model/oidc.go b/internal/model/oidc.go index 1d742543d..c0f2b51fb 100644 --- a/internal/model/oidc.go +++ b/internal/model/oidc.go @@ -39,6 +39,14 @@ func NewOAuth2ConsentSession(subject uuid.UUID, r fosite.Requester) (consent *OA return consent, nil } +// NewOAuth2BlacklistedJTI creates a new OAuth2BlacklistedJTI. +func NewOAuth2BlacklistedJTI(jti string, exp time.Time) (jtiBlacklist OAuth2BlacklistedJTI) { + return OAuth2BlacklistedJTI{ + Signature: fmt.Sprintf("%x", sha256.Sum256([]byte(jti))), + ExpiresAt: exp, + } +} + // NewOAuth2SessionFromRequest creates a new OAuth2Session from a signature and fosite.Requester. func NewOAuth2SessionFromRequest(signature string, r fosite.Requester) (session *OAuth2Session, err error) { var ( @@ -77,12 +85,43 @@ func NewOAuth2SessionFromRequest(signature string, r fosite.Requester) (session }, nil } -// NewOAuth2BlacklistedJTI creates a new OAuth2BlacklistedJTI. -func NewOAuth2BlacklistedJTI(jti string, exp time.Time) (jtiBlacklist OAuth2BlacklistedJTI) { - return OAuth2BlacklistedJTI{ - Signature: fmt.Sprintf("%x", sha256.Sum256([]byte(jti))), - ExpiresAt: exp, +// NewOAuth2PARContext creates a new Pushed Authorization Request Context as a OAuth2PARContext. +func NewOAuth2PARContext(contextID string, r fosite.AuthorizeRequester) (context *OAuth2PARContext, err error) { + var ( + s *OpenIDSession + ok bool + req *fosite.AuthorizeRequest + session []byte + ) + + if s, ok = r.GetSession().(*OpenIDSession); !ok { + return nil, fmt.Errorf("can't convert type '%T' to an *OAuth2Session", r.GetSession()) } + + if session, err = json.Marshal(s); err != nil { + return nil, err + } + + var handled StringSlicePipeDelimited + + if req, ok = r.(*fosite.AuthorizeRequest); ok { + handled = StringSlicePipeDelimited(req.HandledResponseTypes) + } + + return &OAuth2PARContext{ + Signature: contextID, + RequestID: r.GetID(), + ClientID: r.GetClient().GetID(), + RequestedAt: r.GetRequestedAt(), + Scopes: StringSlicePipeDelimited(r.GetRequestedScopes()), + Audience: StringSlicePipeDelimited(r.GetRequestedAudience()), + HandledResponseTypes: handled, + ResponseMode: string(r.GetResponseMode()), + DefaultResponseMode: string(r.GetDefaultResponseMode()), + Revoked: false, + Form: r.GetRequestForm().Encode(), + Session: session, + }, nil } // OAuth2ConsentPreConfig stores information about an OAuth2.0 Pre-Configured Consent. @@ -264,6 +303,70 @@ func (s *OAuth2Session) ToRequest(ctx context.Context, session fosite.Session, s }, nil } +// OAuth2PARContext holds relevant information about a Pushed Authorization Request in order to process the authorization. +type OAuth2PARContext struct { + ID int `db:"id"` + Signature string `db:"signature"` + RequestID string `db:"request_id"` + ClientID string `db:"client_id"` + RequestedAt time.Time `db:"requested_at"` + Scopes StringSlicePipeDelimited `db:"scopes"` + Audience StringSlicePipeDelimited `db:"audience"` + HandledResponseTypes StringSlicePipeDelimited `db:"handled_response_types"` + ResponseMode string `db:"response_mode"` + DefaultResponseMode string `db:"response_mode_default"` + Revoked bool `db:"revoked"` + Form string `db:"form_data"` + Session []byte `db:"session_data"` +} + +func (par *OAuth2PARContext) ToAuthorizeRequest(ctx context.Context, session fosite.Session, store fosite.Storage) (request *fosite.AuthorizeRequest, err error) { + if session != nil { + if err = json.Unmarshal(par.Session, session); err != nil { + return nil, err + } + } + + var ( + client fosite.Client + form url.Values + ) + + if client, err = store.GetClient(ctx, par.ClientID); err != nil { + return nil, err + } + + if form, err = url.ParseQuery(par.Form); err != nil { + return nil, err + } + + request = fosite.NewAuthorizeRequest() + + request.Request = fosite.Request{ + ID: par.RequestID, + RequestedAt: par.RequestedAt, + Client: client, + RequestedScope: fosite.Arguments(par.Scopes), + RequestedAudience: fosite.Arguments(par.Audience), + Form: form, + Session: session, + } + + if par.ResponseMode != "" { + request.ResponseMode = fosite.ResponseModeType(par.ResponseMode) + } + + if par.DefaultResponseMode != "" { + request.DefaultResponseMode = fosite.ResponseModeType(par.DefaultResponseMode) + } + + if len(par.HandledResponseTypes) != 0 { + request.HandledResponseTypes = fosite.Arguments(par.HandledResponseTypes) + } + + return request, nil +} + // OpenIDSession holds OIDC Session information. type OpenIDSession struct { *openid.DefaultSession `json:"id_token"` diff --git a/internal/oidc/client.go b/internal/oidc/client.go index 633161bc6..8388db9e7 100644 --- a/internal/oidc/client.go +++ b/internal/oidc/client.go @@ -1,7 +1,7 @@ package oidc import ( - "fmt" + "strings" "github.com/ory/fosite" "github.com/ory/x/errorsx" @@ -32,6 +32,8 @@ func NewClient(config schema.OpenIDConnectClientConfiguration) (client *Client) ResponseTypes: config.ResponseTypes, ResponseModes: []fosite.ResponseModeType{fosite.ResponseModeDefault}, + EnforcePAR: config.EnforcePAR, + UserinfoSigningAlgorithm: config.UserinfoSigningAlgorithm, Policy: authorization.NewLevel(config.Policy), @@ -46,22 +48,22 @@ func NewClient(config schema.OpenIDConnectClientConfiguration) (client *Client) return client } -// ValidateAuthorizationPolicy is a helper function to validate additional policy constraints on a per-client basis. -func (c *Client) ValidateAuthorizationPolicy(r fosite.Requester) (err error) { +// ValidatePKCEPolicy is a helper function to validate PKCE policy constraints on a per-client basis. +func (c *Client) ValidatePKCEPolicy(r fosite.Requester) (err error) { form := r.GetRequestForm() if c.EnforcePKCE { - if form.Get("code_challenge") == "" { + if form.Get(FormParameterCodeChallenge) == "" { return errorsx.WithStack(fosite.ErrInvalidRequest. WithHint("Clients must include a code_challenge when performing the authorize code flow, but it is missing."). WithDebug("The server is configured in a way that enforces PKCE for this client.")) } if c.EnforcePKCEChallengeMethod { - if method := form.Get("code_challenge_method"); method != c.PKCEChallengeMethod { + if method := form.Get(FormParameterCodeChallengeMethod); method != c.PKCEChallengeMethod { return errorsx.WithStack(fosite.ErrInvalidRequest. - WithHint(fmt.Sprintf("Client must use code_challenge_method=%s, %s is not allowed.", c.PKCEChallengeMethod, method)). - WithDebug(fmt.Sprintf("The server is configured in a way that enforces PKCE %s as challenge method for this client.", c.PKCEChallengeMethod))) + WithHintf("Client must use code_challenge_method=%s, %s is not allowed.", c.PKCEChallengeMethod, method). + WithDebugf("The server is configured in a way that enforces PKCE %s as challenge method for this client.", c.PKCEChallengeMethod)) } } } @@ -69,6 +71,23 @@ func (c *Client) ValidateAuthorizationPolicy(r fosite.Requester) (err error) { return nil } +// ValidatePARPolicy is a helper function to validate additional policy constraints on a per-client basis. +func (c *Client) ValidatePARPolicy(r fosite.Requester, prefix string) (err error) { + form := r.GetRequestForm() + + if c.EnforcePAR { + if requestURI := form.Get(FormParameterRequestURI); !strings.HasPrefix(requestURI, prefix) { + if requestURI == "" { + return errorsx.WithStack(ErrPAREnforcedClientMissingPAR.WithDebug("The request_uri parameter was empty.")) + } + + return errorsx.WithStack(ErrPAREnforcedClientMissingPAR.WithDebugf("The request_uri parameter '%s' is malformed.", requestURI)) + } + } + + return nil +} + // IsAuthenticationLevelSufficient returns if the provided authentication.Level is sufficient for the client of the AutheliaClient. func (c *Client) IsAuthenticationLevelSufficient(level authentication.Level) bool { if level == authentication.NotAuthenticated { @@ -105,7 +124,7 @@ func (c *Client) GetID() string { } // GetHashedSecret returns the Secret. -func (c *Client) GetHashedSecret() []byte { +func (c *Client) GetHashedSecret() (secret []byte) { if c.Secret == nil { return []byte(nil) } @@ -114,7 +133,7 @@ func (c *Client) GetHashedSecret() []byte { } // GetRedirectURIs returns the RedirectURIs. -func (c *Client) GetRedirectURIs() []string { +func (c *Client) GetRedirectURIs() (redirectURIs []string) { return c.RedirectURIs } diff --git a/internal/oidc/client_test.go b/internal/oidc/client_test.go index 23b53aaec..747f1e8c2 100644 --- a/internal/oidc/client_test.go +++ b/internal/oidc/client_test.go @@ -224,7 +224,7 @@ func TestNewClientPKCE(t *testing.T) { expectedEnforcePKCE bool expectedEnforcePKCEChallengeMethod bool expected string - req *fosite.Request + r *fosite.Request err string }{ { @@ -288,8 +288,8 @@ func TestNewClientPKCE(t *testing.T) { assert.Equal(t, tc.expectedEnforcePKCEChallengeMethod, client.EnforcePKCEChallengeMethod) assert.Equal(t, tc.expected, client.PKCEChallengeMethod) - if tc.req != nil { - err := client.ValidateAuthorizationPolicy(tc.req) + if tc.r != nil { + err := client.ValidatePKCEPolicy(tc.r) if tc.err != "" { assert.EqualError(t, err, tc.err) diff --git a/internal/oidc/config.go b/internal/oidc/config.go index 695086c74..1cc7bf098 100644 --- a/internal/oidc/config.go +++ b/internal/oidc/config.go @@ -24,8 +24,8 @@ import ( "github.com/authelia/authelia/v4/internal/utils" ) -func NewConfig(config *schema.OpenIDConnectConfiguration, templates *templates.Provider) *Config { - c := &Config{ +func NewConfig(config *schema.OpenIDConnectConfiguration, templates *templates.Provider) (c *Config) { + c = &Config{ GlobalSecret: []byte(utils.HashSHA256FromString(config.HMACSecret)), SendDebugMessagesToClients: config.EnableClientDebugMessages, MinParameterEntropy: config.MinimumParameterEntropy, @@ -40,18 +40,23 @@ func NewConfig(config *schema.OpenIDConnectConfiguration, templates *templates.P EnforcePublicClients: config.EnforcePKCE != "never", AllowPlainChallengeMethod: config.EnablePKCEPlainChallenge, }, + PAR: PARConfig{ + Enforced: config.PAR.Enforce, + ContextLifespan: config.PAR.ContextLifespan, + URIPrefix: urnPARPrefix, + }, Templates: templates, } c.Strategy.Core = &HMACCoreStrategy{ Enigma: &hmac.HMACStrategy{Config: c}, Config: c, - prefix: tokenPrefixFmt, } return c } +// Config is an implementation of the fosite.Configurator. type Config struct { // GlobalSecret is the global secret used to sign and verify signatures. GlobalSecret []byte @@ -68,7 +73,7 @@ type Config struct { JWTScopeField jwt.JWTScopeFieldEnum JWTMaxDuration time.Duration - Hasher *AdaptiveHasher + Hasher *Hasher Hash HashConfig Strategy StrategyConfig PAR PARConfig @@ -92,11 +97,13 @@ type Config struct { Templates *templates.Provider } +// HashConfig holds specific fosite.Configurator information for hashing. type HashConfig struct { ClientSecrets fosite.Hasher HMAC func() (h hash.Hash) } +// StrategyConfig holds specific fosite.Configurator information for various strategies. type StrategyConfig struct { Core oauth2.CoreStrategy OpenID openid.OpenIDConnectTokenStrategy @@ -106,17 +113,20 @@ type StrategyConfig struct { ClientAuthentication fosite.ClientAuthenticationStrategy } +// PARConfig holds specific fosite.Configurator information for Pushed Authorization Requests. type PARConfig struct { Enforced bool URIPrefix string ContextLifespan time.Duration } +// IssuersConfig holds specific fosite.Configurator information for the issuer. type IssuersConfig struct { IDToken string AccessToken string } +// HandlersConfig holds specific fosite.Configurator handlers configuration information. type HandlersConfig struct { // ResponseMode provides an extension handler for custom response modes. ResponseMode fosite.ResponseModeHandler @@ -137,18 +147,21 @@ type HandlersConfig struct { PushedAuthorizeEndpoint fosite.PushedAuthorizeEndpointHandlers } +// GrantTypeJWTBearerConfig holds specific fosite.Configurator information for the JWT Bearer Grant Type. type GrantTypeJWTBearerConfig struct { OptionalClientAuth bool OptionalJTIClaim bool OptionalIssuedDate bool } +// ProofKeyCodeExchangeConfig holds specific fosite.Configurator information for PKCE. type ProofKeyCodeExchangeConfig struct { Enforce bool EnforcePublicClients bool AllowPlainChallengeMethod bool } +// LifespanConfig holds specific fosite.Configurator information for various lifespans. type LifespanConfig struct { AccessToken time.Duration AuthorizeCode time.Duration @@ -162,6 +175,7 @@ const ( PromptConsent = "consent" ) +// LoadHandlers reloads the handlers based on the current configuration. func (c *Config) LoadHandlers(store *Store, strategy jwt.Signer) { validator := openid.NewOpenIDConnectRequestValidator(strategy, c) @@ -278,6 +292,10 @@ func (c *Config) LoadHandlers(store *Store, strategy jwt.Signer) { if h, ok := handler.(fosite.RevocationHandler); ok { x.Revocation.Append(h) } + + if h, ok := handler.(fosite.PushedAuthorizeEndpointHandler); ok { + x.PushedAuthorizeEndpoint.Append(h) + } } c.Handlers = x @@ -533,7 +551,7 @@ func (c *Config) GetTokenURL(ctx context.Context) (tokenURL string) { // GetSecretsHasher returns the client secrets hashing function. func (c *Config) GetSecretsHasher(ctx context.Context) (hasher fosite.Hasher) { if c.Hash.ClientSecrets == nil { - c.Hash.ClientSecrets, _ = NewAdaptiveHasher() + c.Hash.ClientSecrets, _ = NewHasher() } return c.Hash.ClientSecrets @@ -595,7 +613,7 @@ func (c *Config) EnforcePushedAuthorize(ctx context.Context) bool { // GetPushedAuthorizeContextLifespan is the lifespan of the short-lived PAR context. func (c *Config) GetPushedAuthorizeContextLifespan(ctx context.Context) (lifespan time.Duration) { - if c.PAR.ContextLifespan == 0 { + if c.PAR.ContextLifespan.Seconds() == 0 { c.PAR.ContextLifespan = lifespanPARContextDefault } diff --git a/internal/oidc/const.go b/internal/oidc/const.go index 72598828b..1629ef7c7 100644 --- a/internal/oidc/const.go +++ b/internal/oidc/const.go @@ -110,13 +110,20 @@ const ( PKCEChallengeMethodSHA256 = "S256" ) +const ( + FormParameterRequestURI = "request_uri" + FormParameterCodeChallenge = "code_challenge" + FormParameterCodeChallengeMethod = "code_challenge_method" +) + // Endpoints. const ( - EndpointAuthorization = "authorization" - EndpointToken = "token" - EndpointUserinfo = "userinfo" - EndpointIntrospection = "introspection" - EndpointRevocation = "revocation" + EndpointAuthorization = "authorization" + EndpointToken = "token" + EndpointUserinfo = "userinfo" + EndpointIntrospection = "introspection" + EndpointRevocation = "revocation" + EndpointPushedAuthorizationRequest = "pushed-authorization-request" ) // JWT Headers. @@ -126,7 +133,9 @@ const ( ) const ( - tokenPrefixFmt = "authelia_%s_" //nolint:gosec + tokenPrefixOrgAutheliaFmt = "authelia_%s_" //nolint:gosec + tokenPrefixOrgOryFmt = "ory_%s_" //nolint:gosec + tokenPrefixPartAccessToken = "at" tokenPrefixPartRefreshToken = "rt" tokenPrefixPartAuthorizeCode = "ac" @@ -146,6 +155,8 @@ const ( EndpointPathUserinfo = EndpointPathRoot + "/" + EndpointUserinfo EndpointPathIntrospection = EndpointPathRoot + "/" + EndpointIntrospection EndpointPathRevocation = EndpointPathRoot + "/" + EndpointRevocation + + EndpointPathPushedAuthorizationRequest = EndpointPathRoot + "/" + EndpointPushedAuthorizationRequest ) // Authentication Method Reference Values https://datatracker.ietf.org/doc/html/rfc8176 diff --git a/internal/oidc/hmac.go b/internal/oidc/core_strategy_hmac.go similarity index 92% rename from internal/oidc/hmac.go rename to internal/oidc/core_strategy_hmac.go index 979a98afc..47d52da7a 100644 --- a/internal/oidc/hmac.go +++ b/internal/oidc/core_strategy_hmac.go @@ -19,7 +19,6 @@ type HMACCoreStrategy struct { fosite.RefreshTokenLifespanProvider fosite.AuthorizeCodeLifespanProvider } - prefix string } // AccessTokenSignature implements oauth2.AccessTokenStrategy. @@ -112,11 +111,11 @@ func (h *HMACCoreStrategy) ValidateAuthorizeCode(ctx context.Context, r fosite.R } func (h *HMACCoreStrategy) getPrefix(part string) string { - if len(h.prefix) == 0 { - return "" - } + return h.getCustomPrefix(tokenPrefixOrgAutheliaFmt, part) +} - return fmt.Sprintf(h.prefix, part) +func (h *HMACCoreStrategy) getCustomPrefix(tokenPrefixFmt, part string) string { + return fmt.Sprintf(tokenPrefixFmt, part) } func (h *HMACCoreStrategy) setPrefix(token, part string) string { @@ -124,5 +123,9 @@ func (h *HMACCoreStrategy) setPrefix(token, part string) string { } func (h *HMACCoreStrategy) trimPrefix(token, part string) string { + if strings.HasPrefix(token, h.getCustomPrefix(tokenPrefixOrgOryFmt, part)) { + return strings.TrimPrefix(token, h.getCustomPrefix(tokenPrefixOrgOryFmt, part)) + } + return strings.TrimPrefix(token, h.getPrefix(part)) } diff --git a/internal/oidc/core_strategy_hmac_test.go b/internal/oidc/core_strategy_hmac_test.go new file mode 100644 index 000000000..d390d4bdb --- /dev/null +++ b/internal/oidc/core_strategy_hmac_test.go @@ -0,0 +1,56 @@ +package oidc + +import ( + "fmt" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestHMACCoreStrategy_TrimPrefix(t *testing.T) { + testCases := []struct { + name string + have string + part string + expected string + }{ + {"ShouldTrimAutheliaPrefix", "authelia_at_example", tokenPrefixPartAccessToken, "example"}, + {"ShouldTrimOryPrefix", "ory_at_example", tokenPrefixPartAccessToken, "example"}, + {"ShouldTrimOnlyAutheliaPrefix", "authelia_at_ory_at_example", tokenPrefixPartAccessToken, "ory_at_example"}, + {"ShouldTrimOnlyOryPrefix", "ory_at_authelia_at_example", tokenPrefixPartAccessToken, "authelia_at_example"}, + {"ShouldNotTrimGitHubPrefix", "gh_at_example", tokenPrefixPartAccessToken, "gh_at_example"}, + } + + strategy := &HMACCoreStrategy{} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.expected, strategy.trimPrefix(tc.have, tc.part)) + }) + } +} + +func TestHMACCoreStrategy_GetSetPrefix(t *testing.T) { + testCases := []struct { + name string + have string + expectedSet string + expectedGet string + }{ + {"ShouldAddPrefix", "example", "authelia_%s_example", "authelia_%s_"}, + } + + strategy := &HMACCoreStrategy{} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + for _, part := range []string{tokenPrefixPartAccessToken, tokenPrefixPartAuthorizeCode, tokenPrefixPartRefreshToken} { + t.Run(strings.ToUpper(part), func(t *testing.T) { + assert.Equal(t, fmt.Sprintf(tc.expectedSet, part), strategy.setPrefix(tc.have, part)) + assert.Equal(t, fmt.Sprintf(tc.expectedGet, part), strategy.getPrefix(part)) + }) + } + }) + } +} diff --git a/internal/oidc/discovery.go b/internal/oidc/discovery.go index 2890a71d9..996e5e3f5 100644 --- a/internal/oidc/discovery.go +++ b/internal/oidc/discovery.go @@ -1,7 +1,11 @@ package oidc +import ( + "github.com/authelia/authelia/v4/internal/configuration/schema" +) + // NewOpenIDConnectWellKnownConfiguration generates a new OpenIDConnectWellKnownConfiguration. -func NewOpenIDConnectWellKnownConfiguration(enablePKCEPlainChallenge bool, clients map[string]*Client) (config OpenIDConnectWellKnownConfiguration) { +func NewOpenIDConnectWellKnownConfiguration(c *schema.OpenIDConnectConfiguration, clients map[string]*Client) (config OpenIDConnectWellKnownConfiguration) { config = OpenIDConnectWellKnownConfiguration{ CommonDiscoveryOptions: CommonDiscoveryOptions{ SubjectTypesSupported: []string{ @@ -78,6 +82,9 @@ func NewOpenIDConnectWellKnownConfiguration(enablePKCEPlainChallenge bool, clien SigningAlgorithmRSAWithSHA256, }, }, + PushedAuthorizationDiscoveryOptions: PushedAuthorizationDiscoveryOptions{ + RequirePushedAuthorizationRequests: c.PAR.Enforce, + }, } var pairwise, public bool @@ -96,7 +103,7 @@ func NewOpenIDConnectWellKnownConfiguration(enablePKCEPlainChallenge bool, clien config.SubjectTypesSupported = append(config.SubjectTypesSupported, SubjectTypePairwise) } - if enablePKCEPlainChallenge { + if c.EnablePKCEPlainChallenge { config.CodeChallengeMethodsSupported = append(config.CodeChallengeMethodsSupported, PKCEChallengeMethodPlain) } diff --git a/internal/oidc/discovery_test.go b/internal/oidc/discovery_test.go index 7f99df15c..63ad18b65 100644 --- a/internal/oidc/discovery_test.go +++ b/internal/oidc/discovery_test.go @@ -4,12 +4,15 @@ import ( "testing" "github.com/stretchr/testify/assert" + + "github.com/authelia/authelia/v4/internal/configuration/schema" ) func TestNewOpenIDConnectWellKnownConfiguration(t *testing.T) { testCases := []struct { desc string pkcePlainChallenge bool + enforcePAR bool clients map[string]*Client expectCodeChallengeMethodsSupported, expectSubjectTypesSupported []string @@ -63,7 +66,14 @@ func TestNewOpenIDConnectWellKnownConfiguration(t *testing.T) { for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { - actual := NewOpenIDConnectWellKnownConfiguration(tc.pkcePlainChallenge, tc.clients) + c := schema.OpenIDConnectConfiguration{ + EnablePKCEPlainChallenge: tc.pkcePlainChallenge, + PAR: schema.OpenIDConnectPARConfiguration{ + Enforce: tc.enforcePAR, + }, + } + + actual := NewOpenIDConnectWellKnownConfiguration(&c, tc.clients) for _, codeChallengeMethod := range tc.expectCodeChallengeMethodsSupported { assert.Contains(t, actual.CodeChallengeMethodsSupported, codeChallengeMethod) } diff --git a/internal/oidc/errors.go b/internal/oidc/errors.go index 465689257..58464568c 100644 --- a/internal/oidc/errors.go +++ b/internal/oidc/errors.go @@ -9,11 +9,27 @@ import ( var errPasswordsDoNotMatch = errors.New("the passwords don't match") var ( - ErrIssuerCouldNotDerive = fosite.ErrServerError.WithHint("Could not safely derive the issuer.") - ErrSubjectCouldNotLookup = fosite.ErrServerError.WithHint("Could not lookup user subject.") - ErrConsentCouldNotPerform = fosite.ErrServerError.WithHint("Could not perform consent.") - ErrConsentCouldNotGenerate = fosite.ErrServerError.WithHint("Could not generate the consent session.") - ErrConsentCouldNotSave = fosite.ErrServerError.WithHint("Could not save the consent session.") - ErrConsentCouldNotLookup = fosite.ErrServerError.WithHint("Failed to lookup the consent session.") + // ErrIssuerCouldNotDerive is sent when the issuer couldn't be determined from the headers. + ErrIssuerCouldNotDerive = fosite.ErrServerError.WithHint("Could not safely derive the issuer.") + + // ErrSubjectCouldNotLookup is sent when the Subject Identifier for a user couldn't be generated or obtained from the database. + ErrSubjectCouldNotLookup = fosite.ErrServerError.WithHint("Could not lookup user subject.") + + // ErrConsentCouldNotPerform is sent when the Consent Session couldn't be performed for varying reasons. + ErrConsentCouldNotPerform = fosite.ErrServerError.WithHint("Could not perform consent.") + + // ErrConsentCouldNotGenerate is sent when the Consent Session failed to be generated for some reason, usually a failed UUIDv4 generation. + ErrConsentCouldNotGenerate = fosite.ErrServerError.WithHint("Could not generate the consent session.") + + // ErrConsentCouldNotSave is sent when the Consent Session couldn't be saved to the database. + ErrConsentCouldNotSave = fosite.ErrServerError.WithHint("Could not save the consent session.") + + // ErrConsentCouldNotLookup is sent when the Consent ID is not a known UUID. + ErrConsentCouldNotLookup = fosite.ErrServerError.WithHint("Failed to lookup the consent session.") + + // ErrConsentMalformedChallengeID is sent when the Consent ID is not a UUID. ErrConsentMalformedChallengeID = fosite.ErrServerError.WithHint("Malformed consent session challenge ID.") + + // ErrPAREnforcedClientMissingPAR is sent when a client has EnforcePAR configured but the Authorization Request was not Pushed. + ErrPAREnforcedClientMissingPAR = fosite.ErrInvalidRequest.WithHint("Pushed Authorization Requests are enforced for this client but no such request was sent.") ) diff --git a/internal/oidc/hasher.go b/internal/oidc/hasher.go index 746f92ced..1714ec688 100644 --- a/internal/oidc/hasher.go +++ b/internal/oidc/hasher.go @@ -8,8 +8,9 @@ import ( "github.com/go-crypt/crypt/algorithm/plaintext" ) -func NewAdaptiveHasher() (hasher *AdaptiveHasher, err error) { - hasher = &AdaptiveHasher{} +// NewHasher returns a new Hasher. +func NewHasher() (hasher *Hasher, err error) { + hasher = &Hasher{} if hasher.decoder, err = crypt.NewDefaultDecoder(); err != nil { return nil, err @@ -22,13 +23,13 @@ func NewAdaptiveHasher() (hasher *AdaptiveHasher, err error) { return hasher, nil } -// AdaptiveHasher implements the fosite.Hasher interface without an actual hashing algo. -type AdaptiveHasher struct { +// Hasher implements the fosite.Hasher interface and adaptively compares hashes. +type Hasher struct { decoder algorithm.DecoderRegister } // Compare compares the hash with the data and returns an error if they don't match. -func (h *AdaptiveHasher) Compare(_ context.Context, hash, data []byte) (err error) { +func (h Hasher) Compare(_ context.Context, hash, data []byte) (err error) { var digest algorithm.Digest if digest, err = h.decoder.Decode(string(hash)); err != nil { @@ -43,6 +44,6 @@ func (h *AdaptiveHasher) Compare(_ context.Context, hash, data []byte) (err erro } // Hash creates a new hash from data. -func (h *AdaptiveHasher) Hash(_ context.Context, data []byte) (hash []byte, err error) { +func (h Hasher) Hash(_ context.Context, data []byte) (hash []byte, err error) { return data, nil } diff --git a/internal/oidc/hasher_test.go b/internal/oidc/hasher_test.go index 04f1b0f41..7f757460b 100644 --- a/internal/oidc/hasher_test.go +++ b/internal/oidc/hasher_test.go @@ -9,7 +9,7 @@ import ( ) func TestShouldNotRaiseErrorOnEqualPasswordsPlainText(t *testing.T) { - hasher, err := NewAdaptiveHasher() + hasher, err := NewHasher() require.NoError(t, err) @@ -22,7 +22,7 @@ func TestShouldNotRaiseErrorOnEqualPasswordsPlainText(t *testing.T) { } func TestShouldNotRaiseErrorOnEqualPasswordsPlainTextWithSeparator(t *testing.T) { - hasher, err := NewAdaptiveHasher() + hasher, err := NewHasher() require.NoError(t, err) @@ -35,7 +35,7 @@ func TestShouldNotRaiseErrorOnEqualPasswordsPlainTextWithSeparator(t *testing.T) } func TestShouldRaiseErrorOnNonEqualPasswordsPlainText(t *testing.T) { - hasher, err := NewAdaptiveHasher() + hasher, err := NewHasher() require.NoError(t, err) @@ -48,7 +48,7 @@ func TestShouldRaiseErrorOnNonEqualPasswordsPlainText(t *testing.T) { } func TestShouldHashPassword(t *testing.T) { - hasher := AdaptiveHasher{} + hasher := Hasher{} data := []byte("abc") diff --git a/internal/oidc/provider.go b/internal/oidc/provider.go index fd2581661..54bd1595d 100644 --- a/internal/oidc/provider.go +++ b/internal/oidc/provider.go @@ -37,7 +37,7 @@ func NewOpenIDConnectProvider(config *schema.OpenIDConnectConfiguration, store s provider.Config.LoadHandlers(provider.Store, provider.KeyManager.Strategy()) - provider.discovery = NewOpenIDConnectWellKnownConfiguration(config.EnablePKCEPlainChallenge, provider.Store.clients) + provider.discovery = NewOpenIDConnectWellKnownConfiguration(config, provider.Store.clients) return provider, nil } @@ -50,12 +50,12 @@ func (p *OpenIDConnectProvider) GetOAuth2WellKnownConfiguration(issuer string) O } options.Issuer = issuer + options.JWKSURI = fmt.Sprintf("%s%s", issuer, EndpointPathJWKs) - - options.IntrospectionEndpoint = fmt.Sprintf("%s%s", issuer, EndpointPathIntrospection) - options.TokenEndpoint = fmt.Sprintf("%s%s", issuer, EndpointPathToken) - options.AuthorizationEndpoint = fmt.Sprintf("%s%s", issuer, EndpointPathAuthorization) + options.PushedAuthorizationRequestEndpoint = fmt.Sprintf("%s%s", issuer, EndpointPathPushedAuthorizationRequest) + options.TokenEndpoint = fmt.Sprintf("%s%s", issuer, EndpointPathToken) + options.IntrospectionEndpoint = fmt.Sprintf("%s%s", issuer, EndpointPathIntrospection) options.RevocationEndpoint = fmt.Sprintf("%s%s", issuer, EndpointPathRevocation) return options @@ -72,14 +72,14 @@ func (p *OpenIDConnectProvider) GetOpenIDConnectWellKnownConfiguration(issuer st } options.Issuer = issuer + options.JWKSURI = fmt.Sprintf("%s%s", issuer, EndpointPathJWKs) - - options.IntrospectionEndpoint = fmt.Sprintf("%s%s", issuer, EndpointPathIntrospection) - options.TokenEndpoint = fmt.Sprintf("%s%s", issuer, EndpointPathToken) - options.AuthorizationEndpoint = fmt.Sprintf("%s%s", issuer, EndpointPathAuthorization) - options.RevocationEndpoint = fmt.Sprintf("%s%s", issuer, EndpointPathRevocation) + options.PushedAuthorizationRequestEndpoint = fmt.Sprintf("%s%s", issuer, EndpointPathPushedAuthorizationRequest) + options.TokenEndpoint = fmt.Sprintf("%s%s", issuer, EndpointPathToken) options.UserinfoEndpoint = fmt.Sprintf("%s%s", issuer, EndpointPathUserinfo) + options.IntrospectionEndpoint = fmt.Sprintf("%s%s", issuer, EndpointPathIntrospection) + options.RevocationEndpoint = fmt.Sprintf("%s%s", issuer, EndpointPathRevocation) return options } diff --git a/internal/oidc/store.go b/internal/oidc/store.go index d192ed1b8..9089137e1 100644 --- a/internal/oidc/store.go +++ b/internal/oidc/store.go @@ -165,7 +165,7 @@ func (s *Store) InvalidateAuthorizeCodeSession(ctx context.Context, code string) // This implements a portion of oauth2.AuthorizeCodeStorage. func (s *Store) GetAuthorizeCodeSession(ctx context.Context, code string, session fosite.Session) (request fosite.Requester, err error) { // TODO: Implement the fosite.ErrInvalidatedAuthorizeCode error above. This requires splitting the invalidated sessions and deleted sessions. - return s.loadSessionBySignature(ctx, storage.OAuth2SessionTypeAuthorizeCode, code, session) + return s.loadRequesterBySignature(ctx, storage.OAuth2SessionTypeAuthorizeCode, code, session) } // CreateAccessTokenSession stores the authorization request for a given access token. @@ -190,7 +190,7 @@ func (s *Store) RevokeAccessToken(ctx context.Context, requestID string) (err er // GetAccessTokenSession gets the authorization request for a given access token. // This implements a portion of oauth2.AccessTokenStorage. func (s *Store) GetAccessTokenSession(ctx context.Context, signature string, session fosite.Session) (request fosite.Requester, err error) { - return s.loadSessionBySignature(ctx, storage.OAuth2SessionTypeAccessToken, signature, session) + return s.loadRequesterBySignature(ctx, storage.OAuth2SessionTypeAccessToken, signature, session) } // CreateRefreshTokenSession stores the authorization request for a given refresh token. @@ -223,7 +223,7 @@ func (s *Store) RevokeRefreshTokenMaybeGracePeriod(ctx context.Context, requestI // GetRefreshTokenSession gets the authorization request for a given refresh token. // This implements a portion of oauth2.RefreshTokenStorage. func (s *Store) GetRefreshTokenSession(ctx context.Context, signature string, session fosite.Session) (request fosite.Requester, err error) { - return s.loadSessionBySignature(ctx, storage.OAuth2SessionTypeRefreshToken, signature, session) + return s.loadRequesterBySignature(ctx, storage.OAuth2SessionTypeRefreshToken, signature, session) } // CreatePKCERequestSession stores the authorization request for a given PKCE request. @@ -241,7 +241,7 @@ func (s *Store) DeletePKCERequestSession(ctx context.Context, signature string) // GetPKCERequestSession gets the authorization request for a given PKCE request. // This implements a portion of pkce.PKCERequestStorage. func (s *Store) GetPKCERequestSession(ctx context.Context, signature string, session fosite.Session) (requester fosite.Requester, err error) { - return s.loadSessionBySignature(ctx, storage.OAuth2SessionTypePKCEChallenge, signature, session) + return s.loadRequesterBySignature(ctx, storage.OAuth2SessionTypePKCEChallenge, signature, session) } // CreateOpenIDConnectSession creates an open id connect session for a given authorize code. @@ -263,7 +263,37 @@ func (s *Store) DeleteOpenIDConnectSession(ctx context.Context, authorizeCode st // - or an arbitrary error if an error occurred. // This implements a portion of openid.OpenIDConnectRequestStorage. func (s *Store) GetOpenIDConnectSession(ctx context.Context, authorizeCode string, request fosite.Requester) (r fosite.Requester, err error) { - return s.loadSessionBySignature(ctx, storage.OAuth2SessionTypeOpenIDConnect, authorizeCode, request.GetSession()) + return s.loadRequesterBySignature(ctx, storage.OAuth2SessionTypeOpenIDConnect, authorizeCode, request.GetSession()) +} + +// CreatePARSession stores the pushed authorization request context. The requestURI is used to derive the key. +// This implements a portion of fosite.PARStorage. +func (s *Store) CreatePARSession(ctx context.Context, requestURI string, request fosite.AuthorizeRequester) (err error) { + var par *model.OAuth2PARContext + + if par, err = model.NewOAuth2PARContext(requestURI, request); err != nil { + return err + } + + return s.provider.SaveOAuth2PARContext(ctx, *par) +} + +// GetPARSession gets the push authorization request context. The caller is expected to merge the AuthorizeRequest. +// This implements a portion of fosite.PARStorage. +func (s *Store) GetPARSession(ctx context.Context, requestURI string) (request fosite.AuthorizeRequester, err error) { + var par *model.OAuth2PARContext + + if par, err = s.provider.LoadOAuth2PARContext(ctx, requestURI); err != nil { + return nil, err + } + + return par.ToAuthorizeRequest(ctx, NewSession(), s) +} + +// DeletePARSession deletes the context. +// This implements a portion of fosite.PARStorage. +func (s *Store) DeletePARSession(ctx context.Context, requestURI string) (err error) { + return s.provider.RevokeOAuth2PARContext(ctx, requestURI) } // IsJWTUsed implements an interface required for RFC7523. @@ -280,7 +310,7 @@ func (s *Store) MarkJWTUsedForTime(ctx context.Context, jti string, exp time.Tim return s.SetClientAssertionJWT(ctx, jti, exp) } -func (s *Store) loadSessionBySignature(ctx context.Context, sessionType storage.OAuth2SessionType, signature string, session fosite.Session) (r fosite.Requester, err error) { +func (s *Store) loadRequesterBySignature(ctx context.Context, sessionType storage.OAuth2SessionType, signature string, session fosite.Session) (r fosite.Requester, err error) { var ( sessionModel *model.OAuth2Session ) diff --git a/internal/oidc/types.go b/internal/oidc/types.go index 471a4da14..7403f2fed 100644 --- a/internal/oidc/types.go +++ b/internal/oidc/types.go @@ -119,6 +119,8 @@ type Client struct { ResponseTypes []string ResponseModes []fosite.ResponseModeType + EnforcePAR bool + UserinfoSigningAlgorithm string Policy authorization.Level diff --git a/internal/server/handlers.go b/internal/server/handlers.go index 820c14821..e2d07d1b7 100644 --- a/internal/server/handlers.go +++ b/internal/server/handlers.go @@ -331,6 +331,15 @@ func handleRouter(config *schema.Configuration, providers middlewares.Providers) r.GET("/api/oidc/authorize", policyCORSAuthorization.Middleware(bridgeOIDC(middlewares.NewHTTPToAutheliaHandlerAdaptor(handlers.OpenIDConnectAuthorization)))) r.POST("/api/oidc/authorize", policyCORSAuthorization.Middleware(bridgeOIDC(middlewares.NewHTTPToAutheliaHandlerAdaptor(handlers.OpenIDConnectAuthorization)))) + policyCORSPAR := middlewares.NewCORSPolicyBuilder(). + WithAllowedMethods(fasthttp.MethodOptions, fasthttp.MethodPost). + WithAllowedOrigins(allowedOrigins...). + WithEnabled(utils.IsStringInSliceFold(oidc.EndpointPushedAuthorizationRequest, config.IdentityProviders.OIDC.CORS.Endpoints)). + Build() + + r.OPTIONS(oidc.EndpointPathPushedAuthorizationRequest, policyCORSPAR.HandleOnlyOPTIONS) + r.POST(oidc.EndpointPathPushedAuthorizationRequest, policyCORSPAR.Middleware(bridgeOIDC(middlewares.NewHTTPToAutheliaHandlerAdaptor(handlers.OpenIDConnectPushedAuthorizationRequest)))) + policyCORSToken := middlewares.NewCORSPolicyBuilder(). WithAllowCredentials(true). WithAllowedMethods(fasthttp.MethodOptions, fasthttp.MethodPost). diff --git a/internal/storage/const.go b/internal/storage/const.go index 5ca7a644f..a5e4bf8ae 100644 --- a/internal/storage/const.go +++ b/internal/storage/const.go @@ -13,15 +13,16 @@ const ( tableUserPreferences = "user_preferences" tableWebauthnDevices = "webauthn_devices" + tableOAuth2BlacklistedJTI = "oauth2_blacklisted_jti" tableOAuth2ConsentSession = "oauth2_consent_session" tableOAuth2ConsentPreConfiguration = "oauth2_consent_preconfiguration" + tableOAuth2AccessTokenSession = "oauth2_access_token_session" //nolint:gosec // This is not a hardcoded credential. tableOAuth2AuthorizeCodeSession = "oauth2_authorization_code_session" - tableOAuth2AccessTokenSession = "oauth2_access_token_session" //nolint:gosec // This is not a hardcoded credential. - tableOAuth2RefreshTokenSession = "oauth2_refresh_token_session" //nolint:gosec // This is not a hardcoded credential. - tableOAuth2PKCERequestSession = "oauth2_pkce_request_session" tableOAuth2OpenIDConnectSession = "oauth2_openid_connect_session" - tableOAuth2BlacklistedJTI = "oauth2_blacklisted_jti" + tableOAuth2PARContext = "oauth2_par_context" + tableOAuth2PKCERequestSession = "oauth2_pkce_request_session" + tableOAuth2RefreshTokenSession = "oauth2_refresh_token_session" //nolint:gosec // This is not a hardcoded credential. tableMigrations = "migrations" tableEncryption = "encryption" @@ -32,26 +33,29 @@ type OAuth2SessionType int // Representation of specific OAuth 2.0 session types. const ( - OAuth2SessionTypeAuthorizeCode OAuth2SessionType = iota - OAuth2SessionTypeAccessToken - OAuth2SessionTypeRefreshToken - OAuth2SessionTypePKCEChallenge + OAuth2SessionTypeAccessToken OAuth2SessionType = iota + OAuth2SessionTypeAuthorizeCode OAuth2SessionTypeOpenIDConnect + OAuth2SessionTypePAR + OAuth2SessionTypePKCEChallenge + OAuth2SessionTypeRefreshToken ) // String returns a string representation of this OAuth2SessionType. func (s OAuth2SessionType) String() string { switch s { - case OAuth2SessionTypeAuthorizeCode: - return "authorization code" case OAuth2SessionTypeAccessToken: return "access token" - case OAuth2SessionTypeRefreshToken: - return "refresh token" - case OAuth2SessionTypePKCEChallenge: - return "pkce challenge" + case OAuth2SessionTypeAuthorizeCode: + return "authorization code" case OAuth2SessionTypeOpenIDConnect: return "openid connect" + case OAuth2SessionTypePAR: + return "pushed authorization request context" + case OAuth2SessionTypePKCEChallenge: + return "pkce challenge" + case OAuth2SessionTypeRefreshToken: + return "refresh token" default: return "invalid" } @@ -60,16 +64,18 @@ func (s OAuth2SessionType) String() string { // Table returns the table name for this session type. func (s OAuth2SessionType) Table() string { switch s { - case OAuth2SessionTypeAuthorizeCode: - return tableOAuth2AuthorizeCodeSession case OAuth2SessionTypeAccessToken: return tableOAuth2AccessTokenSession - case OAuth2SessionTypeRefreshToken: - return tableOAuth2RefreshTokenSession - case OAuth2SessionTypePKCEChallenge: - return tableOAuth2PKCERequestSession + case OAuth2SessionTypeAuthorizeCode: + return tableOAuth2AuthorizeCodeSession case OAuth2SessionTypeOpenIDConnect: return tableOAuth2OpenIDConnectSession + case OAuth2SessionTypePAR: + return tableOAuth2PARContext + case OAuth2SessionTypePKCEChallenge: + return tableOAuth2PKCERequestSession + case OAuth2SessionTypeRefreshToken: + return tableOAuth2RefreshTokenSession default: return "" } @@ -119,7 +125,7 @@ const ( ) var ( - reMigration = regexp.MustCompile(`^V(\d{4})\.([^.]+)\.(all|sqlite|postgres|mysql)\.(up|down)\.sql$`) + reMigration = regexp.MustCompile(`^V(?P\d{4})\.(?P[^.]+)\.(?P(all|sqlite|postgres|mysql))\.(?P(up|down))\.sql$`) ) const ( diff --git a/internal/storage/migrations.go b/internal/storage/migrations.go index f634bccdd..18d79aa25 100644 --- a/internal/storage/migrations.go +++ b/internal/storage/migrations.go @@ -130,15 +130,15 @@ func skipMigration(providerName string, up bool, target, prior int, migration *m } func scanMigration(m string) (migration model.SchemaMigration, err error) { - result := reMigration.FindStringSubmatch(m) - - if result == nil || len(result) != 5 { + if !reMigration.MatchString(m) { return model.SchemaMigration{}, errors.New("invalid migration: could not parse the format") } + result := reMigration.FindStringSubmatch(m) + migration = model.SchemaMigration{ - Name: strings.ReplaceAll(result[2], "_", " "), - Provider: result[3], + Name: strings.ReplaceAll(result[reMigration.SubexpIndex("Name")], "_", " "), + Provider: result[reMigration.SubexpIndex("Provider")], } data, err := migrationsFS.ReadFile(fmt.Sprintf("migrations/%s", m)) @@ -148,22 +148,22 @@ func scanMigration(m string) (migration model.SchemaMigration, err error) { migration.Query = string(data) - switch result[4] { + switch direction := result[reMigration.SubexpIndex("Direction")]; direction { case "up": migration.Up = true case "down": migration.Up = false default: - return model.SchemaMigration{}, fmt.Errorf("invalid migration: value in position 4 '%s' must be up or down", result[4]) + return model.SchemaMigration{}, fmt.Errorf("invalid migration: value in Direction group '%s' must be up or down", direction) } - migration.Version, _ = strconv.Atoi(result[1]) + migration.Version, _ = strconv.Atoi(result[reMigration.SubexpIndex("Version")]) switch migration.Provider { case providerAll, providerSQLite, providerMySQL, providerPostgres: break default: - return model.SchemaMigration{}, fmt.Errorf("invalid migration: value in position 3 '%s' must be all, sqlite, postgres, or mysql", result[3]) + return model.SchemaMigration{}, fmt.Errorf("invalid migration: value in Provider group '%s' must be all, sqlite, postgres, or mysql", migration.Provider) } return migration, nil diff --git a/internal/storage/migrations/V0008.OpenIDConnectPAR.all.down.sql b/internal/storage/migrations/V0008.OpenIDConnectPAR.all.down.sql new file mode 100644 index 000000000..1cf22a47d --- /dev/null +++ b/internal/storage/migrations/V0008.OpenIDConnectPAR.all.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS oauth2_par_context; diff --git a/internal/storage/migrations/V0008.OpenIDConnectPAR.mysql.up.sql b/internal/storage/migrations/V0008.OpenIDConnectPAR.mysql.up.sql new file mode 100644 index 000000000..53829ced0 --- /dev/null +++ b/internal/storage/migrations/V0008.OpenIDConnectPAR.mysql.up.sql @@ -0,0 +1,17 @@ +CREATE TABLE IF NOT EXISTS oauth2_par_context ( + id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT, + request_id VARCHAR(40) NOT NULL, + client_id VARCHAR(255) NOT NULL, + signature VARCHAR(255) NOT NULL, + requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + scopes TEXT NOT NULL, + audience TEXT NOT NULL, + handled_response_types TEXT NOT NULL, + response_mode TEXT NOT NULL, + response_mode_default TEXT NOT NULL, + revoked BOOLEAN NOT NULL DEFAULT FALSE, + form_data TEXT NOT NULL, + session_data BLOB NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; + +CREATE UNIQUE INDEX oauth2_par_context_signature_key ON oauth2_par_context (signature); diff --git a/internal/storage/migrations/V0008.OpenIDConnectPAR.postgres.up.sql b/internal/storage/migrations/V0008.OpenIDConnectPAR.postgres.up.sql new file mode 100644 index 000000000..7926146e9 --- /dev/null +++ b/internal/storage/migrations/V0008.OpenIDConnectPAR.postgres.up.sql @@ -0,0 +1,17 @@ +CREATE TABLE IF NOT EXISTS oauth2_par_context ( + id SERIAL CONSTRAINT oauth2_par_context_pkey PRIMARY KEY, + request_id VARCHAR(40) NOT NULL, + client_id VARCHAR(255) NOT NULL, + signature VARCHAR(255) NOT NULL, + requested_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + scopes TEXT NOT NULL, + audience TEXT NULL DEFAULT '', + handled_response_types TEXT NOT NULL DEFAULT '', + response_mode TEXT NOT NULL DEFAULT '', + response_mode_default TEXT NOT NULL DEFAULT '', + revoked BOOLEAN NOT NULL DEFAULT FALSE, + form_data TEXT NOT NULL, + session_data BYTEA NOT NULL +); + +CREATE UNIQUE INDEX oauth2_par_context_signature_key ON oauth2_par_context (signature); diff --git a/internal/storage/migrations/V0008.OpenIDConnectPAR.sqlite.up.sql b/internal/storage/migrations/V0008.OpenIDConnectPAR.sqlite.up.sql new file mode 100644 index 000000000..9ef735998 --- /dev/null +++ b/internal/storage/migrations/V0008.OpenIDConnectPAR.sqlite.up.sql @@ -0,0 +1,17 @@ +CREATE TABLE IF NOT EXISTS oauth2_par_context ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + signature VARCHAR(255) NOT NULL, + request_id VARCHAR(40) NOT NULL, + client_id VARCHAR(255) NOT NULL, + requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + scopes TEXT NOT NULL, + audience TEXT NOT NULL, + handled_response_types TEXT NOT NULL, + response_mode TEXT NOT NULL, + response_mode_default TEXT NOT NULL, + revoked BOOLEAN NOT NULL DEFAULT FALSE, + form_data TEXT NOT NULL, + session_data BLOB NOT NULL +); + +CREATE UNIQUE INDEX oauth2_par_context_signature_key ON oauth2_par_context (signature); diff --git a/internal/storage/migrations_test.go b/internal/storage/migrations_test.go index b60a13628..3eeb155ea 100644 --- a/internal/storage/migrations_test.go +++ b/internal/storage/migrations_test.go @@ -9,7 +9,7 @@ import ( const ( // This is the latest schema version for the purpose of tests. - LatestVersion = 7 + LatestVersion = 8 ) func TestShouldObtainCorrectUpMigrations(t *testing.T) { diff --git a/internal/storage/provider.go b/internal/storage/provider.go index d3c9f3b5a..a31390fe0 100644 --- a/internal/storage/provider.go +++ b/internal/storage/provider.go @@ -24,8 +24,8 @@ type Provider interface { LoadUserInfo(ctx context.Context, username string) (info model.UserInfo, err error) SaveUserOpaqueIdentifier(ctx context.Context, subject model.UserOpaqueIdentifier) (err error) - LoadUserOpaqueIdentifier(ctx context.Context, opaqueUUID uuid.UUID) (subject *model.UserOpaqueIdentifier, err error) - LoadUserOpaqueIdentifiers(ctx context.Context) (opaqueIDs []model.UserOpaqueIdentifier, err error) + LoadUserOpaqueIdentifier(ctx context.Context, identifier uuid.UUID) (subject *model.UserOpaqueIdentifier, err error) + LoadUserOpaqueIdentifiers(ctx context.Context) (identifiers []model.UserOpaqueIdentifier, err error) LoadUserOpaqueIdentifierBySignature(ctx context.Context, service, sectorID, username string) (subject *model.UserOpaqueIdentifier, err error) SaveIdentityVerification(ctx context.Context, verification model.IdentityVerification) (err error) @@ -65,6 +65,10 @@ type Provider interface { DeactivateOAuth2SessionByRequestID(ctx context.Context, sessionType OAuth2SessionType, requestID string) (err error) LoadOAuth2Session(ctx context.Context, sessionType OAuth2SessionType, signature string) (session *model.OAuth2Session, err error) + SaveOAuth2PARContext(ctx context.Context, par model.OAuth2PARContext) (err error) + LoadOAuth2PARContext(ctx context.Context, signature string) (par *model.OAuth2PARContext, err error) + RevokeOAuth2PARContext(ctx context.Context, signature string) (err error) + SaveOAuth2BlacklistedJTI(ctx context.Context, blacklistedJTI model.OAuth2BlacklistedJTI) (err error) LoadOAuth2BlacklistedJTI(ctx context.Context, signature string) (blacklistedJTI *model.OAuth2BlacklistedJTI, err error) diff --git a/internal/storage/sql_provider.go b/internal/storage/sql_provider.go index a55a41cea..2a4cce037 100644 --- a/internal/storage/sql_provider.go +++ b/internal/storage/sql_provider.go @@ -70,6 +70,13 @@ func NewSQLProvider(config *schema.Configuration, name, driverName, dataSourceNa sqlSelectUserOpaqueIdentifiers: fmt.Sprintf(queryFmtSelectUserOpaqueIdentifiers, tableUserOpaqueIdentifier), sqlSelectUserOpaqueIdentifierBySignature: fmt.Sprintf(queryFmtSelectUserOpaqueIdentifierBySignature, tableUserOpaqueIdentifier), + sqlUpsertOAuth2BlacklistedJTI: fmt.Sprintf(queryFmtUpsertOAuth2BlacklistedJTI, tableOAuth2BlacklistedJTI), + sqlSelectOAuth2BlacklistedJTI: fmt.Sprintf(queryFmtSelectOAuth2BlacklistedJTI, tableOAuth2BlacklistedJTI), + + sqlInsertOAuth2PARContext: fmt.Sprintf(queryFmtInsertOAuth2PARContext, tableOAuth2PARContext), + sqlSelectOAuth2PARContext: fmt.Sprintf(queryFmtSelectOAuth2PARContext, tableOAuth2PARContext), + sqlRevokeOAuth2PARContext: fmt.Sprintf(queryFmtRevokeOAuth2Session, tableOAuth2PARContext), + sqlInsertOAuth2ConsentPreConfiguration: fmt.Sprintf(queryFmtInsertOAuth2ConsentPreConfiguration, tableOAuth2ConsentPreConfiguration), sqlSelectOAuth2ConsentPreConfigurations: fmt.Sprintf(queryFmtSelectOAuth2ConsentPreConfigurations, tableOAuth2ConsentPreConfiguration), @@ -79,13 +86,6 @@ func NewSQLProvider(config *schema.Configuration, name, driverName, dataSourceNa sqlUpdateOAuth2ConsentSessionGranted: fmt.Sprintf(queryFmtUpdateOAuth2ConsentSessionGranted, tableOAuth2ConsentSession), sqlSelectOAuth2ConsentSessionByChallengeID: fmt.Sprintf(queryFmtSelectOAuth2ConsentSessionByChallengeID, tableOAuth2ConsentSession), - sqlInsertOAuth2AuthorizeCodeSession: fmt.Sprintf(queryFmtInsertOAuth2Session, tableOAuth2AuthorizeCodeSession), - sqlSelectOAuth2AuthorizeCodeSession: fmt.Sprintf(queryFmtSelectOAuth2Session, tableOAuth2AuthorizeCodeSession), - sqlRevokeOAuth2AuthorizeCodeSession: fmt.Sprintf(queryFmtRevokeOAuth2Session, tableOAuth2AuthorizeCodeSession), - sqlRevokeOAuth2AuthorizeCodeSessionByRequestID: fmt.Sprintf(queryFmtRevokeOAuth2SessionByRequestID, tableOAuth2AuthorizeCodeSession), - sqlDeactivateOAuth2AuthorizeCodeSession: fmt.Sprintf(queryFmtDeactivateOAuth2Session, tableOAuth2AuthorizeCodeSession), - sqlDeactivateOAuth2AuthorizeCodeSessionByRequestID: fmt.Sprintf(queryFmtDeactivateOAuth2SessionByRequestID, tableOAuth2AuthorizeCodeSession), - sqlInsertOAuth2AccessTokenSession: fmt.Sprintf(queryFmtInsertOAuth2Session, tableOAuth2AccessTokenSession), sqlSelectOAuth2AccessTokenSession: fmt.Sprintf(queryFmtSelectOAuth2Session, tableOAuth2AccessTokenSession), sqlRevokeOAuth2AccessTokenSession: fmt.Sprintf(queryFmtRevokeOAuth2Session, tableOAuth2AccessTokenSession), @@ -93,19 +93,12 @@ func NewSQLProvider(config *schema.Configuration, name, driverName, dataSourceNa sqlDeactivateOAuth2AccessTokenSession: fmt.Sprintf(queryFmtDeactivateOAuth2Session, tableOAuth2AccessTokenSession), sqlDeactivateOAuth2AccessTokenSessionByRequestID: fmt.Sprintf(queryFmtDeactivateOAuth2SessionByRequestID, tableOAuth2AccessTokenSession), - sqlInsertOAuth2RefreshTokenSession: fmt.Sprintf(queryFmtInsertOAuth2Session, tableOAuth2RefreshTokenSession), - sqlSelectOAuth2RefreshTokenSession: fmt.Sprintf(queryFmtSelectOAuth2Session, tableOAuth2RefreshTokenSession), - sqlRevokeOAuth2RefreshTokenSession: fmt.Sprintf(queryFmtRevokeOAuth2Session, tableOAuth2RefreshTokenSession), - sqlRevokeOAuth2RefreshTokenSessionByRequestID: fmt.Sprintf(queryFmtRevokeOAuth2SessionByRequestID, tableOAuth2RefreshTokenSession), - sqlDeactivateOAuth2RefreshTokenSession: fmt.Sprintf(queryFmtDeactivateOAuth2Session, tableOAuth2RefreshTokenSession), - sqlDeactivateOAuth2RefreshTokenSessionByRequestID: fmt.Sprintf(queryFmtDeactivateOAuth2SessionByRequestID, tableOAuth2RefreshTokenSession), - - sqlInsertOAuth2PKCERequestSession: fmt.Sprintf(queryFmtInsertOAuth2Session, tableOAuth2PKCERequestSession), - sqlSelectOAuth2PKCERequestSession: fmt.Sprintf(queryFmtSelectOAuth2Session, tableOAuth2PKCERequestSession), - sqlRevokeOAuth2PKCERequestSession: fmt.Sprintf(queryFmtRevokeOAuth2Session, tableOAuth2PKCERequestSession), - sqlRevokeOAuth2PKCERequestSessionByRequestID: fmt.Sprintf(queryFmtRevokeOAuth2SessionByRequestID, tableOAuth2PKCERequestSession), - sqlDeactivateOAuth2PKCERequestSession: fmt.Sprintf(queryFmtDeactivateOAuth2Session, tableOAuth2PKCERequestSession), - sqlDeactivateOAuth2PKCERequestSessionByRequestID: fmt.Sprintf(queryFmtDeactivateOAuth2SessionByRequestID, tableOAuth2PKCERequestSession), + sqlInsertOAuth2AuthorizeCodeSession: fmt.Sprintf(queryFmtInsertOAuth2Session, tableOAuth2AuthorizeCodeSession), + sqlSelectOAuth2AuthorizeCodeSession: fmt.Sprintf(queryFmtSelectOAuth2Session, tableOAuth2AuthorizeCodeSession), + sqlRevokeOAuth2AuthorizeCodeSession: fmt.Sprintf(queryFmtRevokeOAuth2Session, tableOAuth2AuthorizeCodeSession), + sqlRevokeOAuth2AuthorizeCodeSessionByRequestID: fmt.Sprintf(queryFmtRevokeOAuth2SessionByRequestID, tableOAuth2AuthorizeCodeSession), + sqlDeactivateOAuth2AuthorizeCodeSession: fmt.Sprintf(queryFmtDeactivateOAuth2Session, tableOAuth2AuthorizeCodeSession), + sqlDeactivateOAuth2AuthorizeCodeSessionByRequestID: fmt.Sprintf(queryFmtDeactivateOAuth2SessionByRequestID, tableOAuth2AuthorizeCodeSession), sqlInsertOAuth2OpenIDConnectSession: fmt.Sprintf(queryFmtInsertOAuth2Session, tableOAuth2OpenIDConnectSession), sqlSelectOAuth2OpenIDConnectSession: fmt.Sprintf(queryFmtSelectOAuth2Session, tableOAuth2OpenIDConnectSession), @@ -114,8 +107,19 @@ func NewSQLProvider(config *schema.Configuration, name, driverName, dataSourceNa sqlDeactivateOAuth2OpenIDConnectSession: fmt.Sprintf(queryFmtDeactivateOAuth2Session, tableOAuth2OpenIDConnectSession), sqlDeactivateOAuth2OpenIDConnectSessionByRequestID: fmt.Sprintf(queryFmtDeactivateOAuth2SessionByRequestID, tableOAuth2OpenIDConnectSession), - sqlUpsertOAuth2BlacklistedJTI: fmt.Sprintf(queryFmtUpsertOAuth2BlacklistedJTI, tableOAuth2BlacklistedJTI), - sqlSelectOAuth2BlacklistedJTI: fmt.Sprintf(queryFmtSelectOAuth2BlacklistedJTI, tableOAuth2BlacklistedJTI), + sqlInsertOAuth2PKCERequestSession: fmt.Sprintf(queryFmtInsertOAuth2Session, tableOAuth2PKCERequestSession), + sqlSelectOAuth2PKCERequestSession: fmt.Sprintf(queryFmtSelectOAuth2Session, tableOAuth2PKCERequestSession), + sqlRevokeOAuth2PKCERequestSession: fmt.Sprintf(queryFmtRevokeOAuth2Session, tableOAuth2PKCERequestSession), + sqlRevokeOAuth2PKCERequestSessionByRequestID: fmt.Sprintf(queryFmtRevokeOAuth2SessionByRequestID, tableOAuth2PKCERequestSession), + sqlDeactivateOAuth2PKCERequestSession: fmt.Sprintf(queryFmtDeactivateOAuth2Session, tableOAuth2PKCERequestSession), + sqlDeactivateOAuth2PKCERequestSessionByRequestID: fmt.Sprintf(queryFmtDeactivateOAuth2SessionByRequestID, tableOAuth2PKCERequestSession), + + sqlInsertOAuth2RefreshTokenSession: fmt.Sprintf(queryFmtInsertOAuth2Session, tableOAuth2RefreshTokenSession), + sqlSelectOAuth2RefreshTokenSession: fmt.Sprintf(queryFmtSelectOAuth2Session, tableOAuth2RefreshTokenSession), + sqlRevokeOAuth2RefreshTokenSession: fmt.Sprintf(queryFmtRevokeOAuth2Session, tableOAuth2RefreshTokenSession), + sqlRevokeOAuth2RefreshTokenSessionByRequestID: fmt.Sprintf(queryFmtRevokeOAuth2SessionByRequestID, tableOAuth2RefreshTokenSession), + sqlDeactivateOAuth2RefreshTokenSession: fmt.Sprintf(queryFmtDeactivateOAuth2Session, tableOAuth2RefreshTokenSession), + sqlDeactivateOAuth2RefreshTokenSessionByRequestID: fmt.Sprintf(queryFmtDeactivateOAuth2SessionByRequestID, tableOAuth2RefreshTokenSession), sqlInsertMigration: fmt.Sprintf(queryFmtInsertMigration, tableMigrations), sqlSelectMigrations: fmt.Sprintf(queryFmtSelectMigrations, tableMigrations), @@ -224,13 +228,18 @@ type SQLProvider struct { sqlDeactivateOAuth2AccessTokenSession string sqlDeactivateOAuth2AccessTokenSessionByRequestID string - // Table: oauth2_refresh_token_session. - sqlInsertOAuth2RefreshTokenSession string - sqlSelectOAuth2RefreshTokenSession string - sqlRevokeOAuth2RefreshTokenSession string - sqlRevokeOAuth2RefreshTokenSessionByRequestID string - sqlDeactivateOAuth2RefreshTokenSession string - sqlDeactivateOAuth2RefreshTokenSessionByRequestID string + // Table: oauth2_openid_connect_session. + sqlInsertOAuth2OpenIDConnectSession string + sqlSelectOAuth2OpenIDConnectSession string + sqlRevokeOAuth2OpenIDConnectSession string + sqlRevokeOAuth2OpenIDConnectSessionByRequestID string + sqlDeactivateOAuth2OpenIDConnectSession string + sqlDeactivateOAuth2OpenIDConnectSessionByRequestID string + + // Table: oauth2_par_context. + sqlInsertOAuth2PARContext string + sqlSelectOAuth2PARContext string + sqlRevokeOAuth2PARContext string // Table: oauth2_pkce_request_session. sqlInsertOAuth2PKCERequestSession string @@ -240,13 +249,13 @@ type SQLProvider struct { sqlDeactivateOAuth2PKCERequestSession string sqlDeactivateOAuth2PKCERequestSessionByRequestID string - // Table: oauth2_openid_connect_session. - sqlInsertOAuth2OpenIDConnectSession string - sqlSelectOAuth2OpenIDConnectSession string - sqlRevokeOAuth2OpenIDConnectSession string - sqlRevokeOAuth2OpenIDConnectSessionByRequestID string - sqlDeactivateOAuth2OpenIDConnectSession string - sqlDeactivateOAuth2OpenIDConnectSessionByRequestID string + // Table: oauth2_refresh_token_session. + sqlInsertOAuth2RefreshTokenSession string + sqlSelectOAuth2RefreshTokenSession string + sqlRevokeOAuth2RefreshTokenSession string + sqlRevokeOAuth2RefreshTokenSessionByRequestID string + sqlDeactivateOAuth2RefreshTokenSession string + sqlDeactivateOAuth2RefreshTokenSessionByRequestID string sqlUpsertOAuth2BlacklistedJTI string sqlSelectOAuth2BlacklistedJTI string @@ -339,19 +348,19 @@ func (p *SQLProvider) Rollback(ctx context.Context) (err error) { } // SaveUserOpaqueIdentifier saves a new opaque user identifier to the database. -func (p *SQLProvider) SaveUserOpaqueIdentifier(ctx context.Context, opaqueID model.UserOpaqueIdentifier) (err error) { - if _, err = p.db.ExecContext(ctx, p.sqlInsertUserOpaqueIdentifier, opaqueID.Service, opaqueID.SectorID, opaqueID.Username, opaqueID.Identifier); err != nil { - return fmt.Errorf("error inserting user opaque id for user '%s' with opaque id '%s': %w", opaqueID.Username, opaqueID.Identifier.String(), err) +func (p *SQLProvider) SaveUserOpaqueIdentifier(ctx context.Context, subject model.UserOpaqueIdentifier) (err error) { + if _, err = p.db.ExecContext(ctx, p.sqlInsertUserOpaqueIdentifier, subject.Service, subject.SectorID, subject.Username, subject.Identifier); err != nil { + return fmt.Errorf("error inserting user opaque id for user '%s' with opaque id '%s': %w", subject.Username, subject.Identifier.String(), err) } return nil } // LoadUserOpaqueIdentifier selects an opaque user identifier from the database. -func (p *SQLProvider) LoadUserOpaqueIdentifier(ctx context.Context, opaqueUUID uuid.UUID) (opaqueID *model.UserOpaqueIdentifier, err error) { - opaqueID = &model.UserOpaqueIdentifier{} +func (p *SQLProvider) LoadUserOpaqueIdentifier(ctx context.Context, identifier uuid.UUID) (subject *model.UserOpaqueIdentifier, err error) { + subject = &model.UserOpaqueIdentifier{} - if err = p.db.GetContext(ctx, opaqueID, p.sqlSelectUserOpaqueIdentifier, opaqueUUID); err != nil { + if err = p.db.GetContext(ctx, subject, p.sqlSelectUserOpaqueIdentifier, identifier); err != nil { switch { case errors.Is(err, sql.ErrNoRows): return nil, nil @@ -360,11 +369,11 @@ func (p *SQLProvider) LoadUserOpaqueIdentifier(ctx context.Context, opaqueUUID u } } - return opaqueID, nil + return subject, nil } // LoadUserOpaqueIdentifiers selects an opaque user identifiers from the database. -func (p *SQLProvider) LoadUserOpaqueIdentifiers(ctx context.Context) (opaqueIDs []model.UserOpaqueIdentifier, err error) { +func (p *SQLProvider) LoadUserOpaqueIdentifiers(ctx context.Context) (identifiers []model.UserOpaqueIdentifier, err error) { var rows *sqlx.Rows if rows, err = p.db.QueryxContext(ctx, p.sqlSelectUserOpaqueIdentifiers); err != nil { @@ -380,17 +389,17 @@ func (p *SQLProvider) LoadUserOpaqueIdentifiers(ctx context.Context) (opaqueIDs return nil, fmt.Errorf("error selecting user opaque identifiers: error scanning row: %w", err) } - opaqueIDs = append(opaqueIDs, *opaqueID) + identifiers = append(identifiers, *opaqueID) } - return opaqueIDs, nil + return identifiers, nil } -// LoadUserOpaqueIdentifierBySignature selects an opaque user identifier from the database given a service name, sector id, and username. -func (p *SQLProvider) LoadUserOpaqueIdentifierBySignature(ctx context.Context, service, sectorID, username string) (opaqueID *model.UserOpaqueIdentifier, err error) { - opaqueID = &model.UserOpaqueIdentifier{} +// LoadUserOpaqueIdentifierBySignature selects an opaque user identifier from the database given a service name, sector id, and username. +func (p *SQLProvider) LoadUserOpaqueIdentifierBySignature(ctx context.Context, service, sectorID, username string) (subject *model.UserOpaqueIdentifier, err error) { + subject = &model.UserOpaqueIdentifier{} - if err = p.db.GetContext(ctx, opaqueID, p.sqlSelectUserOpaqueIdentifierBySignature, service, sectorID, username); err != nil { + if err = p.db.GetContext(ctx, subject, p.sqlSelectUserOpaqueIdentifierBySignature, service, sectorID, username); err != nil { switch { case errors.Is(err, sql.ErrNoRows): return nil, nil @@ -399,7 +408,7 @@ func (p *SQLProvider) LoadUserOpaqueIdentifierBySignature(ctx context.Context, s } } - return opaqueID, nil + return subject, nil } // SaveOAuth2ConsentSession inserts an OAuth2.0 consent session. @@ -496,22 +505,22 @@ func (p *SQLProvider) SaveOAuth2Session(ctx context.Context, sessionType OAuth2S var query string switch sessionType { - case OAuth2SessionTypeAuthorizeCode: - query = p.sqlInsertOAuth2AuthorizeCodeSession case OAuth2SessionTypeAccessToken: query = p.sqlInsertOAuth2AccessTokenSession - case OAuth2SessionTypeRefreshToken: - query = p.sqlInsertOAuth2RefreshTokenSession - case OAuth2SessionTypePKCEChallenge: - query = p.sqlInsertOAuth2PKCERequestSession + case OAuth2SessionTypeAuthorizeCode: + query = p.sqlInsertOAuth2AuthorizeCodeSession case OAuth2SessionTypeOpenIDConnect: query = p.sqlInsertOAuth2OpenIDConnectSession + case OAuth2SessionTypePKCEChallenge: + query = p.sqlInsertOAuth2PKCERequestSession + case OAuth2SessionTypeRefreshToken: + query = p.sqlInsertOAuth2RefreshTokenSession default: return fmt.Errorf("error inserting oauth2 session for subject '%s' and request id '%s': unknown oauth2 session type '%s'", session.Subject, session.RequestID, sessionType) } if session.Session, err = p.encrypt(session.Session); err != nil { - return fmt.Errorf("error encrypting the oauth2 %s session data for subject '%s' and request id '%s' and challenge id '%s': %w", sessionType, session.Subject, session.RequestID, session.ChallengeID.String(), err) + return fmt.Errorf("error encrypting oauth2 %s session data for subject '%s' and request id '%s' and challenge id '%s': %w", sessionType, session.Subject, session.RequestID, session.ChallengeID.String(), err) } _, err = p.db.ExecContext(ctx, query, @@ -532,16 +541,16 @@ func (p *SQLProvider) RevokeOAuth2Session(ctx context.Context, sessionType OAuth var query string switch sessionType { - case OAuth2SessionTypeAuthorizeCode: - query = p.sqlRevokeOAuth2AuthorizeCodeSession case OAuth2SessionTypeAccessToken: query = p.sqlRevokeOAuth2AccessTokenSession - case OAuth2SessionTypeRefreshToken: - query = p.sqlRevokeOAuth2RefreshTokenSession - case OAuth2SessionTypePKCEChallenge: - query = p.sqlRevokeOAuth2PKCERequestSession + case OAuth2SessionTypeAuthorizeCode: + query = p.sqlRevokeOAuth2AuthorizeCodeSession case OAuth2SessionTypeOpenIDConnect: query = p.sqlRevokeOAuth2OpenIDConnectSession + case OAuth2SessionTypePKCEChallenge: + query = p.sqlRevokeOAuth2PKCERequestSession + case OAuth2SessionTypeRefreshToken: + query = p.sqlRevokeOAuth2RefreshTokenSession default: return fmt.Errorf("error revoking oauth2 session with signature '%s': unknown oauth2 session type '%s'", signature, sessionType.String()) } @@ -558,16 +567,16 @@ func (p *SQLProvider) RevokeOAuth2SessionByRequestID(ctx context.Context, sessio var query string switch sessionType { - case OAuth2SessionTypeAuthorizeCode: - query = p.sqlRevokeOAuth2AuthorizeCodeSessionByRequestID case OAuth2SessionTypeAccessToken: query = p.sqlRevokeOAuth2AccessTokenSessionByRequestID - case OAuth2SessionTypeRefreshToken: - query = p.sqlRevokeOAuth2RefreshTokenSessionByRequestID - case OAuth2SessionTypePKCEChallenge: - query = p.sqlRevokeOAuth2PKCERequestSessionByRequestID + case OAuth2SessionTypeAuthorizeCode: + query = p.sqlRevokeOAuth2AuthorizeCodeSessionByRequestID case OAuth2SessionTypeOpenIDConnect: query = p.sqlRevokeOAuth2OpenIDConnectSessionByRequestID + case OAuth2SessionTypePKCEChallenge: + query = p.sqlRevokeOAuth2PKCERequestSessionByRequestID + case OAuth2SessionTypeRefreshToken: + query = p.sqlRevokeOAuth2RefreshTokenSessionByRequestID default: return fmt.Errorf("error revoking oauth2 session with request id '%s': unknown oauth2 session type '%s'", requestID, sessionType.String()) } @@ -584,16 +593,16 @@ func (p *SQLProvider) DeactivateOAuth2Session(ctx context.Context, sessionType O var query string switch sessionType { - case OAuth2SessionTypeAuthorizeCode: - query = p.sqlDeactivateOAuth2AuthorizeCodeSession case OAuth2SessionTypeAccessToken: query = p.sqlDeactivateOAuth2AccessTokenSession - case OAuth2SessionTypeRefreshToken: - query = p.sqlDeactivateOAuth2RefreshTokenSession - case OAuth2SessionTypePKCEChallenge: - query = p.sqlDeactivateOAuth2PKCERequestSession + case OAuth2SessionTypeAuthorizeCode: + query = p.sqlDeactivateOAuth2AuthorizeCodeSession case OAuth2SessionTypeOpenIDConnect: query = p.sqlDeactivateOAuth2OpenIDConnectSession + case OAuth2SessionTypePKCEChallenge: + query = p.sqlDeactivateOAuth2PKCERequestSession + case OAuth2SessionTypeRefreshToken: + query = p.sqlDeactivateOAuth2RefreshTokenSession default: return fmt.Errorf("error deactivating oauth2 session with signature '%s': unknown oauth2 session type '%s'", signature, sessionType.String()) } @@ -610,16 +619,16 @@ func (p *SQLProvider) DeactivateOAuth2SessionByRequestID(ctx context.Context, se var query string switch sessionType { - case OAuth2SessionTypeAuthorizeCode: - query = p.sqlDeactivateOAuth2AuthorizeCodeSession case OAuth2SessionTypeAccessToken: query = p.sqlDeactivateOAuth2AccessTokenSessionByRequestID - case OAuth2SessionTypeRefreshToken: - query = p.sqlDeactivateOAuth2RefreshTokenSessionByRequestID - case OAuth2SessionTypePKCEChallenge: - query = p.sqlDeactivateOAuth2PKCERequestSessionByRequestID + case OAuth2SessionTypeAuthorizeCode: + query = p.sqlDeactivateOAuth2AuthorizeCodeSession case OAuth2SessionTypeOpenIDConnect: query = p.sqlDeactivateOAuth2OpenIDConnectSessionByRequestID + case OAuth2SessionTypePKCEChallenge: + query = p.sqlDeactivateOAuth2PKCERequestSessionByRequestID + case OAuth2SessionTypeRefreshToken: + query = p.sqlDeactivateOAuth2RefreshTokenSessionByRequestID default: return fmt.Errorf("error deactivating oauth2 session with request id '%s': unknown oauth2 session type '%s'", requestID, sessionType.String()) } @@ -636,16 +645,16 @@ func (p *SQLProvider) LoadOAuth2Session(ctx context.Context, sessionType OAuth2S var query string switch sessionType { - case OAuth2SessionTypeAuthorizeCode: - query = p.sqlSelectOAuth2AuthorizeCodeSession case OAuth2SessionTypeAccessToken: query = p.sqlSelectOAuth2AccessTokenSession - case OAuth2SessionTypeRefreshToken: - query = p.sqlSelectOAuth2RefreshTokenSession - case OAuth2SessionTypePKCEChallenge: - query = p.sqlSelectOAuth2PKCERequestSession + case OAuth2SessionTypeAuthorizeCode: + query = p.sqlSelectOAuth2AuthorizeCodeSession case OAuth2SessionTypeOpenIDConnect: query = p.sqlSelectOAuth2OpenIDConnectSession + case OAuth2SessionTypePKCEChallenge: + query = p.sqlSelectOAuth2PKCERequestSession + case OAuth2SessionTypeRefreshToken: + query = p.sqlSelectOAuth2RefreshTokenSession default: return nil, fmt.Errorf("error selecting oauth2 session: unknown oauth2 session type '%s'", sessionType.String()) } @@ -663,6 +672,45 @@ func (p *SQLProvider) LoadOAuth2Session(ctx context.Context, sessionType OAuth2S return session, nil } +// SaveOAuth2PARContext save a OAuth2PARContext to the database. +func (p *SQLProvider) SaveOAuth2PARContext(ctx context.Context, par model.OAuth2PARContext) (err error) { + if par.Session, err = p.encrypt(par.Session); err != nil { + return fmt.Errorf("error encrypting oauth2 pushed authorization request context data for with signature '%s' and request id '%s': %w", par.Signature, par.RequestID, err) + } + + if _, err = p.db.ExecContext(ctx, p.sqlInsertOAuth2PARContext, + par.Signature, par.RequestID, par.ClientID, par.RequestedAt, par.Scopes, par.Audience, par.HandledResponseTypes, + par.ResponseMode, par.DefaultResponseMode, par.Revoked, par.Form, par.Session); err != nil { + return fmt.Errorf("error inserting oauth2 pushed authorization request context data for with signature '%s' and request id '%s': %w", par.Signature, par.RequestID, err) + } + + return nil +} + +// LoadOAuth2PARContext loads a OAuth2PARContext from the database. +func (p *SQLProvider) LoadOAuth2PARContext(ctx context.Context, signature string) (par *model.OAuth2PARContext, err error) { + par = &model.OAuth2PARContext{} + + if err = p.db.GetContext(ctx, par, p.sqlSelectOAuth2PARContext, signature); err != nil { + return nil, fmt.Errorf("error selecting oauth2 pushed authorization request context with signature '%s': %w", signature, err) + } + + if par.Session, err = p.decrypt(par.Session); err != nil { + return nil, fmt.Errorf("error decrypting oauth2 oauth2 pushed authorization request context data with signature '%s' and request id '%s': %w", signature, par.RequestID, err) + } + + return par, nil +} + +// RevokeOAuth2PARContext marks a OAuth2PARContext as revoked in the database. +func (p *SQLProvider) RevokeOAuth2PARContext(ctx context.Context, signature string) (err error) { + if _, err = p.db.ExecContext(ctx, p.sqlRevokeOAuth2PARContext, signature); err != nil { + return fmt.Errorf("error revoking oauth2 pushed authorization request context with signature '%s': %w", signature, err) + } + + return nil +} + // SaveOAuth2BlacklistedJTI saves a OAuth2BlacklistedJTI to the database. func (p *SQLProvider) SaveOAuth2BlacklistedJTI(ctx context.Context, blacklistedJTI model.OAuth2BlacklistedJTI) (err error) { if _, err = p.db.ExecContext(ctx, p.sqlUpsertOAuth2BlacklistedJTI, blacklistedJTI.Signature, blacklistedJTI.ExpiresAt); err != nil { @@ -762,7 +810,7 @@ func (p *SQLProvider) FindIdentityVerification(ctx context.Context, jti string) // SaveTOTPConfiguration save a TOTP configuration of a given user in the database. func (p *SQLProvider) SaveTOTPConfiguration(ctx context.Context, config model.TOTPConfiguration) (err error) { if config.Secret, err = p.encrypt(config.Secret); err != nil { - return fmt.Errorf("error encrypting the TOTP configuration secret for user '%s': %w", config.Username, err) + return fmt.Errorf("error encrypting TOTP configuration secret for user '%s': %w", config.Username, err) } if _, err = p.db.ExecContext(ctx, p.sqlUpsertTOTPConfig, @@ -806,7 +854,7 @@ func (p *SQLProvider) LoadTOTPConfiguration(ctx context.Context, username string } if config.Secret, err = p.decrypt(config.Secret); err != nil { - return nil, fmt.Errorf("error decrypting the TOTP secret for user '%s': %w", username, err) + return nil, fmt.Errorf("error decrypting TOTP secret for user '%s': %w", username, err) } return config, nil @@ -836,7 +884,7 @@ func (p *SQLProvider) LoadTOTPConfigurations(ctx context.Context, limit, page in // SaveWebauthnDevice saves a registered Webauthn device. func (p *SQLProvider) SaveWebauthnDevice(ctx context.Context, device model.WebauthnDevice) (err error) { if device.PublicKey, err = p.encrypt(device.PublicKey); err != nil { - return fmt.Errorf("error encrypting the Webauthn device public key for user '%s' kid '%x': %w", device.Username, device.KID, err) + return fmt.Errorf("error encrypting Webauthn device public key for user '%s' kid '%x': %w", device.Username, device.KID, err) } if _, err = p.db.ExecContext(ctx, p.sqlUpsertWebauthnDevice, diff --git a/internal/storage/sql_provider_backend_postgres.go b/internal/storage/sql_provider_backend_postgres.go index 9e0c127a1..bc6387a38 100644 --- a/internal/storage/sql_provider_backend_postgres.go +++ b/internal/storage/sql_provider_backend_postgres.go @@ -87,13 +87,6 @@ func NewPostgreSQLProvider(config *schema.Configuration, caCertPool *x509.CertPo provider.sqlUpdateOAuth2ConsentSessionGranted = provider.db.Rebind(provider.sqlUpdateOAuth2ConsentSessionGranted) provider.sqlSelectOAuth2ConsentSessionByChallengeID = provider.db.Rebind(provider.sqlSelectOAuth2ConsentSessionByChallengeID) - provider.sqlInsertOAuth2AuthorizeCodeSession = provider.db.Rebind(provider.sqlInsertOAuth2AuthorizeCodeSession) - provider.sqlRevokeOAuth2AuthorizeCodeSession = provider.db.Rebind(provider.sqlRevokeOAuth2AuthorizeCodeSession) - provider.sqlRevokeOAuth2AuthorizeCodeSessionByRequestID = provider.db.Rebind(provider.sqlRevokeOAuth2AuthorizeCodeSessionByRequestID) - provider.sqlDeactivateOAuth2AuthorizeCodeSession = provider.db.Rebind(provider.sqlDeactivateOAuth2AuthorizeCodeSession) - provider.sqlDeactivateOAuth2AuthorizeCodeSessionByRequestID = provider.db.Rebind(provider.sqlDeactivateOAuth2AuthorizeCodeSessionByRequestID) - provider.sqlSelectOAuth2AuthorizeCodeSession = provider.db.Rebind(provider.sqlSelectOAuth2AuthorizeCodeSession) - provider.sqlInsertOAuth2AccessTokenSession = provider.db.Rebind(provider.sqlInsertOAuth2AccessTokenSession) provider.sqlRevokeOAuth2AccessTokenSession = provider.db.Rebind(provider.sqlRevokeOAuth2AccessTokenSession) provider.sqlRevokeOAuth2AccessTokenSessionByRequestID = provider.db.Rebind(provider.sqlRevokeOAuth2AccessTokenSessionByRequestID) @@ -101,12 +94,23 @@ func NewPostgreSQLProvider(config *schema.Configuration, caCertPool *x509.CertPo provider.sqlDeactivateOAuth2AccessTokenSessionByRequestID = provider.db.Rebind(provider.sqlDeactivateOAuth2AccessTokenSessionByRequestID) provider.sqlSelectOAuth2AccessTokenSession = provider.db.Rebind(provider.sqlSelectOAuth2AccessTokenSession) - provider.sqlInsertOAuth2RefreshTokenSession = provider.db.Rebind(provider.sqlInsertOAuth2RefreshTokenSession) - provider.sqlRevokeOAuth2RefreshTokenSession = provider.db.Rebind(provider.sqlRevokeOAuth2RefreshTokenSession) - provider.sqlRevokeOAuth2RefreshTokenSessionByRequestID = provider.db.Rebind(provider.sqlRevokeOAuth2RefreshTokenSessionByRequestID) - provider.sqlDeactivateOAuth2RefreshTokenSession = provider.db.Rebind(provider.sqlDeactivateOAuth2RefreshTokenSession) - provider.sqlDeactivateOAuth2RefreshTokenSessionByRequestID = provider.db.Rebind(provider.sqlDeactivateOAuth2RefreshTokenSessionByRequestID) - provider.sqlSelectOAuth2RefreshTokenSession = provider.db.Rebind(provider.sqlSelectOAuth2RefreshTokenSession) + provider.sqlInsertOAuth2AuthorizeCodeSession = provider.db.Rebind(provider.sqlInsertOAuth2AuthorizeCodeSession) + provider.sqlRevokeOAuth2AuthorizeCodeSession = provider.db.Rebind(provider.sqlRevokeOAuth2AuthorizeCodeSession) + provider.sqlRevokeOAuth2AuthorizeCodeSessionByRequestID = provider.db.Rebind(provider.sqlRevokeOAuth2AuthorizeCodeSessionByRequestID) + provider.sqlDeactivateOAuth2AuthorizeCodeSession = provider.db.Rebind(provider.sqlDeactivateOAuth2AuthorizeCodeSession) + provider.sqlDeactivateOAuth2AuthorizeCodeSessionByRequestID = provider.db.Rebind(provider.sqlDeactivateOAuth2AuthorizeCodeSessionByRequestID) + provider.sqlSelectOAuth2AuthorizeCodeSession = provider.db.Rebind(provider.sqlSelectOAuth2AuthorizeCodeSession) + + provider.sqlInsertOAuth2OpenIDConnectSession = provider.db.Rebind(provider.sqlInsertOAuth2OpenIDConnectSession) + provider.sqlRevokeOAuth2OpenIDConnectSession = provider.db.Rebind(provider.sqlRevokeOAuth2OpenIDConnectSession) + provider.sqlRevokeOAuth2OpenIDConnectSessionByRequestID = provider.db.Rebind(provider.sqlRevokeOAuth2OpenIDConnectSessionByRequestID) + provider.sqlDeactivateOAuth2OpenIDConnectSession = provider.db.Rebind(provider.sqlDeactivateOAuth2OpenIDConnectSession) + provider.sqlDeactivateOAuth2OpenIDConnectSessionByRequestID = provider.db.Rebind(provider.sqlDeactivateOAuth2OpenIDConnectSessionByRequestID) + provider.sqlSelectOAuth2OpenIDConnectSession = provider.db.Rebind(provider.sqlSelectOAuth2OpenIDConnectSession) + + provider.sqlInsertOAuth2PARContext = provider.db.Rebind(provider.sqlInsertOAuth2PARContext) + provider.sqlRevokeOAuth2PARContext = provider.db.Rebind(provider.sqlRevokeOAuth2PARContext) + provider.sqlSelectOAuth2PARContext = provider.db.Rebind(provider.sqlSelectOAuth2PARContext) provider.sqlInsertOAuth2PKCERequestSession = provider.db.Rebind(provider.sqlInsertOAuth2PKCERequestSession) provider.sqlRevokeOAuth2PKCERequestSession = provider.db.Rebind(provider.sqlRevokeOAuth2PKCERequestSession) @@ -115,12 +119,12 @@ func NewPostgreSQLProvider(config *schema.Configuration, caCertPool *x509.CertPo provider.sqlDeactivateOAuth2PKCERequestSessionByRequestID = provider.db.Rebind(provider.sqlDeactivateOAuth2PKCERequestSessionByRequestID) provider.sqlSelectOAuth2PKCERequestSession = provider.db.Rebind(provider.sqlSelectOAuth2PKCERequestSession) - provider.sqlInsertOAuth2OpenIDConnectSession = provider.db.Rebind(provider.sqlInsertOAuth2OpenIDConnectSession) - provider.sqlRevokeOAuth2OpenIDConnectSession = provider.db.Rebind(provider.sqlRevokeOAuth2OpenIDConnectSession) - provider.sqlRevokeOAuth2OpenIDConnectSessionByRequestID = provider.db.Rebind(provider.sqlRevokeOAuth2OpenIDConnectSessionByRequestID) - provider.sqlDeactivateOAuth2OpenIDConnectSession = provider.db.Rebind(provider.sqlDeactivateOAuth2OpenIDConnectSession) - provider.sqlDeactivateOAuth2OpenIDConnectSessionByRequestID = provider.db.Rebind(provider.sqlDeactivateOAuth2OpenIDConnectSessionByRequestID) - provider.sqlSelectOAuth2OpenIDConnectSession = provider.db.Rebind(provider.sqlSelectOAuth2OpenIDConnectSession) + provider.sqlInsertOAuth2RefreshTokenSession = provider.db.Rebind(provider.sqlInsertOAuth2RefreshTokenSession) + provider.sqlRevokeOAuth2RefreshTokenSession = provider.db.Rebind(provider.sqlRevokeOAuth2RefreshTokenSession) + provider.sqlRevokeOAuth2RefreshTokenSessionByRequestID = provider.db.Rebind(provider.sqlRevokeOAuth2RefreshTokenSessionByRequestID) + provider.sqlDeactivateOAuth2RefreshTokenSession = provider.db.Rebind(provider.sqlDeactivateOAuth2RefreshTokenSession) + provider.sqlDeactivateOAuth2RefreshTokenSessionByRequestID = provider.db.Rebind(provider.sqlDeactivateOAuth2RefreshTokenSessionByRequestID) + provider.sqlSelectOAuth2RefreshTokenSession = provider.db.Rebind(provider.sqlSelectOAuth2RefreshTokenSession) provider.sqlSelectOAuth2BlacklistedJTI = provider.db.Rebind(provider.sqlSelectOAuth2BlacklistedJTI) diff --git a/internal/storage/sql_provider_queries.go b/internal/storage/sql_provider_queries.go index f062f8afb..5f1aa3cec 100644 --- a/internal/storage/sql_provider_queries.go +++ b/internal/storage/sql_provider_queries.go @@ -314,6 +314,19 @@ const ( SET active = FALSE WHERE request_id = ?;` + queryFmtSelectOAuth2PARContext = ` + SELECT id, signature, request_id, client_id, requested_at, scopes, audience, + handled_response_types, response_mode, response_mode_default, revoked, + form_data, session_data + FROM %s + WHERE signature = ? AND revoked = FALSE;` + + queryFmtInsertOAuth2PARContext = ` + INSERT INTO %s (signature, request_id, client_id, requested_at, scopes, audience, + handled_response_types, response_mode, response_mode_default, revoked, + form_data, session_data) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);` + queryFmtSelectOAuth2BlacklistedJTI = ` SELECT id, signature, expires_at FROM %s diff --git a/internal/suites/suite_cli_test.go b/internal/suites/suite_cli_test.go index 6176dfbd6..a77507efe 100644 --- a/internal/suites/suite_cli_test.go +++ b/internal/suites/suite_cli_test.go @@ -1132,6 +1132,7 @@ func (s *CLISuite) TestStorage05ShouldChangeEncryptionKey() { s.Assert().Contains(output, "\n\n\tTable (oauth2_openid_connect_session): N/A\n\t\tInvalid Rows: 0\n\t\tTotal Rows: 0\n") s.Assert().Contains(output, "\n\n\tTable (oauth2_pkce_request_session): N/A\n\t\tInvalid Rows: 0\n\t\tTotal Rows: 0\n") s.Assert().Contains(output, "\n\n\tTable (oauth2_refresh_token_session): N/A\n\t\tInvalid Rows: 0\n\t\tTotal Rows: 0\n") + s.Assert().Contains(output, "\n\n\tTable (oauth2_par_context): N/A\n\t\tInvalid Rows: 0\n\t\tTotal Rows: 0\n") s.Assert().Contains(output, "\n\n\tTable (totp_configurations): FAILURE\n\t\tInvalid Rows: 4\n\t\tTotal Rows: 4\n") s.Assert().Contains(output, "\n\n\tTable (webauthn_devices): N/A\n\t\tInvalid Rows: 0\n\t\tTotal Rows: 0\n") @@ -1149,6 +1150,7 @@ func (s *CLISuite) TestStorage05ShouldChangeEncryptionKey() { s.Assert().Contains(output, "\n\n\tTable (oauth2_openid_connect_session): N/A\n\t\tInvalid Rows: 0\n\t\tTotal Rows: 0\n") s.Assert().Contains(output, "\n\n\tTable (oauth2_pkce_request_session): N/A\n\t\tInvalid Rows: 0\n\t\tTotal Rows: 0\n") s.Assert().Contains(output, "\n\n\tTable (oauth2_refresh_token_session): N/A\n\t\tInvalid Rows: 0\n\t\tTotal Rows: 0\n") + s.Assert().Contains(output, "\n\n\tTable (oauth2_par_context): N/A\n\t\tInvalid Rows: 0\n\t\tTotal Rows: 0\n") s.Assert().Contains(output, "\n\n\tTable (totp_configurations): SUCCESS\n\t\tInvalid Rows: 0\n\t\tTotal Rows: 4\n") s.Assert().Contains(output, "\n\n\tTable (webauthn_devices): N/A\n\t\tInvalid Rows: 0\n\t\tTotal Rows: 0\n") diff --git a/internal/utils/strings.go b/internal/utils/strings.go index b56aed5eb..3e9dc12cd 100644 --- a/internal/utils/strings.go +++ b/internal/utils/strings.go @@ -104,8 +104,13 @@ func IsStringSliceContainsAll(needles []string, haystack []string) (inSlice bool // IsStringSliceContainsAny checks if the haystack contains any of the strings in the needles. func IsStringSliceContainsAny(needles []string, haystack []string) (inSlice bool) { + return IsStringSliceContainsAnyF(needles, haystack, IsStringInSlice) +} + +// IsStringSliceContainsAnyF checks if the haystack contains any of the strings in the needles using the isInSlice func. +func IsStringSliceContainsAnyF(needles []string, haystack []string, isInSlice func(needle string, haystack []string) bool) (inSlice bool) { for _, n := range needles { - if IsStringInSlice(n, haystack) { + if isInSlice(n, haystack) { return true } }