From 68f9742c0502d77dbfbf5bfc8aa9f66ede03e203 Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Tue, 6 Feb 2024 12:58:47 +0100 Subject: [PATCH] :sparkles: [#83] Implement JWT user-info response data processing --- docker/README.md | 15 +- docker/import/test-realm.json | 87 ++++- mozilla_django_oidc_db/admin.py | 1 + mozilla_django_oidc_db/backends.py | 54 ++- mozilla_django_oidc_db/jwt.py | 53 +++ mozilla_django_oidc_db/utils.py | 14 + ...est_return_jwt_from_userinfo_endpoint.yaml | 351 ++++++++++++++++++ tests/test_integration_oidc_flow_variants.py | 49 ++- 8 files changed, 583 insertions(+), 41 deletions(-) create mode 100644 mozilla_django_oidc_db/jwt.py create mode 100644 tests/cassettes/test_integration_oidc_flow_variants/test_return_jwt_from_userinfo_endpoint.yaml diff --git a/docker/README.md b/docker/README.md index ca24494..09ed3aa 100644 --- a/docker/README.md +++ b/docker/README.md @@ -13,7 +13,7 @@ You can now log in via `http://localhost:8080` with the `admin`/`admin` credenti In short - exporting through the admin UI (rightfully) obfuscates client secrets and user credentials. However, for reproducible builds/environments, we want to include -this data in the Realm export. +this data in the Realm export(s). Ensure the service is up and running through docker-compose. @@ -25,9 +25,10 @@ chmod o+rwx ./docker/import/ Then open another terminal and run: - ```bash - docker-compose exec keycloak ./bin/kc.sh \ - export \ - --file /opt/keycloak/data/import/test-realm.json \ - --realm test - ``` +```bash +docker-compose exec keycloak \ + /opt/keycloak/bin/kc.sh \ + export \ + --file /opt/keycloak/data/import/test-realm.json \ + --realm test +``` diff --git a/docker/import/test-realm.json b/docker/import/test-realm.json index 6271fa5..e8c3d86 100644 --- a/docker/import/test-realm.json +++ b/docker/import/test-realm.json @@ -245,6 +245,7 @@ "attributes" : { } } ], "security-admin-console" : [ ], + "test-userinfo-jwt" : [ ], "admin-cli" : [ ], "testid" : [ ], "account-console" : [ ], @@ -513,7 +514,9 @@ "publicClient" : true, "frontchannelLogout" : false, "protocol" : "openid-connect", - "attributes" : { }, + "attributes" : { + "post.logout.redirect.uris" : "+" + }, "authenticationFlowBindingOverrides" : { }, "fullScopeAllowed" : false, "nodeReRegistrationTimeout" : 0, @@ -539,7 +542,9 @@ "publicClient" : false, "frontchannelLogout" : false, "protocol" : "openid-connect", - "attributes" : { }, + "attributes" : { + "post.logout.redirect.uris" : "+" + }, "authenticationFlowBindingOverrides" : { }, "fullScopeAllowed" : false, "nodeReRegistrationTimeout" : 0, @@ -565,7 +570,9 @@ "publicClient" : false, "frontchannelLogout" : false, "protocol" : "openid-connect", - "attributes" : { }, + "attributes" : { + "post.logout.redirect.uris" : "+" + }, "authenticationFlowBindingOverrides" : { }, "fullScopeAllowed" : false, "nodeReRegistrationTimeout" : 0, @@ -618,6 +625,51 @@ } ], "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "email" ], "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "42a22604-c3d9-48a7-9186-e8ef84e05223", + "clientId" : "test-userinfo-jwt", + "name" : "", + "description" : "", + "rootUrl" : "", + "adminUrl" : "", + "baseUrl" : "", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "secret" : "ktGlGUELd1FR7dTXc84L7dJzUTjCtw9S", + "redirectUris" : [ "http://testserver/*", "http://127.0.0.1:8000/*", "http://localhost:8000/*" ], + "webOrigins" : [ "http://127.0.0.1:8000" ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : true, + "serviceAccountsEnabled" : false, + "publicClient" : false, + "frontchannelLogout" : true, + "protocol" : "openid-connect", + "attributes" : { + "client.secret.creation.time" : "1707218309", + "user.info.response.signature.alg" : "RS256", + "oauth2.device.authorization.grant.enabled" : "false", + "backchannel.logout.revoke.offline.tokens" : "false", + "use.refresh.tokens" : "true", + "oidc.ciba.grant.enabled" : "false", + "backchannel.logout.session.required" : "true", + "client_credentials.use_refresh_token" : "false", + "tls.client.certificate.bound.access.tokens" : "false", + "require.pushed.authorization.requests" : "false", + "acr.loa.map" : "{}", + "display.on.consent.screen" : "false", + "token.response.type.bearer.lower-case" : "false" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : true, + "nodeReRegistrationTimeout" : -1, + "defaultClientScopes" : [ "web-origins", "kvk", "acr", "roles", "profile", "bsn", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] }, { "id" : "adf4ad83-4550-4619-9231-73bd8d700f45", "clientId" : "testid", @@ -644,12 +696,20 @@ "frontchannelLogout" : true, "protocol" : "openid-connect", "attributes" : { - "oidc.ciba.grant.enabled" : "false", "client.secret.creation.time" : "1707141299", - "backchannel.logout.session.required" : "true", + "user.info.response.signature.alg" : "RS256", + "post.logout.redirect.uris" : "+", "oauth2.device.authorization.grant.enabled" : "false", + "backchannel.logout.revoke.offline.tokens" : "false", + "use.refresh.tokens" : "true", + "oidc.ciba.grant.enabled" : "false", + "backchannel.logout.session.required" : "true", + "client_credentials.use_refresh_token" : "false", + "tls.client.certificate.bound.access.tokens" : "false", + "require.pushed.authorization.requests" : "false", + "acr.loa.map" : "{}", "display.on.consent.screen" : "false", - "backchannel.logout.revoke.offline.tokens" : "false" + "token.response.type.bearer.lower-case" : "false" }, "authenticationFlowBindingOverrides" : { }, "fullScopeAllowed" : true, @@ -663,6 +723,7 @@ "config" : { "user.session.note" : "client_id", "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", "id.token.claim" : "true", "access.token.claim" : "true", "claim.name" : "client_id", @@ -677,6 +738,7 @@ "config" : { "user.session.note" : "clientAddress", "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", "id.token.claim" : "true", "access.token.claim" : "true", "claim.name" : "clientAddress", @@ -691,6 +753,7 @@ "config" : { "user.session.note" : "clientHost", "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", "id.token.claim" : "true", "access.token.claim" : "true", "claim.name" : "clientHost", @@ -1165,6 +1228,7 @@ "config" : { "introspection.token.claim" : "true", "multivalued" : "true", + "userinfo.token.claim" : "true", "user.attribute" : "foo", "id.token.claim" : "true", "access.token.claim" : "true", @@ -1205,7 +1269,8 @@ "config" : { "id.token.claim" : "true", "introspection.token.claim" : "true", - "access.token.claim" : "true" + "access.token.claim" : "true", + "userinfo.token.claim" : "true" } } ] }, { @@ -1299,7 +1364,7 @@ "subType" : "anonymous", "subComponents" : { }, "config" : { - "allowed-protocol-mapper-types" : [ "saml-role-list-mapper", "saml-user-property-mapper", "oidc-address-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-user-attribute-mapper", "oidc-full-name-mapper", "oidc-usermodel-property-mapper", "oidc-usermodel-attribute-mapper" ] + "allowed-protocol-mapper-types" : [ "oidc-full-name-mapper", "oidc-usermodel-property-mapper", "oidc-usermodel-attribute-mapper", "saml-user-attribute-mapper", "saml-role-list-mapper", "oidc-address-mapper", "saml-user-property-mapper", "oidc-sha256-pairwise-sub-mapper" ] } }, { "id" : "c6b13ddf-1676-4e33-85d7-c778891156b3", @@ -1324,7 +1389,7 @@ "subType" : "authenticated", "subComponents" : { }, "config" : { - "allowed-protocol-mapper-types" : [ "oidc-address-mapper", "saml-role-list-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-usermodel-attribute-mapper", "saml-user-property-mapper", "oidc-usermodel-property-mapper", "oidc-full-name-mapper", "saml-user-attribute-mapper" ] + "allowed-protocol-mapper-types" : [ "oidc-sha256-pairwise-sub-mapper", "oidc-address-mapper", "saml-role-list-mapper", "saml-user-property-mapper", "saml-user-attribute-mapper", "oidc-usermodel-attribute-mapper", "oidc-usermodel-property-mapper", "oidc-full-name-mapper" ] } }, { "id" : "9557d357-cc12-443e-bba6-a89e89b22c2e", @@ -1916,8 +1981,12 @@ "cibaExpiresIn" : "120", "cibaAuthRequestedUserHint" : "login_hint", "oauth2DeviceCodeLifespan" : "600", + "clientOfflineSessionMaxLifespan" : "0", "oauth2DevicePollingInterval" : "5", + "clientSessionIdleTimeout" : "0", "parRequestUriLifespan" : "60", + "clientSessionMaxLifespan" : "0", + "clientOfflineSessionIdleTimeout" : "0", "cibaInterval" : "5", "realmReusableOtpCode" : "false" }, diff --git a/mozilla_django_oidc_db/admin.py b/mozilla_django_oidc_db/admin.py index 4c0009a..df6966a 100644 --- a/mozilla_django_oidc_db/admin.py +++ b/mozilla_django_oidc_db/admin.py @@ -64,6 +64,7 @@ class OpenIDConnectConfigAdmin(SingletonModelAdmin): "oidc_state_size", "oidc_exempt_urls", "userinfo_claims_source", + "userinfo_endpoint_accept_header", ), "classes": [ "collapse in", diff --git a/mozilla_django_oidc_db/backends.py b/mozilla_django_oidc_db/backends.py index 8c07e22..a9af791 100644 --- a/mozilla_django_oidc_db/backends.py +++ b/mozilla_django_oidc_db/backends.py @@ -1,20 +1,22 @@ import fnmatch import logging -from typing import Any, Dict, Generic, List, TypeVar, cast +from typing import Any, Generic, TypeVar, cast from django.contrib.auth import get_user_model from django.contrib.auth.models import Group from django.core.exceptions import ObjectDoesNotExist +import requests from glom import glom from mozilla_django_oidc.auth import ( OIDCAuthenticationBackend as _OIDCAuthenticationBackend, ) from .constants import UserInformationClaimsSources +from .jwt import verify_and_decode_token from .mixins import GetAttributeMixin, SoloConfigMixin from .models import OpenIDConnectConfig -from .utils import obfuscate_claims +from .utils import extract_content_type, obfuscate_claims logger = logging.getLogger(__name__) @@ -27,6 +29,9 @@ class OIDCAuthenticationBackend( """ Modifies the default OIDCAuthenticationBackend to use a configurable claim as unique identifier (default `sub`). + + .. todo:: It would make sense to set up ``self.session = requests.Session()`` for + connection pooling so that subsequent calls are a bit more performant. """ config_identifier_field = "username_claim" @@ -75,7 +80,46 @@ def get_userinfo(self, access_token, id_token, payload): return payload logger.debug("Retrieving user information from userinfo endpoint") - return super().get_userinfo(access_token, id_token, payload) + + # copy of upstream get_userinfo which doesn't support application/jwt yet. + # Overridden to handle application/jwt responses. + # See https://github.com/mozilla/mozilla-django-oidc/issues/517 + # + # Specifying the preferred format in the ``Accept`` header does not work with + # Keycloak, as it depends on the client settings. + user_response = requests.get( + self.OIDC_OP_USER_ENDPOINT, + headers={ + "Authorization": "Bearer {0}".format(access_token), + }, + verify=self.get_settings("OIDC_VERIFY_SSL", True), + timeout=self.get_settings("OIDC_TIMEOUT", None), + proxies=self.get_settings("OIDC_PROXY", None), + ) + user_response.raise_for_status() + + # From https://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse + # + # > The UserInfo Endpoint MUST return a content-type header to indicate which + # > format is being returned. + content_type = extract_content_type(user_response.headers["Content-Type"]) + match content_type: + case "application/json": + # the default case of upstream library + return user_response.json() + case "application/jwt": + token = user_response.content + # get the key from the configured keys endpoint + # XXX only tested with RS256, no idea how this behaves on other algorithms + key = self.retrieve_matching_jwk(token) + payload = verify_and_decode_token(token, key) + return payload + case _: + raise ValueError( + f"Got an invalid Content-Type header value ({content_type}) " + "according to OpenID Connect Core 1.0 standard. Contact your " + "vendor." + ) def authenticate(self, *args, **kwargs): if not self.config.enabled: @@ -83,7 +127,7 @@ def authenticate(self, *args, **kwargs): return super().authenticate(*args, **kwargs) - def get_user_instance_values(self, claims) -> Dict[str, Any]: + def get_user_instance_values(self, claims) -> dict[str, Any]: """ Map the names and values of the claims to the fields of the User model """ @@ -163,7 +207,7 @@ def update_user_superuser_status(self, user, claims): """ groups_claim = self.config.groups_claim # can't do an isinstance check here - superuser_group_names = cast(List[str], self.config.superuser_group_names) + superuser_group_names = cast(list[str], self.config.superuser_group_names) if not superuser_group_names: return diff --git a/mozilla_django_oidc_db/jwt.py b/mozilla_django_oidc_db/jwt.py new file mode 100644 index 0000000..90c6d18 --- /dev/null +++ b/mozilla_django_oidc_db/jwt.py @@ -0,0 +1,53 @@ +""" +Support for user info JWT verification and decoding. + +The bulk of the implementation is taken from mozilla-django-oidc where the access token +is processed, but adapted for non-hardcoded/configured parameters. + +In the case of Keycloak for example, the token signing algorithm is configured on the +server and can change on a whim. +""" + +import json +from typing import Any + +from django.core.exceptions import SuspiciousOperation +from django.utils.encoding import smart_bytes + +from josepy.jwk import JWK +from josepy.jws import JWS + + +def verify_and_decode_token(token: bytes, key) -> dict[str, Any]: + """ + Verify that the token was not tampered with and if okay, return the payload. + + This is mostly taken from + :meth:`mozilla_django_oidc.auth.OIDCAuthenticationBackend._verify_jws`. + """ + + jws = JWS.from_compact(token) + + # validate the signing algorithm + if (alg := jws.signature.combined.alg) is None: + raise SuspiciousOperation("No alg value found in header") + + # one of the most common implementation weaknesses -> attacker can supply 'none' + # algorithm + if alg.name == "none": + raise SuspiciousOperation("'none' for alg value is not allowed") + + # process key parameter which was/may have been loaded from keys endpoint. I'm u + # Unsure what the type of this parameter can be :/ + match key: + case str(): + jwk = JWK.load(smart_bytes(key)) + case _: + jwk = JWK.from_json(key) + # address some missing upstream Self type declarations + assert isinstance(jwk, JWK) + + if not jws.verify(jwk): + raise SuspiciousOperation("JWS token verification failed.") + + return json.loads(jws.payload.decode("utf-8")) diff --git a/mozilla_django_oidc_db/utils.py b/mozilla_django_oidc_db/utils.py index c035f3e..5033bf6 100644 --- a/mozilla_django_oidc_db/utils.py +++ b/mozilla_django_oidc_db/utils.py @@ -2,6 +2,7 @@ from typing import Any, List from glom import assign, glom +from requests.utils import _parse_content_type_header def obfuscate_claim_value(value: Any) -> str: @@ -27,3 +28,16 @@ def obfuscate_claims(claims: dict, claims_to_obfuscate: List[str]) -> dict: claim_value = glom(copied_claims, claim_name) assign(copied_claims, claim_name, obfuscate_claim_value(claim_value)) return copied_claims + + +def extract_content_type(ct_header: str) -> str: + """ + Get the content type + parameters from content type header. + + This is internal API since we use a requests internal utility, which may be + removed/modified at any time. However, this is a deliberate choices since I trust + requests to have a correct implementation more than coming up with one myself. + """ + content_type, _ = _parse_content_type_header(ct_header) + # discard the params, we only want the content type itself + return content_type diff --git a/tests/cassettes/test_integration_oidc_flow_variants/test_return_jwt_from_userinfo_endpoint.yaml b/tests/cassettes/test_integration_oidc_flow_variants/test_return_jwt_from_userinfo_endpoint.yaml new file mode 100644 index 0000000..92d6e4f --- /dev/null +++ b/tests/cassettes/test_integration_oidc_flow_variants/test_return_jwt_from_userinfo_endpoint.yaml @@ -0,0 +1,351 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - python-requests/2.31.0 + method: GET + uri: http://localhost:8080/realms/test/.well-known/openid-configuration + response: + body: + string: '{"issuer":"http://localhost:8080/realms/test","authorization_endpoint":"http://localhost:8080/realms/test/protocol/openid-connect/auth","token_endpoint":"http://localhost:8080/realms/test/protocol/openid-connect/token","introspection_endpoint":"http://localhost:8080/realms/test/protocol/openid-connect/token/introspect","userinfo_endpoint":"http://localhost:8080/realms/test/protocol/openid-connect/userinfo","end_session_endpoint":"http://localhost:8080/realms/test/protocol/openid-connect/logout","frontchannel_logout_session_supported":true,"frontchannel_logout_supported":true,"jwks_uri":"http://localhost:8080/realms/test/protocol/openid-connect/certs","check_session_iframe":"http://localhost:8080/realms/test/protocol/openid-connect/login-status-iframe.html","grant_types_supported":["authorization_code","implicit","refresh_token","password","client_credentials","urn:openid:params:grant-type:ciba","urn:ietf:params:oauth:grant-type:device_code"],"acr_values_supported":["0","1"],"response_types_supported":["code","none","id_token","token","id_token + token","code id_token","code token","code id_token token"],"subject_types_supported":["public","pairwise"],"id_token_signing_alg_values_supported":["PS384","ES384","RS384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512"],"id_token_encryption_alg_values_supported":["RSA-OAEP","RSA-OAEP-256","RSA1_5"],"id_token_encryption_enc_values_supported":["A256GCM","A192GCM","A128GCM","A128CBC-HS256","A192CBC-HS384","A256CBC-HS512"],"userinfo_signing_alg_values_supported":["PS384","ES384","RS384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512","none"],"userinfo_encryption_alg_values_supported":["RSA-OAEP","RSA-OAEP-256","RSA1_5"],"userinfo_encryption_enc_values_supported":["A256GCM","A192GCM","A128GCM","A128CBC-HS256","A192CBC-HS384","A256CBC-HS512"],"request_object_signing_alg_values_supported":["PS384","ES384","RS384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512","none"],"request_object_encryption_alg_values_supported":["RSA-OAEP","RSA-OAEP-256","RSA1_5"],"request_object_encryption_enc_values_supported":["A256GCM","A192GCM","A128GCM","A128CBC-HS256","A192CBC-HS384","A256CBC-HS512"],"response_modes_supported":["query","fragment","form_post","query.jwt","fragment.jwt","form_post.jwt","jwt"],"registration_endpoint":"http://localhost:8080/realms/test/clients-registrations/openid-connect","token_endpoint_auth_methods_supported":["private_key_jwt","client_secret_basic","client_secret_post","tls_client_auth","client_secret_jwt"],"token_endpoint_auth_signing_alg_values_supported":["PS384","ES384","RS384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512"],"introspection_endpoint_auth_methods_supported":["private_key_jwt","client_secret_basic","client_secret_post","tls_client_auth","client_secret_jwt"],"introspection_endpoint_auth_signing_alg_values_supported":["PS384","ES384","RS384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512"],"authorization_signing_alg_values_supported":["PS384","ES384","RS384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512"],"authorization_encryption_alg_values_supported":["RSA-OAEP","RSA-OAEP-256","RSA1_5"],"authorization_encryption_enc_values_supported":["A256GCM","A192GCM","A128GCM","A128CBC-HS256","A192CBC-HS384","A256CBC-HS512"],"claims_supported":["aud","sub","iss","auth_time","name","given_name","family_name","preferred_username","email","acr"],"claim_types_supported":["normal"],"claims_parameter_supported":true,"scopes_supported":["openid","email","roles","phone","profile","address","kvk","web-origins","microprofile-jwt","acr","offline_access","bsn"],"request_parameter_supported":true,"request_uri_parameter_supported":true,"require_request_uri_registration":true,"code_challenge_methods_supported":["plain","S256"],"tls_client_certificate_bound_access_tokens":true,"revocation_endpoint":"http://localhost:8080/realms/test/protocol/openid-connect/revoke","revocation_endpoint_auth_methods_supported":["private_key_jwt","client_secret_basic","client_secret_post","tls_client_auth","client_secret_jwt"],"revocation_endpoint_auth_signing_alg_values_supported":["PS384","ES384","RS384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512"],"backchannel_logout_supported":true,"backchannel_logout_session_supported":true,"device_authorization_endpoint":"http://localhost:8080/realms/test/protocol/openid-connect/auth/device","backchannel_token_delivery_modes_supported":["poll","ping"],"backchannel_authentication_endpoint":"http://localhost:8080/realms/test/protocol/openid-connect/ext/ciba/auth","backchannel_authentication_request_signing_alg_values_supported":["PS384","ES384","RS384","ES256","RS256","ES512","PS256","PS512","RS512"],"require_pushed_authorization_requests":false,"pushed_authorization_request_endpoint":"http://localhost:8080/realms/test/protocol/openid-connect/ext/par/request","mtls_endpoint_aliases":{"token_endpoint":"http://localhost:8080/realms/test/protocol/openid-connect/token","revocation_endpoint":"http://localhost:8080/realms/test/protocol/openid-connect/revoke","introspection_endpoint":"http://localhost:8080/realms/test/protocol/openid-connect/token/introspect","device_authorization_endpoint":"http://localhost:8080/realms/test/protocol/openid-connect/auth/device","registration_endpoint":"http://localhost:8080/realms/test/clients-registrations/openid-connect","userinfo_endpoint":"http://localhost:8080/realms/test/protocol/openid-connect/userinfo","pushed_authorization_request_endpoint":"http://localhost:8080/realms/test/protocol/openid-connect/ext/par/request","backchannel_authentication_endpoint":"http://localhost:8080/realms/test/protocol/openid-connect/ext/ciba/auth"},"authorization_response_iss_parameter_supported":true}' + headers: + Cache-Control: + - no-cache, must-revalidate, no-transform, no-store + Content-Type: + - application/json;charset=UTF-8 + Referrer-Policy: + - no-referrer + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - 1; mode=block + content-length: + - '5847' + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - python-requests/2.31.0 + method: GET + uri: http://localhost:8080/realms/test/protocol/openid-connect/auth?response_type=code&scope=openid+email+profile+bsn+kvk&client_id=test-userinfo-jwt&redirect_uri=http%3A%2F%2Ftestserver%2Foidc%2Fcallback%2F&state=not-a-random-string&nonce=not-a-random-string + response: + body: + string: "\n\n\n\n \n + \ \n \n\n \n Sign + in to test\n \n \n \n \n \n \n \n\n\n\n
\n + \
\n
test
\n
\n
\n + \
\n

+ \ Sign in to your account\n\n

\n
\n
\n + \
\n\n\n
\n + \
\n
\n
\n \n\n \n\n\n
\n\n
\n \n\n
\n + \ \n \n + \
\n\n\n
\n\n
\n
\n + \
\n
\n + \
\n\n
\n\n
\n \n \n
\n + \
\n
\n
\n \n\n\n\n\n\n + \
\n
\n\n
\n
\n\n\n" + headers: + Cache-Control: + - no-store, must-revalidate, max-age=0 + Content-Language: + - en + Content-Security-Policy: + - frame-src 'self'; frame-ancestors 'self'; object-src 'none'; + Content-Type: + - text/html;charset=utf-8 + Referrer-Policy: + - no-referrer + Set-Cookie: + - AUTH_SESSION_ID=7b74f8f5-b1cd-4cbf-8e74-80ebf25a4ff0; Version=1; Path=/realms/test/; + SameSite=None; Secure; HttpOnly + - AUTH_SESSION_ID_LEGACY=7b74f8f5-b1cd-4cbf-8e74-80ebf25a4ff0; Version=1; Path=/realms/test/; + HttpOnly + - KC_RESTART=eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJlNzE1ZTA1MS02Y2RiLTQ4Y2MtYjRmNC1mMDcyMmM4MWY5ZDMifQ.eyJjaWQiOiJ0ZXN0LXVzZXJpbmZvLWp3dCIsInB0eSI6Im9wZW5pZC1jb25uZWN0IiwicnVyaSI6Imh0dHA6Ly90ZXN0c2VydmVyL29pZGMvY2FsbGJhY2svIiwiYWN0IjoiQVVUSEVOVElDQVRFIiwibm90ZXMiOnsic2NvcGUiOiJvcGVuaWQgZW1haWwgcHJvZmlsZSBic24ga3ZrIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL3JlYWxtcy90ZXN0IiwicmVzcG9uc2VfdHlwZSI6ImNvZGUiLCJyZWRpcmVjdF91cmkiOiJodHRwOi8vdGVzdHNlcnZlci9vaWRjL2NhbGxiYWNrLyIsInN0YXRlIjoibm90LWEtcmFuZG9tLXN0cmluZyIsIm5vbmNlIjoibm90LWEtcmFuZG9tLXN0cmluZyJ9fQ.xY-IoxiydsxSw872wBSkDsBVo2V85hMIQye3_UX-vUs; + Version=1; Path=/realms/test/; HttpOnly + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Robots-Tag: + - none + X-XSS-Protection: + - 1; mode=block + content-length: + - '4496' + status: + code: 200 + message: OK +- request: + body: username=testuser&password=testuser&credentialId=&login=Sign+In + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '63' + Content-Type: + - application/x-www-form-urlencoded + Cookie: + - AUTH_SESSION_ID_LEGACY=7b74f8f5-b1cd-4cbf-8e74-80ebf25a4ff0; KC_RESTART=eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJlNzE1ZTA1MS02Y2RiLTQ4Y2MtYjRmNC1mMDcyMmM4MWY5ZDMifQ.eyJjaWQiOiJ0ZXN0LXVzZXJpbmZvLWp3dCIsInB0eSI6Im9wZW5pZC1jb25uZWN0IiwicnVyaSI6Imh0dHA6Ly90ZXN0c2VydmVyL29pZGMvY2FsbGJhY2svIiwiYWN0IjoiQVVUSEVOVElDQVRFIiwibm90ZXMiOnsic2NvcGUiOiJvcGVuaWQgZW1haWwgcHJvZmlsZSBic24ga3ZrIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL3JlYWxtcy90ZXN0IiwicmVzcG9uc2VfdHlwZSI6ImNvZGUiLCJyZWRpcmVjdF91cmkiOiJodHRwOi8vdGVzdHNlcnZlci9vaWRjL2NhbGxiYWNrLyIsInN0YXRlIjoibm90LWEtcmFuZG9tLXN0cmluZyIsIm5vbmNlIjoibm90LWEtcmFuZG9tLXN0cmluZyJ9fQ.xY-IoxiydsxSw872wBSkDsBVo2V85hMIQye3_UX-vUs + User-Agent: + - python-requests/2.31.0 + method: POST + uri: http://localhost:8080/realms/test/login-actions/authenticate?session_code=mIyvpMMrKzXQXEhwzl2yCHg-tk8gmLwpaG8QNctGeI0&execution=5d476fe6-1c62-4f0a-9d3e-9e1f49df4766&client_id=test-userinfo-jwt&tab_id=HEmkx1GfJgE + response: + body: + string: '' + headers: + Cache-Control: + - no-store, must-revalidate, max-age=0 + Content-Security-Policy: + - frame-src 'self'; frame-ancestors 'self'; object-src 'none'; + Location: + - http://testserver/oidc/callback/?state=not-a-random-string&session_state=7b74f8f5-b1cd-4cbf-8e74-80ebf25a4ff0&iss=http%3A%2F%2Flocalhost%3A8080%2Frealms%2Ftest&code=09b3c015-f256-48a3-aa6a-030da15d8edf.7b74f8f5-b1cd-4cbf-8e74-80ebf25a4ff0.42a22604-c3d9-48a7-9186-e8ef84e05223 + Referrer-Policy: + - no-referrer + Set-Cookie: + - KEYCLOAK_LOCALE=; Version=1; Comment=Expiring cookie; Expires=Thu, 01-Jan-1970 + 00:00:10 GMT; Max-Age=0; Path=/realms/test/; HttpOnly + - KC_RESTART=; Version=1; Expires=Thu, 01-Jan-1970 00:00:10 GMT; Max-Age=0; + Path=/realms/test/; HttpOnly + - KC_AUTH_STATE=; Version=1; Expires=Thu, 01-Jan-1970 00:00:10 GMT; Max-Age=0; + Path=/realms/test/ + - KEYCLOAK_IDENTITY=eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJlNzE1ZTA1MS02Y2RiLTQ4Y2MtYjRmNC1mMDcyMmM4MWY5ZDMifQ.eyJleHAiOjE3MDcyNTY1ODAsImlhdCI6MTcwNzIyMDU4MCwianRpIjoiYjVlYTYyNjYtNGM1My00ZWYwLWE5MDctMWQ5MzRlZjdkOGIxIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL3JlYWxtcy90ZXN0Iiwic3ViIjoiYWExMGNmYzctMmM0ZC00MWY2LThmYWMtN2JmNDA1YzU3MmM0IiwidHlwIjoiU2VyaWFsaXplZC1JRCIsInNlc3Npb25fc3RhdGUiOiI3Yjc0ZjhmNS1iMWNkLTRjYmYtOGU3NC04MGViZjI1YTRmZjAiLCJzaWQiOiI3Yjc0ZjhmNS1iMWNkLTRjYmYtOGU3NC04MGViZjI1YTRmZjAiLCJzdGF0ZV9jaGVja2VyIjoia1Z1bXc5ZXpTMnlsY3VTTUZGTUdIWHVjN2V6akxuNmZqcm9RRGszRnowUSJ9.KrlKCX7Oid3r6eCW3j8u5Or4EhPnqQSkquafwhqnT6U; + Version=1; Path=/realms/test/; SameSite=None; Secure; HttpOnly + - KEYCLOAK_IDENTITY_LEGACY=eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJlNzE1ZTA1MS02Y2RiLTQ4Y2MtYjRmNC1mMDcyMmM4MWY5ZDMifQ.eyJleHAiOjE3MDcyNTY1ODAsImlhdCI6MTcwNzIyMDU4MCwianRpIjoiYjVlYTYyNjYtNGM1My00ZWYwLWE5MDctMWQ5MzRlZjdkOGIxIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL3JlYWxtcy90ZXN0Iiwic3ViIjoiYWExMGNmYzctMmM0ZC00MWY2LThmYWMtN2JmNDA1YzU3MmM0IiwidHlwIjoiU2VyaWFsaXplZC1JRCIsInNlc3Npb25fc3RhdGUiOiI3Yjc0ZjhmNS1iMWNkLTRjYmYtOGU3NC04MGViZjI1YTRmZjAiLCJzaWQiOiI3Yjc0ZjhmNS1iMWNkLTRjYmYtOGU3NC04MGViZjI1YTRmZjAiLCJzdGF0ZV9jaGVja2VyIjoia1Z1bXc5ZXpTMnlsY3VTTUZGTUdIWHVjN2V6akxuNmZqcm9RRGszRnowUSJ9.KrlKCX7Oid3r6eCW3j8u5Or4EhPnqQSkquafwhqnT6U; + Version=1; Path=/realms/test/; HttpOnly + - KEYCLOAK_SESSION=test/aa10cfc7-2c4d-41f6-8fac-7bf405c572c4/7b74f8f5-b1cd-4cbf-8e74-80ebf25a4ff0; + Version=1; Expires=Tue, 06-Feb-2024 21:56:20 GMT; Max-Age=36000; Path=/realms/test/; + SameSite=None; Secure + - KEYCLOAK_SESSION_LEGACY=test/aa10cfc7-2c4d-41f6-8fac-7bf405c572c4/7b74f8f5-b1cd-4cbf-8e74-80ebf25a4ff0; + Version=1; Expires=Tue, 06-Feb-2024 21:56:20 GMT; Max-Age=36000; Path=/realms/test/ + - KEYCLOAK_REMEMBER_ME=; Version=1; Comment=Expiring cookie; Expires=Thu, 01-Jan-1970 + 00:00:10 GMT; Max-Age=0; Path=/realms/test/; HttpOnly + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Robots-Tag: + - none + X-XSS-Protection: + - 1; mode=block + content-length: + - '0' + status: + code: 302 + message: Found +- request: + body: client_id=test-userinfo-jwt&client_secret=ktGlGUELd1FR7dTXc84L7dJzUTjCtw9S&grant_type=authorization_code&code=09b3c015-f256-48a3-aa6a-030da15d8edf.7b74f8f5-b1cd-4cbf-8e74-80ebf25a4ff0.42a22604-c3d9-48a7-9186-e8ef84e05223&redirect_uri=http%3A%2F%2Ftestserver%2Foidc%2Fcallback%2F + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '278' + Content-Type: + - application/x-www-form-urlencoded + User-Agent: + - python-requests/2.31.0 + method: POST + uri: http://localhost:8080/realms/test/protocol/openid-connect/token + response: + body: + string: '{"access_token":"eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI0VU5RQWN2VWN2LURGVU94XzRPMWd0MTNPZEpTb3RxRUtQWnVyczJ2UVc4In0.eyJleHAiOjE3MDcyMjA4ODAsImlhdCI6MTcwNzIyMDU4MCwiYXV0aF90aW1lIjoxNzA3MjIwNTgwLCJqdGkiOiI4NDI4ZWMwNS1lODMxLTQwNjgtYWYwZi1lNjg4MjhlNjE1MzciLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvcmVhbG1zL3Rlc3QiLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiYWExMGNmYzctMmM0ZC00MWY2LThmYWMtN2JmNDA1YzU3MmM0IiwidHlwIjoiQmVhcmVyIiwiYXpwIjoidGVzdC11c2VyaW5mby1qd3QiLCJub25jZSI6Im5vdC1hLXJhbmRvbS1zdHJpbmciLCJzZXNzaW9uX3N0YXRlIjoiN2I3NGY4ZjUtYjFjZC00Y2JmLThlNzQtODBlYmYyNWE0ZmYwIiwiYWNyIjoiMSIsImFsbG93ZWQtb3JpZ2lucyI6WyJodHRwOi8vMTI3LjAuMC4xOjgwMDAiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImRlZmF1bHQtcm9sZXMtdGVzdCIsIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6Im9wZW5pZCBlbWFpbCBwcm9maWxlIGt2ayBic24iLCJzaWQiOiI3Yjc0ZjhmNS1iMWNkLTRjYmYtOGU3NC04MGViZjI1YTRmZjAiLCJrdmsiOiIwMTIzNDU2NzgiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsInByZWZlcnJlZF91c2VybmFtZSI6InRlc3R1c2VyIiwiYnNuIjoiMDAwMDAwMDAwIn0.G_7lL_u4UCknKlul29Evxkqmx5dvv_aQyBjbB5CNZldk8_8k3E3PFgMcflOAprCvMkVyrukwLfU4GOMd7RkSR5K7C33D2gi7dYi6wEhhjBSdmnYIXpGI41uyaxXZ3mprhHpiSDTXWP7ZjNraBwOouBChkceS-7C9A83nWn4xh4nf9RsY89C1tEh0n05jBd7fdVyfyp6WDcsq_LH4KrwRdUm286z5kw8gQ3PI7R0RiUC__aKmbyWRERa7VzGuzTg46fAmaguFO1SEcCnnDQLBILoJHKXwXa4lw87GuWAMMTXNkYyLT9rbmaHjjuwN9JNDJbpIPtbgcgtvT6_a1w9LCQ","expires_in":300,"refresh_expires_in":1800,"refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJlNzE1ZTA1MS02Y2RiLTQ4Y2MtYjRmNC1mMDcyMmM4MWY5ZDMifQ.eyJleHAiOjE3MDcyMjIzODAsImlhdCI6MTcwNzIyMDU4MCwianRpIjoiYzUyMGNkZTgtZjE3ZS00NDhlLWEyNTYtOWFiNTNiOTE5OWRiIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL3JlYWxtcy90ZXN0IiwiYXVkIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL3JlYWxtcy90ZXN0Iiwic3ViIjoiYWExMGNmYzctMmM0ZC00MWY2LThmYWMtN2JmNDA1YzU3MmM0IiwidHlwIjoiUmVmcmVzaCIsImF6cCI6InRlc3QtdXNlcmluZm8tand0Iiwibm9uY2UiOiJub3QtYS1yYW5kb20tc3RyaW5nIiwic2Vzc2lvbl9zdGF0ZSI6IjdiNzRmOGY1LWIxY2QtNGNiZi04ZTc0LTgwZWJmMjVhNGZmMCIsInNjb3BlIjoib3BlbmlkIGVtYWlsIHByb2ZpbGUga3ZrIGJzbiIsInNpZCI6IjdiNzRmOGY1LWIxY2QtNGNiZi04ZTc0LTgwZWJmMjVhNGZmMCJ9.nbMnBNWXG844HI1xUpBEBlTsoUSdgx5KrGAn_qb-qfI","token_type":"Bearer","id_token":"eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI0VU5RQWN2VWN2LURGVU94XzRPMWd0MTNPZEpTb3RxRUtQWnVyczJ2UVc4In0.eyJleHAiOjE3MDcyMjA4ODAsImlhdCI6MTcwNzIyMDU4MCwiYXV0aF90aW1lIjoxNzA3MjIwNTgwLCJqdGkiOiI4YTVkYjNlMS1mZGIwLTRmYWMtYjBhZC00N2JiYzYyMGE0NDgiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvcmVhbG1zL3Rlc3QiLCJhdWQiOiJ0ZXN0LXVzZXJpbmZvLWp3dCIsInN1YiI6ImFhMTBjZmM3LTJjNGQtNDFmNi04ZmFjLTdiZjQwNWM1NzJjNCIsInR5cCI6IklEIiwiYXpwIjoidGVzdC11c2VyaW5mby1qd3QiLCJub25jZSI6Im5vdC1hLXJhbmRvbS1zdHJpbmciLCJzZXNzaW9uX3N0YXRlIjoiN2I3NGY4ZjUtYjFjZC00Y2JmLThlNzQtODBlYmYyNWE0ZmYwIiwiYXRfaGFzaCI6ImZWdnhhb2w2R0k4c2pVeXcxMXlrRFEiLCJhY3IiOiIxIiwic2lkIjoiN2I3NGY4ZjUtYjFjZC00Y2JmLThlNzQtODBlYmYyNWE0ZmYwIiwia3ZrIjoiMDEyMzQ1Njc4IiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJ0ZXN0dXNlciIsImJzbiI6IjAwMDAwMDAwMCJ9.CID1q-qKPNvxPYNYUlIyOa30f-FRigsUsHzRrUc-k8J-hA9kHWPbGQsbbJdkyYEC8aHbA9FlHmr1TmjdVTzh_KXkvihPMP6cTVZGMZmQM07NtjVrmte_1MeqcZotab1cBYW899gjTrkGZ97zjy6nMOpcvJkJf2-EB2XunZZvvKSEdGulKj0Yk5UN-MpntIpFkau2Rnz0tj5fW8pocN0P9r4rXFcutb0-dCqdEODx4NTafwzK_RZMmrB53fXeEfeTKRLiFirWOQ3ZvTvmIM8iLS5dbWPvtsKdF6Zuub_0KhD25Qd25nxB1ZcmFHymEn0er3bdvcXfOgxhu13h3qCNUw","not-before-policy":0,"session_state":"7b74f8f5-b1cd-4cbf-8e74-80ebf25a4ff0","scope":"openid + email profile kvk bsn"}' + headers: + Cache-Control: + - no-store + Content-Type: + - application/json + Pragma: + - no-cache + Referrer-Policy: + - no-referrer + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - 1; mode=block + content-length: + - '3533' + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - python-requests/2.31.0 + method: GET + uri: http://localhost:8080/realms/test/protocol/openid-connect/certs + response: + body: + string: '{"keys":[{"kid":"4UNQAcvUcv-DFUOx_4O1gt13OdJSotqEKPZurs2vQW8","kty":"RSA","alg":"RS256","use":"sig","n":"2DOZ0qHie73SuFVR7civrl6r82YUiAghfzaMowjCg0o06AF--2lIS7vNV_PbsVVznPAAMqVrNG-8CcevEzvVZMQD9nH4DI7xlOxK0lrYu8rmMeSfOvXVbBVsWBZe0jnGNukZqjwmRE5__ttJdxPfIBT5-2L6mguQbDfhSUEEdIW7y7UfOXvqLqEcBtoIEB-ORKDTUIQwGZM5mSCy-cY3cHvvZfZVgaUUy5NvujPRXTMje4n_hG0KfEV-40G9qC2_Xvx4EooJzBZ6FSThiWhCpwhIvzcQqB6M9lHW7nU6wADhYPNCa2OKWvphwZ_zbrF4B9dmS6Zli5rBvbox9Hh45w","e":"AQAB","x5c":["MIIClzCCAX8CBgGNeYaMLTANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDDAR0ZXN0MB4XDTI0MDIwNTEzNDYxN1oXDTM0MDIwNTEzNDc1N1owDzENMAsGA1UEAwwEdGVzdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANgzmdKh4nu90rhVUe3Ir65eq/NmFIgIIX82jKMIwoNKNOgBfvtpSEu7zVfz27FVc5zwADKlazRvvAnHrxM71WTEA/Zx+AyO8ZTsStJa2LvK5jHknzr11WwVbFgWXtI5xjbpGao8JkROf/7bSXcT3yAU+fti+poLkGw34UlBBHSFu8u1Hzl76i6hHAbaCBAfjkSg01CEMBmTOZkgsvnGN3B772X2VYGlFMuTb7oz0V0zI3uJ/4RtCnxFfuNBvagtv178eBKKCcwWehUk4YloQqcISL83EKgejPZR1u51OsAA4WDzQmtjilr6YcGf826xeAfXZkumZYuawb26MfR4eOcCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAsnQG/Yi2g1XTCJn74hWv9MjxVAaZb4gBAc2AWm5VgAjhFEM9h6x6m1mQkq7JM4rIdAj8jw55Ok9CBVBIqq4G4cME3eUvVytkj2lC9zcRoAivjjZF2HPg7zNPa2TTR50asmHPRokppV6gewO/C+o5as+4P2zqDXBh61aRd/9kdQfkg14LBbH5/dYccAuvUqlTYC4IEPCvVmBNC1xsMjf0vohvoSjm9vL2bfqG/RJH0ScdCjOd5d2zju4/e2oVdluWm+vzKBQplc7tVMuKpn6LcLmVHiGNAl+EBIZH+WVLlTx0D1+kbHZsfLYG53lQg2LsvurRbWyF/a5fVM/oLTn5ag=="],"x5t":"H5xfs1pRtvX0HyVTskx7eTXx88U","x5t#S256":"XurVtKAIEyc4w9HCGOhnjoRHnYu4d9HCn_5YHmkScJg"},{"kid":"TV3Tl5jIY1nrJLSb53UKEubLR5gYiq9slq1SsDDg1HU","kty":"RSA","alg":"RSA-OAEP","use":"enc","n":"pNvU3ecpVHbJT4bCOEpw6cnV1yi65tB3I0bRF2ilLVOY944QRAGnjBBECPIzNbgqavghYp1j75F2nq6_ny1CYfoaxTV2iDpRUw8_f7sliYbl8FrLLat0S25ItlZrg5TEJHObvOqlG2_nXoeH36MRWwNhms2uCqfhn5VgtenIzpQIBolnM7zzGp21NvdJ1C_ZAUzkXC-l3oQ-BXTtpEVM4h2KpYh4gfZJWCbYij5d1e1YApKD6V61_Cs3Oa2OY7CAUyq5kgAWJZFDB6CpzIr226u3bV7F9RbrQu3Ybc_Lv33EwykscLznKWZY2Mbs3Iz_rFNv3sVX_vHpH4DHWlKu7Q","e":"AQAB","x5c":["MIIClzCCAX8CBgGNeYaMlzANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDDAR0ZXN0MB4XDTI0MDIwNTEzNDYxN1oXDTM0MDIwNTEzNDc1N1owDzENMAsGA1UEAwwEdGVzdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKTb1N3nKVR2yU+GwjhKcOnJ1dcouubQdyNG0RdopS1TmPeOEEQBp4wQRAjyMzW4Kmr4IWKdY++Rdp6uv58tQmH6GsU1dog6UVMPP3+7JYmG5fBayy2rdEtuSLZWa4OUxCRzm7zqpRtv516Hh9+jEVsDYZrNrgqn4Z+VYLXpyM6UCAaJZzO88xqdtTb3SdQv2QFM5Fwvpd6EPgV07aRFTOIdiqWIeIH2SVgm2Io+XdXtWAKSg+letfwrNzmtjmOwgFMquZIAFiWRQwegqcyK9turt21exfUW60Lt2G3Py799xMMpLHC85ylmWNjG7NyM/6xTb97FV/7x6R+Ax1pSru0CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAQGJHeTYSMvp0yndbIn7DLohO9lom5nRrx/bLyb7TiRfogyJEF6rQZ66CAkQFk5eMF878fsHTuMVjtmXVBnhojhVmK91HwjsNQu/8xR6QMXNKJQMvHR245vwUGxlWRw/36ObM1D7QjCd/q+FonpBEY4m5Y6Uz1U0HR2Cbh0E2afVlPLeV+F0LKrlyVMdIaWBGWftCGIKDAHaG/PD66zbAKtxerv2fBIDq100WHPhd57BZxX+2aGJp1IaRDgkxV0E/CjEy3+Knd8xbAgUSW0Tl6OTC75exIvlbzeluEBe0wlapAb7WvBKYsipSW8G8Ey7tjoolDT4AU82EaKUPstiMnA=="],"x5t":"AlfHDI0FOPQpt3RBAILt0dtW1yw","x5t#S256":"a7bhm8-JsnfY7bL_m8Yl72hgmp5516VZlFcVloKzk08"}]}' + headers: + Cache-Control: + - no-cache + Content-Type: + - application/json;charset=UTF-8 + Referrer-Policy: + - no-referrer + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - 1; mode=block + content-length: + - '2909' + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI0VU5RQWN2VWN2LURGVU94XzRPMWd0MTNPZEpTb3RxRUtQWnVyczJ2UVc4In0.eyJleHAiOjE3MDcyMjA4ODAsImlhdCI6MTcwNzIyMDU4MCwiYXV0aF90aW1lIjoxNzA3MjIwNTgwLCJqdGkiOiI4NDI4ZWMwNS1lODMxLTQwNjgtYWYwZi1lNjg4MjhlNjE1MzciLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvcmVhbG1zL3Rlc3QiLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiYWExMGNmYzctMmM0ZC00MWY2LThmYWMtN2JmNDA1YzU3MmM0IiwidHlwIjoiQmVhcmVyIiwiYXpwIjoidGVzdC11c2VyaW5mby1qd3QiLCJub25jZSI6Im5vdC1hLXJhbmRvbS1zdHJpbmciLCJzZXNzaW9uX3N0YXRlIjoiN2I3NGY4ZjUtYjFjZC00Y2JmLThlNzQtODBlYmYyNWE0ZmYwIiwiYWNyIjoiMSIsImFsbG93ZWQtb3JpZ2lucyI6WyJodHRwOi8vMTI3LjAuMC4xOjgwMDAiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImRlZmF1bHQtcm9sZXMtdGVzdCIsIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6Im9wZW5pZCBlbWFpbCBwcm9maWxlIGt2ayBic24iLCJzaWQiOiI3Yjc0ZjhmNS1iMWNkLTRjYmYtOGU3NC04MGViZjI1YTRmZjAiLCJrdmsiOiIwMTIzNDU2NzgiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsInByZWZlcnJlZF91c2VybmFtZSI6InRlc3R1c2VyIiwiYnNuIjoiMDAwMDAwMDAwIn0.G_7lL_u4UCknKlul29Evxkqmx5dvv_aQyBjbB5CNZldk8_8k3E3PFgMcflOAprCvMkVyrukwLfU4GOMd7RkSR5K7C33D2gi7dYi6wEhhjBSdmnYIXpGI41uyaxXZ3mprhHpiSDTXWP7ZjNraBwOouBChkceS-7C9A83nWn4xh4nf9RsY89C1tEh0n05jBd7fdVyfyp6WDcsq_LH4KrwRdUm286z5kw8gQ3PI7R0RiUC__aKmbyWRERa7VzGuzTg46fAmaguFO1SEcCnnDQLBILoJHKXwXa4lw87GuWAMMTXNkYyLT9rbmaHjjuwN9JNDJbpIPtbgcgtvT6_a1w9LCQ + Connection: + - keep-alive + User-Agent: + - python-requests/2.31.0 + method: GET + uri: http://localhost:8080/realms/test/protocol/openid-connect/userinfo + response: + body: + string: eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI0VU5RQWN2VWN2LURGVU94XzRPMWd0MTNPZEpTb3RxRUtQWnVyczJ2UVc4In0.eyJzdWIiOiJhYTEwY2ZjNy0yYzRkLTQxZjYtOGZhYy03YmY0MDVjNTcyYzQiLCJrdmsiOiIwMTIzNDU2NzgiLCJhdWQiOiJ0ZXN0LXVzZXJpbmZvLWp3dCIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL3JlYWxtcy90ZXN0IiwicHJlZmVycmVkX3VzZXJuYW1lIjoidGVzdHVzZXIiLCJic24iOiIwMDAwMDAwMDAifQ.YcTZtrW9MrbriMFhjxNMQF0urfUniiS4D8ZXYnLa3slyyfBc9dz1G0zCq07FDVbEyhu8dfmUIm_y_nHHHhECpGhGYBAZ7UhT-RSGITy1D-lxZtpUfb-ckzphLkWh86OqmEk4KWmQgiVh1lK6OGcEpTKm_KYUtvHCwGQfAmlSxDUNTRO5ZivwuQsx5nLRDRnelTP8R4Hj74_38OGN9mwi1RXAW6qJJVtnvNe4RDEHuVhRWkMFgU-TksO0AUlKYx7h1QegPbVxCkeSYxf6TmSO8xy5X4U3-6w4l9Ld4f2MWeLjUh1fey97MH9JS7gNZGK0OjBPK-lBGxD8TOeUsC138g + headers: + Cache-Control: + - no-cache + Content-Type: + - application/jwt + Referrer-Policy: + - no-referrer + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + X-Content-Type-Options: + - nosniff + X-XSS-Protection: + - 1; mode=block + content-length: + - '729' + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - python-requests/2.31.0 + method: GET + uri: http://localhost:8080/realms/test/protocol/openid-connect/certs + response: + body: + string: '{"keys":[{"kid":"4UNQAcvUcv-DFUOx_4O1gt13OdJSotqEKPZurs2vQW8","kty":"RSA","alg":"RS256","use":"sig","n":"2DOZ0qHie73SuFVR7civrl6r82YUiAghfzaMowjCg0o06AF--2lIS7vNV_PbsVVznPAAMqVrNG-8CcevEzvVZMQD9nH4DI7xlOxK0lrYu8rmMeSfOvXVbBVsWBZe0jnGNukZqjwmRE5__ttJdxPfIBT5-2L6mguQbDfhSUEEdIW7y7UfOXvqLqEcBtoIEB-ORKDTUIQwGZM5mSCy-cY3cHvvZfZVgaUUy5NvujPRXTMje4n_hG0KfEV-40G9qC2_Xvx4EooJzBZ6FSThiWhCpwhIvzcQqB6M9lHW7nU6wADhYPNCa2OKWvphwZ_zbrF4B9dmS6Zli5rBvbox9Hh45w","e":"AQAB","x5c":["MIIClzCCAX8CBgGNeYaMLTANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDDAR0ZXN0MB4XDTI0MDIwNTEzNDYxN1oXDTM0MDIwNTEzNDc1N1owDzENMAsGA1UEAwwEdGVzdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANgzmdKh4nu90rhVUe3Ir65eq/NmFIgIIX82jKMIwoNKNOgBfvtpSEu7zVfz27FVc5zwADKlazRvvAnHrxM71WTEA/Zx+AyO8ZTsStJa2LvK5jHknzr11WwVbFgWXtI5xjbpGao8JkROf/7bSXcT3yAU+fti+poLkGw34UlBBHSFu8u1Hzl76i6hHAbaCBAfjkSg01CEMBmTOZkgsvnGN3B772X2VYGlFMuTb7oz0V0zI3uJ/4RtCnxFfuNBvagtv178eBKKCcwWehUk4YloQqcISL83EKgejPZR1u51OsAA4WDzQmtjilr6YcGf826xeAfXZkumZYuawb26MfR4eOcCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAsnQG/Yi2g1XTCJn74hWv9MjxVAaZb4gBAc2AWm5VgAjhFEM9h6x6m1mQkq7JM4rIdAj8jw55Ok9CBVBIqq4G4cME3eUvVytkj2lC9zcRoAivjjZF2HPg7zNPa2TTR50asmHPRokppV6gewO/C+o5as+4P2zqDXBh61aRd/9kdQfkg14LBbH5/dYccAuvUqlTYC4IEPCvVmBNC1xsMjf0vohvoSjm9vL2bfqG/RJH0ScdCjOd5d2zju4/e2oVdluWm+vzKBQplc7tVMuKpn6LcLmVHiGNAl+EBIZH+WVLlTx0D1+kbHZsfLYG53lQg2LsvurRbWyF/a5fVM/oLTn5ag=="],"x5t":"H5xfs1pRtvX0HyVTskx7eTXx88U","x5t#S256":"XurVtKAIEyc4w9HCGOhnjoRHnYu4d9HCn_5YHmkScJg"},{"kid":"TV3Tl5jIY1nrJLSb53UKEubLR5gYiq9slq1SsDDg1HU","kty":"RSA","alg":"RSA-OAEP","use":"enc","n":"pNvU3ecpVHbJT4bCOEpw6cnV1yi65tB3I0bRF2ilLVOY944QRAGnjBBECPIzNbgqavghYp1j75F2nq6_ny1CYfoaxTV2iDpRUw8_f7sliYbl8FrLLat0S25ItlZrg5TEJHObvOqlG2_nXoeH36MRWwNhms2uCqfhn5VgtenIzpQIBolnM7zzGp21NvdJ1C_ZAUzkXC-l3oQ-BXTtpEVM4h2KpYh4gfZJWCbYij5d1e1YApKD6V61_Cs3Oa2OY7CAUyq5kgAWJZFDB6CpzIr226u3bV7F9RbrQu3Ybc_Lv33EwykscLznKWZY2Mbs3Iz_rFNv3sVX_vHpH4DHWlKu7Q","e":"AQAB","x5c":["MIIClzCCAX8CBgGNeYaMlzANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDDAR0ZXN0MB4XDTI0MDIwNTEzNDYxN1oXDTM0MDIwNTEzNDc1N1owDzENMAsGA1UEAwwEdGVzdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKTb1N3nKVR2yU+GwjhKcOnJ1dcouubQdyNG0RdopS1TmPeOEEQBp4wQRAjyMzW4Kmr4IWKdY++Rdp6uv58tQmH6GsU1dog6UVMPP3+7JYmG5fBayy2rdEtuSLZWa4OUxCRzm7zqpRtv516Hh9+jEVsDYZrNrgqn4Z+VYLXpyM6UCAaJZzO88xqdtTb3SdQv2QFM5Fwvpd6EPgV07aRFTOIdiqWIeIH2SVgm2Io+XdXtWAKSg+letfwrNzmtjmOwgFMquZIAFiWRQwegqcyK9turt21exfUW60Lt2G3Py799xMMpLHC85ylmWNjG7NyM/6xTb97FV/7x6R+Ax1pSru0CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAQGJHeTYSMvp0yndbIn7DLohO9lom5nRrx/bLyb7TiRfogyJEF6rQZ66CAkQFk5eMF878fsHTuMVjtmXVBnhojhVmK91HwjsNQu/8xR6QMXNKJQMvHR245vwUGxlWRw/36ObM1D7QjCd/q+FonpBEY4m5Y6Uz1U0HR2Cbh0E2afVlPLeV+F0LKrlyVMdIaWBGWftCGIKDAHaG/PD66zbAKtxerv2fBIDq100WHPhd57BZxX+2aGJp1IaRDgkxV0E/CjEy3+Knd8xbAgUSW0Tl6OTC75exIvlbzeluEBe0wlapAb7WvBKYsipSW8G8Ey7tjoolDT4AU82EaKUPstiMnA=="],"x5t":"AlfHDI0FOPQpt3RBAILt0dtW1yw","x5t#S256":"a7bhm8-JsnfY7bL_m8Yl72hgmp5516VZlFcVloKzk08"}]}' + headers: + Cache-Control: + - no-cache + Content-Type: + - application/json;charset=UTF-8 + Referrer-Policy: + - no-referrer + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - 1; mode=block + content-length: + - '2909' + status: + code: 200 + message: OK +version: 1 diff --git a/tests/test_integration_oidc_flow_variants.py b/tests/test_integration_oidc_flow_variants.py index c630d0d..3703f68 100644 --- a/tests/test_integration_oidc_flow_variants.py +++ b/tests/test_integration_oidc_flow_variants.py @@ -2,10 +2,8 @@ import pytest -from mozilla_django_oidc_db.models import ( - OpenIDConnectConfig, - UserInformationClaimsSources, -) +from mozilla_django_oidc_db.constants import UserInformationClaimsSources +from mozilla_django_oidc_db.models import OpenIDConnectConfig from .utils import keycloak_login @@ -83,19 +81,30 @@ def test_credentials_in_basic_auth_header( assert b"secret=" not in token_request.body -# @pytest.mark.vcr -# def test_return_jwt_from_userinfo_endpoint( -# keycloak_config: OpenIDConnectConfig, -# mock_state_and_nonce, -# client, -# django_user_model, -# vcr, -# ): -# keycloak_config.userinfo_claims_source = ( -# UserInformationClaimsSources.userinfo_endpoint -# ) -# keycloak_config.save() - -# django_login_response = client.get(reverse("login")) -# # simulate login to Keycloak -# redirect_uri = keycloak_login(django_login_response["Location"]) +@pytest.mark.vcr +def test_return_jwt_from_userinfo_endpoint( + keycloak_config: OpenIDConnectConfig, + mock_state_and_nonce, + client, + django_user_model, +): + # Set up client configured to return JWT from userinfo endpoint instead of plain + # JSON. Credentials from ``docker/import`` realm export. + keycloak_config.oidc_rp_client_id = "test-userinfo-jwt" + keycloak_config.oidc_rp_client_secret = "ktGlGUELd1FR7dTXc84L7dJzUTjCtw9S" + keycloak_config.userinfo_claims_source = ( + UserInformationClaimsSources.userinfo_endpoint + ) + keycloak_config.save() + + django_login_response = client.get(reverse("login")) + # simulate login to Keycloak + redirect_uri = keycloak_login(django_login_response["Location"]) + + # complete the login flow on our end + callback_response = client.get(redirect_uri) + assert callback_response.status_code == 302 + assert callback_response["Location"] == "/admin/" + + # a user was created + assert django_user_model.objects.count() == 1