Merge pull request #3187 from mposolda/master

KEYCLOAK-3349 Support for 'request' and 'request_uri' parameters
This commit is contained in:
Marek Posolda 2016-09-02 21:58:25 +02:00 committed by GitHub
commit 404aa69adb
39 changed files with 1330 additions and 423 deletions

View file

@ -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>();
}
}

View file

@ -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);
}

View file

@ -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");
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;
}
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");
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

View file

@ -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);
}

View file

@ -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());

View file

@ -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;

View file

@ -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) {

View file

@ -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";

View file

@ -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;
}

View file

@ -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 (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 (additionalReqParams != null) {
for (String paramName : additionalReqParams.keySet()) {
clientSession.setNote(CLIENT_SESSION_NOTE_ADDITIONAL_REQ_PARAMS_PREFIX + paramName, additionalReqParams.get(paramName));
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() {

View file

@ -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;
}
}

View file

@ -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);
}
}
}

View file

@ -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();
}
}

View file

@ -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> {
}
}

View file

@ -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();
}

View file

@ -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;
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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;
}

View file

@ -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 {

View file

@ -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;

View file

@ -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;

View file

@ -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;
}
}
}

View file

@ -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;
@ -76,6 +81,7 @@ import org.keycloak.models.UserProvider;
import org.keycloak.representations.idm.AuthDetailsRepresentation;
import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.rest.resource.TestingExportImportResource;
import static org.keycloak.exportimport.ExportImportConfig.*;
@ -564,22 +570,6 @@ public class TestingResourceProvider implements RealmResourceProvider {
return result;
}
@GET
@Path("/run-import")
@Produces(MediaType.APPLICATION_JSON)
public Response runImport() {
new ExportImportManager(session).runImport();
return Response.ok().build();
}
@GET
@Path("/run-export")
@Produces(MediaType.APPLICATION_JSON)
public Response runExport() {
new ExportImportManager(session).runExport();
return Response.ok().build();
}
@GET
@Path("/valid-credentials")
@Produces(MediaType.APPLICATION_JSON)
@ -647,83 +637,14 @@ public class TestingResourceProvider implements RealmResourceProvider {
return ModelToRepresentation.toRepresentation(user);
}
@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);
}
@GET
@Path("/get-users-per-file")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Integer getUsersPerFile() {
String usersPerFile = System.getProperty(USERS_PER_FILE, String.valueOf(DEFAULT_USERS_PER_FILE));
return Integer.parseInt(usersPerFile.trim());
}
@PUT
@Path("/set-users-per-file")
@Consumes(MediaType.APPLICATION_JSON)
public void setUsersPerFile(@QueryParam("usersPerFile") Integer usersPerFile) {
System.setProperty(USERS_PER_FILE, String.valueOf(usersPerFile));
}
@GET
@Path("/get-dir")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public String getDir() {
return System.getProperty(DIR);
}
@PUT
@Path("/set-dir")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public String setDir(@QueryParam("dir") String dir) {
return System.setProperty(DIR, dir);
}
@PUT
@Path("/export-import-provider")
@Consumes(MediaType.APPLICATION_JSON)
public void setProvider(@QueryParam("exportImportProvider") String exportImportProvider) {
System.setProperty(PROVIDER, exportImportProvider);
}
@PUT
@Path("/export-import-file")
@Consumes(MediaType.APPLICATION_JSON)
public void setFile(@QueryParam("file") String file) {
System.setProperty(FILE, file);
}
@PUT
@Path("/export-import-action")
@Consumes(MediaType.APPLICATION_JSON)
public void setAction(@QueryParam("exportImportAction") String exportImportAction) {
System.setProperty(ACTION, exportImportAction);
}
@PUT
@Path("/set-realm-name")
@Consumes(MediaType.APPLICATION_JSON)
public void setRealmName(@QueryParam("realmName") String realmName) {
if (realmName != null && !realmName.isEmpty()) {
System.setProperty(REALM_NAME, realmName);
} else {
System.getProperties().remove(REALM_NAME);
}
}
@GET
@Path("/get-test-dir")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public String getExportImportTestDirectory() {
System.setProperty("project.build.directory", "target");
String absolutePath = new File(System.getProperty("project.build.directory", "target")).getAbsolutePath();
return absolutePath;
}
}

