From 8df36fbf28ed3a2b5a2f185df9701d198dea9ac4 Mon Sep 17 00:00:00 2001 From: Takashi Norimatsu Date: Wed, 21 Jul 2021 10:09:32 +0900 Subject: [PATCH] KEYCLOAK-18828 FAPI-CIBA-ID1 conformance test : Additional checks of signed authentication request --- ...baSignedAuthenticationRequestExecutor.java | 53 +++++++++ .../keycloak/testsuite/client/CIBATest.java | 101 ++++++++++++++++++ 2 files changed, 154 insertions(+) diff --git a/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/clientpolicy/executor/SecureCibaSignedAuthenticationRequestExecutor.java b/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/clientpolicy/executor/SecureCibaSignedAuthenticationRequestExecutor.java index 0695e89b4e..6f84033722 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/clientpolicy/executor/SecureCibaSignedAuthenticationRequestExecutor.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/clientpolicy/executor/SecureCibaSignedAuthenticationRequestExecutor.java @@ -17,6 +17,8 @@ package org.keycloak.protocol.oidc.grants.ciba.clientpolicy.executor; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; import javax.ws.rs.core.MultivaluedMap; @@ -24,12 +26,14 @@ import javax.ws.rs.core.MultivaluedMap; import org.jboss.logging.Logger; import org.keycloak.OAuthErrorException; import org.keycloak.common.util.Time; +import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.grants.ciba.clientpolicy.context.BackchannelAuthenticationRequestContext; import org.keycloak.protocol.oidc.grants.ciba.endpoints.request.BackchannelAuthenticationEndpointRequest; import org.keycloak.protocol.oidc.grants.ciba.endpoints.request.BackchannelAuthenticationEndpointRequestParser; import org.keycloak.representations.idm.ClientPolicyExecutorConfigurationRepresentation; +import org.keycloak.services.Urls; import org.keycloak.services.clientpolicy.ClientPolicyContext; import org.keycloak.services.clientpolicy.ClientPolicyException; import org.keycloak.services.clientpolicy.executor.ClientPolicyExecutorProvider; @@ -163,6 +167,55 @@ public class SecureCibaSignedAuthenticationRequestExecutor implements ClientPoli throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "signed authentication request's available period is long"); } + // check whether "aud" claim exists + List aud = new ArrayList(); + JsonNode audience = signedAuthReq.get("aud"); + if (audience == null) { + logger.trace("aud claim not incuded."); + throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "Missing parameter in the 'request' object: aud"); + } + if (audience.isArray()) { + for (JsonNode node : audience) aud.add(node.asText()); + } else { + aud.add(audience.asText()); + } + if (aud.isEmpty()) { + logger.trace("aud claim not incuded."); + throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "Missing parameter value in the 'request' object: aud"); + } + + // check whether "aud" claim points to this keycloak as authz server + String authzServerIss = Urls.realmIssuer(session.getContext().getUri().getBaseUri(), session.getContext().getRealm().getName()); + if (!aud.contains(authzServerIss)) { + logger.trace("aud not points to the intended realm."); + throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "Invalid parameter in the 'request' object: aud"); + } + + // check whether "iss" claim exists + if (signedAuthReq.get("iss") == null) { + logger.trace("iss claim not incuded."); + throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "Missing parameter in the 'request' object: iss"); + } + + ClientModel client = session.getContext().getClient(); + String iss = signedAuthReq.get("iss").asText(); + if (!iss.equals(client.getClientId())) { + logger.trace("iss claim not match client's identity."); + throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "Invalid parameter in the 'request' object: iss"); + } + + // check whether "iat" claim exists + if (signedAuthReq.get("iat") == null) { + logger.trace("iat claim not incuded."); + throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "Missing parameter in the signed authentication request: iat"); + } + + // check whether "jti" claim exists + if (signedAuthReq.get("jti") == null) { + logger.trace("jti claim not incuded."); + throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "Missing parameter in the signed authentication request: jti"); + } + logger.trace("Passed."); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/CIBATest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/CIBATest.java index 8013f30ec1..d6a44587b4 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/CIBATest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/CIBATest.java @@ -1302,11 +1302,112 @@ public class CIBATest extends AbstractClientPoliciesTest { assertThat(response.getError(), is(OAuthErrorException.INVALID_REQUEST)); assertThat(response.getErrorDescription(), is("signed authentication request's available period is long")); + useRequestUri = true; + bindingMessage = "Flughafen-Wien-Schwechat"; + requestObject = createPartialAuthorizationEndpointRequestObject(username, bindingMessage); + requestObject.exp(requestObject.getIat() + Long.valueOf(300)); + requestObject.nbf(requestObject.getIat()); + requestObject.audience(null); + + registerSharedAuthenticationRequest(requestObject, TEST_CLIENT_NAME, sigAlg, useRequestUri); + + // user Backchannel Authentication Request + response = oauth.doBackchannelAuthenticationRequest(TEST_CLIENT_NAME, TEST_CLIENT_PASSWORD, username, bindingMessage, null, null); + assertThat(response.getStatusCode(), is(equalTo(400))); + assertThat(response.getError(), is(OAuthErrorException.INVALID_REQUEST)); + assertThat(response.getErrorDescription(), is("Missing parameter in the 'request' object: aud")); + + useRequestUri = false; + bindingMessage = "Stuttgart-Hauptbahnhof"; + requestObject = createPartialAuthorizationEndpointRequestObject(username, bindingMessage); + requestObject.exp(requestObject.getIat() + Long.valueOf(300)); + requestObject.nbf(requestObject.getIat()); + requestObject.audience("https://example.com"); + + registerSharedAuthenticationRequest(requestObject, TEST_CLIENT_NAME, sigAlg, useRequestUri); + + // user Backchannel Authentication Request + response = oauth.doBackchannelAuthenticationRequest(TEST_CLIENT_NAME, TEST_CLIENT_PASSWORD, username, bindingMessage, null, null); + assertThat(response.getStatusCode(), is(equalTo(400))); + assertThat(response.getError(), is(OAuthErrorException.INVALID_REQUEST)); + assertThat(response.getErrorDescription(), is("Invalid parameter in the 'request' object: aud")); + + useRequestUri = true; + bindingMessage = "Flughafen-Wien-Schwechat"; + requestObject = createPartialAuthorizationEndpointRequestObject(username, bindingMessage); + requestObject.exp(requestObject.getIat() + Long.valueOf(300)); + requestObject.nbf(requestObject.getIat()); + requestObject.audience(Urls.realmIssuer(new URI(suiteContext.getAuthServerInfo().getContextRoot().toString() + "/auth"), REALM_NAME), "https://example.com"); + + registerSharedAuthenticationRequest(requestObject, TEST_CLIENT_NAME, sigAlg, useRequestUri); + + // user Backchannel Authentication Request + response = oauth.doBackchannelAuthenticationRequest(TEST_CLIENT_NAME, TEST_CLIENT_PASSWORD, username, bindingMessage, null, null); + assertThat(response.getStatusCode(), is(equalTo(400))); + assertThat(response.getError(), is(OAuthErrorException.INVALID_REQUEST)); + assertThat(response.getErrorDescription(), is("Missing parameter in the 'request' object: iss")); + + useRequestUri = false; + bindingMessage = "Stuttgart-Hauptbahnhof"; + requestObject = createPartialAuthorizationEndpointRequestObject(username, bindingMessage); + requestObject.exp(requestObject.getIat() + Long.valueOf(300)); + requestObject.nbf(requestObject.getIat()); + requestObject.audience(Urls.realmIssuer(new URI(suiteContext.getAuthServerInfo().getContextRoot().toString() + "/auth"), REALM_NAME), "https://example.com"); + requestObject.issuer(TEST_CLIENT_NAME + TEST_CLIENT_NAME); + + registerSharedAuthenticationRequest(requestObject, TEST_CLIENT_NAME, sigAlg, useRequestUri); + + // user Backchannel Authentication Request + response = oauth.doBackchannelAuthenticationRequest(TEST_CLIENT_NAME, TEST_CLIENT_PASSWORD, username, bindingMessage, null, null); + assertThat(response.getStatusCode(), is(equalTo(400))); + assertThat(response.getError(), is(OAuthErrorException.INVALID_REQUEST)); + assertThat(response.getErrorDescription(), is("Invalid parameter in the 'request' object: iss")); + + useRequestUri = true; + bindingMessage = "Flughafen-Wien-Schwechat"; + requestObject = createPartialAuthorizationEndpointRequestObject(username, bindingMessage); + requestObject.exp(requestObject.getIat() + Long.valueOf(300)); + requestObject.nbf(requestObject.getIat()); + requestObject.audience(Urls.realmIssuer(new URI(suiteContext.getAuthServerInfo().getContextRoot().toString() + "/auth"), REALM_NAME), "https://example.com"); + requestObject.issuer(TEST_CLIENT_NAME); + requestObject.iat(null); + requestObject.id(null); + + registerSharedAuthenticationRequest(requestObject, TEST_CLIENT_NAME, sigAlg, useRequestUri); + + // user Backchannel Authentication Request + response = oauth.doBackchannelAuthenticationRequest(TEST_CLIENT_NAME, TEST_CLIENT_PASSWORD, username, bindingMessage, null, null); + assertThat(response.getStatusCode(), is(equalTo(400))); + assertThat(response.getError(), is(OAuthErrorException.INVALID_REQUEST)); + assertThat(response.getErrorDescription(), is("Missing parameter in the signed authentication request: iat")); + + useRequestUri = false; + bindingMessage = "Stuttgart-Hauptbahnhof"; + requestObject = createPartialAuthorizationEndpointRequestObject(username, bindingMessage); + requestObject.exp(requestObject.getIat() + Long.valueOf(300)); + requestObject.nbf(requestObject.getIat()); + requestObject.audience(Urls.realmIssuer(new URI(suiteContext.getAuthServerInfo().getContextRoot().toString() + "/auth"), REALM_NAME), "https://example.com"); + requestObject.issuer(TEST_CLIENT_NAME); + requestObject.iat(Long.valueOf(Time.currentTime())); + requestObject.id(null); + + registerSharedAuthenticationRequest(requestObject, TEST_CLIENT_NAME, sigAlg, useRequestUri); + + // user Backchannel Authentication Request + response = oauth.doBackchannelAuthenticationRequest(TEST_CLIENT_NAME, TEST_CLIENT_PASSWORD, username, bindingMessage, null, null); + assertThat(response.getStatusCode(), is(equalTo(400))); + assertThat(response.getError(), is(OAuthErrorException.INVALID_REQUEST)); + assertThat(response.getErrorDescription(), is("Missing parameter in the signed authentication request: jti")); + useRequestUri = true; bindingMessage = "Brno-hlavni-nadrazif"; requestObject = createPartialAuthorizationEndpointRequestObject(username, bindingMessage); requestObject.exp(requestObject.getIat() + Long.valueOf(300)); requestObject.nbf(requestObject.getIat()); + requestObject.audience(Urls.realmIssuer(new URI(suiteContext.getAuthServerInfo().getContextRoot().toString() + "/auth"), REALM_NAME), "https://example.com"); + requestObject.issuer(TEST_CLIENT_NAME); + requestObject.id(org.keycloak.models.utils.KeycloakModelUtils.generateId()); + requestObject.iat(Long.valueOf(Time.currentTime())); registerSharedAuthenticationRequest(requestObject, TEST_CLIENT_NAME, sigAlg, useRequestUri);