Merge pull request #3187 from mposolda/master
KEYCLOAK-3349 Support for 'request' and 'request_uri' parameters
This commit is contained in:
commit
404aa69adb
39 changed files with 1330 additions and 423 deletions
|
@ -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>();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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> {
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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();
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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">
|
||||
|
|
Loading…
Reference in a new issue