View file

@ -0,0 +1,142 @@
/*
* 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.io.File;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.keycloak.exportimport.ExportImportManager;
import org.keycloak.models.KeycloakSession;
import static org.keycloak.exportimport.ExportImportConfig.ACTION;
import static org.keycloak.exportimport.ExportImportConfig.DEFAULT_USERS_PER_FILE;
import static org.keycloak.exportimport.ExportImportConfig.DIR;
import static org.keycloak.exportimport.ExportImportConfig.FILE;
import static org.keycloak.exportimport.ExportImportConfig.PROVIDER;
import static org.keycloak.exportimport.ExportImportConfig.REALM_NAME;
import static org.keycloak.exportimport.ExportImportConfig.USERS_PER_FILE;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class TestingExportImportResource {
private final KeycloakSession session;
public TestingExportImportResource(KeycloakSession session) {
this.session = session;
}
@GET
@Path("/run-import")
@Produces(MediaType.APPLICATION_JSON)
public Response runImport() {
new ExportImportManager(session).runImport();
return Response.ok().build();
}
@GET
@Path("/run-export")
@Produces(MediaType.APPLICATION_JSON)
public Response runExport() {
new ExportImportManager(session).runExport();
return Response.ok().build();
}
@GET
@Path("/get-users-per-file")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Integer getUsersPerFile() {
String usersPerFile = System.getProperty(USERS_PER_FILE, String.valueOf(DEFAULT_USERS_PER_FILE));
return Integer.parseInt(usersPerFile.trim());
}
@PUT
@Path("/set-users-per-file")
@Consumes(MediaType.APPLICATION_JSON)
public void setUsersPerFile(@QueryParam("usersPerFile") Integer usersPerFile) {
System.setProperty(USERS_PER_FILE, String.valueOf(usersPerFile));
}
@GET
@Path("/get-dir")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public String getDir() {
return System.getProperty(DIR);
}
@PUT
@Path("/set-dir")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public String setDir(@QueryParam("dir") String dir) {
return System.setProperty(DIR, dir);
}
@PUT
@Path("/export-import-provider")
@Consumes(MediaType.APPLICATION_JSON)
public void setProvider(@QueryParam("exportImportProvider") String exportImportProvider) {
System.setProperty(PROVIDER, exportImportProvider);
}
@PUT
@Path("/export-import-file")
@Consumes(MediaType.APPLICATION_JSON)
public void setFile(@QueryParam("file") String file) {
System.setProperty(FILE, file);
}
@PUT
@Path("/export-import-action")
@Consumes(MediaType.APPLICATION_JSON)
public void setAction(@QueryParam("exportImportAction") String exportImportAction) {
System.setProperty(ACTION, exportImportAction);
}
@PUT
@Path("/set-realm-name")
@Consumes(MediaType.APPLICATION_JSON)
public void setRealmName(@QueryParam("realmName") String realmName) {
if (realmName != null && !realmName.isEmpty()) {
System.setProperty(REALM_NAME, realmName);
} else {
System.getProperties().remove(REALM_NAME);
}
}
@GET
@Path("/get-test-dir")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public String getExportImportTestDirectory() {
System.setProperty("project.build.directory", "target");
String absolutePath = new File(System.getProperty("project.build.directory", "target")).getAbsolutePath();
return absolutePath;
}
}

View file

@ -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();
}
}

View file

@ -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();
}

View file

@ -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();
}
}

View file

@ -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();
}

View file

@ -0,0 +1,93 @@
/*
* 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.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public interface TestingExportImportResource {
@GET
@Path("/run-import")
@Produces(MediaType.APPLICATION_JSON)
public Response runImport();
@GET
@Path("/run-export")
@Produces(MediaType.APPLICATION_JSON)
public Response runExport();
@GET
@Path("/get-users-per-file")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Integer getUsersPerFile();
@PUT
@Path("/set-users-per-file")
@Consumes(MediaType.APPLICATION_JSON)
public void setUsersPerFile(@QueryParam("usersPerFile") Integer usersPerFile);
@GET
@Path("/get-dir")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public String getDir();
@PUT
@Path("/set-dir")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public String setDir(@QueryParam("dir") String dir);
@PUT
@Path("/export-import-provider")
@Consumes(MediaType.APPLICATION_JSON)
public void setProvider(@QueryParam("exportImportProvider") String exportImportProvider);
@PUT
@Path("/export-import-file")
@Consumes(MediaType.APPLICATION_JSON)
public void setFile(@QueryParam("file") String file);
@PUT
@Path("/export-import-action")
@Consumes(MediaType.APPLICATION_JSON)
public void setAction(@QueryParam("exportImportAction") String exportImportAction);
@PUT
@Path("/set-realm-name")
@Consumes(MediaType.APPLICATION_JSON)
public void setRealmName(@QueryParam("realmName") String realmName);
@GET
@Path("/get-test-dir")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public String getExportImportTestDirectory();
}

View file

@ -17,8 +17,8 @@
package org.keycloak.testsuite.client.resources;
import java.util.Date;
import java.util.List;
import org.keycloak.representations.idm.AdminEventRepresentation;
import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
import org.keycloak.representations.idm.EventRepresentation;
@ -37,7 +37,6 @@ import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.Map;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.keycloak.exportimport.ExportImportManager;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
@ -205,16 +204,6 @@ public interface TestingResource {
@Path("/update-pass-through-auth-state")
@Produces(MediaType.APPLICATION_JSON)
AuthenticatorState updateAuthenticator(AuthenticatorState state);
@GET
@Path("/run-import")
@Produces(MediaType.APPLICATION_JSON)
public Response runImport();
@GET
@Path("/run-export")
@Produces(MediaType.APPLICATION_JSON)
public Response runExport();
@GET
@Path("/valid-credentials")
@ -250,53 +239,7 @@ public interface TestingResource {
@Produces(MediaType.APPLICATION_JSON)
public UserRepresentation getUserByServiceAccountClient(@QueryParam("realmName") String realmName, @QueryParam("clientId") String clientId);
@Path("export-import")
TestingExportImportResource exportImport();
@GET
@Path("/get-users-per-file")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Integer getUsersPerFile();
@PUT
@Path("/set-users-per-file")
@Consumes(MediaType.APPLICATION_JSON)
public void setUsersPerFile(@QueryParam("usersPerFile") Integer usersPerFile);
@GET
@Path("/get-dir")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public String getDir();
@PUT
@Path("/set-dir")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public String setDir(@QueryParam("dir") String dir);
@PUT
@Path("/export-import-provider")
@Consumes(MediaType.APPLICATION_JSON)
public void setProvider(@QueryParam("exportImportProvider") String exportImportProvider);
@PUT
@Path("/export-import-file")
@Consumes(MediaType.APPLICATION_JSON)
public void setFile(@QueryParam("file") String file);
@PUT
@Path("/export-import-action")
@Consumes(MediaType.APPLICATION_JSON)
public void setAction(@QueryParam("exportImportAction") String exportImportAction);
@PUT
@Path("/set-realm-name")
@Consumes(MediaType.APPLICATION_JSON)
public void setRealmName(@QueryParam("realmName") String realmName);
@GET
@Path("/get-test-dir")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public String getExportImportTestDirectory();
}

View file

@ -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;
}

View file

@ -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() {

View file

@ -75,11 +75,11 @@ public class ExportImportTest extends AbstractExportImportTest {
@Test
public void testDirFullExportImport() throws Throwable {
testingClient.testing().setProvider(DirExportProviderFactory.PROVIDER_ID);
String targetDirPath = testingClient.testing().getExportImportTestDirectory()+ File.separator + "dirExport";
testingClient.testing().exportImport().setProvider(DirExportProviderFactory.PROVIDER_ID);
String targetDirPath = testingClient.testing().exportImport().getExportImportTestDirectory()+ File.separator + "dirExport";
DirExportProvider.recursiveDeleteDir(new File(targetDirPath));
testingClient.testing().setDir(targetDirPath);
testingClient.testing().setUsersPerFile(ExportImportConfig.DEFAULT_USERS_PER_FILE);
testingClient.testing().exportImport().setDir(targetDirPath);
testingClient.testing().exportImport().setUsersPerFile(ExportImportConfig.DEFAULT_USERS_PER_FILE);
testFullExportImport();
@ -89,11 +89,13 @@ public class ExportImportTest extends AbstractExportImportTest {
@Test
public void testDirRealmExportImport() throws Throwable {
testingClient.testing().setProvider(DirExportProviderFactory.PROVIDER_ID);
String targetDirPath = testingClient.testing().getExportImportTestDirectory() + File.separator + "dirRealmExport";
testingClient.testing()
.exportImport()
.setProvider(DirExportProviderFactory.PROVIDER_ID);
String targetDirPath = testingClient.testing().exportImport().getExportImportTestDirectory() + File.separator + "dirRealmExport";
DirExportProvider.recursiveDeleteDir(new File(targetDirPath));
testingClient.testing().setDir(targetDirPath);
testingClient.testing().setUsersPerFile(3);
testingClient.testing().exportImport().setDir(targetDirPath);
testingClient.testing().exportImport().setUsersPerFile(3);
testRealmExportImport();
@ -104,18 +106,18 @@ public class ExportImportTest extends AbstractExportImportTest {
@Test
public void testSingleFileFullExportImport() throws Throwable {
testingClient.testing().setProvider(SingleFileExportProviderFactory.PROVIDER_ID);
String targetFilePath = testingClient.testing().getExportImportTestDirectory() + File.separator + "singleFile-full.json";
testingClient.testing().setFile(targetFilePath);
testingClient.testing().exportImport().setProvider(SingleFileExportProviderFactory.PROVIDER_ID);
String targetFilePath = testingClient.testing().exportImport().getExportImportTestDirectory() + File.separator + "singleFile-full.json";
testingClient.testing().exportImport().setFile(targetFilePath);
testFullExportImport();
}
@Test
public void testSingleFileRealmExportImport() throws Throwable {
testingClient.testing().setProvider(SingleFileExportProviderFactory.PROVIDER_ID);
String targetFilePath = testingClient.testing().getExportImportTestDirectory() + File.separator + "singleFile-realm.json";
testingClient.testing().setFile(targetFilePath);
testingClient.testing().exportImport().setProvider(SingleFileExportProviderFactory.PROVIDER_ID);
String targetFilePath = testingClient.testing().exportImport().getExportImportTestDirectory() + File.separator + "singleFile-realm.json";
testingClient.testing().exportImport().setFile(targetFilePath);
testRealmExportImport();
}
@ -126,14 +128,14 @@ public class ExportImportTest extends AbstractExportImportTest {
removeRealm("test-realm");
// Set the realm, which doesn't have builtin clients/roles inside JSON
testingClient.testing().setProvider(SingleFileExportProviderFactory.PROVIDER_ID);
testingClient.testing().exportImport().setProvider(SingleFileExportProviderFactory.PROVIDER_ID);
URL url = ExportImportTest.class.getResource("/model/testrealm.json");
String targetFilePath = new File(url.getFile()).getAbsolutePath();
testingClient.testing().setFile(targetFilePath);
testingClient.testing().exportImport().setFile(targetFilePath);
testingClient.testing().setAction(ExportImportConfig.ACTION_IMPORT);
testingClient.testing().exportImport().setAction(ExportImportConfig.ACTION_IMPORT);
testingClient.testing().runImport();
testingClient.testing().exportImport().runImport();
RealmResource testRealmRealm = adminClient.realm("test-realm");
@ -158,14 +160,14 @@ public class ExportImportTest extends AbstractExportImportTest {
realm.components().add(component);
testingClient.testing().setProvider(SingleFileExportProviderFactory.PROVIDER_ID);
testingClient.testing().exportImport().setProvider(SingleFileExportProviderFactory.PROVIDER_ID);
String targetFilePath = testingClient.testing().getExportImportTestDirectory() + File.separator + "singleFile-realm.json";
testingClient.testing().setFile(targetFilePath);
testingClient.testing().setAction(ExportImportConfig.ACTION_EXPORT);
testingClient.testing().setRealmName("component-realm");
String targetFilePath = testingClient.testing().exportImport().getExportImportTestDirectory() + File.separator + "singleFile-realm.json";
testingClient.testing().exportImport().setFile(targetFilePath);
testingClient.testing().exportImport().setAction(ExportImportConfig.ACTION_EXPORT);
testingClient.testing().exportImport().setRealmName("component-realm");
testingClient.testing().runExport();
testingClient.testing().exportImport().runExport();
// Delete some realm (and some data in admin realm)
adminClient.realm("component-realm").remove();
@ -173,9 +175,9 @@ public class ExportImportTest extends AbstractExportImportTest {
Assert.assertEquals(3, adminClient.realms().findAll().size());
// Configure import
testingClient.testing().setAction(ExportImportConfig.ACTION_IMPORT);
testingClient.testing().exportImport().setAction(ExportImportConfig.ACTION_IMPORT);
testingClient.testing().runImport();
testingClient.testing().exportImport().runImport();
realmRep = realm.toRepresentation();
@ -203,10 +205,10 @@ public class ExportImportTest extends AbstractExportImportTest {
}
private void testFullExportImport() throws LifecycleException {
testingClient.testing().setAction(ExportImportConfig.ACTION_EXPORT);
testingClient.testing().setRealmName("");
testingClient.testing().exportImport().setAction(ExportImportConfig.ACTION_EXPORT);
testingClient.testing().exportImport().setRealmName("");
testingClient.testing().runExport();
testingClient.testing().exportImport().runExport();
removeRealm("test");
removeRealm("test-realm");
@ -218,9 +220,9 @@ public class ExportImportTest extends AbstractExportImportTest {
assertNotAuthenticated("test", "user3", "password");
// Configure import
testingClient.testing().setAction(ExportImportConfig.ACTION_IMPORT);
testingClient.testing().exportImport().setAction(ExportImportConfig.ACTION_IMPORT);
testingClient.testing().runImport();
testingClient.testing().exportImport().runImport();
// Ensure data are imported back
Assert.assertEquals(3, adminClient.realms().findAll().size());
@ -232,10 +234,10 @@ public class ExportImportTest extends AbstractExportImportTest {
}
private void testRealmExportImport() throws LifecycleException {
testingClient.testing().setAction(ExportImportConfig.ACTION_EXPORT);
testingClient.testing().setRealmName("test");
testingClient.testing().exportImport().setAction(ExportImportConfig.ACTION_EXPORT);
testingClient.testing().exportImport().setRealmName("test");
testingClient.testing().runExport();
testingClient.testing().exportImport().runExport();
// Delete some realm (and some data in admin realm)
adminClient.realm("test").remove();
@ -248,9 +250,9 @@ public class ExportImportTest extends AbstractExportImportTest {
assertNotAuthenticated("test", "user3", "password");
// Configure import
testingClient.testing().setAction(ExportImportConfig.ACTION_IMPORT);
testingClient.testing().exportImport().setAction(ExportImportConfig.ACTION_IMPORT);
testingClient.testing().runImport();
testingClient.testing().exportImport().runImport();
// Ensure data are imported back, but just for "test" realm
Assert.assertEquals(3, adminClient.realms().findAll().size());
@ -273,27 +275,4 @@ public class ExportImportTest extends AbstractExportImportTest {
Assert.assertEquals(expectedResult, testingClient.testing().validCredentials(realmName, username, password));
}
private static String getExportImportTestDirectory() {
String dirPath = null;
String relativeDirExportImportPath = "testsuite" + File.separator +
"integration-arquillian" + File.separator +
"tests" + File.separator +
"base" + File.separator +
"target" + File.separator +
"export-import";
if (System.getProperties().containsKey("maven.home")) {
dirPath = System.getProperty("user.dir").replaceFirst("testsuite.integration.*", Matcher.quoteReplacement(relativeDirExportImportPath));
} else {
for (String c : System.getProperty("java.class.path").split(File.pathSeparator)) {
if (c.contains(File.separator + "testsuite" + File.separator + "integration-arquillian" + File.separator)) {
dirPath = c.replaceFirst("testsuite.integration-arquillian.*", Matcher.quoteReplacement(relativeDirExportImportPath));
}
}
}
String absolutePath = new File(dirPath).getAbsolutePath();
return absolutePath;
}
}

View file

@ -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 requestUriParam() {
driver.navigate().to(oauth.getLoginFormUrl() + "&request_uri=https%3A%2F%2Flocalhost%3A60784%2Fexport%2FqzHTG11W48.jwt");
public void requestUriParamUnsigned() 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());
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 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());
// 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

View file

@ -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();
}

View file

@ -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

View file

@ -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();

View file

@ -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">