keycloak-uncached
Changes
services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java 26(+5 -21)
services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java 158(+37 -121)
services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequest.java 88(+88 -0)
services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequestParserProcessor.java 73(+73 -0)
services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointQueryStringParser.java 52(+52 -0)
services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestObjectParser.java 88(+88 -0)
services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestParser.java 122(+122 -0)
services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java 11(+11 -0)
services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java 31(+18 -13)
services/src/main/java/org/keycloak/services/resources/admin/IdentityProvidersResource.java 4(+2 -2)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingOIDCEndpointsApplicationResource.java 137(+137 -0)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestApplicationResourceProvider.java 11(+10 -1)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestApplicationResourceProviderFactory.java 27(+26 -1)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java 15(+10 -5)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestApplicationResource.java 4(+4 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestApplicationResourceUrls.java 48(+48 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestOIDCEndpointsApplicationResource.java 58(+58 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java 23(+23 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java 29(+21 -8)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java 114(+99 -15)
Details
diff --git a/server-spi/src/main/java/org/keycloak/broker/provider/AbstractIdentityProviderFactory.java b/server-spi/src/main/java/org/keycloak/broker/provider/AbstractIdentityProviderFactory.java
index e8be6d6..953797b 100755
--- a/server-spi/src/main/java/org/keycloak/broker/provider/AbstractIdentityProviderFactory.java
+++ b/server-spi/src/main/java/org/keycloak/broker/provider/AbstractIdentityProviderFactory.java
@@ -50,7 +50,7 @@ public abstract class AbstractIdentityProviderFactory<T extends IdentityProvider
}
@Override
- public Map<String, String> parseConfig(InputStream inputStream) {
+ public Map<String, String> parseConfig(KeycloakSession session, InputStream inputStream) {
return new HashMap<String, String>();
}
}
diff --git a/server-spi/src/main/java/org/keycloak/broker/provider/IdentityProviderFactory.java b/server-spi/src/main/java/org/keycloak/broker/provider/IdentityProviderFactory.java
index fba8e64..93ef720 100755
--- a/server-spi/src/main/java/org/keycloak/broker/provider/IdentityProviderFactory.java
+++ b/server-spi/src/main/java/org/keycloak/broker/provider/IdentityProviderFactory.java
@@ -17,6 +17,7 @@
package org.keycloak.broker.provider;
import org.keycloak.models.IdentityProviderModel;
+import org.keycloak.models.KeycloakSession;
import org.keycloak.provider.ProviderFactory;
import java.io.InputStream;
@@ -47,8 +48,9 @@ public interface IdentityProviderFactory<T extends IdentityProvider> extends Pro
* <p>Creates an {@link IdentityProvider} based on the configuration from
* <code>inputStream</code>.</p>
*
+ * @param session
* @param inputStream The input stream from where configuration will be loaded from..
* @return
*/
- Map<String, String> parseConfig(InputStream inputStream);
+ Map<String, String> parseConfig(KeycloakSession session, InputStream inputStream);
}
\ No newline at end of file
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java
index 789d381..86dd4e8 100644
--- a/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java
@@ -40,6 +40,7 @@ import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.crypto.RSAProvider;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.ClientModel;
+import org.keycloak.models.ModelException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
@@ -166,30 +167,13 @@ public class JWTClientAuthenticator extends AbstractClientAuthenticator {
}
protected PublicKey getSignatureValidationKey(ClientModel client, ClientAuthenticationFlowContext context) {
- CertificateRepresentation certInfo = CertificateInfoHelper.getCertificateFromClient(client, ATTR_PREFIX);
-
- String encodedCertificate = certInfo.getCertificate();
- String encodedPublicKey = certInfo.getPublicKey();
-
- if (encodedCertificate == null && encodedPublicKey == null) {
- Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "unauthorized_client", "Client '" + client.getClientId() + "' doesn't have certificate or publicKey configured");
- context.failure(AuthenticationFlowError.CLIENT_CREDENTIALS_SETUP_REQUIRED, challengeResponse);
- return null;
- }
-
- if (encodedCertificate != null && encodedPublicKey != null) {
- Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "unauthorized_client", "Client '" + client.getClientId() + "' has both publicKey and certificate configured");
+ try {
+ return CertificateInfoHelper.getSignatureValidationKey(client, ATTR_PREFIX);
+ } catch (ModelException me) {
+ Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "unauthorized_client", me.getMessage());
context.failure(AuthenticationFlowError.CLIENT_CREDENTIALS_SETUP_REQUIRED, challengeResponse);
return null;
}
-
- // TODO: Caching of publicKeys / certificates, so it doesn't need to be always computed from pem. For performance reasons...
- if (encodedCertificate != null) {
- X509Certificate clientCert = KeycloakModelUtils.getCertificate(encodedCertificate);
- return clientCert.getPublicKey();
- } else {
- return KeycloakModelUtils.getPublicKey(encodedPublicKey);
- }
}
@Override
diff --git a/services/src/main/java/org/keycloak/broker/oidc/KeycloakOIDCIdentityProviderFactory.java b/services/src/main/java/org/keycloak/broker/oidc/KeycloakOIDCIdentityProviderFactory.java
index d3e8eba..56f24a2 100755
--- a/services/src/main/java/org/keycloak/broker/oidc/KeycloakOIDCIdentityProviderFactory.java
+++ b/services/src/main/java/org/keycloak/broker/oidc/KeycloakOIDCIdentityProviderFactory.java
@@ -18,6 +18,7 @@ package org.keycloak.broker.oidc;
import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
import org.keycloak.models.IdentityProviderModel;
+import org.keycloak.models.KeycloakSession;
import java.io.InputStream;
import java.util.Map;
@@ -45,8 +46,8 @@ public class KeycloakOIDCIdentityProviderFactory extends AbstractIdentityProvide
}
@Override
- public Map<String, String> parseConfig(InputStream inputStream) {
- return OIDCIdentityProviderFactory.parseOIDCConfig(inputStream);
+ public Map<String, String> parseConfig(KeycloakSession session, InputStream inputStream) {
+ return OIDCIdentityProviderFactory.parseOIDCConfig(session, inputStream);
}
diff --git a/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderFactory.java b/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderFactory.java
index 82c2cdd..a0e5017 100755
--- a/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderFactory.java
+++ b/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderFactory.java
@@ -19,6 +19,7 @@ package org.keycloak.broker.oidc;
import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
import org.keycloak.jose.jwk.JWK;
import org.keycloak.models.IdentityProviderModel;
+import org.keycloak.models.KeycloakSession;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.jose.jwk.JSONWebKeySet;
import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation;
@@ -56,11 +57,11 @@ public class OIDCIdentityProviderFactory extends AbstractIdentityProviderFactory
}
@Override
- public Map<String, String> parseConfig(InputStream inputStream) {
- return parseOIDCConfig(inputStream);
+ public Map<String, String> parseConfig(KeycloakSession session, InputStream inputStream) {
+ return parseOIDCConfig(session, inputStream);
}
- protected static Map<String, String> parseOIDCConfig(InputStream inputStream) {
+ protected static Map<String, String> parseOIDCConfig(KeycloakSession session, InputStream inputStream) {
OIDCConfigurationRepresentation rep;
try {
rep = JsonSerialization.readValue(inputStream, OIDCConfigurationRepresentation.class);
@@ -74,14 +75,14 @@ public class OIDCIdentityProviderFactory extends AbstractIdentityProviderFactory
config.setTokenUrl(rep.getTokenEndpoint());
config.setUserInfoUrl(rep.getUserinfoEndpoint());
if (rep.getJwksUri() != null) {
- sendJwksRequest(rep, config);
+ sendJwksRequest(session, rep, config);
}
return config.getConfig();
}
- protected static void sendJwksRequest(OIDCConfigurationRepresentation rep, OIDCIdentityProviderConfig config) {
+ protected static void sendJwksRequest(KeycloakSession session, OIDCConfigurationRepresentation rep, OIDCIdentityProviderConfig config) {
try {
- JSONWebKeySet keySet = JWKSUtils.sendJwksRequest(rep.getJwksUri());
+ JSONWebKeySet keySet = JWKSUtils.sendJwksRequest(session, rep.getJwksUri());
PublicKey key = JWKSUtils.getKeyForUse(keySet, JWK.Use.SIG);
if (key == null) {
logger.supportedJwkNotFound(JWK.Use.SIG.asString());
diff --git a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderFactory.java b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderFactory.java
index dac6750..2617995 100755
--- a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderFactory.java
+++ b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderFactory.java
@@ -24,6 +24,7 @@ import org.keycloak.dom.saml.v2.metadata.IDPSSODescriptorType;
import org.keycloak.dom.saml.v2.metadata.KeyDescriptorType;
import org.keycloak.dom.saml.v2.metadata.KeyTypes;
import org.keycloak.models.IdentityProviderModel;
+import org.keycloak.models.KeycloakSession;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import org.keycloak.saml.common.exceptions.ParsingException;
import org.keycloak.saml.common.util.DocumentUtil;
@@ -54,7 +55,7 @@ public class SAMLIdentityProviderFactory extends AbstractIdentityProviderFactory
}
@Override
- public Map<String, String> parseConfig(InputStream inputStream) {
+ public Map<String, String> parseConfig(KeycloakSession session, InputStream inputStream) {
try {
Object parsedObject = new SAMLParser().parse(inputStream);
EntityDescriptorType entityType;
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
index 931b04c..1b4db29 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
@@ -17,11 +17,6 @@
package org.keycloak.protocol.oidc.endpoints;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-
import javax.ws.rs.GET;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
@@ -42,6 +37,8 @@ import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.RealmModel;
import org.keycloak.protocol.AuthorizationEndpointBase;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
+import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequest;
+import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequestParserProcessor;
import org.keycloak.protocol.oidc.utils.OIDCRedirectUriBuilder;
import org.keycloak.protocol.oidc.utils.OIDCResponseMode;
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
@@ -67,43 +64,10 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
/**
* Prefix used to store additional HTTP GET params from original client request into {@link ClientSessionModel} note to be available later in Authenticators, RequiredActions etc. Prefix is used to
* prevent collisions with internally used notes.
- *
+ *
* @see ClientSessionModel#getNote(String)
- * @see #KNOWN_REQ_PARAMS
- * @see #additionalReqParams
*/
public static final String CLIENT_SESSION_NOTE_ADDITIONAL_REQ_PARAMS_PREFIX = "client_request_param_";
- /**
- * Max number of additional req params copied into client session note to prevent DoS attacks
- *
- * @see #additionalReqParams
- */
- public static final int ADDITIONAL_REQ_PARAMS_MAX_MUMBER = 5;
- /**
- * Max size of additional req param value copied into client session note to prevent DoS attacks - params with longer value are ignored
- *
- * @see #additionalReqParams
- */
- public static final int ADDITIONAL_REQ_PARAMS_MAX_SIZE = 200;
-
- /** Set of known protocol GET params not to be stored into {@link #additionalReqParams} */
- private static final Set<String> KNOWN_REQ_PARAMS = new HashSet<>();
- static {
- KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.CLIENT_ID_PARAM);
- KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.RESPONSE_TYPE_PARAM);
- KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.RESPONSE_MODE_PARAM);
- KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.REDIRECT_URI_PARAM);
- KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.STATE_PARAM);
- KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.SCOPE_PARAM);
- KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.LOGIN_HINT_PARAM);
- KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.PROMPT_PARAM);
- KNOWN_REQ_PARAMS.add(AdapterConstants.KC_IDP_HINT);
- KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.NONCE_PARAM);
- KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.MAX_AGE_PARAM);
- KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.UI_LOCALES_PARAM);
- KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.REQUEST_PARAM);
- KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.REQUEST_URI_PARAM);
- }
private enum Action {
REGISTER, CODE, FORGOT_CREDENTIALS
@@ -116,19 +80,8 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
private OIDCResponseType parsedResponseType;
private OIDCResponseMode parsedResponseMode;
- private String clientId;
+ private AuthorizationEndpointRequest request;
private String redirectUri;
- private String redirectUriParam;
- private String responseType;
- private String responseMode;
- private String state;
- private String scope;
- private String loginHint;
- private String prompt;
- private String nonce;
- private String maxAge;
- private String idpHint;
- protected Map<String, String> additionalReqParams = new HashMap<>();
public AuthorizationEndpoint(RealmModel realm, EventBuilder event) {
super(realm, event);
@@ -139,34 +92,25 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
public Response build() {
MultivaluedMap<String, String> params = uriInfo.getQueryParameters();
- clientId = params.getFirst(OIDCLoginProtocol.CLIENT_ID_PARAM);
- responseType = params.getFirst(OIDCLoginProtocol.RESPONSE_TYPE_PARAM);
- responseMode = params.getFirst(OIDCLoginProtocol.RESPONSE_MODE_PARAM);
- redirectUriParam = params.getFirst(OIDCLoginProtocol.REDIRECT_URI_PARAM);
- state = params.getFirst(OIDCLoginProtocol.STATE_PARAM);
- scope = params.getFirst(OIDCLoginProtocol.SCOPE_PARAM);
- loginHint = params.getFirst(OIDCLoginProtocol.LOGIN_HINT_PARAM);
- prompt = params.getFirst(OIDCLoginProtocol.PROMPT_PARAM);
- idpHint = params.getFirst(AdapterConstants.KC_IDP_HINT);
- nonce = params.getFirst(OIDCLoginProtocol.NONCE_PARAM);
- maxAge = params.getFirst(OIDCLoginProtocol.MAX_AGE_PARAM);
-
- extractAdditionalReqParams(params);
+ String clientId = params.getFirst(OIDCLoginProtocol.CLIENT_ID_PARAM);
checkSsl();
checkRealm();
- checkClient();
+ checkClient(clientId);
+
+ request = AuthorizationEndpointRequestParserProcessor.parseRequest(event, session, client, params);
+
checkRedirectUri();
Response errorResponse = checkResponseType();
if (errorResponse != null) {
return errorResponse;
}
- if (!TokenUtil.isOIDCRequest(scope)) {
+ if (!TokenUtil.isOIDCRequest(request.getScope())) {
logger.oidcScopeMissing();
}
- errorResponse = checkOIDCParams(params);
+ errorResponse = checkOIDCParams();
if (errorResponse != null) {
return errorResponse;
}
@@ -186,27 +130,6 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
throw new RuntimeException("Unknown action " + action);
}
- protected void extractAdditionalReqParams(MultivaluedMap<String, String> params) {
- for (String paramName : params.keySet()) {
- if (!KNOWN_REQ_PARAMS.contains(paramName)) {
- String value = params.getFirst(paramName);
- if (value != null && value.trim().isEmpty()) {
- value = null;
- }
- if (value != null && value.length() <= ADDITIONAL_REQ_PARAMS_MAX_SIZE) {
- if (additionalReqParams.size() >= ADDITIONAL_REQ_PARAMS_MAX_MUMBER) {
- logger.debug("Maximal number of additional OIDC params (" + ADDITIONAL_REQ_PARAMS_MAX_MUMBER + ") exceeded, ignoring rest of them!");
- break;
- }
- additionalReqParams.put(paramName, value);
- } else {
- logger.debug("OIDC Additional param " + paramName + " ignored because value is empty or longer than " + ADDITIONAL_REQ_PARAMS_MAX_SIZE);
- }
- }
-
- }
- }
-
public AuthorizationEndpoint register() {
event.event(EventType.REGISTER);
action = Action.REGISTER;
@@ -243,7 +166,7 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
}
}
- private void checkClient() {
+ private void checkClient(String clientId) {
if (clientId == null) {
event.error(Errors.INVALID_REQUEST);
throw new ErrorPageException(session, Messages.MISSING_PARAMETER, OIDCLoginProtocol.CLIENT_ID_PARAM);
@@ -271,6 +194,8 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
}
private Response checkResponseType() {
+ String responseType = request.getResponseType();
+
if (responseType == null) {
logger.missingParameter(OAuth2Constants.RESPONSE_TYPE);
event.error(Errors.INVALID_REQUEST);
@@ -292,7 +217,7 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
OIDCResponseMode parsedResponseMode = null;
try {
- parsedResponseMode = OIDCResponseMode.parse(responseMode, parsedResponseType);
+ parsedResponseMode = OIDCResponseMode.parse(request.getResponseMode(), parsedResponseType);
} catch (IllegalArgumentException iae) {
logger.invalidParameter(OIDCLoginProtocol.RESPONSE_MODE_PARAM);
event.error(Errors.INVALID_REQUEST);
@@ -325,20 +250,8 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
return null;
}
- private Response checkOIDCParams(MultivaluedMap<String, String> params) {
- if (params.getFirst(OIDCLoginProtocol.REQUEST_PARAM) != null) {
- logger.unsupportedParameter(OIDCLoginProtocol.REQUEST_PARAM);
- event.error(Errors.INVALID_REQUEST);
- return redirectErrorToClient(parsedResponseMode, OAuthErrorException.REQUEST_NOT_SUPPORTED, null);
- }
-
- if (params.getFirst(OIDCLoginProtocol.REQUEST_URI_PARAM) != null) {
- logger.unsupportedParameter(OIDCLoginProtocol.REQUEST_URI_PARAM);
- event.error(Errors.INVALID_REQUEST);
- return redirectErrorToClient(parsedResponseMode, OAuthErrorException.REQUEST_URI_NOT_SUPPORTED, null);
- }
-
- if (parsedResponseType.isImplicitOrHybridFlow() && nonce == null) {
+ private Response checkOIDCParams() {
+ if (parsedResponseType.isImplicitOrHybridFlow() && request.getNonce() == null) {
logger.missingParameter(OIDCLoginProtocol.NONCE_PARAM);
event.error(Errors.INVALID_REQUEST);
return redirectErrorToClient(parsedResponseMode, OAuthErrorException.INVALID_REQUEST, "Missing parameter: nonce");
@@ -355,14 +268,16 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
errorResponseBuilder.addParam(OAuth2Constants.ERROR_DESCRIPTION, errorDescription);
}
- if (state != null) {
- errorResponseBuilder.addParam(OAuth2Constants.STATE, state);
+ if (request.getState() != null) {
+ errorResponseBuilder.addParam(OAuth2Constants.STATE, request.getState());
}
return errorResponseBuilder.build();
}
private void checkRedirectUri() {
+ String redirectUriParam = request.getRedirectUriParam();
+
event.detail(Details.REDIRECT_URI, redirectUriParam);
redirectUri = RedirectUtils.verifyRedirectUri(uriInfo, redirectUriParam, realm, client);
@@ -377,27 +292,28 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
clientSession.setAuthMethod(OIDCLoginProtocol.LOGIN_PROTOCOL);
clientSession.setRedirectUri(redirectUri);
clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name());
- clientSession.setNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM, responseType);
- clientSession.setNote(OIDCLoginProtocol.REDIRECT_URI_PARAM, redirectUriParam);
+ clientSession.setNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM, request.getResponseType());
+ clientSession.setNote(OIDCLoginProtocol.REDIRECT_URI_PARAM, request.getRedirectUriParam());
clientSession.setNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
- if (state != null) clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, state);
- if (nonce != null) clientSession.setNote(OIDCLoginProtocol.NONCE_PARAM, nonce);
- if (maxAge != null) clientSession.setNote(OIDCLoginProtocol.MAX_AGE_PARAM, maxAge);
- if (scope != null) clientSession.setNote(OIDCLoginProtocol.SCOPE_PARAM, scope);
- if (loginHint != null) clientSession.setNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, loginHint);
- if (prompt != null) clientSession.setNote(OIDCLoginProtocol.PROMPT_PARAM, prompt);
- if (idpHint != null) clientSession.setNote(AdapterConstants.KC_IDP_HINT, idpHint);
- if (responseMode != null) clientSession.setNote(OIDCLoginProtocol.RESPONSE_MODE_PARAM, responseMode);
-
- if (additionalReqParams != null) {
- for (String paramName : additionalReqParams.keySet()) {
- clientSession.setNote(CLIENT_SESSION_NOTE_ADDITIONAL_REQ_PARAMS_PREFIX + paramName, additionalReqParams.get(paramName));
+ if (request.getState() != null) clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, request.getState());
+ if (request.getNonce() != null) clientSession.setNote(OIDCLoginProtocol.NONCE_PARAM, request.getNonce());
+ if (request.getMaxAge() != null) clientSession.setNote(OIDCLoginProtocol.MAX_AGE_PARAM, String.valueOf(request.getMaxAge()));
+ if (request.getScope() != null) clientSession.setNote(OIDCLoginProtocol.SCOPE_PARAM, request.getScope());
+ if (request.getLoginHint() != null) clientSession.setNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, request.getLoginHint());
+ if (request.getPrompt() != null) clientSession.setNote(OIDCLoginProtocol.PROMPT_PARAM, request.getPrompt());
+ if (request.getIdpHint() != null) clientSession.setNote(AdapterConstants.KC_IDP_HINT, request.getIdpHint());
+ if (request.getResponseMode() != null) clientSession.setNote(OIDCLoginProtocol.RESPONSE_MODE_PARAM, request.getResponseMode());
+
+ if (request.getAdditionalReqParams() != null) {
+ for (String paramName : request.getAdditionalReqParams().keySet()) {
+ clientSession.setNote(CLIENT_SESSION_NOTE_ADDITIONAL_REQ_PARAMS_PREFIX + paramName, request.getAdditionalReqParams().get(paramName));
}
}
}
private Response buildAuthorizationCodeAuthorizationResponse() {
+ String idpHint = request.getIdpHint();
if (idpHint != null && !"".equals(idpHint)) {
IdentityProviderModel identityProviderModel = realm.getIdentityProviderByAlias(idpHint);
@@ -413,7 +329,7 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
this.event.event(EventType.LOGIN);
clientSession.setNote(Details.AUTH_TYPE, CODE_AUTH_TYPE);
- return handleBrowserAuthenticationRequest(clientSession, new OIDCLoginProtocol(session, realm, uriInfo, headers, event), TokenUtil.hasPrompt(prompt, OIDCLoginProtocol.PROMPT_VALUE_NONE), false);
+ return handleBrowserAuthenticationRequest(clientSession, new OIDCLoginProtocol(session, realm, uriInfo, headers, event), TokenUtil.hasPrompt(request.getPrompt(), OIDCLoginProtocol.PROMPT_VALUE_NONE), false);
}
private Response buildRegister() {
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequest.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequest.java
new file mode 100644
index 0000000..998a58c
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequest.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.protocol.oidc.endpoints.request;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class AuthorizationEndpointRequest {
+
+ String clientId;
+ String redirectUriParam;
+ String responseType;
+ String responseMode;
+ String state;
+ String scope;
+ String loginHint;
+ String prompt;
+ String nonce;
+ Integer maxAge;
+ String idpHint;
+ Map<String, String> additionalReqParams = new HashMap<>();
+
+ public String getClientId() {
+ return clientId;
+ }
+
+ public String getRedirectUriParam() {
+ return redirectUriParam;
+ }
+
+ public String getResponseType() {
+ return responseType;
+ }
+
+ public String getResponseMode() {
+ return responseMode;
+ }
+
+ public String getState() {
+ return state;
+ }
+
+ public String getScope() {
+ return scope;
+ }
+
+ public String getLoginHint() {
+ return loginHint;
+ }
+
+ public String getPrompt() {
+ return prompt;
+ }
+
+ public String getNonce() {
+ return nonce;
+ }
+
+ public Integer getMaxAge() {
+ return maxAge;
+ }
+
+ public String getIdpHint() {
+ return idpHint;
+ }
+
+ public Map<String, String> getAdditionalReqParams() {
+ return additionalReqParams;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequestParserProcessor.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequestParserProcessor.java
new file mode 100644
index 0000000..8db219c
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequestParserProcessor.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.protocol.oidc.endpoints.request;
+
+import java.io.InputStream;
+import java.util.Map;
+
+import javax.ws.rs.core.MultivaluedMap;
+
+import org.keycloak.common.util.StreamUtil;
+import org.keycloak.connections.httpclient.HttpClientProvider;
+import org.keycloak.events.Errors;
+import org.keycloak.events.EventBuilder;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
+import org.keycloak.services.ErrorPageException;
+import org.keycloak.services.ServicesLogger;
+import org.keycloak.services.messages.Messages;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class AuthorizationEndpointRequestParserProcessor {
+
+ private static final ServicesLogger logger = ServicesLogger.ROOT_LOGGER;
+
+ public static AuthorizationEndpointRequest parseRequest(EventBuilder event, KeycloakSession session, ClientModel client, MultivaluedMap<String, String> requestParams) {
+ try {
+ AuthorizationEndpointRequest request = new AuthorizationEndpointRequest();
+
+ new AuthzEndpointQueryStringParser(requestParams).parseRequest(request);
+
+ String requestParam = requestParams.getFirst(OIDCLoginProtocol.REQUEST_PARAM);
+ String requestUriParam = requestParams.getFirst(OIDCLoginProtocol.REQUEST_URI_PARAM);
+
+ if (requestParam != null && requestUriParam != null) {
+ throw new RuntimeException("Illegal to use both 'request' and 'request_uri' parameters together");
+ }
+
+ if (requestParam != null) {
+ new AuthzEndpointRequestObjectParser(requestParam, client).parseRequest(request);
+ } else if (requestUriParam != null) {
+ InputStream is = session.getProvider(HttpClientProvider.class).get(requestUriParam);
+ String retrievedRequest = StreamUtil.readString(is);
+
+ new AuthzEndpointRequestObjectParser(retrievedRequest, client).parseRequest(request);
+ }
+
+ return request;
+
+ } catch (Exception e) {
+ logger.invalidRequest(e);
+ event.error(Errors.INVALID_REQUEST);
+ throw new ErrorPageException(session, Messages.INVALID_REQUEST);
+ }
+ }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointQueryStringParser.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointQueryStringParser.java
new file mode 100644
index 0000000..8384fdc
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointQueryStringParser.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.protocol.oidc.endpoints.request;
+
+import java.util.Set;
+
+import javax.ws.rs.core.MultivaluedMap;
+
+/**
+ * Parse the parameters from request queryString
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+class AuthzEndpointQueryStringParser extends AuthzEndpointRequestParser {
+
+ private final MultivaluedMap<String, String> requestParams;
+
+ public AuthzEndpointQueryStringParser(MultivaluedMap<String, String> requestParams) {
+ this.requestParams = requestParams;
+ }
+
+ @Override
+ protected String getParameter(String paramName) {
+ return requestParams.getFirst(paramName);
+ }
+
+ @Override
+ protected Integer getIntParameter(String paramName) {
+ String paramVal = requestParams.getFirst(paramName);
+ return paramVal==null ? null : Integer.parseInt(paramVal);
+ }
+
+ @Override
+ protected Set<String> keySet() {
+ return requestParams.keySet();
+ }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestObjectParser.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestObjectParser.java
new file mode 100644
index 0000000..658a2c0
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestObjectParser.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.protocol.oidc.endpoints.request;
+
+import java.security.PublicKey;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator;
+import org.keycloak.jose.jws.Algorithm;
+import org.keycloak.jose.jws.JWSHeader;
+import org.keycloak.jose.jws.JWSInput;
+import org.keycloak.jose.jws.crypto.RSAProvider;
+import org.keycloak.models.ClientModel;
+import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
+import org.keycloak.services.util.CertificateInfoHelper;
+import org.keycloak.util.JsonSerialization;
+
+/**
+ * Parse the parameters from OIDC "request" object
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+class AuthzEndpointRequestObjectParser extends AuthzEndpointRequestParser {
+
+ private final Map<String, Object> requestParams;
+
+ public AuthzEndpointRequestObjectParser(String requestObject, ClientModel client) throws Exception {
+ JWSInput input = new JWSInput(requestObject);
+ JWSHeader header = input.getHeader();
+
+ Algorithm requestedSignatureAlgorithm = OIDCAdvancedConfigWrapper.fromClientModel(client).getRequestObjectSignatureAlg();
+
+ if (requestedSignatureAlgorithm != null && requestedSignatureAlgorithm != header.getAlgorithm()) {
+ throw new RuntimeException("Request object signed with different algorithm than client requested algorithm");
+ }
+
+ if (header.getAlgorithm() == Algorithm.none) {
+ this.requestParams = JsonSerialization.readValue(input.getContent(), TypedHashMap.class);
+ } else if (header.getAlgorithm() == Algorithm.RS256) {
+ PublicKey clientPublicKey = CertificateInfoHelper.getSignatureValidationKey(client, JWTClientAuthenticator.ATTR_PREFIX);
+ boolean verified = RSAProvider.verify(input, clientPublicKey);
+ if (!verified) {
+ throw new RuntimeException("Failed to verify signature on 'request' object");
+ }
+
+ this.requestParams = JsonSerialization.readValue(input.getContent(), TypedHashMap.class);
+ } else {
+ throw new RuntimeException("Unsupported JWA algorithm used for signed request");
+ }
+ }
+
+ @Override
+ protected String getParameter(String paramName) {
+ Object val = this.requestParams.get(paramName);
+ return val==null ? null : val.toString();
+ }
+
+ @Override
+ protected Integer getIntParameter(String paramName) {
+ Object val = this.requestParams.get(paramName);
+ return val==null ? null : Integer.parseInt(getParameter(paramName));
+ }
+
+ @Override
+ protected Set<String> keySet() {
+ return requestParams.keySet();
+ }
+
+ static class TypedHashMap extends HashMap<String, Object> {
+ }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestParser.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestParser.java
new file mode 100644
index 0000000..e322d4b
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestParser.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.protocol.oidc.endpoints.request;
+
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.keycloak.constants.AdapterConstants;
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
+import org.keycloak.services.ServicesLogger;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+abstract class AuthzEndpointRequestParser {
+
+ private static final ServicesLogger logger = ServicesLogger.ROOT_LOGGER;
+
+ /**
+ * Max number of additional req params copied into client session note to prevent DoS attacks
+ *
+ */
+ public static final int ADDITIONAL_REQ_PARAMS_MAX_MUMBER = 5;
+
+ /**
+ * Max size of additional req param value copied into client session note to prevent DoS attacks - params with longer value are ignored
+ *
+ */
+ public static final int ADDITIONAL_REQ_PARAMS_MAX_SIZE = 200;
+
+ /** Set of known protocol GET params not to be stored into additionalReqParams} */
+ private static final Set<String> KNOWN_REQ_PARAMS = new HashSet<>();
+ static {
+ KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.CLIENT_ID_PARAM);
+ KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.RESPONSE_TYPE_PARAM);
+ KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.RESPONSE_MODE_PARAM);
+ KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.REDIRECT_URI_PARAM);
+ KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.STATE_PARAM);
+ KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.SCOPE_PARAM);
+ KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.LOGIN_HINT_PARAM);
+ KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.PROMPT_PARAM);
+ KNOWN_REQ_PARAMS.add(AdapterConstants.KC_IDP_HINT);
+ KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.NONCE_PARAM);
+ KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.MAX_AGE_PARAM);
+ KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.UI_LOCALES_PARAM);
+ KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.REQUEST_PARAM);
+ KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.REQUEST_URI_PARAM);
+ }
+
+
+ public void parseRequest(AuthorizationEndpointRequest request) {
+ String clientId = getParameter(OIDCLoginProtocol.CLIENT_ID_PARAM);
+
+ if (request.clientId != null && !request.clientId.equals(clientId)) {
+ throw new IllegalArgumentException("The client_id parameter doesn't match the one from OIDC 'request' or 'request_uri'");
+ }
+
+ request.clientId = clientId;
+ request.responseType = replaceIfNotNull(request.responseType, getParameter(OIDCLoginProtocol.RESPONSE_TYPE_PARAM));
+ request.responseMode = replaceIfNotNull(request.responseMode, getParameter(OIDCLoginProtocol.RESPONSE_MODE_PARAM));
+ request.redirectUriParam = replaceIfNotNull(request.redirectUriParam, getParameter(OIDCLoginProtocol.REDIRECT_URI_PARAM));
+ request.state = replaceIfNotNull(request.state, getParameter(OIDCLoginProtocol.STATE_PARAM));
+ request.scope = replaceIfNotNull(request.scope, getParameter(OIDCLoginProtocol.SCOPE_PARAM));
+ request.loginHint = replaceIfNotNull(request.loginHint, getParameter(OIDCLoginProtocol.LOGIN_HINT_PARAM));
+ request.prompt = replaceIfNotNull(request.prompt, getParameter(OIDCLoginProtocol.PROMPT_PARAM));
+ request.idpHint = replaceIfNotNull(request.idpHint, getParameter(AdapterConstants.KC_IDP_HINT));
+ request.nonce = replaceIfNotNull(request.nonce, getParameter(OIDCLoginProtocol.NONCE_PARAM));
+ request.maxAge = replaceIfNotNull(request.maxAge, getIntParameter(OIDCLoginProtocol.MAX_AGE_PARAM));
+
+ extractAdditionalReqParams(request.additionalReqParams);
+ }
+
+
+ protected void extractAdditionalReqParams(Map<String, String> additionalReqParams) {
+ for (String paramName : keySet()) {
+ if (!KNOWN_REQ_PARAMS.contains(paramName)) {
+ String value = getParameter(paramName);
+ if (value != null && value.trim().isEmpty()) {
+ value = null;
+ }
+ if (value != null && value.length() <= ADDITIONAL_REQ_PARAMS_MAX_SIZE) {
+ if (additionalReqParams.size() >= ADDITIONAL_REQ_PARAMS_MAX_MUMBER) {
+ logger.debug("Maximal number of additional OIDC params (" + ADDITIONAL_REQ_PARAMS_MAX_MUMBER + ") exceeded, ignoring rest of them!");
+ break;
+ }
+ additionalReqParams.put(paramName, value);
+ } else {
+ logger.debug("OIDC Additional param " + paramName + " ignored because value is empty or longer than " + ADDITIONAL_REQ_PARAMS_MAX_SIZE);
+ }
+ }
+
+ }
+ }
+
+ protected <T> T replaceIfNotNull(T previousVal, T newVal) {
+ return newVal==null ? previousVal : newVal;
+ }
+
+
+ protected abstract String getParameter(String paramName);
+
+ protected abstract Integer getIntParameter(String paramName);
+
+ protected abstract Set<String> keySet();
+
+}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCAdvancedConfigWrapper.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCAdvancedConfigWrapper.java
index 37fe204..3233690 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCAdvancedConfigWrapper.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCAdvancedConfigWrapper.java
@@ -30,6 +30,8 @@ public class OIDCAdvancedConfigWrapper {
private static final String USER_INFO_RESPONSE_SIGNATURE_ALG = "user.info.response.signature.alg";
+ private static final String REQUEST_OBJECT_SIGNATURE_ALG = "request.object.signature.alg";
+
private final ClientModel clientModel;
private final ClientRepresentation clientRep;
@@ -62,6 +64,16 @@ public class OIDCAdvancedConfigWrapper {
return getUserInfoSignedResponseAlg() != null;
}
+ public Algorithm getRequestObjectSignatureAlg() {
+ String alg = getAttribute(REQUEST_OBJECT_SIGNATURE_ALG);
+ return alg==null ? null : Enum.valueOf(Algorithm.class, alg);
+ }
+
+ public void setRequestObjectSignatureAlg(Algorithm alg) {
+ String algStr = alg==null ? null : alg.toString();
+ setAttribute(REQUEST_OBJECT_SIGNATURE_ALG, algStr);
+ }
+
private String getAttribute(String attrKey) {
if (clientModel != null) {
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java
index 3c4c3aa..c263405 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java
@@ -66,6 +66,7 @@ public class OIDCLoginProtocol implements LoginProtocol {
public static final String REQUEST_PARAM = "request";
public static final String REQUEST_URI_PARAM = "request_uri";
public static final String UI_LOCALES_PARAM = OAuth2Constants.UI_LOCALES_PARAM;
+ public static final String CLAIMS_PARAM = "claims";
public static final String LOGOUT_REDIRECT_URI = "OIDC_LOGOUT_REDIRECT_URI";
public static final String ISSUER = "iss";
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java
index dfca144..800630c 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java
@@ -50,6 +50,8 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
public static final List<String> DEFAULT_USER_INFO_SIGNING_ALG_VALUES_SUPPORTED = list(Algorithm.RS256.toString());
+ public static final List<String> DEFAULT_REQUEST_OBJECT_SIGNING_ALG_VALUES_SUPPORTED = list(Algorithm.none.toString(), Algorithm.RS256.toString());
+
public static final List<String> DEFAULT_GRANT_TYPES_SUPPORTED = list(OAuth2Constants.AUTHORIZATION_CODE, OAuth2Constants.IMPLICIT, OAuth2Constants.REFRESH_TOKEN, OAuth2Constants.PASSWORD, OAuth2Constants.CLIENT_CREDENTIALS);
public static final List<String> DEFAULT_RESPONSE_TYPES_SUPPORTED = list(OAuth2Constants.CODE, OIDCResponseType.NONE, OIDCResponseType.ID_TOKEN, OIDCResponseType.TOKEN, "id_token token", "code id_token", "code token", "code id_token token");
@@ -93,6 +95,7 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
config.setIdTokenSigningAlgValuesSupported(DEFAULT_ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED);
config.setUserInfoSigningAlgValuesSupported(DEFAULT_USER_INFO_SIGNING_ALG_VALUES_SUPPORTED);
+ config.setRequestObjectSigningAlgValuesSupported(DEFAULT_REQUEST_OBJECT_SIGNING_ALG_VALUES_SUPPORTED);
config.setResponseTypesSupported(DEFAULT_RESPONSE_TYPES_SUPPORTED);
config.setSubjectTypesSupported(DEFAULT_SUBJECT_TYPES_SUPPORTED);
config.setResponseModesSupported(DEFAULT_RESPONSE_MODES_SUPPORTED);
@@ -107,8 +110,8 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
config.setScopesSupported(SCOPES_SUPPORTED);
- config.setRequestParameterSupported(false);
- config.setRequestUriParameterSupported(false);
+ config.setRequestParameterSupported(true);
+ config.setRequestUriParameterSupported(true);
return config;
}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java b/services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java
index ee5241b..181e0d2 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java
@@ -67,6 +67,9 @@ public class OIDCConfigurationRepresentation {
@JsonProperty("userinfo_signing_alg_values_supported")
private List<String> userInfoSigningAlgValuesSupported;
+ @JsonProperty("request_object_signing_alg_values_supported")
+ private List<String> requestObjectSigningAlgValuesSupported;
+
@JsonProperty("response_modes_supported")
private List<String> responseModesSupported;
@@ -195,6 +198,14 @@ public class OIDCConfigurationRepresentation {
this.userInfoSigningAlgValuesSupported = userInfoSigningAlgValuesSupported;
}
+ public List<String> getRequestObjectSigningAlgValuesSupported() {
+ return requestObjectSigningAlgValuesSupported;
+ }
+
+ public void setRequestObjectSigningAlgValuesSupported(List<String> requestObjectSigningAlgValuesSupported) {
+ this.requestObjectSigningAlgValuesSupported = requestObjectSigningAlgValuesSupported;
+ }
+
public List<String> getResponseModesSupported() {
return responseModesSupported;
}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/utils/JWKSUtils.java b/services/src/main/java/org/keycloak/protocol/oidc/utils/JWKSUtils.java
index c856a81..d8f7fe7 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/utils/JWKSUtils.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/utils/JWKSUtils.java
@@ -18,12 +18,15 @@
package org.keycloak.protocol.oidc.utils;
import java.io.IOException;
+import java.io.InputStream;
import java.security.PublicKey;
-import org.keycloak.broker.provider.util.SimpleHttp;
+import org.keycloak.common.util.StreamUtil;
+import org.keycloak.connections.httpclient.HttpClientProvider;
import org.keycloak.jose.jwk.JSONWebKeySet;
import org.keycloak.jose.jwk.JWK;
import org.keycloak.jose.jwk.JWKParser;
+import org.keycloak.models.KeycloakSession;
import org.keycloak.util.JsonSerialization;
/**
@@ -31,8 +34,9 @@ import org.keycloak.util.JsonSerialization;
*/
public class JWKSUtils {
- public static JSONWebKeySet sendJwksRequest(String jwksURI) throws IOException {
- String keySetString = SimpleHttp.doGet(jwksURI).asString();
+ public static JSONWebKeySet sendJwksRequest(KeycloakSession session, String jwksURI) throws IOException {
+ InputStream is = session.getProvider(HttpClientProvider.class).get(jwksURI);
+ String keySetString = StreamUtil.readString(is);
return JsonSerialization.readValue(keySetString, JSONWebKeySet.class);
}
diff --git a/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java b/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java
index a95cc09..e1939ef 100644
--- a/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java
+++ b/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java
@@ -89,14 +89,12 @@ public class DescriptionConverter {
}
client.setClientAuthenticatorType(clientAuthFactory.getId());
- // Externalize to ClientAuthenticator itself?
- if (authMethod != null && authMethod.equals(OIDCLoginProtocol.PRIVATE_KEY_JWT)) {
-
- PublicKey publicKey = retrievePublicKey(clientOIDC);
- if (publicKey == null) {
- throw new ClientRegistrationException("Didn't find key of supported keyType for use " + JWK.Use.SIG.asString());
- }
+ PublicKey publicKey = retrievePublicKey(session, clientOIDC);
+ if (authMethod != null && authMethod.equals(OIDCLoginProtocol.PRIVATE_KEY_JWT) && publicKey == null) {
+ throw new ClientRegistrationException("Didn't find key of supported keyType for use " + JWK.Use.SIG.asString());
+ }
+ if (publicKey != null) {
String publicKeyPem = KeycloakModelUtils.getPemFromKey(publicKey);
CertificateRepresentation rep = new CertificateRepresentation();
@@ -104,20 +102,24 @@ public class DescriptionConverter {
CertificateInfoHelper.updateClientRepresentationCertificateInfo(client, rep, JWTClientAuthenticator.ATTR_PREFIX);
}
+ OIDCAdvancedConfigWrapper configWrapper = OIDCAdvancedConfigWrapper.fromClientRepresentation(client);
if (clientOIDC.getUserinfoSignedResponseAlg() != null) {
- String userInfoSignedResponseAlg = clientOIDC.getUserinfoSignedResponseAlg();
- Algorithm algorithm = Enum.valueOf(Algorithm.class, userInfoSignedResponseAlg);
+ Algorithm algorithm = Enum.valueOf(Algorithm.class, clientOIDC.getUserinfoSignedResponseAlg());
+ configWrapper.setUserInfoSignedResponseAlg(algorithm);
+ }
- OIDCAdvancedConfigWrapper.fromClientRepresentation(client).setUserInfoSignedResponseAlg(algorithm);
+ if (clientOIDC.getRequestObjectSigningAlg() != null) {
+ Algorithm algorithm = Enum.valueOf(Algorithm.class, clientOIDC.getRequestObjectSigningAlg());
+ configWrapper.setRequestObjectSignatureAlg(algorithm);
}
return client;
}
- private static PublicKey retrievePublicKey(OIDCClientRepresentation clientOIDC) {
+ private static PublicKey retrievePublicKey(KeycloakSession session, OIDCClientRepresentation clientOIDC) {
if (clientOIDC.getJwksUri() == null && clientOIDC.getJwks() == null) {
- throw new ClientRegistrationException("Requested client authentication method '%s' but jwks_uri nor jwks were available in config");
+ return null;
}
if (clientOIDC.getJwksUri() != null && clientOIDC.getJwks() != null) {
@@ -129,7 +131,7 @@ public class DescriptionConverter {
keySet = clientOIDC.getJwks();
} else {
try {
- keySet = JWKSUtils.sendJwksRequest(clientOIDC.getJwksUri());
+ keySet = JWKSUtils.sendJwksRequest(session, clientOIDC.getJwksUri());
} catch (IOException ioe) {
throw new ClientRegistrationException("Failed to send JWKS request to specified jwks_uri", ioe);
}
@@ -166,6 +168,9 @@ public class DescriptionConverter {
if (config.isUserInfoSignatureRequired()) {
response.setUserinfoSignedResponseAlg(config.getUserInfoSignedResponseAlg().toString());
}
+ if (config.getRequestObjectSignatureAlg() != null) {
+ response.setRequestObjectSigningAlg(config.getRequestObjectSignatureAlg().toString());
+ }
return response;
}
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/IdentityProvidersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/IdentityProvidersResource.java
index 958a849..8e7c9ac 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/IdentityProvidersResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/IdentityProvidersResource.java
@@ -116,7 +116,7 @@ public class IdentityProvidersResource {
InputPart file = formDataMap.get("file").get(0);
InputStream inputStream = file.getBody(InputStream.class, null);
IdentityProviderFactory providerFactory = getProviderFactorytById(providerId);
- Map<String, String> config = providerFactory.parseConfig(inputStream);
+ Map<String, String> config = providerFactory.parseConfig(session, inputStream);
return config;
}
@@ -143,7 +143,7 @@ public class IdentityProvidersResource {
try {
IdentityProviderFactory providerFactory = getProviderFactorytById(providerId);
Map<String, String> config;
- config = providerFactory.parseConfig(inputStream);
+ config = providerFactory.parseConfig(session, inputStream);
return config;
} finally {
try {
diff --git a/services/src/main/java/org/keycloak/services/ServicesLogger.java b/services/src/main/java/org/keycloak/services/ServicesLogger.java
index de7eef7..af6c4c1 100644
--- a/services/src/main/java/org/keycloak/services/ServicesLogger.java
+++ b/services/src/main/java/org/keycloak/services/ServicesLogger.java
@@ -430,4 +430,8 @@ public interface ServicesLogger extends BasicLogger {
@Message(id=96, value="Not found JWK of supported keyType under jwks_uri for usage: %s")
void supportedJwkNotFound(String usage);
+ @LogMessage(level = WARN)
+ @Message(id=97, value="Invalid request")
+ void invalidRequest(@Cause Throwable t);
+
}
diff --git a/services/src/main/java/org/keycloak/services/util/CertificateInfoHelper.java b/services/src/main/java/org/keycloak/services/util/CertificateInfoHelper.java
index b309927..359d28d 100644
--- a/services/src/main/java/org/keycloak/services/util/CertificateInfoHelper.java
+++ b/services/src/main/java/org/keycloak/services/util/CertificateInfoHelper.java
@@ -17,9 +17,18 @@
package org.keycloak.services.util;
+import java.security.PublicKey;
+import java.security.cert.X509Certificate;
import java.util.HashMap;
+import javax.ws.rs.core.Response;
+
+import org.keycloak.authentication.AuthenticationFlowError;
+import org.keycloak.authentication.ClientAuthenticationFlowContext;
+import org.keycloak.authentication.authenticators.client.ClientAuthUtil;
import org.keycloak.models.ClientModel;
+import org.keycloak.models.ModelException;
+import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.representations.idm.CertificateRepresentation;
import org.keycloak.representations.idm.ClientRepresentation;
@@ -34,6 +43,8 @@ public class CertificateInfoHelper {
public static final String PUBLIC_KEY = "public.key";
+ // CLIENT MODEL METHODS
+
public static CertificateRepresentation getCertificateFromClient(ClientModel client, String attributePrefix) {
String privateKeyAttribute = attributePrefix + "." + PRIVATE_KEY;
String certificateAttribute = attributePrefix + "." + X509CERTIFICATE;
@@ -75,6 +86,32 @@ public class CertificateInfoHelper {
}
+ public static PublicKey getSignatureValidationKey(ClientModel client, String attributePrefix) throws ModelException {
+ CertificateRepresentation certInfo = getCertificateFromClient(client, attributePrefix);
+
+ String encodedCertificate = certInfo.getCertificate();
+ String encodedPublicKey = certInfo.getPublicKey();
+
+ if (encodedCertificate == null && encodedPublicKey == null) {
+ throw new ModelException("Client doesn't have certificate or publicKey configured");
+ }
+
+ if (encodedCertificate != null && encodedPublicKey != null) {
+ throw new ModelException("Client has both publicKey and certificate configured");
+ }
+
+ // TODO: Caching of publicKeys / certificates, so it doesn't need to be always computed from pem. For performance reasons...
+ if (encodedCertificate != null) {
+ X509Certificate clientCert = KeycloakModelUtils.getCertificate(encodedCertificate);
+ return clientCert.getPublicKey();
+ } else {
+ return KeycloakModelUtils.getPublicKey(encodedPublicKey);
+ }
+ }
+
+
+ // CLIENT REPRESENTATION METHODS
+
public static void updateClientRepresentationCertificateInfo(ClientRepresentation client, CertificateRepresentation rep, String attributePrefix) {
String privateKeyAttribute = attributePrefix + "." + PRIVATE_KEY;
String certificateAttribute = attributePrefix + "." + X509CERTIFICATE;
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingOIDCEndpointsApplicationResource.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingOIDCEndpointsApplicationResource.java
new file mode 100644
index 0000000..6ea488f
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingOIDCEndpointsApplicationResource.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.rest.resource;
+
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.MediaType;
+
+import org.jboss.resteasy.annotations.cache.NoCache;
+import org.jboss.resteasy.spi.BadRequestException;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.jose.jwk.JSONWebKeySet;
+import org.keycloak.jose.jwk.JWK;
+import org.keycloak.jose.jwk.JWKBuilder;
+import org.keycloak.jose.jws.Algorithm;
+import org.keycloak.jose.jws.JWSBuilder;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
+import org.keycloak.testsuite.rest.TestApplicationResourceProviderFactory;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class TestingOIDCEndpointsApplicationResource {
+
+ public static final String PRIVATE_KEY = "privateKey";
+ public static final String PUBLIC_KEY = "publicKey";
+
+ private final TestApplicationResourceProviderFactory.OIDCClientData clientData;
+
+ public TestingOIDCEndpointsApplicationResource(TestApplicationResourceProviderFactory.OIDCClientData oidcClientData) {
+ this.clientData = oidcClientData;
+ }
+
+ @GET
+ @Produces(MediaType.APPLICATION_JSON)
+ @Path("/generate-keys")
+ @NoCache
+ public Map<String, String> generateKeys() {
+ try {
+ KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
+ generator.initialize(2048);
+ clientData.setSigningKeyPair(generator.generateKeyPair());
+ } catch (NoSuchAlgorithmException e) {
+ throw new BadRequestException("Error generating signing keypair", e);
+ }
+
+ String privateKeyPem = KeycloakModelUtils.getPemFromKey(clientData.getSigningKeyPair().getPrivate());
+ String publicKeyPem = KeycloakModelUtils.getPemFromKey(clientData.getSigningKeyPair().getPublic());
+
+ Map<String, String> res = new HashMap<>();
+ res.put(PRIVATE_KEY, privateKeyPem);
+ res.put(PUBLIC_KEY, publicKeyPem);
+ return res;
+ }
+
+
+ @GET
+ @Produces(MediaType.APPLICATION_JSON)
+ @Path("/get-jwks")
+ @NoCache
+ public JSONWebKeySet getJwks() {
+ JSONWebKeySet keySet = new JSONWebKeySet();
+
+ if (clientData.getSigningKeyPair() == null) {
+ keySet.setKeys(new JWK[] {});
+ } else {
+ keySet.setKeys(new JWK[] { JWKBuilder.create().rs256(clientData.getSigningKeyPair().getPublic()) });
+ }
+
+ return keySet;
+ }
+
+
+ @GET
+ @Path("/set-oidc-request")
+ @Produces(org.keycloak.utils.MediaType.APPLICATION_JWT)
+ @NoCache
+ public void setOIDCRequest(@QueryParam("realmName") String realmName, @QueryParam("clientId") String clientId,
+ @QueryParam("redirectUri") String redirectUri, @QueryParam("maxAge") String maxAge,
+ @QueryParam("jwaAlgorithm") String jwaAlgorithm) {
+ Map<String, Object> oidcRequest = new HashMap<>();
+ oidcRequest.put(OIDCLoginProtocol.CLIENT_ID_PARAM, clientId);
+ oidcRequest.put(OIDCLoginProtocol.RESPONSE_TYPE_PARAM, OAuth2Constants.CODE);
+ oidcRequest.put(OIDCLoginProtocol.REDIRECT_URI_PARAM, redirectUri);
+ if (maxAge != null) {
+ oidcRequest.put(OIDCLoginProtocol.MAX_AGE_PARAM, Integer.parseInt(maxAge));
+ }
+
+ Algorithm alg = Enum.valueOf(Algorithm.class, jwaAlgorithm);
+ if (alg == Algorithm.none) {
+ clientData.setOidcRequest(new JWSBuilder().jsonContent(oidcRequest).none());
+ } else if (alg == Algorithm.RS256) {
+ if (clientData.getSigningKeyPair() == null) {
+ throw new BadRequestException("Requested RS256, but signing key not set");
+ }
+
+ PrivateKey privateKey = clientData.getSigningKeyPair().getPrivate();
+ clientData.setOidcRequest(new JWSBuilder().jsonContent(oidcRequest).rsa256(privateKey));
+ } else {
+ throw new BadRequestException("Unknown argument: " + jwaAlgorithm);
+ }
+ }
+
+
+ @GET
+ @Path("/get-oidc-request")
+ @Produces(org.keycloak.utils.MediaType.APPLICATION_JWT)
+ @NoCache
+ public String getOIDCRequest() {
+ return clientData.getOidcRequest();
+ }
+}
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestApplicationResourceProvider.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestApplicationResourceProvider.java
index bc63c99..5f392a0 100644
--- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestApplicationResourceProvider.java
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestApplicationResourceProvider.java
@@ -27,6 +27,8 @@ import org.keycloak.representations.adapters.action.PushNotBeforeAction;
import org.keycloak.representations.adapters.action.TestAvailabilityAction;
import org.keycloak.services.resource.RealmResourceProvider;
import org.keycloak.services.resources.RealmsResource;
+import org.keycloak.testsuite.rest.resource.TestingExportImportResource;
+import org.keycloak.testsuite.rest.resource.TestingOIDCEndpointsApplicationResource;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
@@ -53,14 +55,16 @@ public class TestApplicationResourceProvider implements RealmResourceProvider {
private final BlockingQueue<LogoutAction> adminLogoutActions;
private final BlockingQueue<PushNotBeforeAction> adminPushNotBeforeActions;
private final BlockingQueue<TestAvailabilityAction> adminTestAvailabilityAction;
+ private final TestApplicationResourceProviderFactory.OIDCClientData oidcClientData;
public TestApplicationResourceProvider(KeycloakSession session, BlockingQueue<LogoutAction> adminLogoutActions,
BlockingQueue<PushNotBeforeAction> adminPushNotBeforeActions,
- BlockingQueue<TestAvailabilityAction> adminTestAvailabilityAction) {
+ BlockingQueue<TestAvailabilityAction> adminTestAvailabilityAction, TestApplicationResourceProviderFactory.OIDCClientData oidcClientData) {
this.session = session;
this.adminLogoutActions = adminLogoutActions;
this.adminPushNotBeforeActions = adminPushNotBeforeActions;
this.adminTestAvailabilityAction = adminTestAvailabilityAction;
+ this.oidcClientData = oidcClientData;
}
@POST
@@ -164,6 +168,11 @@ public class TestApplicationResourceProvider implements RealmResourceProvider {
return sb.toString();
}
+ @Path("/oidc-client-endpoints")
+ public TestingOIDCEndpointsApplicationResource getTestingOIDCClientEndpoints() {
+ return new TestingOIDCEndpointsApplicationResource(oidcClientData);
+ }
+
@Override
public Object getResource() {
return this;
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestApplicationResourceProviderFactory.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestApplicationResourceProviderFactory.java
index 98ca2ba..6bd7dc2 100644
--- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestApplicationResourceProviderFactory.java
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestApplicationResourceProviderFactory.java
@@ -28,6 +28,7 @@ import org.keycloak.representations.adapters.action.TestAvailabilityAction;
import org.keycloak.services.resource.RealmResourceProvider;
import org.keycloak.services.resource.RealmResourceProviderFactory;
+import java.security.KeyPair;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;
@@ -40,9 +41,11 @@ public class TestApplicationResourceProviderFactory implements RealmResourceProv
private BlockingQueue<PushNotBeforeAction> pushNotBeforeActions = new LinkedBlockingDeque<>();
private BlockingQueue<TestAvailabilityAction> testAvailabilityActions = new LinkedBlockingDeque<>();
+ private final OIDCClientData oidcClientData = new OIDCClientData();
+
@Override
public RealmResourceProvider create(KeycloakSession session) {
- return new TestApplicationResourceProvider(session, adminLogoutActions, pushNotBeforeActions, testAvailabilityActions);
+ return new TestApplicationResourceProvider(session, adminLogoutActions, pushNotBeforeActions, testAvailabilityActions, oidcClientData);
}
@Override
@@ -62,4 +65,26 @@ public class TestApplicationResourceProviderFactory implements RealmResourceProv
return "app";
}
+
+ public static class OIDCClientData {
+
+ private KeyPair signingKeyPair;
+ private String oidcRequest;
+
+ public KeyPair getSigningKeyPair() {
+ return signingKeyPair;
+ }
+
+ public void setSigningKeyPair(KeyPair signingKeyPair) {
+ this.signingKeyPair = signingKeyPair;
+ }
+
+ public String getOidcRequest() {
+ return oidcRequest;
+ }
+
+ public void setOidcRequest(String oidcRequest) {
+ this.oidcRequest = oidcRequest;
+ }
+ }
}
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java
index 78b7e70..2085cfa 100644
--- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java
@@ -23,15 +23,20 @@ import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import org.infinispan.Cache;
+import org.keycloak.OAuth2Constants;
import org.keycloak.common.util.Time;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.events.Event;
import org.keycloak.events.admin.AdminEvent;
import org.keycloak.events.admin.ResourceType;
+import org.keycloak.jose.jws.Algorithm;
+import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.ModelToRepresentation;
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
+import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequest;
import org.keycloak.representations.idm.AdminEventRepresentation;
import org.keycloak.representations.idm.EventRepresentation;
import org.keycloak.services.managers.ClientSessionCode;
@@ -632,14 +637,14 @@ public class TestingResourceProvider implements RealmResourceProvider {
return ModelToRepresentation.toRepresentation(user);
}
- private RealmModel getRealmByName(String realmName) {
- RealmProvider realmProvider = session.getProvider(RealmProvider.class);
- return realmProvider.getRealmByName(realmName);
- }
-
@Path("/export-import")
public TestingExportImportResource getExportImportResource() {
return new TestingExportImportResource(session);
}
+ private RealmModel getRealmByName(String realmName) {
+ RealmProvider realmProvider = session.getProvider(RealmProvider.class);
+ return realmProvider.getRealmByName(realmName);
+ }
+
}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestApplicationResource.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestApplicationResource.java
index 2efc26b..2d277fd 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestApplicationResource.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestApplicationResource.java
@@ -20,7 +20,9 @@ package org.keycloak.testsuite.client.resources;
import org.keycloak.representations.adapters.action.LogoutAction;
import org.keycloak.representations.adapters.action.PushNotBeforeAction;
import org.keycloak.representations.adapters.action.TestAvailabilityAction;
+import org.keycloak.testsuite.rest.resource.TestingOIDCEndpointsApplicationResource;
+import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
@@ -53,4 +55,6 @@ public interface TestApplicationResource {
@Path("/clear-admin-actions")
Response clearAdminActions();
+ @Path("/oidc-client-endpoints")
+ TestOIDCEndpointsApplicationResource oidcClientEndpoints();
}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestApplicationResourceUrls.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestApplicationResourceUrls.java
new file mode 100644
index 0000000..8c5f98b
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestApplicationResourceUrls.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.client.resources;
+
+import javax.ws.rs.core.UriBuilder;
+
+import org.keycloak.testsuite.util.OAuthClient;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class TestApplicationResourceUrls {
+
+ private static UriBuilder oidcClientEndpoints() {
+ return UriBuilder.fromUri(OAuthClient.AUTH_SERVER_ROOT)
+ .path(TestApplicationResource.class)
+ .path(TestApplicationResource.class, "oidcClientEndpoints");
+ }
+
+ public static String clientRequestUri() {
+ UriBuilder builder = oidcClientEndpoints()
+ .path(TestOIDCEndpointsApplicationResource.class, "getOIDCRequest");
+
+ return builder.build().toString();
+ }
+
+ public static String clientJwksUri() {
+ UriBuilder builder = oidcClientEndpoints()
+ .path(TestOIDCEndpointsApplicationResource.class, "getJwks");
+
+ return builder.build().toString();
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestOIDCEndpointsApplicationResource.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestOIDCEndpointsApplicationResource.java
new file mode 100644
index 0000000..54d6c35
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestOIDCEndpointsApplicationResource.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.client.resources;
+
+import java.util.Map;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.MediaType;
+
+import org.keycloak.jose.jwk.JSONWebKeySet;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public interface TestOIDCEndpointsApplicationResource {
+
+ @GET
+ @Produces(MediaType.APPLICATION_JSON)
+ @Path("/generate-keys")
+ Map<String, String> generateKeys();
+
+
+ @GET
+ @Produces(MediaType.APPLICATION_JSON)
+ @Path("/get-jwks")
+ JSONWebKeySet getJwks();
+
+
+ @GET
+ @Path("/set-oidc-request")
+ @Produces(org.keycloak.utils.MediaType.APPLICATION_JWT)
+ void setOIDCRequest(@QueryParam("realmName") String realmName, @QueryParam("clientId") String clientId,
+ @QueryParam("redirectUri") String redirectUri, @QueryParam("maxAge") String maxAge,
+ @QueryParam("jwaAlgorithm") String jwaAlgorithm);
+
+ @GET
+ @Path("/get-oidc-request")
+ @Produces(org.keycloak.utils.MediaType.APPLICATION_JWT)
+ String getOIDCRequest();
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
index 586351d..2253828 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
@@ -105,6 +105,10 @@ public class OAuthClient {
private String nonce;
+ private String request;
+
+ private String requestUri;
+
private Map<String, PublicKey> publicKeys = new HashMap<>();
public void init(Keycloak adminClient, WebDriver driver) {
@@ -121,6 +125,9 @@ public class OAuthClient {
clientSessionState = null;
clientSessionHost = null;
maxAge = null;
+ nonce = null;
+ request = null;
+ requestUri = null;
}
public AuthorizationEndpointResponse doLogin(String username, String password) {
@@ -536,6 +543,12 @@ public class OAuthClient {
if (maxAge != null) {
b.queryParam(OIDCLoginProtocol.MAX_AGE_PARAM, maxAge);
}
+ if (request != null) {
+ b.queryParam(OIDCLoginProtocol.REQUEST_PARAM, request);
+ }
+ if (requestUri != null) {
+ b.queryParam(OIDCLoginProtocol.REQUEST_URI_PARAM, requestUri);
+ }
return b.build(realm).toString();
}
@@ -644,6 +657,16 @@ public class OAuthClient {
return this;
}
+ public OAuthClient request(String request) {
+ this.request = request;
+ return this;
+ }
+
+ public OAuthClient requestUri(String requestUri) {
+ this.requestUri = requestUri;
+ return this;
+ }
+
public String getRealm() {
return realm;
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java
index 608d7a7..19d6413 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java
@@ -50,12 +50,16 @@ import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.oidc.OIDCClientRepresentation;
import org.keycloak.testsuite.Assert;
+import org.keycloak.testsuite.client.resources.TestApplicationResourceUrls;
+import org.keycloak.testsuite.client.resources.TestOIDCEndpointsApplicationResource;
+import org.keycloak.testsuite.rest.resource.TestingOIDCEndpointsApplicationResource;
import org.keycloak.testsuite.util.OAuthClient;
import java.security.PrivateKey;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
+import java.util.Map;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
@@ -236,8 +240,11 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest {
clientRep.setGrantTypes(Collections.singletonList(OAuth2Constants.CLIENT_CREDENTIALS));
clientRep.setTokenEndpointAuthMethod(OIDCLoginProtocol.PRIVATE_KEY_JWT);
- // Corresponds to PRIVATE_KEY
- JSONWebKeySet keySet = loadJson(getClass().getResourceAsStream("/clientreg-test/jwks.json"), JSONWebKeySet.class);
+ // Generate keys for client
+ TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
+ Map<String, String> generatedKeys = oidcClientEndpointsResource.generateKeys();
+
+ JSONWebKeySet keySet = oidcClientEndpointsResource.getJwks();
clientRep.setJwks(keySet);
OIDCClientRepresentation response = reg.oidc().create(clientRep);
@@ -246,7 +253,7 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest {
Assert.assertNull(response.getClientSecretExpiresAt());
// Tries to authenticate client with privateKey JWT
- String signedJwt = getClientSignedJWT(response.getClientId());
+ String signedJwt = getClientSignedJWT(response.getClientId(), generatedKeys.get(TestingOIDCEndpointsApplicationResource.PRIVATE_KEY));
OAuthClient.AccessTokenResponse accessTokenResponse = doClientCredentialsGrantRequest(signedJwt);
Assert.assertEquals(200, accessTokenResponse.getStatusCode());
AccessToken accessToken = oauth.verifyToken(accessTokenResponse.getAccessToken());
@@ -260,8 +267,11 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest {
clientRep.setGrantTypes(Collections.singletonList(OAuth2Constants.CLIENT_CREDENTIALS));
clientRep.setTokenEndpointAuthMethod(OIDCLoginProtocol.PRIVATE_KEY_JWT);
- // Use the realmKey for client authentication too
- clientRep.setJwksUri(oauth.getCertsUrl(REALM_NAME));
+ // Generate keys for client
+ TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
+ Map<String, String> generatedKeys = oidcClientEndpointsResource.generateKeys();
+
+ clientRep.setJwksUri(TestApplicationResourceUrls.clientJwksUri());
OIDCClientRepresentation response = reg.oidc().create(clientRep);
Assert.assertEquals(OIDCLoginProtocol.PRIVATE_KEY_JWT, response.getTokenEndpointAuthMethod());
@@ -269,7 +279,7 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest {
Assert.assertNull(response.getClientSecretExpiresAt());
// Tries to authenticate client with privateKey JWT
- String signedJwt = getClientSignedJWT(response.getClientId());
+ String signedJwt = getClientSignedJWT(response.getClientId(), generatedKeys.get(TestingOIDCEndpointsApplicationResource.PRIVATE_KEY));
OAuthClient.AccessTokenResponse accessTokenResponse = doClientCredentialsGrantRequest(signedJwt);
Assert.assertEquals(200, accessTokenResponse.getStatusCode());
AccessToken accessToken = oauth.verifyToken(accessTokenResponse.getAccessToken());
@@ -280,24 +290,27 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest {
public void testSignaturesRequired() throws Exception {
OIDCClientRepresentation clientRep = createRep();
clientRep.setUserinfoSignedResponseAlg(Algorithm.RS256.toString());
+ clientRep.setRequestObjectSigningAlg(Algorithm.RS256.toString());
OIDCClientRepresentation response = reg.oidc().create(clientRep);
Assert.assertEquals(Algorithm.RS256.toString(), response.getUserinfoSignedResponseAlg());
+ Assert.assertEquals(Algorithm.RS256.toString(), response.getRequestObjectSigningAlg());
Assert.assertNotNull(response.getClientSecret());
// Test Keycloak representation
ClientRepresentation kcClient = getClient(response.getClientId());
OIDCAdvancedConfigWrapper config = OIDCAdvancedConfigWrapper.fromClientRepresentation(kcClient);
Assert.assertEquals(config.getUserInfoSignedResponseAlg(), Algorithm.RS256);
+ Assert.assertEquals(config.getRequestObjectSignatureAlg(), Algorithm.RS256);
}
// Client auth with signedJWT - helper methods
- private String getClientSignedJWT(String clientId) {
+ private String getClientSignedJWT(String clientId, String privateKeyPem) {
String realmInfoUrl = KeycloakUriBuilder.fromUri(getAuthServerRoot()).path(ServiceUrlConstants.REALM_INFO_PATH).build(REALM_NAME).toString();
- PrivateKey privateKey = KeycloakModelUtils.getPrivateKey(PRIVATE_KEY);
+ PrivateKey privateKey = KeycloakModelUtils.getPrivateKey(privateKeyPem);
// Use token-endpoint as audience as OIDC conformance testsuite is using it too.
JWTClientCredentialsProvider jwtProvider = new JWTClientCredentialsProvider() {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java
index 191581e..e01057d 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java
@@ -19,30 +19,42 @@ package org.keycloak.testsuite.oidc;
import java.util.List;
-
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.OAuthErrorException;
+import org.keycloak.admin.client.resource.ClientResource;
+import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator;
import org.keycloak.common.util.Time;
import org.keycloak.events.Details;
+import org.keycloak.jose.jws.Algorithm;
import org.keycloak.models.Constants;
+import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.representations.IDToken;
+import org.keycloak.representations.idm.CertificateRepresentation;
+import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.EventRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.services.util.CertificateInfoHelper;
import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.TestRealmKeycloakTest;
import org.keycloak.testsuite.admin.AbstractAdminTest;
+import org.keycloak.testsuite.admin.ApiUtil;
+import org.keycloak.testsuite.client.resources.TestApplicationResourceUrls;
+import org.keycloak.testsuite.client.resources.TestOIDCEndpointsApplicationResource;
import org.keycloak.testsuite.pages.AccountUpdateProfilePage;
import org.keycloak.testsuite.pages.AppPage;
+import org.keycloak.testsuite.pages.ErrorPage;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.OAuthGrantPage;
+import org.keycloak.testsuite.rest.resource.TestingOIDCEndpointsApplicationResource;
import org.keycloak.testsuite.util.ClientManager;
import org.keycloak.testsuite.util.OAuthClient;
+import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@@ -68,6 +80,9 @@ public class OIDCAdvancedRequestParamsTest extends TestRealmKeycloakTest {
@Page
protected OAuthGrantPage grantPage;
+ @Page
+ protected ErrorPage errorPage;
+
@Override
public void configureTestRealm(RealmRepresentation testRealm) {
@@ -308,29 +323,98 @@ public class OIDCAdvancedRequestParamsTest extends TestRealmKeycloakTest {
// REQUEST & REQUEST_URI
@Test
- public void requestParam() {
- driver.navigate().to(oauth.getLoginFormUrl() + "&request=abc");
+ public void requestParamUnsigned() throws Exception {
+ String validRedirectUri = oauth.getRedirectUri();
+ TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
- assertFalse(loginPage.isCurrent());
+ // Send request object with invalid redirect uri.
+ oidcClientEndpointsResource.setOIDCRequest("test", "test-app", "http://invalid", null, Algorithm.none.toString());
+ String requestStr = oidcClientEndpointsResource.getOIDCRequest();
+
+ oauth.request(requestStr);
+ oauth.openLoginForm();
+ Assert.assertTrue(errorPage.isCurrent());
+ assertEquals("Invalid parameter: redirect_uri", errorPage.getError());
+
+ // Assert the value from request object has bigger priority then from the query parameter.
+ oauth.redirectUri("http://invalid");
+ oidcClientEndpointsResource.setOIDCRequest("test", "test-app", validRedirectUri, "10", Algorithm.none.toString());
+ requestStr = oidcClientEndpointsResource.getOIDCRequest();
+
+ oauth.request(requestStr);
+ OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password");
+ Assert.assertNotNull(response.getCode());
+ Assert.assertEquals("mystate", response.getState());
assertTrue(appPage.isCurrent());
+ }
- // Assert error response was sent because not logged in
- OAuthClient.AuthorizationEndpointResponse resp = new OAuthClient.AuthorizationEndpointResponse(oauth);
- Assert.assertNull(resp.getCode());
- Assert.assertEquals(OAuthErrorException.REQUEST_NOT_SUPPORTED, resp.getError());
+ @Test
+ public void requestUriParamUnsigned() throws Exception {
+ String validRedirectUri = oauth.getRedirectUri();
+ TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
+
+ // Send request object with invalid redirect uri.
+ oidcClientEndpointsResource.setOIDCRequest("test", "test-app", "http://invalid", null, Algorithm.none.toString());
+
+ oauth.requestUri(TestApplicationResourceUrls.clientRequestUri());
+ oauth.openLoginForm();
+ Assert.assertTrue(errorPage.isCurrent());
+ assertEquals("Invalid parameter: redirect_uri", errorPage.getError());
+
+ // Assert the value from request object has bigger priority then from the query parameter.
+ oauth.redirectUri("http://invalid");
+ oidcClientEndpointsResource.setOIDCRequest("test", "test-app", validRedirectUri, "10", Algorithm.none.toString());
+
+ OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password");
+ Assert.assertNotNull(response.getCode());
+ Assert.assertEquals("mystate", response.getState());
+ assertTrue(appPage.isCurrent());
}
@Test
- public void requestUriParam() {
- driver.navigate().to(oauth.getLoginFormUrl() + "&request_uri=https%3A%2F%2Flocalhost%3A60784%2Fexport%2FqzHTG11W48.jwt");
+ public void requestUriParamSigned() throws Exception {
+ String validRedirectUri = oauth.getRedirectUri();
+ TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
+
+ // Set required signature for request_uri
+ ClientResource clientResource = ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app");
+ ClientRepresentation clientRep = clientResource.toRepresentation();
+ OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setRequestObjectSignatureAlg(Algorithm.RS256);
+ clientResource.update(clientRep);
+
+ // Verify unsigned request_uri will fail
+ oidcClientEndpointsResource.setOIDCRequest("test", "test-app", validRedirectUri, "10", Algorithm.none.toString());
+ oauth.requestUri(TestApplicationResourceUrls.clientRequestUri());
+ oauth.openLoginForm();
+ Assert.assertTrue(errorPage.isCurrent());
+ assertEquals("Invalid Request", errorPage.getError());
- assertFalse(loginPage.isCurrent());
+ // Generate keypair for client
+ String clientPublicKeyPem = oidcClientEndpointsResource.generateKeys().get(TestingOIDCEndpointsApplicationResource.PUBLIC_KEY);
+
+ // Verify signed request_uri will fail due to failed signature validation
+ oidcClientEndpointsResource.setOIDCRequest("test", "test-app", validRedirectUri, "10", Algorithm.RS256.toString());
+ oauth.openLoginForm();
+ Assert.assertTrue(errorPage.isCurrent());
+ assertEquals("Invalid Request", errorPage.getError());
+
+
+ // Update clientModel with publicKey for signing
+ clientRep = clientResource.toRepresentation();
+ CertificateRepresentation cert = new CertificateRepresentation();
+ cert.setPublicKey(clientPublicKeyPem);
+ CertificateInfoHelper.updateClientRepresentationCertificateInfo(clientRep, cert, JWTClientAuthenticator.ATTR_PREFIX);
+ clientResource.update(clientRep);
+
+ // Check signed request_uri will pass
+ OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password");
+ Assert.assertNotNull(response.getCode());
+ Assert.assertEquals("mystate", response.getState());
assertTrue(appPage.isCurrent());
- // Assert error response was sent because not logged in
- OAuthClient.AuthorizationEndpointResponse resp = new OAuthClient.AuthorizationEndpointResponse(oauth);
- Assert.assertNull(resp.getCode());
- Assert.assertEquals(OAuthErrorException.REQUEST_URI_NOT_SUPPORTED, resp.getError());
+ // Revert requiring signature for client
+ OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setRequestObjectSignatureAlg(null);
+ clientResource.update(clientRep);
}
// LOGIN_HINT
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCWellKnownProviderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCWellKnownProviderTest.java
index 91ccc7e..7fea657 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCWellKnownProviderTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCWellKnownProviderTest.java
@@ -87,6 +87,7 @@ public class OIDCWellKnownProviderTest extends AbstractKeycloakTest {
Assert.assertNames(oidcConfig.getSubjectTypesSupported(), "public");
Assert.assertNames(oidcConfig.getIdTokenSigningAlgValuesSupported(), Algorithm.RS256.toString());
Assert.assertNames(oidcConfig.getUserInfoSigningAlgValuesSupported(), Algorithm.RS256.toString());
+ Assert.assertNames(oidcConfig.getRequestObjectSigningAlgValuesSupported(), Algorithm.none.toString(), Algorithm.RS256.toString());
// Client authentication
Assert.assertNames(oidcConfig.getTokenEndpointAuthMethodsSupported(), "client_secret_basic", "client_secret_post", "private_key_jwt");
@@ -101,8 +102,8 @@ public class OIDCWellKnownProviderTest extends AbstractKeycloakTest {
Assert.assertNames(oidcConfig.getScopesSupported(), OAuth2Constants.SCOPE_OPENID, OAuth2Constants.OFFLINE_ACCESS);
// Request and Request_Uri
- Assert.assertFalse(oidcConfig.getRequestParameterSupported());
- Assert.assertFalse(oidcConfig.getRequestUriParameterSupported());
+ Assert.assertTrue(oidcConfig.getRequestParameterSupported());
+ Assert.assertTrue(oidcConfig.getRequestUriParameterSupported());
} finally {
client.close();
}
diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
index 4f15d6d..356cbb2 100644
--- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
+++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
@@ -240,6 +240,8 @@ fine-oidc-endpoint-conf=Fine Grain OpenID Connect Configuration
fine-oidc-endpoint-conf.tooltip=Expand this section to configure advanced settings of this client related to OpenID Connect protocol
user-info-signed-response-alg=User Info Signed Response Algorithm
user-info-signed-response-alg.tooltip=JWA algorithm used for signed User Info Endpoint response. If set to 'unsigned', then User Info Response won't be signed and will be returned in application/json format.
+request-object-signature-alg=Request Object Signature Algorithm
+request-object-signature-alg.tooltip=JWA algorithm, which client needs to use when sending OIDC request object specified by 'request' or 'request_uri' parameters. If set to 'any', then Request object can be signed by any algorithm (including 'none' ).
fine-saml-endpoint-conf=Fine Grain SAML Endpoint Configuration
fine-saml-endpoint-conf.tooltip=Expand this section to configure exact URLs for Assertion Consumer and Single Logout Service.
assertion-consumer-post-binding-url=Assertion Consumer Service POST Binding URL
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
index 24efd88..3bbd471 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
@@ -797,6 +797,12 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates,
"RS256"
];
+ $scope.requestObjectSignatureAlgorithms = [
+ "any",
+ "none",
+ "RS256"
+ ];
+
$scope.realm = realm;
$scope.samlAuthnStatement = false;
$scope.samlMultiValuedRoles = false;
@@ -898,7 +904,11 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates,
}
}
- $scope.userInfoSignedResponseAlg = getSignatureAlgorithm('user.info.response');
+ var attrVal1 = $scope.client.attributes['user.info.response.signature.alg'];
+ $scope.userInfoSignedResponseAlg = attrVal1==null ? 'unsigned' : attrVal1;
+
+ var attrVal2 = $scope.client.attributes['request.object.signature.alg'];
+ $scope.requestObjectSignatureAlg = attrVal2==null ? 'any' : attrVal2;
}
if (!$scope.create) {
@@ -964,23 +974,20 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates,
};
$scope.changeUserInfoSignedResponseAlg = function() {
- changeSignatureAlgorithm('user.info.response', $scope.userInfoSignedResponseAlg);
+ if ($scope.userInfoSignedResponseAlg === 'unsigned') {
+ $scope.client.attributes['user.info.response.signature.alg'] = null;
+ } else {
+ $scope.client.attributes['user.info.response.signature.alg'] = $scope.userInfoSignedResponseAlg;
+ }
};
- function changeSignatureAlgorithm(attrPrefix, attrValue) {
- var attrName = attrPrefix + '.signature.alg';
- if (attrValue === 'unsigned') {
- $scope.client.attributes[attrName] = null;
+ $scope.changeRequestObjectSignatureAlg = function() {
+ if ($scope.requestObjectSignatureAlg === 'any') {
+ $scope.client.attributes['request.object.signature.alg'] = null;
} else {
- $scope.client.attributes[attrName] = attrValue;
+ $scope.client.attributes['request.object.signature.alg'] = $scope.requestObjectSignatureAlg;
}
- }
-
- function getSignatureAlgorithm(attrPrefix) {
- var attrName = attrPrefix + '.signature.alg';
- var attrVal = $scope.client.attributes[attrName];
- return attrVal==null ? 'unsigned' : attrVal;
- }
+ };
$scope.$watch(function() {
return $location.path();
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html b/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
index af10a22..d8f0d24 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
@@ -348,6 +348,19 @@
</div>
<kc-tooltip>{{:: 'user-info-signed-response-alg.tooltip' | translate}}</kc-tooltip>
</div>
+ <div class="form-group clearfix block" data-ng-show="protocol == 'openid-connect'">
+ <label class="col-md-2 control-label" for="requestObjectSignatureAlg">{{:: 'request-object-signature-alg' | translate}}</label>
+ <div class="col-sm-6">
+ <div>
+ <select class="form-control" id="requestObjectSignatureAlg"
+ ng-change="changeRequestObjectSignatureAlg()"
+ ng-model="requestObjectSignatureAlg"
+ ng-options="sig for sig in requestObjectSignatureAlgorithms">
+ </select>
+ </div>
+ </div>
+ <kc-tooltip>{{:: 'request-object-signature-alg.tooltip' | translate}}</kc-tooltip>
+ </div>
</fieldset>
<div class="form-group">