unsecure url has principal
KEYCLOAK-2550 Typo in userguide KEYCLOAK-1928 Kerberos working with IBM JDK KEYCLOAK-1928 Remove sun.jdk.jgss module KEYCLOAK-1928 Fix kerberos with adapter on JDK7 KPR-147 - Initial login scenarios around admin password - test KEYCLOAK-2561 Fix issues with blank password KEYCLOAK-2559 Missing add/remove button for 'Valid Redirect URIs' in a client settings form Added simple test for JPA performance (with many users). Fixed "re-import" operation logging. Fixed for Timer.saveData() Fixed for Timer.saveData() ManyUsersTest: ArrayList --> LinkedList Fix AbstractUserTest Fix parentheses in login page object Add tests for IDP initiated login KEYCLOAK-1040 Allow import of realm keys (like we do for SAML) KEYCLOAK-2556 Remove required for client create root url and saml endpoint KEYCLOAK-2555 ForbiddenException when importing test realm or creating test user KEYCLOAK-2553 Unexpected form behavior while creating a client KEYCLOAK-2551 Broken navigation links while creating/editing a Client Mapper
This commit is contained in:
parent
a0696fcb97
commit
37584a24e0
87 changed files with 1684 additions and 263 deletions
|
@ -18,6 +18,8 @@
|
|||
package org.keycloak.adapters.jetty.core;
|
||||
|
||||
import org.eclipse.jetty.security.DefaultUserIdentity;
|
||||
import org.eclipse.jetty.security.IdentityService;
|
||||
import org.eclipse.jetty.security.LoginService;
|
||||
import org.eclipse.jetty.security.ServerAuthException;
|
||||
import org.eclipse.jetty.security.UserAuthentication;
|
||||
import org.eclipse.jetty.security.authentication.DeferredAuthentication;
|
||||
|
@ -135,10 +137,44 @@ public abstract class AbstractKeycloakJettyAuthenticator extends LoginAuthentica
|
|||
return new DefaultUserIdentity(theSubject, principal, theRoles);
|
||||
}
|
||||
|
||||
private class DummyLoginService implements LoginService {
|
||||
@Override
|
||||
public String getName() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserIdentity login(String username, Object credentials) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean validate(UserIdentity user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdentityService getIdentityService() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setIdentityService(IdentityService service) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logout(UserIdentity user) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setConfiguration(AuthConfiguration configuration) {
|
||||
//super.setConfiguration(configuration);
|
||||
initializeKeycloak();
|
||||
// need this so that getUserPrincipal does not throw NPE
|
||||
_loginService = new DummyLoginService();
|
||||
String error = configuration.getInitParameter(FormAuthenticator.__FORM_ERROR_PAGE);
|
||||
setErrorPage(error);
|
||||
}
|
||||
|
|
|
@ -89,6 +89,7 @@ public class OIDCFilterSessionStore extends FilterSessionStore implements Adapte
|
|||
|
||||
protected void cleanSession(HttpSession session) {
|
||||
session.removeAttribute(KeycloakAccount.class.getName());
|
||||
session.removeAttribute(KeycloakSecurityContext.class.getName());
|
||||
clearSavedRequest(session);
|
||||
}
|
||||
|
||||
|
@ -160,6 +161,7 @@ public class OIDCFilterSessionStore extends FilterSessionStore implements Adapte
|
|||
SerializableKeycloakAccount sAccount = new SerializableKeycloakAccount(roles, account.getPrincipal(), securityContext);
|
||||
HttpSession httpSession = request.getSession();
|
||||
httpSession.setAttribute(KeycloakAccount.class.getName(), sAccount);
|
||||
httpSession.setAttribute(KeycloakSecurityContext.class.getName(), sAccount.getKeycloakSecurityContext());
|
||||
if (idMapper != null) idMapper.map(account.getKeycloakSecurityContext().getToken().getClientSession(), account.getPrincipal().getName(), httpSession.getId());
|
||||
//String username = securityContext.getToken().getSubject();
|
||||
//log.fine("userSessionManagement.login: " + username);
|
||||
|
|
|
@ -69,12 +69,22 @@ public class CatalinaSessionTokenStore extends CatalinaAdapterSessionStore imple
|
|||
// just in case session got serialized
|
||||
if (session.getDeployment() == null) session.setCurrentRequestInfo(deployment, this);
|
||||
|
||||
if (session.isActive() && !session.getDeployment().isAlwaysRefreshToken()) return;
|
||||
if (session.isActive() && !session.getDeployment().isAlwaysRefreshToken()) {
|
||||
request.setAttribute(KeycloakSecurityContext.class.getName(), session);
|
||||
request.setUserPrincipal(account.getPrincipal());
|
||||
request.setAuthType("KEYCLOAK");
|
||||
return;
|
||||
}
|
||||
|
||||
// FYI: A refresh requires same scope, so same roles will be set. Otherwise, refresh will fail and token will
|
||||
// not be updated
|
||||
boolean success = session.refreshExpiredToken(false);
|
||||
if (success && session.isActive()) return;
|
||||
if (success && session.isActive()) {
|
||||
request.setAttribute(KeycloakSecurityContext.class.getName(), session);
|
||||
request.setUserPrincipal(account.getPrincipal());
|
||||
request.setAuthType("KEYCLOAK");
|
||||
return;
|
||||
}
|
||||
|
||||
// Refresh failed, so user is already logged out from keycloak. Cleanup and expire our session
|
||||
log.fine("Cleanup and expire session " + catalinaSession.getId() + " after failed refresh");
|
||||
|
@ -85,6 +95,8 @@ public class CatalinaSessionTokenStore extends CatalinaAdapterSessionStore imple
|
|||
}
|
||||
|
||||
protected void cleanSession(Session catalinaSession) {
|
||||
catalinaSession.getSession().removeAttribute(KeycloakSecurityContext.class.getName());
|
||||
catalinaSession.getSession().removeAttribute(SerializableKeycloakAccount.class.getName());
|
||||
catalinaSession.getSession().removeAttribute(OidcKeycloakAccount.class.getName());
|
||||
catalinaSession.setPrincipal(null);
|
||||
catalinaSession.setAuthType(null);
|
||||
|
@ -164,6 +176,7 @@ public class CatalinaSessionTokenStore extends CatalinaAdapterSessionStore imple
|
|||
session.setPrincipal(principal);
|
||||
session.setAuthType("KEYCLOAK");
|
||||
session.getSession().setAttribute(SerializableKeycloakAccount.class.getName(), sAccount);
|
||||
session.getSession().setAttribute(KeycloakSecurityContext.class.getName(), account.getKeycloakSecurityContext());
|
||||
String username = securityContext.getToken().getSubject();
|
||||
log.fine("userSessionManagement.login: " + username);
|
||||
this.sessionManagement.login(session);
|
||||
|
|
|
@ -92,7 +92,8 @@ public class ServletSessionTokenStore implements AdapterTokenStore {
|
|||
} else {
|
||||
log.debug("Refresh failed. Account was not active. Returning null and invalidating Http session");
|
||||
try {
|
||||
session.setAttribute(KeycloakUndertowAccount.class.getName(), null);
|
||||
session.removeAttribute(KeycloakUndertowAccount.class.getName());
|
||||
session.removeAttribute(KeycloakSecurityContext.class.getName());
|
||||
session.invalidate();
|
||||
} catch (Exception e) {
|
||||
log.debug("Failed to invalidate session, might already be invalidated");
|
||||
|
@ -106,6 +107,7 @@ public class ServletSessionTokenStore implements AdapterTokenStore {
|
|||
final ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY);
|
||||
HttpSession session = getSession(true);
|
||||
session.setAttribute(KeycloakUndertowAccount.class.getName(), account);
|
||||
session.setAttribute(KeycloakSecurityContext.class.getName(), account.getKeycloakSecurityContext());
|
||||
sessionManagement.login(servletRequestContext.getDeployment().getSessionManager());
|
||||
}
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ import io.undertow.server.HttpServerExchange;
|
|||
import io.undertow.server.session.Session;
|
||||
import io.undertow.util.Sessions;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.KeycloakSecurityContext;
|
||||
import org.keycloak.adapters.AdapterTokenStore;
|
||||
import org.keycloak.adapters.KeycloakDeployment;
|
||||
import org.keycloak.adapters.OidcKeycloakAccount;
|
||||
|
@ -101,6 +102,7 @@ public class UndertowSessionTokenStore implements AdapterTokenStore {
|
|||
public void saveAccountInfo(OidcKeycloakAccount account) {
|
||||
Session session = Sessions.getOrCreateSession(exchange);
|
||||
session.setAttribute(KeycloakUndertowAccount.class.getName(), account);
|
||||
session.setAttribute(KeycloakSecurityContext.class.getName(), account.getKeycloakSecurityContext());
|
||||
sessionManagement.login(session.getSessionManager());
|
||||
}
|
||||
|
||||
|
@ -111,6 +113,7 @@ public class UndertowSessionTokenStore implements AdapterTokenStore {
|
|||
KeycloakUndertowAccount account = (KeycloakUndertowAccount)session.getAttribute(KeycloakUndertowAccount.class.getName());
|
||||
if (account == null) return;
|
||||
session.removeAttribute(KeycloakUndertowAccount.class.getName());
|
||||
session.removeAttribute(KeycloakSecurityContext.class.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -18,6 +18,8 @@
|
|||
package org.keycloak.adapters.saml.jetty;
|
||||
|
||||
import org.eclipse.jetty.security.DefaultUserIdentity;
|
||||
import org.eclipse.jetty.security.IdentityService;
|
||||
import org.eclipse.jetty.security.LoginService;
|
||||
import org.eclipse.jetty.security.ServerAuthException;
|
||||
import org.eclipse.jetty.security.UserAuthentication;
|
||||
import org.eclipse.jetty.security.authentication.DeferredAuthentication;
|
||||
|
@ -135,12 +137,46 @@ public abstract class AbstractSamlAuthenticator extends LoginAuthenticator {
|
|||
|
||||
}
|
||||
|
||||
private class DummyLoginService implements LoginService {
|
||||
@Override
|
||||
public String getName() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserIdentity login(String username, Object credentials) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean validate(UserIdentity user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdentityService getIdentityService() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setIdentityService(IdentityService service) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logout(UserIdentity user) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void setConfiguration(AuthConfiguration configuration) {
|
||||
//super.setConfiguration(configuration);
|
||||
initializeKeycloak();
|
||||
// need this so that getUserPrincipal does not throw NPE
|
||||
_loginService = new DummyLoginService();
|
||||
String error = configuration.getInitParameter(FormAuthenticator.__FORM_ERROR_PAGE);
|
||||
setErrorPage(error);
|
||||
}
|
||||
|
|
|
@ -167,9 +167,9 @@ public abstract class AbstractSamlAuthenticatorValve extends FormAuthenticator i
|
|||
@Override
|
||||
public void invoke(Request request, Response response) throws IOException, ServletException {
|
||||
log.fine("*********************** SAML ************");
|
||||
if (request.getRequestURI().substring(request.getContextPath().length()).endsWith("/saml")) {
|
||||
CatalinaHttpFacade facade = new CatalinaHttpFacade(response, request);
|
||||
SamlDeployment deployment = deploymentContext.resolveDeployment(facade);
|
||||
if (request.getRequestURI().substring(request.getContextPath().length()).endsWith("/saml")) {
|
||||
if (deployment != null && deployment.isConfigured()) {
|
||||
SamlSessionStore tokenStore = getSessionStore(request, facade, deployment);
|
||||
SamlAuthenticator authenticator = new CatalinaSamlEndpoint(facade, deployment, tokenStore);
|
||||
|
@ -180,6 +180,7 @@ public abstract class AbstractSamlAuthenticatorValve extends FormAuthenticator i
|
|||
}
|
||||
|
||||
try {
|
||||
getSessionStore(request, facade, deployment).isLoggedIn(); // sets request UserPrincipal if logged in. we do this so that the UserPrincipal is available on unsecured, unconstrainted URLs
|
||||
super.invoke(request, response);
|
||||
} finally {
|
||||
}
|
||||
|
|
|
@ -17,6 +17,9 @@
|
|||
|
||||
package org.keycloak.common.constants;
|
||||
|
||||
import org.ietf.jgss.GSSException;
|
||||
import org.ietf.jgss.Oid;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
|
@ -31,19 +34,33 @@ public class KerberosConstants {
|
|||
/**
|
||||
* OID of SPNEGO mechanism. See http://www.oid-info.com/get/1.3.6.1.5.5.2
|
||||
*/
|
||||
public static final String SPNEGO_OID = "1.3.6.1.5.5.2";
|
||||
private static final String SPNEGO_OID_STR = "1.3.6.1.5.5.2";
|
||||
public static final Oid SPNEGO_OID;
|
||||
|
||||
|
||||
/**
|
||||
* OID of Kerberos v5 mechanism. See http://www.oid-info.com/get/1.2.840.113554.1.2.2
|
||||
*/
|
||||
public static final String KRB5_OID = "1.2.840.113554.1.2.2";
|
||||
private static final String KRB5_OID_STR = "1.2.840.113554.1.2.2";
|
||||
public static final Oid KRB5_OID;
|
||||
|
||||
|
||||
/**
|
||||
* OID of Kerberos v5 name. See http://www.oid-info.com/get/1.2.840.113554.1.2.2.1
|
||||
*/
|
||||
public static final String KRB5_NAME_OID = "1.2.840.113554.1.2.2.1";
|
||||
private static final String KRB5_NAME_OID_STR = "1.2.840.113554.1.2.2.1";
|
||||
public static final Oid KRB5_NAME_OID;
|
||||
|
||||
|
||||
static {
|
||||
try {
|
||||
KRB5_OID = new Oid(KerberosConstants.KRB5_OID_STR);
|
||||
KRB5_NAME_OID = new Oid(KerberosConstants.KRB5_NAME_OID_STR);
|
||||
SPNEGO_OID = new Oid(KerberosConstants.SPNEGO_OID_STR);
|
||||
} catch (GSSException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,227 @@
|
|||
/*
|
||||
* 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.common.util;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.MalformedURLException;
|
||||
import java.security.PrivilegedExceptionAction;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.security.auth.Subject;
|
||||
import javax.security.auth.kerberos.KerberosPrincipal;
|
||||
import javax.security.auth.kerberos.KerberosTicket;
|
||||
import javax.security.auth.login.AppConfigurationEntry;
|
||||
import javax.security.auth.login.Configuration;
|
||||
|
||||
import org.ietf.jgss.GSSCredential;
|
||||
import org.ietf.jgss.GSSManager;
|
||||
import org.ietf.jgss.GSSName;
|
||||
import org.keycloak.common.constants.KerberosConstants;
|
||||
|
||||
/**
|
||||
* Provides abstraction to handle differences between various JDK vendors (Sun, IBM)
|
||||
*
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public abstract class KerberosJdkProvider {
|
||||
|
||||
public abstract Configuration createJaasConfigurationForServer(String keytab, String serverPrincipal, boolean debug);
|
||||
public abstract Configuration createJaasConfigurationForUsernamePasswordLogin(boolean debug);
|
||||
|
||||
public abstract KerberosTicket gssCredentialToKerberosTicket(KerberosTicket kerberosTicket, GSSCredential gssCredential);
|
||||
|
||||
|
||||
|
||||
public GSSCredential kerberosTicketToGSSCredential(KerberosTicket kerberosTicket) {
|
||||
return kerberosTicketToGSSCredential(kerberosTicket, GSSCredential.DEFAULT_LIFETIME, GSSCredential.INITIATE_ONLY);
|
||||
}
|
||||
|
||||
// Actually same on both JDKs
|
||||
public GSSCredential kerberosTicketToGSSCredential(KerberosTicket kerberosTicket, final int lifetime, final int usage) {
|
||||
try {
|
||||
final GSSManager gssManager = GSSManager.getInstance();
|
||||
|
||||
KerberosPrincipal kerberosPrincipal = kerberosTicket.getClient();
|
||||
String krbPrincipalName = kerberosTicket.getClient().getName();
|
||||
final GSSName gssName = gssManager.createName(krbPrincipalName, KerberosConstants.KRB5_NAME_OID);
|
||||
|
||||
Set<KerberosPrincipal> principals = Collections.singleton(kerberosPrincipal);
|
||||
Set<GSSName> publicCreds = Collections.singleton(gssName);
|
||||
Set<KerberosTicket> privateCreds = Collections.singleton(kerberosTicket);
|
||||
Subject subject = new Subject(false, principals, publicCreds, privateCreds);
|
||||
|
||||
return Subject.doAs(subject, new PrivilegedExceptionAction<GSSCredential>() {
|
||||
|
||||
@Override
|
||||
public GSSCredential run() throws Exception {
|
||||
return gssManager.createCredential(gssName, lifetime, KerberosConstants.KRB5_OID, usage);
|
||||
}
|
||||
|
||||
});
|
||||
} catch (Exception e) {
|
||||
throw new KerberosSerializationUtils.KerberosSerializationException("Unexpected exception during convert KerberosTicket to GSSCredential", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static KerberosJdkProvider getProvider() {
|
||||
if (KerberosSerializationUtils.JAVA_INFO.contains("IBM")) {
|
||||
return new IBMJDKProvider();
|
||||
} else {
|
||||
return new SunJDKProvider();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// IMPL Subclasses
|
||||
|
||||
|
||||
// Works for Oracle and OpenJDK
|
||||
private static class SunJDKProvider extends KerberosJdkProvider {
|
||||
|
||||
|
||||
@Override
|
||||
public Configuration createJaasConfigurationForServer(final String keytab, final String serverPrincipal, final boolean debug) {
|
||||
return new Configuration() {
|
||||
|
||||
@Override
|
||||
public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
|
||||
Map<String, Object> options = new HashMap<>();
|
||||
options.put("storeKey", "true");
|
||||
options.put("doNotPrompt", "true");
|
||||
options.put("isInitiator", "false");
|
||||
options.put("useKeyTab", "true");
|
||||
|
||||
options.put("keyTab", keytab);
|
||||
options.put("principal", serverPrincipal);
|
||||
options.put("debug", String.valueOf(debug));
|
||||
AppConfigurationEntry kerberosLMConfiguration = new AppConfigurationEntry("com.sun.security.auth.module.Krb5LoginModule", AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options);
|
||||
return new AppConfigurationEntry[] { kerberosLMConfiguration };
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Configuration createJaasConfigurationForUsernamePasswordLogin(final boolean debug) {
|
||||
return new Configuration() {
|
||||
|
||||
@Override
|
||||
public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
|
||||
Map<String, Object> options = new HashMap<>();
|
||||
options.put("storeKey", "true");
|
||||
options.put("debug", String.valueOf(debug));
|
||||
AppConfigurationEntry kerberosLMConfiguration = new AppConfigurationEntry("com.sun.security.auth.module.Krb5LoginModule", AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options);
|
||||
return new AppConfigurationEntry[] { kerberosLMConfiguration };
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// Note: input kerberosTicket is null for Sun based JDKs
|
||||
@Override
|
||||
public KerberosTicket gssCredentialToKerberosTicket(KerberosTicket kerberosTicket, GSSCredential gssCredential) {
|
||||
try {
|
||||
Class<?> gssUtil = Class.forName("com.sun.security.jgss.GSSUtil");
|
||||
Method createSubject = gssUtil.getMethod("createSubject", GSSName.class, GSSCredential.class);
|
||||
Subject subject = (Subject) createSubject.invoke(null, null, gssCredential);
|
||||
Set<KerberosTicket> kerberosTickets = subject.getPrivateCredentials(KerberosTicket.class);
|
||||
Iterator<KerberosTicket> iterator = kerberosTickets.iterator();
|
||||
if (iterator.hasNext()) {
|
||||
return iterator.next();
|
||||
} else {
|
||||
throw new KerberosSerializationUtils.KerberosSerializationException("Not available kerberosTicket in subject credentials. Subject was: " + subject.toString());
|
||||
}
|
||||
} catch (KerberosSerializationUtils.KerberosSerializationException ke) {
|
||||
throw ke;
|
||||
} catch (Exception e) {
|
||||
throw new KerberosSerializationUtils.KerberosSerializationException("Unexpected error during convert GSSCredential to KerberosTicket", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// Works for IBM JDK
|
||||
private static class IBMJDKProvider extends KerberosJdkProvider {
|
||||
|
||||
@Override
|
||||
public Configuration createJaasConfigurationForServer(String keytab, final String serverPrincipal, final boolean debug) {
|
||||
final String keytabUrl = getKeytabURL(keytab);
|
||||
|
||||
return new Configuration() {
|
||||
|
||||
@Override
|
||||
public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
|
||||
Map<String, Object> options = new HashMap<>();
|
||||
options.put("noAddress", "true");
|
||||
options.put("credsType","acceptor");
|
||||
options.put("useKeytab", keytabUrl);
|
||||
options.put("principal", serverPrincipal);
|
||||
options.put("debug", String.valueOf(debug));
|
||||
|
||||
AppConfigurationEntry kerberosLMConfiguration = new AppConfigurationEntry("com.ibm.security.auth.module.Krb5LoginModule", AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options);
|
||||
return new AppConfigurationEntry[] { kerberosLMConfiguration };
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private String getKeytabURL(String keytab) {
|
||||
try {
|
||||
return new File(keytab).toURI().toURL().toString();
|
||||
} catch (MalformedURLException mfe) {
|
||||
System.err.println("Invalid keytab location specified in configuration: " + keytab);
|
||||
mfe.printStackTrace();
|
||||
return keytab;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Configuration createJaasConfigurationForUsernamePasswordLogin(final boolean debug) {
|
||||
return new Configuration() {
|
||||
|
||||
@Override
|
||||
public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
|
||||
Map<String, Object> options = new HashMap<>();
|
||||
options.put("credsType","initiator");
|
||||
options.put("noAddress", "true");
|
||||
options.put("debug", String.valueOf(debug));
|
||||
AppConfigurationEntry kerberosLMConfiguration = new AppConfigurationEntry("com.ibm.security.auth.module.Krb5LoginModule", AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options);
|
||||
return new AppConfigurationEntry[] { kerberosLMConfiguration };
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// For IBM, kerberosTicket was set on JAAS Subject, so we can just return it
|
||||
@Override
|
||||
public KerberosTicket gssCredentialToKerberosTicket(KerberosTicket kerberosTicket, GSSCredential gssCredential) {
|
||||
if (kerberosTicket == null) {
|
||||
throw new KerberosSerializationUtils.KerberosSerializationException("Not available kerberosTicket in subject credentials in IBM JDK");
|
||||
} else {
|
||||
return kerberosTicket;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -25,22 +25,13 @@ import java.io.ObjectInputStream;
|
|||
import java.io.ObjectOutput;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.io.Serializable;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Iterator;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.security.auth.Subject;
|
||||
import javax.security.auth.kerberos.KerberosTicket;
|
||||
|
||||
import org.ietf.jgss.GSSCredential;
|
||||
import org.ietf.jgss.GSSException;
|
||||
import org.ietf.jgss.GSSManager;
|
||||
import org.ietf.jgss.Oid;
|
||||
import org.keycloak.common.constants.KerberosConstants;
|
||||
import org.keycloak.common.util.reflections.Reflections;
|
||||
import sun.security.jgss.GSSCredentialImpl;
|
||||
import sun.security.jgss.GSSManagerImpl;
|
||||
import sun.security.jgss.krb5.Krb5InitCredential;
|
||||
import sun.security.jgss.krb5.Krb5NameElement;
|
||||
import sun.security.jgss.spi.GSSCredentialSpi;
|
||||
import sun.security.krb5.Credentials;
|
||||
|
||||
/**
|
||||
* Provides serialization/deserialization of kerberos {@link org.ietf.jgss.GSSCredential}, so it can be transmitted from auth-server to the application
|
||||
|
@ -50,18 +41,9 @@ import sun.security.krb5.Credentials;
|
|||
*/
|
||||
public class KerberosSerializationUtils {
|
||||
|
||||
public static final Oid KRB5_OID;
|
||||
public static final Oid KRB5_NAME_OID;
|
||||
public static final String JAVA_INFO;
|
||||
|
||||
static {
|
||||
try {
|
||||
KRB5_OID = new Oid(KerberosConstants.KRB5_OID);
|
||||
KRB5_NAME_OID = new Oid(KerberosConstants.KRB5_NAME_OID);
|
||||
} catch (GSSException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
String javaVersion = System.getProperty("java.version");
|
||||
String javaRuntimeVersion = System.getProperty("java.runtime.version");
|
||||
String javaVendor = System.getProperty("java.vendor");
|
||||
|
@ -72,50 +54,17 @@ public class KerberosSerializationUtils {
|
|||
private KerberosSerializationUtils() {
|
||||
}
|
||||
|
||||
public static String serializeCredential(GSSCredential gssCredential) throws KerberosSerializationException {
|
||||
public static String serializeCredential(KerberosTicket kerberosTicket, GSSCredential gssCredential) throws KerberosSerializationException {
|
||||
try {
|
||||
if (gssCredential == null) {
|
||||
throw new KerberosSerializationException("Null credential given as input");
|
||||
}
|
||||
|
||||
if (!(gssCredential instanceof GSSCredentialImpl)) {
|
||||
throw new KerberosSerializationException("Unknown credential type: " + gssCredential.getClass());
|
||||
}
|
||||
kerberosTicket = KerberosJdkProvider.getProvider().gssCredentialToKerberosTicket(kerberosTicket, gssCredential);
|
||||
|
||||
GSSCredentialImpl gssCredImpl = (GSSCredentialImpl) gssCredential;
|
||||
Oid[] mechs = gssCredImpl.getMechs();
|
||||
|
||||
for (Oid oid : mechs) {
|
||||
if (oid.equals(KRB5_OID)) {
|
||||
int usage = gssCredImpl.getUsage(oid);
|
||||
boolean initiate = (usage == GSSCredential.INITIATE_ONLY || usage == GSSCredential.INITIATE_AND_ACCEPT);
|
||||
|
||||
GSSCredentialSpi credentialSpi = gssCredImpl.getElement(oid, initiate);
|
||||
if (credentialSpi instanceof Krb5InitCredential) {
|
||||
Krb5InitCredential credential = (Krb5InitCredential) credentialSpi;
|
||||
KerberosTicket kerberosTicket = new KerberosTicket(credential.getEncoded(),
|
||||
credential.getClient(),
|
||||
credential.getServer(),
|
||||
credential.getSessionKey().getEncoded(),
|
||||
credential.getSessionKeyType(),
|
||||
credential.getFlags(),
|
||||
credential.getAuthTime(),
|
||||
credential.getStartTime(),
|
||||
credential.getEndTime(),
|
||||
credential.getRenewTill(),
|
||||
credential.getClientAddresses());
|
||||
return serialize(kerberosTicket);
|
||||
} else {
|
||||
throw new KerberosSerializationException("Unsupported type of credentialSpi: " + credentialSpi.getClass());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new KerberosSerializationException("Kerberos credential not found. Available mechanisms: " + mechs);
|
||||
} catch (IOException e) {
|
||||
throw new KerberosSerializationException("Exception occured", e);
|
||||
} catch (GSSException e) {
|
||||
throw new KerberosSerializationException("Exception occured", e);
|
||||
throw new KerberosSerializationException("Unexpected exception when serialize GSSCredential", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -132,32 +81,12 @@ public class KerberosSerializationUtils {
|
|||
}
|
||||
|
||||
KerberosTicket ticket = (KerberosTicket) deserializedCred;
|
||||
String fullName = ticket.getClient().getName();
|
||||
|
||||
Method getInstance = Reflections.findDeclaredMethod(Krb5NameElement.class, "getInstance", String.class, Oid.class);
|
||||
Krb5NameElement krb5Name = Reflections.invokeMethod(true, getInstance, Krb5NameElement.class, null, fullName, KRB5_NAME_OID);
|
||||
|
||||
Credentials krb5CredsInternal = new Credentials(
|
||||
ticket.getEncoded(),
|
||||
ticket.getClient().getName(),
|
||||
ticket.getServer().getName(),
|
||||
ticket.getSessionKey().getEncoded(),
|
||||
ticket.getSessionKeyType(),
|
||||
ticket.getFlags(),
|
||||
ticket.getAuthTime(),
|
||||
ticket.getStartTime(),
|
||||
ticket.getEndTime(),
|
||||
ticket.getRenewTill(),
|
||||
ticket.getClientAddresses()
|
||||
);
|
||||
|
||||
Method getInstance2 = Reflections.findDeclaredMethod(Krb5InitCredential.class, "getInstance", Krb5NameElement.class, Credentials.class);
|
||||
Krb5InitCredential initCredential = Reflections.invokeMethod(true, getInstance2, Krb5InitCredential.class, null, krb5Name, krb5CredsInternal);
|
||||
|
||||
GSSManagerImpl manager = (GSSManagerImpl) GSSManager.getInstance();
|
||||
return new GSSCredentialImpl(manager, initCredential);
|
||||
return KerberosJdkProvider.getProvider().kerberosTicketToGSSCredential(ticket);
|
||||
} catch (KerberosSerializationException ke) {
|
||||
throw ke;
|
||||
} catch (Exception ioe) {
|
||||
throw new KerberosSerializationException("Exception occured", ioe);
|
||||
throw new KerberosSerializationException("Unexpected exception when deserialize GSSCredential", ioe);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
59
core/src/main/java/org/keycloak/KeyPairVerifier.java
Normal file
59
core/src/main/java/org/keycloak/KeyPairVerifier.java
Normal file
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
import org.keycloak.common.VerificationException;
|
||||
import org.keycloak.common.util.PemUtils;
|
||||
import org.keycloak.jose.jws.JWSBuilder;
|
||||
import org.keycloak.jose.jws.JWSInput;
|
||||
import org.keycloak.jose.jws.crypto.RSAProvider;
|
||||
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public class KeyPairVerifier {
|
||||
|
||||
public static void verify(String privateKeyPem, String publicKeyPem) throws VerificationException {
|
||||
PrivateKey privateKey;
|
||||
try {
|
||||
privateKey = PemUtils.decodePrivateKey(privateKeyPem);
|
||||
} catch (Exception e) {
|
||||
throw new VerificationException("Failed to decode private key");
|
||||
}
|
||||
|
||||
PublicKey publicKey;
|
||||
try {
|
||||
publicKey = PemUtils.decodePublicKey(publicKeyPem);
|
||||
} catch (Exception e) {
|
||||
throw new VerificationException("Failed to decode public key");
|
||||
}
|
||||
|
||||
try {
|
||||
String jws = new JWSBuilder().content("content".getBytes()).rsa256(privateKey);
|
||||
if (!RSAProvider.verify(new JWSInput(jws), publicKey)) {
|
||||
throw new VerificationException("Keys don't match");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new VerificationException("Keys don't match");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -28,6 +28,9 @@ import java.io.ObjectOutputStream;
|
|||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Available in secured requests under HttpServlerRequest.getAttribute()
|
||||
* Also available in HttpSession.getAttribute under the classname of this class
|
||||
*
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
|
|
69
core/src/test/java/org/keycloak/KeyPairVerifierTest.java
Normal file
69
core/src/test/java/org/keycloak/KeyPairVerifierTest.java
Normal file
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.common.VerificationException;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public class KeyPairVerifierTest {
|
||||
|
||||
String privateKey1 = "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=";
|
||||
String publicKey1 = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB";
|
||||
|
||||
String privateKey2048 = "-----BEGIN RSA PRIVATE KEY-----\n" + "MIIEpQIBAAKCAQEA4V3MpOnuKsdBbR1UzNjK9o5meEMQ4s5Vpykhv1DpqTilKOiE\n"
|
||||
+ "H7VQ/XtjNxw0yjnFBilCnpK6yN9mDEHbBEzaRjtdrgVhkIejiaXFBP5MBhUQ5l9u\n" + "8E3IZC3E8pwDjVF0Z9u0R4lGeUg2k6O+NKumqIvxoLCTuG0zf53bctGsRd57LuFi\n"
|
||||
+ "pgCkNyxvscOhulsbEMYrLwlb5bMGgx9v+RCnwvunNEb7RK+5pzP+iH1MRejRsX+U\n" + "7h9zHRn2gQhIl7SzG9GXebuPWr4KKwfMHWy0PEuQrsfWRXm9/dTEavbfNkv5E53z\n"
|
||||
+ "WXjWyf93ezkVhBX0YoXmf6UO7PAlvsrjno3TuwIDAQABAoIBAQC5iCAOcCtLemhp\n" + "bOlADwXgPtErFoNTROyMxjbrKrCCSIjniawj8oAvfiHq38Sx6ydBcDxREZjF/+wi\n"
|
||||
+ "ESE+hAp6ISt5NSLh+lhu3FK7TqLFqxgTn+NT36Umm+t0k231LGa5jcz3y5KCDCoq\n" + "F3ZiJCH6xeLxGA00mmn4GLvt5aF+jiO80ICGs4iUg99IoXhc5u/VU0hB5J78BinW\n"
|
||||
+ "inkCABuBNkDLgIqc9BoH4L5MOx3zDqzmHffeq9+2V4X7NiD5QyiyWtABaQpEIY5k\n" + "R48RTno6xN3hvG48/DwkO2gABSLQ/OJd3Hupv4wlmmSc1xo93CaV44hq2i2GsU1i\n"
|
||||
+ "m6d3xDW5AoGBAPCfkvPkqr88xg+8Cu3G/3GFpUsQ0VEme+8dIjXMTJHa13K7xaRh\n" + "GHCVg4a8oHJ/P/vNSwvPyR71iRX4csqkKSaprvJk8vxbU539unmHWKkfUHrywQlz\n"
|
||||
+ "q4OuXOjOdvILLOTsu3/+k6vAIE6SZJiDmf2eGxi9Qbm5rlxE3h3HRAKfAoGBAO/E\n" + "ogHV86LmnJTJbx1hP3IfRHk0qaiSj35ljlAz+3v6GN/KSUYCWTtp2GjRIKY3qQ8I\n"
|
||||
+ "7l+PVTFg3SY7cPq2C9TE+6xroiWkUd2JldPLYSxpWpFNYlo709SzmLquDho+fwJC\n" + "nAxoxKghsXJarz7TRfNyFqDXscS6oQLurU9P5lVlAoGBAJh1QvLtS5Jnu0Z06qfF\n"
|
||||
+ "kkwnVZe+TCGStKvIVciobUts0V2Mw6lnK8kJspBIK5DgN3YfmREe0lufTwBwrqre\n" + "YIRytro2ZA6o/s332ZLuwqpFgQSlktGeTGnerFeFma+6jPNvW025y27jCJVABCTu\n"
|
||||
+ "HT+oUZrXLzGyCFvF9sX/X4QZAoGBAICap4r0h0nJCBOGN+M6Vh2QR9n7NUUF15Gk\n" + "R0EdoLZO3yiqB8NVXydPDpSqFykQkc1OrQz0hG2H1xa6q07OdmoZfiRtVvt5t69s\n"
|
||||
+ "LMD9RZHcsIdfSnG7xVNBQZpf4ZCSFO3RbIH7b//+kn8TxQudptd9SkXba65prBM2\n" + "kh8IbDNBAoGAVsKvkruH7RK7CimDSWcdAKvHARqkjs/PoeKEEY8Yu6zf0Z9TQM5l\n"
|
||||
+ "uC9EwBamYcSusWRcdcz+9HYG58XFnmXq+3EUuFbJ+Ljb8YWBgePjSHDoS/6+/+zq\n" + "B1b5uQp/jYFbYQl50UPRPTF+ul1eQoy7F43Ngj3/5cDRarFZe3ZTzZo=\n"
|
||||
+ "-----END RSA PRIVATE KEY-----";
|
||||
String publicKey2048 = "-----BEGIN PUBLIC KEY-----\n" + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4V3MpOnuKsdBbR1UzNjK\n"
|
||||
+ "9o5meEMQ4s5Vpykhv1DpqTilKOiEH7VQ/XtjNxw0yjnFBilCnpK6yN9mDEHbBEza\n" + "RjtdrgVhkIejiaXFBP5MBhUQ5l9u8E3IZC3E8pwDjVF0Z9u0R4lGeUg2k6O+NKum\n"
|
||||
+ "qIvxoLCTuG0zf53bctGsRd57LuFipgCkNyxvscOhulsbEMYrLwlb5bMGgx9v+RCn\n" + "wvunNEb7RK+5pzP+iH1MRejRsX+U7h9zHRn2gQhIl7SzG9GXebuPWr4KKwfMHWy0\n"
|
||||
+ "PEuQrsfWRXm9/dTEavbfNkv5E53zWXjWyf93ezkVhBX0YoXmf6UO7PAlvsrjno3T\n" + "uwIDAQAB\n" + "-----END PUBLIC KEY-----";
|
||||
|
||||
@Test
|
||||
public void verify() throws Exception {
|
||||
KeyPairVerifier.verify(privateKey1, publicKey1);
|
||||
KeyPairVerifier.verify(privateKey2048, publicKey2048);
|
||||
|
||||
try {
|
||||
KeyPairVerifier.verify(privateKey1, publicKey2048);
|
||||
Assert.fail("Expected VerificationException");
|
||||
} catch (VerificationException e) {
|
||||
}
|
||||
|
||||
try {
|
||||
KeyPairVerifier.verify(privateKey2048, publicKey1);
|
||||
Assert.fail("Expected VerificationException");
|
||||
} catch (VerificationException e) {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -28,7 +28,6 @@
|
|||
<module name="javax.api"/>
|
||||
<module name="javax.activation.api"/>
|
||||
<module name="sun.jdk" optional="true" />
|
||||
<module name="sun.jdk.jgss" optional="true" />
|
||||
</dependencies>
|
||||
|
||||
</module>
|
||||
|
|
|
@ -33,7 +33,6 @@
|
|||
<module name="javax.api"/>
|
||||
<module name="javax.activation.api"/>
|
||||
<module name="sun.jdk" optional="true" />
|
||||
<module name="sun.jdk.jgss" optional="true" />
|
||||
</dependencies>
|
||||
|
||||
</module>
|
||||
|
|
|
@ -28,7 +28,6 @@
|
|||
<module name="javax.api"/>
|
||||
<module name="javax.activation.api"/>
|
||||
<module name="sun.jdk" optional="true" />
|
||||
<module name="sun.jdk.jgss" optional="true" />
|
||||
</dependencies>
|
||||
|
||||
</module>
|
||||
|
|
|
@ -33,7 +33,6 @@
|
|||
<module name="javax.api"/>
|
||||
<module name="javax.activation.api"/>
|
||||
<module name="sun.jdk" optional="true" />
|
||||
<module name="sun.jdk.jgss" optional="true" />
|
||||
</dependencies>
|
||||
|
||||
</module>
|
||||
|
|
|
@ -28,7 +28,6 @@
|
|||
<module name="javax.api"/>
|
||||
<module name="javax.activation.api"/>
|
||||
<module name="sun.jdk" optional="true" />
|
||||
<module name="sun.jdk.jgss" optional="true" />
|
||||
</dependencies>
|
||||
|
||||
</module>
|
||||
|
|
|
@ -33,7 +33,6 @@
|
|||
<module name="javax.api"/>
|
||||
<module name="javax.activation.api"/>
|
||||
<module name="sun.jdk" optional="true" />
|
||||
<module name="sun.jdk.jgss" optional="true" />
|
||||
</dependencies>
|
||||
|
||||
</module>
|
||||
|
|
|
@ -28,7 +28,6 @@
|
|||
<module name="javax.api"/>
|
||||
<module name="javax.activation.api"/>
|
||||
<module name="sun.jdk" optional="true" />
|
||||
<module name="sun.jdk.jgss" optional="true" />
|
||||
</dependencies>
|
||||
|
||||
</module>
|
||||
|
|
|
@ -33,7 +33,6 @@
|
|||
<module name="javax.api"/>
|
||||
<module name="javax.activation.api"/>
|
||||
<module name="sun.jdk" optional="true" />
|
||||
<module name="sun.jdk.jgss" optional="true" />
|
||||
</dependencies>
|
||||
|
||||
</module>
|
||||
|
|
|
@ -28,7 +28,6 @@
|
|||
<module name="javax.api"/>
|
||||
<module name="javax.activation.api"/>
|
||||
<module name="sun.jdk" optional="true" />
|
||||
<module name="sun.jdk.jgss" optional="true" />
|
||||
</dependencies>
|
||||
|
||||
</module>
|
||||
|
|
|
@ -33,7 +33,6 @@
|
|||
<module name="javax.api"/>
|
||||
<module name="javax.activation.api"/>
|
||||
<module name="sun.jdk" optional="true" />
|
||||
<module name="sun.jdk.jgss" optional="true" />
|
||||
</dependencies>
|
||||
|
||||
</module>
|
||||
|
|
|
@ -28,7 +28,6 @@
|
|||
<module name="javax.api"/>
|
||||
<module name="javax.activation.api"/>
|
||||
<module name="sun.jdk" optional="true" />
|
||||
<module name="sun.jdk.jgss" optional="true" />
|
||||
</dependencies>
|
||||
|
||||
</module>
|
||||
|
|
|
@ -28,7 +28,6 @@
|
|||
<module name="javax.api"/>
|
||||
<module name="javax.activation.api"/>
|
||||
<module name="sun.jdk" optional="true" />
|
||||
<module name="sun.jdk.jgss" optional="true" />
|
||||
</dependencies>
|
||||
|
||||
</module>
|
||||
|
|
12
docbook/auth-server-docs/reference/en/en-US/modules/adapter-context.xml
Executable file
12
docbook/auth-server-docs/reference/en/en-US/modules/adapter-context.xml
Executable file
|
@ -0,0 +1,12 @@
|
|||
<chapter>
|
||||
<title>KeycloakSecurityContext</title>
|
||||
<para>
|
||||
The <literal>KeycloakSecurityContext</literal> interface is available if you need to look at the access token directly. This context is also useful if you need to
|
||||
get the encoded access token so you can make additional REST invocations. In servlet environments it is available in secured invocations as an attribute in HttpServletRequest.
|
||||
Or, it is available in secure and insecure requests in the HttpSession for browser apps.
|
||||
<programlisting>
|
||||
httpServletRequest.getAttribute(KeycloakSecurityContext.class.getName());
|
||||
httpServletRequest.getSession().getAttribute(KeycloakSecurityContext.class.getName());
|
||||
</programlisting>
|
||||
</para>
|
||||
</chapter>
|
|
@ -155,7 +155,7 @@ Authorization: basic BASE64(client-id + ':' + client-secret)
|
|||
</para>
|
||||
<para>
|
||||
To retrieve the Adapter Configuration then do a HTTP GET to:
|
||||
<literal><KEYCLOAK URL>//realms/<realm>/clients-registrations/installation/<client id></literal>
|
||||
<literal><KEYCLOAK URL>//realms/<realm>/clients-registrations/install/<client id></literal>
|
||||
</para>
|
||||
<para>
|
||||
No authentication is required for public clients. This means that for the JavaScript adapter you can
|
||||
|
|
|
@ -52,7 +52,7 @@ public abstract class CommonKerberosConfig {
|
|||
return getConfig().get(KerberosConstants.KEYTAB);
|
||||
}
|
||||
|
||||
public boolean getDebug() {
|
||||
public boolean isDebug() {
|
||||
return Boolean.valueOf(getConfig().get(KerberosConstants.DEBUG));
|
||||
}
|
||||
|
||||
|
|
|
@ -17,16 +17,18 @@
|
|||
|
||||
package org.keycloak.federation.kerberos.impl;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.security.auth.Subject;
|
||||
import javax.security.auth.login.AppConfigurationEntry;
|
||||
import javax.security.auth.callback.Callback;
|
||||
import javax.security.auth.callback.CallbackHandler;
|
||||
import javax.security.auth.callback.UnsupportedCallbackException;
|
||||
import javax.security.auth.login.Configuration;
|
||||
import javax.security.auth.login.LoginContext;
|
||||
import javax.security.auth.login.LoginException;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.util.KerberosJdkProvider;
|
||||
import org.keycloak.federation.kerberos.CommonKerberosConfig;
|
||||
|
||||
/**
|
||||
|
@ -36,20 +38,32 @@ public class KerberosServerSubjectAuthenticator {
|
|||
|
||||
private static final Logger logger = Logger.getLogger(KerberosServerSubjectAuthenticator.class);
|
||||
|
||||
private static final CallbackHandler NO_CALLBACK_HANDLER = new CallbackHandler() {
|
||||
|
||||
@Override
|
||||
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
|
||||
throw new UnsupportedCallbackException(callbacks[0]);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
private final CommonKerberosConfig config;
|
||||
private LoginContext loginContext;
|
||||
|
||||
|
||||
public KerberosServerSubjectAuthenticator(CommonKerberosConfig config) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
|
||||
public Subject authenticateServerSubject() throws LoginException {
|
||||
Configuration config = createJaasConfiguration();
|
||||
loginContext = new LoginContext("does-not-matter", null, null, config);
|
||||
loginContext = new LoginContext("does-not-matter", null, NO_CALLBACK_HANDLER, config);
|
||||
loginContext.login();
|
||||
return loginContext.getSubject();
|
||||
}
|
||||
|
||||
|
||||
public void logoutServerSubject() {
|
||||
if (loginContext != null) {
|
||||
try {
|
||||
|
@ -60,24 +74,9 @@ public class KerberosServerSubjectAuthenticator {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
protected Configuration createJaasConfiguration() {
|
||||
return new Configuration() {
|
||||
|
||||
@Override
|
||||
public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
|
||||
Map<String, Object> options = new HashMap<String, Object>();
|
||||
options.put("storeKey", "true");
|
||||
options.put("doNotPrompt", "true");
|
||||
options.put("isInitiator", "false");
|
||||
options.put("useKeyTab", "true");
|
||||
|
||||
options.put("keyTab", config.getKeyTab());
|
||||
options.put("principal", config.getServerPrincipal());
|
||||
options.put("debug", String.valueOf(config.getDebug()));
|
||||
AppConfigurationEntry kerberosLMConfiguration = new AppConfigurationEntry("com.sun.security.auth.module.Krb5LoginModule", AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options);
|
||||
return new AppConfigurationEntry[] { kerberosLMConfiguration };
|
||||
}
|
||||
};
|
||||
return KerberosJdkProvider.getProvider().createJaasConfigurationForServer(config.getKeyTab(), config.getServerPrincipal(), config.isDebug());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@ import javax.security.auth.login.LoginContext;
|
|||
import javax.security.auth.login.LoginException;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.util.KerberosJdkProvider;
|
||||
import org.keycloak.federation.kerberos.CommonKerberosConfig;
|
||||
import org.keycloak.models.ModelException;
|
||||
|
||||
|
@ -58,7 +59,7 @@ public class KerberosUsernamePasswordAuthenticator {
|
|||
* @return true if user available
|
||||
*/
|
||||
public boolean isUserAvailable(String username) {
|
||||
logger.debug("Checking existence of user: " + username);
|
||||
logger.debugf("Checking existence of user: %s", username);
|
||||
try {
|
||||
String principal = getKerberosPrincipal(username);
|
||||
loginContext = new LoginContext("does-not-matter", null,
|
||||
|
@ -70,7 +71,7 @@ public class KerberosUsernamePasswordAuthenticator {
|
|||
throw new IllegalStateException("Didn't expect to end here");
|
||||
} catch (LoginException le) {
|
||||
String message = le.getMessage();
|
||||
logger.debug("Message from kerberos: " + message);
|
||||
logger.debugf("Message from kerberos: %s", message);
|
||||
|
||||
checkKerberosServerAvailable(le);
|
||||
|
||||
|
@ -128,6 +129,7 @@ public class KerberosUsernamePasswordAuthenticator {
|
|||
return loginContext.getSubject();
|
||||
}
|
||||
|
||||
|
||||
public void logoutSubject() {
|
||||
if (loginContext != null) {
|
||||
try {
|
||||
|
@ -139,7 +141,6 @@ public class KerberosUsernamePasswordAuthenticator {
|
|||
}
|
||||
|
||||
|
||||
|
||||
protected String getKerberosPrincipal(String username) throws LoginException {
|
||||
if (username.contains("@")) {
|
||||
String[] tokens = username.split("@");
|
||||
|
@ -156,6 +157,7 @@ public class KerberosUsernamePasswordAuthenticator {
|
|||
return username + "@" + config.getKerberosRealm();
|
||||
}
|
||||
|
||||
|
||||
protected CallbackHandler createJaasCallbackHandler(final String principal, final String password) {
|
||||
return new CallbackHandler() {
|
||||
|
||||
|
@ -176,17 +178,8 @@ public class KerberosUsernamePasswordAuthenticator {
|
|||
};
|
||||
}
|
||||
|
||||
protected Configuration createJaasConfiguration() {
|
||||
return new Configuration() {
|
||||
|
||||
@Override
|
||||
public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
|
||||
Map<String, Object> options = new HashMap<String, Object>();
|
||||
options.put("storeKey", "true");
|
||||
options.put("debug", String.valueOf(config.getDebug()));
|
||||
AppConfigurationEntry kerberosLMConfiguration = new AppConfigurationEntry("com.sun.security.auth.module.Krb5LoginModule", AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options);
|
||||
return new AppConfigurationEntry[] { kerberosLMConfiguration };
|
||||
}
|
||||
};
|
||||
protected Configuration createJaasConfiguration() {
|
||||
return KerberosJdkProvider.getProvider().createJaasConfigurationForUsernamePasswordLogin(config.isDebug());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,15 +19,21 @@ package org.keycloak.federation.kerberos.impl;
|
|||
|
||||
import java.io.IOException;
|
||||
import java.security.PrivilegedExceptionAction;
|
||||
import java.util.Iterator;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.security.auth.Subject;
|
||||
import javax.security.auth.kerberos.KerberosTicket;
|
||||
|
||||
import org.ietf.jgss.Oid;
|
||||
import org.keycloak.common.constants.KerberosConstants;
|
||||
import org.keycloak.common.util.Base64;
|
||||
import org.ietf.jgss.GSSContext;
|
||||
import org.ietf.jgss.GSSCredential;
|
||||
import org.ietf.jgss.GSSException;
|
||||
import org.ietf.jgss.GSSManager;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.util.KerberosJdkProvider;
|
||||
import org.keycloak.federation.kerberos.CommonKerberosConfig;
|
||||
import org.keycloak.common.util.KerberosSerializationUtils;
|
||||
|
||||
|
@ -45,6 +51,7 @@ public class SPNEGOAuthenticator {
|
|||
private boolean authenticated = false;
|
||||
private String authenticatedKerberosPrincipal = null;
|
||||
private GSSCredential delegationCredential;
|
||||
private KerberosTicket kerberosTicket;
|
||||
private String responseToken = null;
|
||||
|
||||
public SPNEGOAuthenticator(CommonKerberosConfig kerberosConfig, KerberosServerSubjectAuthenticator kerberosSubjectAuthenticator, String spnegoToken) {
|
||||
|
@ -61,6 +68,14 @@ public class SPNEGOAuthenticator {
|
|||
try {
|
||||
Subject serverSubject = kerberosSubjectAuthenticator.authenticateServerSubject();
|
||||
authenticated = Subject.doAs(serverSubject, new AcceptSecContext());
|
||||
|
||||
// kerberosTicketis available in IBM JDK in case that GSSContext supports delegated credentials
|
||||
Set<KerberosTicket> kerberosTickets = serverSubject.getPrivateCredentials(KerberosTicket.class);
|
||||
Iterator<KerberosTicket> iterator = kerberosTickets.iterator();
|
||||
if (iterator.hasNext()) {
|
||||
kerberosTicket = iterator.next();
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.warn("SPNEGO login failed", e);
|
||||
} finally {
|
||||
|
@ -89,7 +104,7 @@ public class SPNEGOAuthenticator {
|
|||
if (log.isTraceEnabled()) {
|
||||
log.trace("Serializing credential " + delegationCredential);
|
||||
}
|
||||
return KerberosSerializationUtils.serializeCredential(delegationCredential);
|
||||
return KerberosSerializationUtils.serializeCredential(kerberosTicket, delegationCredential);
|
||||
} catch (KerberosSerializationUtils.KerberosSerializationException kse) {
|
||||
log.warn("Couldn't serialize credential: " + delegationCredential, kse);
|
||||
return null;
|
||||
|
@ -150,7 +165,10 @@ public class SPNEGOAuthenticator {
|
|||
|
||||
protected GSSContext establishContext() throws GSSException, IOException {
|
||||
GSSManager manager = GSSManager.getInstance();
|
||||
GSSContext gssContext = manager.createContext((GSSCredential) null);
|
||||
|
||||
Oid[] supportedMechs = new Oid[] { KerberosConstants.KRB5_OID, KerberosConstants.SPNEGO_OID };
|
||||
GSSCredential gssCredential = manager.createCredential(null, GSSCredential.INDEFINITE_LIFETIME, supportedMechs, GSSCredential.ACCEPT_ONLY);
|
||||
GSSContext gssContext = manager.createContext(gssCredential);
|
||||
|
||||
byte[] inputToken = Base64.decode(spnegoToken);
|
||||
byte[] respToken = gssContext.acceptSecContext(inputToken, 0, inputToken.length);
|
||||
|
|
|
@ -22,6 +22,7 @@ import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
|
|||
import org.jboss.resteasy.client.jaxrs.ResteasyWebTarget;
|
||||
import org.keycloak.admin.client.Config;
|
||||
import org.keycloak.admin.client.resource.BasicAuthFilter;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.representations.AccessTokenResponse;
|
||||
|
||||
import javax.ws.rs.BadRequestException;
|
||||
|
@ -34,8 +35,11 @@ import java.util.Date;
|
|||
*/
|
||||
public class TokenManager {
|
||||
|
||||
private static final long DEFAULT_MIN_VALIDITY = 30;
|
||||
|
||||
private AccessTokenResponse currentToken;
|
||||
private Date expirationTime;
|
||||
private long expirationTime;
|
||||
private long minTokenValidity = DEFAULT_MIN_VALIDITY;
|
||||
private final Config config;
|
||||
private final ResteasyClient client;
|
||||
|
||||
|
@ -73,10 +77,11 @@ public class TokenManager {
|
|||
|
||||
TokenService tokenService = target.proxy(TokenService.class);
|
||||
|
||||
AccessTokenResponse response = tokenService.grantToken(config.getRealm(), form.asMap());
|
||||
int requestTime = Time.currentTime();
|
||||
currentToken = tokenService.grantToken(config.getRealm(), form.asMap());
|
||||
expirationTime = requestTime + currentToken.getExpiresIn();
|
||||
|
||||
defineCurrentToken(response);
|
||||
return response;
|
||||
return currentToken;
|
||||
}
|
||||
|
||||
public AccessTokenResponse refreshToken(){
|
||||
|
@ -95,27 +100,22 @@ public class TokenManager {
|
|||
TokenService tokenService = target.proxy(TokenService.class);
|
||||
|
||||
try {
|
||||
AccessTokenResponse response = tokenService.refreshToken(config.getRealm(), form.asMap());
|
||||
defineCurrentToken(response);
|
||||
return response;
|
||||
int requestTime = Time.currentTime();
|
||||
currentToken = tokenService.refreshToken(config.getRealm(), form.asMap());
|
||||
expirationTime = requestTime + currentToken.getExpiresIn();
|
||||
|
||||
return currentToken;
|
||||
} catch (BadRequestException e) {
|
||||
return grantToken();
|
||||
}
|
||||
}
|
||||
|
||||
private void setExpirationTime() {
|
||||
Calendar cal = Calendar.getInstance();
|
||||
cal.add(Calendar.SECOND, (int) currentToken.getExpiresIn());
|
||||
expirationTime = cal.getTime();
|
||||
public void setMinTokenValidity(long minTokenValidity) {
|
||||
this.minTokenValidity = minTokenValidity;
|
||||
}
|
||||
|
||||
private boolean tokenExpired() {
|
||||
return new Date().after(expirationTime);
|
||||
}
|
||||
|
||||
private void defineCurrentToken(AccessTokenResponse accessTokenResponse){
|
||||
currentToken = accessTokenResponse;
|
||||
setExpirationTime();
|
||||
return (Time.currentTime() + minTokenValidity) >= expirationTime;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -36,10 +36,10 @@ public interface TokenService {
|
|||
|
||||
@POST
|
||||
@Path("/realms/{realm}/protocol/openid-connect/token")
|
||||
public AccessTokenResponse grantToken(@PathParam("realm") String realm, MultivaluedMap<String, String> map);
|
||||
AccessTokenResponse grantToken(@PathParam("realm") String realm, MultivaluedMap<String, String> map);
|
||||
|
||||
@POST
|
||||
@Path("/realms/{realm}/protocol/openid-connect/token")
|
||||
public AccessTokenResponse refreshToken(@PathParam("realm") String realm, MultivaluedMap<String, String> map);
|
||||
AccessTokenResponse refreshToken(@PathParam("realm") String realm, MultivaluedMap<String, String> map);
|
||||
|
||||
}
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
|
||||
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!--
|
||||
~ Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
~ and other contributors as indicated by the @author tags.
|
||||
|
@ -19,17 +16,8 @@
|
|||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<module xmlns="urn:jboss:module:1.3" name="sun.jdk.jgss">
|
||||
<resources>
|
||||
</resources>
|
||||
<dependencies>
|
||||
<system export="true">
|
||||
<paths>
|
||||
<path name="sun/security/jgss" />
|
||||
<path name="sun/security/jgss/spi" />
|
||||
<path name="sun/security/jgss/krb5" />
|
||||
</paths>
|
||||
</system>
|
||||
</dependencies>
|
||||
|
||||
</module>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
|
||||
<changeSet author="keycloak" id="1.9.1">
|
||||
<modifyDataType tableName="REALM" columnName="PRIVATE_KEY" newDataType="VARCHAR(4096)"/>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
|
@ -30,4 +30,5 @@
|
|||
<include file="META-INF/jpa-changelog-1.7.0.xml"/>
|
||||
<include file="META-INF/jpa-changelog-1.8.0.xml"/>
|
||||
<include file="META-INF/jpa-changelog-1.9.0.xml"/>
|
||||
<include file="META-INF/jpa-changelog-1.9.1.xml"/>
|
||||
</databaseChangeLog>
|
||||
|
|
|
@ -86,7 +86,7 @@ public class Pbkdf2PasswordHashProvider implements PasswordHashProviderFactory,
|
|||
byte[] key = getSecretKeyFactory().generateSecret(spec).getEncoded();
|
||||
return Base64.encodeBytes(key);
|
||||
} catch (InvalidKeySpecException e) {
|
||||
throw new RuntimeException("Credential could not be encoded");
|
||||
throw new RuntimeException("Credential could not be encoded", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -101,7 +101,7 @@ public class Pbkdf2PasswordHashProvider implements PasswordHashProviderFactory,
|
|||
try {
|
||||
return SecretKeyFactory.getInstance(PBKDF2_ALGORITHM);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException("PBKDF2 algorithm not found");
|
||||
throw new RuntimeException("PBKDF2 algorithm not found", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -71,7 +71,7 @@ public class CredentialValidation {
|
|||
|
||||
|
||||
public static boolean validateHashedCredential(KeycloakSession session, RealmModel realm, UserModel user, String unhashedCredValue, UserCredentialValueModel credential) {
|
||||
if(unhashedCredValue == null){
|
||||
if (unhashedCredValue == null || unhashedCredValue.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -695,6 +695,16 @@ public class RepresentationToModel {
|
|||
|
||||
if ("GENERATE".equals(rep.getPublicKey())) {
|
||||
KeycloakModelUtils.generateRealmKeys(realm);
|
||||
} else {
|
||||
if (rep.getPrivateKey() != null && rep.getPublicKey() != null) {
|
||||
realm.setPrivateKeyPem(rep.getPrivateKey());
|
||||
realm.setPublicKeyPem(rep.getPublicKey());
|
||||
realm.setCodeSecret(KeycloakModelUtils.generateCodeSecret());
|
||||
}
|
||||
|
||||
if (rep.getCertificate() != null) {
|
||||
realm.setCertificatePem(rep.getCertificate());
|
||||
}
|
||||
}
|
||||
|
||||
if(rep.isInternationalizationEnabled() != null){
|
||||
|
|
|
@ -621,7 +621,7 @@ public class AccountService extends AbstractSecuredLocalService {
|
|||
}
|
||||
}
|
||||
|
||||
if (Validation.isEmpty(passwordNew)) {
|
||||
if (Validation.isBlank(passwordNew)) {
|
||||
setReferrerOnPage();
|
||||
return account.setError(Messages.MISSING_PASSWORD).createResponse(AccountPages.PASSWORD);
|
||||
}
|
||||
|
|
|
@ -20,7 +20,10 @@ import org.jboss.resteasy.annotations.cache.NoCache;
|
|||
import org.jboss.resteasy.spi.BadRequestException;
|
||||
import org.jboss.resteasy.spi.NotFoundException;
|
||||
import org.jboss.resteasy.spi.ResteasyProviderFactory;
|
||||
import org.keycloak.KeyPairVerifier;
|
||||
import org.keycloak.common.ClientConnection;
|
||||
import org.keycloak.common.VerificationException;
|
||||
import org.keycloak.common.util.PemUtils;
|
||||
import org.keycloak.events.Event;
|
||||
import org.keycloak.events.EventQuery;
|
||||
import org.keycloak.events.EventStoreProvider;
|
||||
|
@ -30,6 +33,8 @@ import org.keycloak.events.admin.AdminEventQuery;
|
|||
import org.keycloak.events.admin.OperationType;
|
||||
import org.keycloak.exportimport.ClientDescriptionConverter;
|
||||
import org.keycloak.exportimport.ClientDescriptionConverterFactory;
|
||||
import org.keycloak.jose.jws.JWSBuilder;
|
||||
import org.keycloak.jose.jws.JWSInput;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.GroupModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
@ -74,8 +79,11 @@ import javax.ws.rs.core.Context;
|
|||
import javax.ws.rs.core.HttpHeaders;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.Response.Status;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
|
@ -236,6 +244,14 @@ public class RealmAdminResource {
|
|||
|
||||
logger.debug("updating realm: " + realm.getName());
|
||||
try {
|
||||
if (!"GENERATE".equals(rep.getPublicKey()) && (rep.getPrivateKey() != null && rep.getPublicKey() != null)) {
|
||||
try {
|
||||
KeyPairVerifier.verify(rep.getPrivateKey(), rep.getPublicKey());
|
||||
} catch (VerificationException e) {
|
||||
return ErrorResponse.error(e.getMessage(), Status.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
RepresentationToModel.updateRealm(rep, realm);
|
||||
|
||||
// Refresh periodic sync tasks for configured federationProviders
|
||||
|
@ -253,7 +269,7 @@ public class RealmAdminResource {
|
|||
throw e;
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage(), e);
|
||||
return ErrorResponse.error("Failed to update " + rep.getRealm() + " Realm.", Response.Status.INTERNAL_SERVER_ERROR);
|
||||
return ErrorResponse.error("Failed to update realm", Response.Status.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -93,6 +93,7 @@ import org.keycloak.services.managers.BruteForceProtector;
|
|||
import org.keycloak.services.managers.UserSessionManager;
|
||||
import org.keycloak.services.resources.AccountService;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.services.validation.Validation;
|
||||
|
||||
/**
|
||||
* Base resource for managing users
|
||||
|
@ -707,6 +708,9 @@ public class UsersResource {
|
|||
if (pass == null || pass.getValue() == null || !CredentialRepresentation.PASSWORD.equals(pass.getType())) {
|
||||
throw new BadRequestException("No password provided");
|
||||
}
|
||||
if (Validation.isBlank(pass.getValue())) {
|
||||
throw new BadRequestException("Empty password not allowed");
|
||||
}
|
||||
|
||||
UserCredentialModel cred = RepresentationToModel.convertCredential(pass);
|
||||
try {
|
||||
|
|
|
@ -152,6 +152,7 @@ integration-arquillian
|
|||
│
|
||||
├──console (activated by -Pconsole-ui-tests)
|
||||
├──mod_auth_mellon (activated by -Pmod_auth_mellon)
|
||||
├──console_no_users (activated by -Pconsole-ui-no-users-tests)
|
||||
└──...
|
||||
```
|
||||
|
||||
|
|
|
@ -17,7 +17,9 @@
|
|||
package org.keycloak.testsuite.arquillian;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -34,6 +36,8 @@ public final class TestContext {
|
|||
|
||||
private boolean adminLoggedIn;
|
||||
|
||||
private final Map customContext = new HashMap<>();
|
||||
|
||||
public TestContext(SuiteContext suiteContext, Class testClass) {
|
||||
this.suiteContext = suiteContext;
|
||||
this.testClass = testClass;
|
||||
|
@ -88,4 +92,12 @@ public final class TestContext {
|
|||
+ (isAdapterTest() ? "App server container: " + getAppServerInfo() + "\n" : "");
|
||||
}
|
||||
|
||||
public Object getCustomValue(Object key) {
|
||||
return customContext.get(key);
|
||||
}
|
||||
|
||||
public void setCustomValue(Object key, Object value) {
|
||||
customContext.put(key, value);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
package org.keycloak.testsuite.auth.page;
|
||||
|
||||
import org.openqa.selenium.By;
|
||||
import org.openqa.selenium.WebElement;
|
||||
import org.openqa.selenium.support.FindBy;
|
||||
|
||||
|
@ -38,7 +39,8 @@ public class WelcomePage extends AuthServer {
|
|||
private WebElement createButton;
|
||||
|
||||
public boolean isPasswordSet() {
|
||||
return !driver.getPageSource().contains("Please create an initial admin user to get started.");
|
||||
return !(driver.getPageSource().contains("Please create an initial admin user to get started.") ||
|
||||
driver.getPageSource().contains("You need local access to create the initial admin user."));
|
||||
}
|
||||
|
||||
public void setPassword(String username, String password) {
|
||||
|
@ -58,4 +60,8 @@ public class WelcomePage extends AuthServer {
|
|||
}
|
||||
}
|
||||
|
||||
public void navigateToAdminConsole() {
|
||||
driver.findElement(By.linkText("Administration Console")).click();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ public abstract class Login extends AuthRealm {
|
|||
@Override
|
||||
public UriBuilder createUriBuilder() {
|
||||
return super.createUriBuilder()
|
||||
.path((getProtocol().equals(OIDC) || getProtocol().equals(SAML)) ? "protocol/" : "" + "{" + PROTOCOL + "}" + (getProtocol().equals(OIDC) ? "/auth" : ""));
|
||||
.path(((getProtocol().equals(OIDC) || getProtocol().equals(SAML)) ? "protocol/" : "") + "{" + PROTOCOL + "}" + (getProtocol().equals(OIDC) ? "/auth" : ""));
|
||||
}
|
||||
|
||||
public void setProtocol(String protocol) {
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
package org.keycloak.testsuite.auth.page.login;
|
||||
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
|
||||
/**
|
||||
* @author mhajas
|
||||
*/
|
||||
public class SAMLIDPInitiatedLogin extends SAMLRedirectLogin {
|
||||
|
||||
public void setUrlName(String urlName) {
|
||||
setUriParameter("clientUrlName", urlName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UriBuilder createUriBuilder() {
|
||||
return super.createUriBuilder().path("clients/{clientUrlName}");
|
||||
}
|
||||
}
|
||||
|
|
@ -43,6 +43,8 @@ public class IOUtil {
|
|||
|
||||
private static final Logger log = Logger.getLogger(IOUtil.class);
|
||||
|
||||
public static final File PROJECT_BUILD_DIRECTORY = new File(System.getProperty("project.build.directory", "target"));
|
||||
|
||||
public static <T> T loadJson(InputStream is, Class<T> type) {
|
||||
try {
|
||||
return JsonSerialization.readValue(is, type);
|
||||
|
|
|
@ -14,15 +14,28 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.testsuite.util;
|
||||
|
||||
import java.text.MessageFormat;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.jfree.chart.ChartFactory;
|
||||
import org.jfree.chart.ChartUtilities;
|
||||
import org.jfree.chart.JFreeChart;
|
||||
import org.jfree.chart.plot.PlotOrientation;
|
||||
import org.jfree.data.xy.XYSeries;
|
||||
import org.jfree.data.xy.XYSeriesCollection;
|
||||
import static org.keycloak.testsuite.util.IOUtil.PROJECT_BUILD_DIRECTORY;
|
||||
import static org.jgroups.util.Util.assertTrue;
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -30,45 +43,129 @@ import java.util.Map;
|
|||
*/
|
||||
public class Timer {
|
||||
|
||||
private static Long time;
|
||||
public static final Timer DEFAULT = new Timer();
|
||||
|
||||
private static final Map<String, List<Long>> stats = new HashMap<>();
|
||||
protected final Logger log = Logger.getLogger(Timer.class);
|
||||
|
||||
public static void time() {
|
||||
time = new Date().getTime();
|
||||
}
|
||||
protected static final File DATA_DIR = new File(PROJECT_BUILD_DIRECTORY, "stats/data");
|
||||
protected static final File CHARTS_DIR = new File(PROJECT_BUILD_DIRECTORY, "stats/charts");
|
||||
|
||||
public static void time(String operation) {
|
||||
public static final String DEFAULT_OPERATION = "DEFAULT_OPERATION";
|
||||
|
||||
private Long time;
|
||||
private String operation = DEFAULT_OPERATION;
|
||||
private final Map<String, List<Long>> stats = new TreeMap<>();
|
||||
|
||||
public long elapsedTime() {
|
||||
long elapsedTime = 0;
|
||||
if (time == null) {
|
||||
System.out.println(MessageFormat.format("Starting timer for operation {0}", operation));
|
||||
time();
|
||||
} else {
|
||||
long timeOrig = time;
|
||||
time();
|
||||
logOperation(operation, time - timeOrig);
|
||||
System.out.println(MessageFormat.format("Operation {0} took {1} ms", operation, time - timeOrig));
|
||||
elapsedTime = new Date().getTime() - time;
|
||||
}
|
||||
return elapsedTime;
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
reset(operation); // log last operation
|
||||
}
|
||||
|
||||
public void reset(String operation) {
|
||||
reset(operation, true);
|
||||
}
|
||||
|
||||
public void reset(String newOperation, boolean logOperationOnChange) {
|
||||
if (time != null) {
|
||||
if (operation.equals(newOperation) || logOperationOnChange) {
|
||||
logOperation(operation, elapsedTime());
|
||||
}
|
||||
}
|
||||
time = new Date().getTime();
|
||||
if (!operation.equals(newOperation)) {
|
||||
operation = newOperation;
|
||||
log.info(String.format("Operation '%s' started.", newOperation));
|
||||
}
|
||||
}
|
||||
|
||||
private static void logOperation(String operation, long delta) {
|
||||
private void logOperation(String operation, long duration) {
|
||||
if (!stats.containsKey(operation)) {
|
||||
stats.put(operation, new ArrayList<Long>());
|
||||
}
|
||||
stats.get(operation).add(delta);
|
||||
stats.get(operation).add(duration);
|
||||
log.info(String.format("Operation '%s' took: %s ms", operation, duration));
|
||||
}
|
||||
|
||||
public static void printStats() {
|
||||
if (!stats.isEmpty()) {
|
||||
System.out.println("OPERATION STATS:");
|
||||
public void clearStats() {
|
||||
clearStats(true, true, true);
|
||||
}
|
||||
|
||||
public void clearStats(boolean logStats, boolean saveData, boolean saveCharts) {
|
||||
if (logStats) {
|
||||
log.info("Timer Statistics:");
|
||||
for (String op : stats.keySet()) {
|
||||
long sum = 0;
|
||||
for (Long t : stats.get(op)) {
|
||||
sum += t;
|
||||
for (Long duration : stats.get(op)) {
|
||||
sum += duration;
|
||||
}
|
||||
log.info(String.format("Operation '%s' average: %s ms", op, sum / stats.get(op).size()));
|
||||
}
|
||||
}
|
||||
if (PROJECT_BUILD_DIRECTORY.exists()) {
|
||||
DATA_DIR.mkdirs();
|
||||
CHARTS_DIR.mkdirs();
|
||||
for (String op : stats.keySet()) {
|
||||
if (saveData) {
|
||||
saveData(op);
|
||||
}
|
||||
if (saveCharts) {
|
||||
saveChart(op);
|
||||
}
|
||||
}
|
||||
System.out.println(MessageFormat.format("Operation {0} average time: {1,number,#} ms", op, sum / stats.get(op).size()));
|
||||
}
|
||||
stats.clear();
|
||||
}
|
||||
|
||||
private void saveData(String op) {
|
||||
try {
|
||||
File f = new File(DATA_DIR, op.replace(" ", "_") + ".txt");
|
||||
if (!f.createNewFile()) {
|
||||
throw new IOException("Couldn't create file: " + f);
|
||||
}
|
||||
OutputStream stream = new BufferedOutputStream(new FileOutputStream(f));
|
||||
for (Long duration : stats.get(op)) {
|
||||
IOUtils.write(duration.toString(), stream);
|
||||
IOUtils.write("\n", stream);
|
||||
}
|
||||
stream.flush();
|
||||
IOUtils.closeQuietly(stream);
|
||||
} catch (IOException ex) {
|
||||
log.error("Unable to save data for operation '" + op + "'", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void saveChart(String op) {
|
||||
XYSeries series = new XYSeries(op);
|
||||
int i = 0;
|
||||
for (Long duration : stats.get(op)) {
|
||||
series.add(++i, duration);
|
||||
}
|
||||
final XYSeriesCollection data = new XYSeriesCollection(series);
|
||||
final JFreeChart chart = ChartFactory.createXYLineChart(
|
||||
op,
|
||||
"Operations",
|
||||
"Duration (ms)",
|
||||
data,
|
||||
PlotOrientation.VERTICAL,
|
||||
true,
|
||||
true,
|
||||
false
|
||||
);
|
||||
try {
|
||||
ChartUtilities.saveChartAsPNG(
|
||||
new File(CHARTS_DIR, op.replace(" ", "_") + ".png"),
|
||||
chart, 640, 480);
|
||||
} catch (IOException ex) {
|
||||
log.warn("Unable to save chart for operation '" + op + "'.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
*/
|
||||
package org.keycloak.testsuite;
|
||||
|
||||
import java.io.File;
|
||||
import org.keycloak.testsuite.arquillian.TestContext;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
@ -36,7 +37,6 @@ import org.keycloak.admin.client.resource.RealmsResource;
|
|||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import static org.keycloak.testsuite.admin.Users.setPasswordFor;
|
||||
import org.keycloak.testsuite.arquillian.AuthServerTestEnricher;
|
||||
import org.keycloak.testsuite.arquillian.SuiteContext;
|
||||
import org.keycloak.testsuite.auth.page.WelcomePage;
|
||||
|
@ -52,6 +52,8 @@ import org.keycloak.testsuite.auth.page.login.OIDCLogin;
|
|||
import org.keycloak.testsuite.auth.page.login.UpdatePassword;
|
||||
import org.keycloak.testsuite.util.Timer;
|
||||
import org.keycloak.testsuite.util.WaitUtils;
|
||||
import static org.keycloak.testsuite.admin.Users.setPasswordFor;
|
||||
import static org.keycloak.testsuite.admin.Users.setPasswordFor;
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -124,7 +126,6 @@ public abstract class AbstractKeycloakTest {
|
|||
public void afterAbstractKeycloakTest() {
|
||||
// removeTestRealms(); // keeping test realms after test to be able to inspect failures, instead deleting existing realms before import
|
||||
// keycloak.close(); // keeping admin connection open
|
||||
Timer.printStats();
|
||||
}
|
||||
|
||||
private void updateMasterAdminPassword() {
|
||||
|
|
|
@ -62,6 +62,9 @@ public class ChangePasswordTest extends AbstractAccountManagementTest {
|
|||
|
||||
testRealmChangePasswordPage.changePasswords(correctPassword, NEW_PASSWORD, NEW_PASSWORD + "-mismatch");
|
||||
assertAlertError();
|
||||
|
||||
testRealmChangePasswordPage.changePasswords(correctPassword, " ", " ");
|
||||
assertAlertError();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
0
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/ResetCredentialsTest.java
Executable file → Normal file
0
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/ResetCredentialsTest.java
Executable file → Normal file
|
@ -27,6 +27,7 @@ import org.keycloak.representations.idm.RealmRepresentation;
|
|||
import org.keycloak.testsuite.adapter.AbstractServletsAdapterTest;
|
||||
import org.keycloak.testsuite.adapter.page.*;
|
||||
import org.keycloak.testsuite.admin.ApiUtil;
|
||||
import org.keycloak.testsuite.auth.page.login.SAMLIDPInitiatedLogin;
|
||||
import org.keycloak.testsuite.util.IOUtil;
|
||||
import org.w3c.dom.Document;
|
||||
|
||||
|
@ -81,6 +82,9 @@ public abstract class AbstractSAMLServletsAdapterTest extends AbstractServletsAd
|
|||
@Page
|
||||
private SalesPostSigTransientServlet salesPostSigTransientServletPage;
|
||||
|
||||
@Page
|
||||
private SAMLIDPInitiatedLogin samlidpInitiatedLogin;
|
||||
|
||||
@Deployment(name = BadClientSalesPostSigServlet.DEPLOYMENT_NAME)
|
||||
protected static WebArchive badClientSalesPostSig() {
|
||||
return samlServletDeployment(BadClientSalesPostSigServlet.DEPLOYMENT_NAME, SendUsernameServlet.class);
|
||||
|
@ -458,4 +462,20 @@ public abstract class AbstractSAMLServletsAdapterTest extends AbstractServletsAd
|
|||
assertTrue(driver.getPageSource().contains("Forbidden") || driver.getPageSource().contains("Status 403"));
|
||||
salesPostSigTransientServletPage.logout();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void idpInitiatedLogin() {
|
||||
samlidpInitiatedLogin.setAuthRealm(SAMLSERVLETDEMO);
|
||||
samlidpInitiatedLogin.setUrlName("employee2");
|
||||
samlidpInitiatedLogin.navigateTo();
|
||||
samlidpInitiatedLogin.form().login(bburkeUser);
|
||||
|
||||
employee2ServletPage.navigateTo();
|
||||
assertTrue(driver.getPageSource().contains("principal=bburke"));
|
||||
|
||||
salesPostSigServletPage.navigateTo();
|
||||
assertTrue(driver.getPageSource().contains("principal=bburke"));
|
||||
|
||||
employee2ServletPage.logout();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
package org.keycloak.testsuite.user;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
import static javax.ws.rs.core.Response.Status.CREATED;
|
||||
import org.keycloak.admin.client.resource.UserResource;
|
||||
import org.keycloak.admin.client.resource.UsersResource;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import static org.keycloak.testsuite.admin.ApiUtil.getCreatedId;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import org.keycloak.testsuite.AbstractAuthTest;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author tkyjovsk
|
||||
*/
|
||||
public abstract class AbstractUserTest extends AbstractAuthTest {
|
||||
|
||||
protected UsersResource users() {
|
||||
return testRealmResource().users();
|
||||
}
|
||||
|
||||
protected UserResource user(UserRepresentation user) {
|
||||
if (user.getId()==null) {
|
||||
throw new IllegalStateException("User id cannot be null.");
|
||||
}
|
||||
return user(user.getId());
|
||||
}
|
||||
|
||||
protected UserResource user(String id) {
|
||||
return users().get(id);
|
||||
}
|
||||
|
||||
public static UserRepresentation createUserRep(String username) {
|
||||
UserRepresentation user = new UserRepresentation();
|
||||
user.setUsername(username);
|
||||
user.setEmail(username + "@email.test");
|
||||
return user;
|
||||
}
|
||||
|
||||
public UserRepresentation createUser(UserRepresentation user) {
|
||||
return createUser(users(), user);
|
||||
}
|
||||
|
||||
public UserRepresentation createUser(UsersResource users, UserRepresentation user) {
|
||||
Response response = users.create(user);
|
||||
assertEquals(CREATED.getStatusCode(), response.getStatus());
|
||||
user.setId(getCreatedId(response));
|
||||
response.close();
|
||||
return user;
|
||||
}
|
||||
|
||||
}
|
|
@ -299,7 +299,8 @@
|
|||
],
|
||||
"adminUrl": "http://localhost:8080/employee2",
|
||||
"attributes": {
|
||||
"saml.authnstatement": "true"
|
||||
"saml.authnstatement": "true",
|
||||
"saml_idp_initiated_sso_url_name" : "employee2"
|
||||
},
|
||||
"protocolMappers": [
|
||||
{
|
||||
|
|
|
@ -60,7 +60,7 @@
|
|||
<property name="enabled">${auth.server.wildfly}</property>
|
||||
<property name="adapterImplClass">org.jboss.as.arquillian.container.managed.ManagedDeployableContainer</property>
|
||||
<property name="jbossHome">${keycloak.home}</property>
|
||||
<property name="javaVmArguments">-Djboss.socket.binding.port-offset=${auth.server.port.offset} -Xms64m -Xmx512m -XX:MaxPermSize=256m ${adapter.test.props}</property>
|
||||
<property name="javaVmArguments">-Djboss.socket.binding.port-offset=${auth.server.port.offset} -Djboss.bind.address=0.0.0.0 -Xms64m -Xmx512m -XX:MaxPermSize=256m ${adapter.test.props}</property>
|
||||
<property name="managementPort">${auth.server.management.port}</property>
|
||||
<property name="startupTimeoutInSeconds">${startup.timeout.sec}</property>
|
||||
</configuration>
|
||||
|
@ -131,7 +131,7 @@
|
|||
<property name="enabled">${auth.server.eap7}</property>
|
||||
<property name="adapterImplClass">org.jboss.as.arquillian.container.managed.ManagedDeployableContainer</property>
|
||||
<property name="jbossHome">${keycloak.home}</property>
|
||||
<property name="javaVmArguments">-Djboss.socket.binding.port-offset=${auth.server.port.offset} -Xms64m -Xmx512m -XX:MaxPermSize=256m ${adapter.test.props}</property>
|
||||
<property name="javaVmArguments">-Djboss.socket.binding.port-offset=${auth.server.port.offset} -Djboss.bind.address=0.0.0.0 -Xms64m -Xmx512m -XX:MaxPermSize=256m ${adapter.test.props}</property>
|
||||
<property name="startupTimeoutInSeconds">${startup.timeout.sec}</property>
|
||||
<property name="managementPort">${auth.server.management.port}</property>
|
||||
</configuration>
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
<?xml version="1.0"?>
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
|
||||
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.keycloak.testsuite</groupId>
|
||||
<artifactId>integration-arquillian-tests-other</artifactId>
|
||||
<version>1.9.1.Final-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>integration-arquillian-tests-console-no-users</artifactId>
|
||||
|
||||
<name>Admin Console UI Tests - Without pre-configured accounts</name>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-resources-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>copy-admin-user-json-file</id>
|
||||
<phase>none</phase>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
|
@ -0,0 +1,112 @@
|
|||
package org.keycloak.testsuite.console.pages;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.NetworkInterface;
|
||||
import java.net.URL;
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
import java.util.List;
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.FixMethodOrder;
|
||||
import org.junit.Test;
|
||||
import org.junit.runners.MethodSorters;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.testsuite.AbstractKeycloakTest;
|
||||
import org.keycloak.testsuite.auth.page.WelcomePage;
|
||||
import org.keycloak.testsuite.auth.page.login.OIDCLogin;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
|
||||
public class WelcomePageTest extends AbstractKeycloakTest {
|
||||
|
||||
@Page
|
||||
private WelcomePage welcomePage;
|
||||
|
||||
@Page
|
||||
protected OIDCLogin loginPage;
|
||||
|
||||
@Override
|
||||
public void addTestRealms(List<RealmRepresentation> testRealms) {
|
||||
// no operation
|
||||
}
|
||||
|
||||
/*
|
||||
* Leave out client initialization and creation of a user account. We
|
||||
* don't need those.
|
||||
*/
|
||||
@Before
|
||||
@Override
|
||||
public void beforeAbstractKeycloakTest() {
|
||||
setDefaultPageUriParameters();
|
||||
driverSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to resolve the floating IP address. This is where EAP/WildFly
|
||||
* will be accessible. See "-Djboss.bind.address=0.0.0.0".
|
||||
*
|
||||
* @return
|
||||
* @throws Exception
|
||||
*/
|
||||
private String getFloatingIpAddress() throws Exception {
|
||||
Enumeration<NetworkInterface> netInterfaces = NetworkInterface.getNetworkInterfaces();
|
||||
for (NetworkInterface ni : Collections.list(netInterfaces)) {
|
||||
Enumeration<InetAddress> inetAddresses = ni.getInetAddresses();
|
||||
for (InetAddress a : Collections.list(inetAddresses)) {
|
||||
if (!a.isLoopbackAddress() && a.isSiteLocalAddress()) {
|
||||
return a.getHostAddress();
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private URL getPublicServerUrl() throws Exception {
|
||||
String floatingIp = getFloatingIpAddress();
|
||||
if (floatingIp == null) {
|
||||
throw new RuntimeException("Could not determine floating IP address.");
|
||||
}
|
||||
return new URL("http", floatingIp, welcomePage.getInjectedUrl().getPort(), "");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_1_LocalAccessNoAdmin() throws Exception {
|
||||
welcomePage.navigateTo();
|
||||
Assert.assertFalse("Welcome page did not ask to create a new admin user.", welcomePage.isPasswordSet());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_2_RemoteAccessNoAdmin() throws Exception {
|
||||
driver.navigate().to(getPublicServerUrl());
|
||||
Assert.assertFalse("Welcome page did not ask to create a new admin user.", welcomePage.isPasswordSet());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_3_LocalAccessWithAdmin() throws Exception {
|
||||
welcomePage.navigateTo();
|
||||
welcomePage.setPassword("admin", "admin");
|
||||
Assert.assertTrue(driver.getPageSource().contains("User created"));
|
||||
|
||||
welcomePage.navigateTo();
|
||||
Assert.assertTrue("Welcome page asked to set admin password.", welcomePage.isPasswordSet());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_4_RemoteAccessWithAdmin() throws Exception {
|
||||
driver.navigate().to(getPublicServerUrl());
|
||||
Assert.assertTrue("Welcome page asked to set admin password.", welcomePage.isPasswordSet());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_5_AccessCreatedAdminAccount() throws Exception {
|
||||
welcomePage.navigateToAdminConsole();
|
||||
loginPage.form().login("admin", "admin");
|
||||
Assert.assertFalse("Login with 'admin:admin' failed",
|
||||
driver.getPageSource().contains("Invalid username or password."));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
# Keycloak JPA Performance Tests
|
||||
|
||||
## How to run
|
||||
|
||||
1. Build the Arquilian Base Testsuite module: `/testsuite/integration-arquillian/base`
|
||||
2. Run the test from this module using `mvn test` or `mvn clean test`.
|
||||
|
||||
Optional parameters:
|
||||
```
|
||||
-Dmany.users.count=10000
|
||||
-Dmany.users.batch=1000
|
||||
```
|
||||
|
||||
### With MySQL
|
||||
|
||||
Start dockerized MySQL:
|
||||
```
|
||||
docker run --name mysql-keycloak -e MYSQL_ROOT_PASSWORD=keycloak -e MYSQL_DATABASE=keycloak -e MYSQL_USER=keycloak -e MYSQL_PASSWORD=keycloak -d -p 3306:3306 mysql
|
||||
```
|
||||
|
||||
Additional test parameters:
|
||||
```
|
||||
-Pclean-jpa
|
||||
-Dkeycloak.connectionsJpa.url=jdbc:mysql://localhost/keycloak
|
||||
-Dkeycloak.connectionsJpa.driver=com.mysql.jdbc.Driver
|
||||
-Dkeycloak.connectionsJpa.user=keycloak
|
||||
-Dkeycloak.connectionsJpa.password=keycloak
|
||||
```
|
||||
|
||||
### With PostgreSQL
|
||||
|
||||
Start dockerized PostgreSQL:
|
||||
```
|
||||
docker run --name postgres-keycloak -e POSTGRES_PASSWORD=keycloak -d -p 5432:5432 postgres
|
||||
```
|
||||
|
||||
Additional test parameters:
|
||||
```
|
||||
-Pclean-jpa
|
||||
-Dkeycloak.connectionsJpa.url=jdbc:postgresql://localhost/postgres
|
||||
-Dkeycloak.connectionsJpa.driver=org.postgresql.Driver
|
||||
-Dkeycloak.connectionsJpa.user=postgres
|
||||
-Dkeycloak.connectionsJpa.password=keycloak
|
||||
```
|
||||
|
||||
## Reports
|
||||
|
||||
Test creates reports in `target/stats`.
|
|
@ -0,0 +1,34 @@
|
|||
<?xml version="1.0"?>
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
|
||||
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.keycloak.testsuite</groupId>
|
||||
<artifactId>integration-arquillian-tests-other</artifactId>
|
||||
<version>1.9.1.Final-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>integration-arquillian-tests-jpa-performance</artifactId>
|
||||
|
||||
<name>Keycloak JPA Performance Tests</name>
|
||||
|
||||
</project>
|
|
@ -0,0 +1,119 @@
|
|||
package org.keycloak.testsuite.user;
|
||||
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.testsuite.util.Timer;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
import static org.junit.Assert.fail;
|
||||
import org.keycloak.admin.client.resource.RealmResource;
|
||||
import static org.keycloak.testsuite.util.IOUtil.PROJECT_BUILD_DIRECTORY;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author tkyjovsk
|
||||
*/
|
||||
public class ManyUsersTest extends AbstractUserTest {
|
||||
|
||||
private static final int COUNT = Integer.parseInt(System.getProperty("many.users.count", "10000"));
|
||||
private static final int BATCH = Integer.parseInt(System.getProperty("many.users.batch", "1000"));
|
||||
|
||||
private static final String REALM = "realm_with_many_users";
|
||||
|
||||
private List<UserRepresentation> users;
|
||||
|
||||
private final Timer realmTimer = new Timer();
|
||||
private final Timer usersTimer = new Timer();
|
||||
|
||||
protected RealmResource realmResource() {
|
||||
return realmsResouce().realm(REALM);
|
||||
}
|
||||
|
||||
@Before
|
||||
public void before() {
|
||||
users = new LinkedList<>();
|
||||
for (int i = 0; i < COUNT; i++) {
|
||||
users.add(createUserRep("user" + i));
|
||||
}
|
||||
|
||||
realmTimer.reset("create realm before test");
|
||||
RealmRepresentation realm = new RealmRepresentation();
|
||||
realm.setRealm(REALM);
|
||||
realmsResouce().create(realm);
|
||||
}
|
||||
|
||||
@After
|
||||
public void after() {
|
||||
realmTimer.clearStats(true, true, false);
|
||||
usersTimer.clearStats();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void manyUsers() throws IOException {
|
||||
RealmRepresentation realm = realmResource().toRepresentation();
|
||||
realm.setUsers(users);
|
||||
|
||||
// CREATE
|
||||
realmTimer.reset("create " + users.size() + " users");
|
||||
usersTimer.reset("create " + BATCH + " users");
|
||||
int i = 0;
|
||||
for (UserRepresentation user : users) {
|
||||
createUser(realmResource().users(), user);
|
||||
if (++i % BATCH == 0) {
|
||||
usersTimer.reset();
|
||||
log.info("Created users: " + i + " / " + users.size());
|
||||
}
|
||||
}
|
||||
if (i % BATCH != 0) {
|
||||
usersTimer.reset();
|
||||
log.info("Created users: " + i + " / " + users.size());
|
||||
}
|
||||
|
||||
// SAVE REALM
|
||||
realmTimer.reset("save realm with " + users.size() + " users");
|
||||
File realmFile = new File(PROJECT_BUILD_DIRECTORY, REALM + ".json");
|
||||
JsonSerialization.writeValueToStream(new BufferedOutputStream(new FileOutputStream(realmFile)), realm);
|
||||
|
||||
// DELETE REALM
|
||||
realmTimer.reset("delete realm with " + users.size() + " users");
|
||||
realmResource().remove();
|
||||
try {
|
||||
realmResource().toRepresentation();
|
||||
fail("realm not deleted");
|
||||
} catch (Exception ex) {
|
||||
log.debug("realm deleted");
|
||||
}
|
||||
|
||||
// RE-IMPORT SAVED REALM
|
||||
realmTimer.reset("re-import realm with " + realm.getUsers().size() + " users");
|
||||
realmsResouce().create(realm);
|
||||
realmTimer.reset("load " + realm.getUsers().size() + " users");
|
||||
users = realmResource().users().search("", 0, Integer.MAX_VALUE);
|
||||
|
||||
// DELETE INDIVIDUAL USERS
|
||||
realmTimer.reset("delete " + users.size() + " users");
|
||||
usersTimer.reset("delete " + BATCH + " users", false);
|
||||
i = 0;
|
||||
for (UserRepresentation user : users) {
|
||||
realmResource().users().get(user.getId()).remove();
|
||||
if (++i % BATCH == 0) {
|
||||
usersTimer.reset();
|
||||
log.info("Deleted users: " + i + " / " + users.size());
|
||||
}
|
||||
}
|
||||
if (i % BATCH != 0) {
|
||||
usersTimer.reset();
|
||||
log.info("Deleted users: " + i + " / " + users.size());
|
||||
}
|
||||
realmTimer.reset();
|
||||
}
|
||||
|
||||
}
|
|
@ -134,12 +134,24 @@
|
|||
<module>console</module>
|
||||
</modules>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>console-ui-no-users-tests</id>
|
||||
<modules>
|
||||
<module>console_no_users</module>
|
||||
</modules>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>mod_auth_mellon</id>
|
||||
<modules>
|
||||
<module>mod_auth_mellon</module>
|
||||
</modules>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>jpa-performance</id>
|
||||
<modules>
|
||||
<module>jpa-performance</module>
|
||||
</modules>
|
||||
</profile>
|
||||
</profiles>
|
||||
|
||||
</project>
|
||||
|
|
|
@ -108,6 +108,7 @@
|
|||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<configuration>
|
||||
<systemPropertyVariables>
|
||||
<project.build.directory>${project.build.directory}</project.build.directory>
|
||||
<browser>${browser}</browser>
|
||||
<firefox_binary>${firefox_binary}</firefox_binary>
|
||||
<shouldDeploy>false</shouldDeploy>
|
||||
|
@ -223,6 +224,12 @@
|
|||
<version>2.1.0.Alpha3</version><!-- TODO upgrade <arquillian-graphene.version> and use ${arquillian-graphene.version} -->
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>jfree</groupId>
|
||||
<artifactId>jfreechart</artifactId>
|
||||
<version>1.0.13</version>
|
||||
</dependency>
|
||||
|
||||
<!-- <dependency>
|
||||
<groupId>org.arquillian.extension</groupId>
|
||||
<artifactId>arquillian-recorder-reporter-impl</artifactId>
|
||||
|
@ -411,10 +418,83 @@
|
|||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>xml-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.liquibase</groupId>
|
||||
<artifactId>liquibase-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
|
||||
<!-- MySQL -->
|
||||
<profile>
|
||||
<activation>
|
||||
<property>
|
||||
<name>keycloak.connectionsJpa.driver</name>
|
||||
<value>com.mysql.jdbc.Driver</value>
|
||||
</property>
|
||||
</activation>
|
||||
<id>mysql</id>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>mysql</groupId>
|
||||
<artifactId>mysql-connector-java</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</profile>
|
||||
|
||||
<!-- PostgreSQL -->
|
||||
<profile>
|
||||
<activation>
|
||||
<property>
|
||||
<name>keycloak.connectionsJpa.driver</name>
|
||||
<value>org.postgresql.Driver</value>
|
||||
</property>
|
||||
</activation>
|
||||
<id>postgresql</id>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<version>${postgresql.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</profile>
|
||||
|
||||
<profile>
|
||||
<id>clean-jpa</id>
|
||||
<build>
|
||||
<pluginManagement>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.liquibase</groupId>
|
||||
<artifactId>liquibase-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<changeLogFile>META-INF/jpa-changelog-master.xml</changeLogFile>
|
||||
|
||||
<url>${keycloak.connectionsJpa.url}</url>
|
||||
<driver>${keycloak.connectionsJpa.driver}</driver>
|
||||
<username>${keycloak.connectionsJpa.user}</username>
|
||||
<password>${keycloak.connectionsJpa.password}</password>
|
||||
|
||||
<promptOnNonLocalDatabase>false</promptOnNonLocalDatabase>
|
||||
<databaseClass>${keycloak.connectionsJpa.liquibaseDatabaseClass}</databaseClass>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>clean-jpa</id>
|
||||
<phase>clean</phase>
|
||||
<goals>
|
||||
<goal>dropAll</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</pluginManagement>
|
||||
</build>
|
||||
</profile>
|
||||
|
||||
<profile>
|
||||
<id>auth-server-wildfly</id>
|
||||
<properties>
|
||||
|
|
|
@ -138,6 +138,12 @@ public class AdapterTestStrategy extends ExternalResource {
|
|||
String pageSource = driver.getPageSource();
|
||||
System.out.println(pageSource);
|
||||
Assert.assertTrue(pageSource.contains("parameter=hello"));
|
||||
// test that user principal and KeycloakSecurityContext available
|
||||
driver.navigate().to(APP_SERVER_BASE_URL + "/input-portal/insecure");
|
||||
System.out.println("insecure: ");
|
||||
System.out.println(driver.getPageSource());
|
||||
Assert.assertTrue(driver.getPageSource().contains("Insecure Page"));
|
||||
if (System.getProperty("insecure.user.principal.unsupported") == null) Assert.assertTrue(driver.getPageSource().contains("UserPrincipal"));
|
||||
|
||||
// test logout
|
||||
|
||||
|
|
|
@ -100,6 +100,7 @@ public class FilterAdapterTest {
|
|||
|
||||
@Test
|
||||
public void testSavedPostRequest() throws Exception {
|
||||
System.setProperty("insecure.user.principal.unsupported", "true");
|
||||
testStrategy.testSavedPostRequest();
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,9 @@
|
|||
|
||||
package org.keycloak.testsuite.adapter;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.keycloak.KeycloakSecurityContext;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
@ -35,6 +38,17 @@ public class InputServlet extends HttpServlet {
|
|||
@Override
|
||||
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
|
||||
String appBase = System.getProperty("app.server.base.url", "http://localhost:8081");
|
||||
if (req.getRequestURI().endsWith("insecure")) {
|
||||
if (System.getProperty("insecure.user.principal.unsupported") == null) Assert.assertNotNull(req.getUserPrincipal());
|
||||
if (System.getProperty("insecure.user.principal.unsupported") == null) Assert.assertNotNull(req.getAttribute(KeycloakSecurityContext.class.getName()));
|
||||
resp.setContentType("text/html");
|
||||
PrintWriter pw = resp.getWriter();
|
||||
pw.printf("<html><head><title>%s</title></head><body>", "Insecure Page");
|
||||
if (req.getUserPrincipal() != null) pw.printf("UserPrincipal: " + req.getUserPrincipal().getName());
|
||||
pw.print("</body></html>");
|
||||
pw.flush();
|
||||
return;
|
||||
}
|
||||
String actionUrl = appBase + "/input-portal/secured/post";
|
||||
|
||||
|
||||
|
|
|
@ -48,8 +48,8 @@ public class ConcurrencyTest extends AbstractClientTest {
|
|||
|
||||
private static final Logger log = Logger.getLogger(ConcurrencyTest.class);
|
||||
|
||||
private static final int DEFAULT_THREADS = 10;
|
||||
private static final int DEFAULT_ITERATIONS = 100;
|
||||
private static final int DEFAULT_THREADS = 5;
|
||||
private static final int DEFAULT_ITERATIONS = 20;
|
||||
|
||||
// If enabled only one request is allowed at the time. Useful for checking that test is working.
|
||||
private static final boolean SYNCHRONIZED = false;
|
||||
|
|
|
@ -25,6 +25,7 @@ import org.keycloak.admin.client.resource.ServerInfoResource;
|
|||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.idm.RoleRepresentation;
|
||||
|
@ -32,6 +33,7 @@ import org.keycloak.services.managers.RealmManager;
|
|||
import org.keycloak.testsuite.KeycloakServer;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
import javax.ws.rs.BadRequestException;
|
||||
import javax.ws.rs.NotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
|
@ -51,6 +53,10 @@ import static org.junit.Assert.fail;
|
|||
*/
|
||||
public class RealmTest extends AbstractClientTest {
|
||||
|
||||
public static final String PRIVATE_KEY = "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=";
|
||||
public static final String PUBLIC_KEY = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB";
|
||||
public static final String CERTIFICATE = "MIICsTCCAZkCBgFTLB5bhDANBgkqhkiG9w0BAQsFADAcMRowGAYDVQQDDBFhZG1pbi1jbGllbnQtdGVzdDAeFw0xNjAyMjkwODIwMDBaFw0yNjAyMjgwODIxNDBaMBwxGjAYBgNVBAMMEWFkbWluLWNsaWVudC10ZXN0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAquzJtpAlpTFnJzILjTOHW+SOWav1eIsCtlAqiFTvBskbod6b4BtVaR3FVrQm8rFiwDOIEWT3IG3ZIz0LKYxnqvuffyLHGHjiroqrR63kY9Wa9B790lSEWVaGeNOMnKleqKu5QUNfL3wVebUh/C/QfxZ29R1EIbxNe2ThN8yuIca8Ltn43D5VlyatptojffxpCYiYqAmIwQDaq1um2cQ+4rPBLxC5jM9UBvYOMUP4u0caNSaPI1o9lHVKgTtWcdQzUeMmAGsnLV26XGhA/OwRduUxksumR1kh/KSqowasjgSrpVqtF/uo5TY57s7drD+zKG58cdHLreclB9AQNvNwZwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBh4iwg8GnadeQP52pV5vKJ4Z8A1R2aYCzoW7Lc3FI/pXWX9Af5dKILX5O2j/daamPS+WtDWxIuwvZC5drrkvJn/r8e4KstnXQzPQggIJbI9v3wfIX3VlFvwvZVGiuE5PSLSWb0L57PEojZVpIU5bLchq4yRSD2zK4dWX8Y6I/D40a74KDvPOlEL8405/T1iW7ytKT9awNJW04N91owoI+kdUL+DMnnGzIxDAoYAeZI/1vcwoaH24zyTLGItkzpKxqLOdB05cnxn5jCWY2Hyd1zqtRkadhgZaqu4lcDHAHEMDp6dEjLZW8ym8bnlto+MD2y//CsyPCzyCLlA726vrli";
|
||||
|
||||
@Test
|
||||
public void getRealms() {
|
||||
List<RealmRepresentation> realms = keycloak.realms().findAll();
|
||||
|
@ -331,4 +337,80 @@ public class RealmTest extends AbstractClientTest {
|
|||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void uploadRealmKeys() throws Exception {
|
||||
String originalPublicKey = realm.toRepresentation().getPublicKey();
|
||||
|
||||
RealmRepresentation rep = new RealmRepresentation();
|
||||
rep.setPrivateKey("INVALID");
|
||||
rep.setPublicKey(PUBLIC_KEY);
|
||||
|
||||
try {
|
||||
realm.update(rep);
|
||||
fail("Expected BadRequestException");
|
||||
} catch (BadRequestException e) {
|
||||
}
|
||||
|
||||
rep.setPrivateKey(PRIVATE_KEY);
|
||||
rep.setPublicKey("INVALID");
|
||||
|
||||
try {
|
||||
realm.update(rep);
|
||||
fail("Expected BadRequestException");
|
||||
} catch (BadRequestException e) {
|
||||
}
|
||||
|
||||
assertEquals(originalPublicKey, realm.toRepresentation().getPublicKey());
|
||||
|
||||
rep.setPublicKey(PUBLIC_KEY);
|
||||
realm.update(rep);
|
||||
|
||||
assertEquals(PUBLIC_KEY, rep.getPublicKey());
|
||||
|
||||
String privateKey2048 = IOUtils.toString(getClass().getResourceAsStream("/keys/private2048.pem"));
|
||||
String publicKey2048 = IOUtils.toString(getClass().getResourceAsStream("/keys/public2048.pem"));
|
||||
|
||||
rep.setPrivateKey(privateKey2048);
|
||||
|
||||
try {
|
||||
realm.update(rep);
|
||||
fail("Expected BadRequestException");
|
||||
} catch (BadRequestException e) {
|
||||
}
|
||||
|
||||
assertEquals(PUBLIC_KEY, realm.toRepresentation().getPublicKey());
|
||||
|
||||
rep.setPublicKey(publicKey2048);
|
||||
|
||||
realm.update(rep);
|
||||
|
||||
assertEquals(publicKey2048, realm.toRepresentation().getPublicKey());
|
||||
|
||||
String privateKey4096 = IOUtils.toString(getClass().getResourceAsStream("/keys/private4096.pem"));
|
||||
String publicKey4096 = IOUtils.toString(getClass().getResourceAsStream("/keys/public4096.pem"));
|
||||
rep.setPrivateKey(privateKey4096);
|
||||
rep.setPublicKey(publicKey4096);
|
||||
|
||||
realm.update(rep);
|
||||
|
||||
assertEquals(publicKey4096, realm.toRepresentation().getPublicKey());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void uploadCertificate() throws IOException {
|
||||
RealmRepresentation rep = new RealmRepresentation();
|
||||
rep.setCertificate(CERTIFICATE);
|
||||
|
||||
realm.update(rep);
|
||||
|
||||
assertEquals(CERTIFICATE, rep.getCertificate());
|
||||
|
||||
String certificate = IOUtils.toString(getClass().getResourceAsStream("/keys/certificate.pem"));
|
||||
rep.setCertificate(certificate);
|
||||
|
||||
realm.update(rep);
|
||||
|
||||
assertEquals(certificate, rep.getCertificate());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -646,6 +646,23 @@ public class UserTest extends AbstractClientTest {
|
|||
assertEquals("Keycloak Account Management", driver.getTitle());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resetUserInvalidPassword() {
|
||||
String userId = createUser("user1", "user1@localhost");
|
||||
|
||||
try {
|
||||
CredentialRepresentation cred = new CredentialRepresentation();
|
||||
cred.setType(CredentialRepresentation.PASSWORD);
|
||||
cred.setValue(" ");
|
||||
cred.setTemporary(false);
|
||||
realm.users().get(userId).resetPassword(cred);
|
||||
fail("Expected failure");
|
||||
} catch (ClientErrorException e) {
|
||||
assertEquals(400, e.getResponse().getStatus());
|
||||
e.getResponse().close();
|
||||
}
|
||||
}
|
||||
|
||||
private void switchEditUsernameAllowedOn() {
|
||||
RealmRepresentation rep = realm.toRepresentation();
|
||||
rep.setEditUsernameAllowed(true);
|
||||
|
|
|
@ -192,6 +192,10 @@ public abstract class AbstractKerberosTest {
|
|||
loginPage.login("jduke", "theduke");
|
||||
changePasswordPage.assertCurrent();
|
||||
|
||||
// Bad existing password
|
||||
changePasswordPage.changePassword("theduke-invalid", "newPass", "newPass");
|
||||
Assert.assertTrue(driver.getPageSource().contains("Invalid existing password."));
|
||||
|
||||
// Change password is not possible as editMode is READ_ONLY
|
||||
changePasswordPage.changePassword("theduke", "newPass", "newPass");
|
||||
Assert.assertTrue(driver.getPageSource().contains("You can't update your password as your account is read only"));
|
||||
|
|
|
@ -17,6 +17,9 @@
|
|||
|
||||
package org.keycloak.testsuite.keycloaksaml;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.keycloak.KeycloakSecurityContext;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
@ -34,6 +37,16 @@ public class InputServlet extends HttpServlet {
|
|||
@Override
|
||||
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
|
||||
String appBase = System.getProperty("app.server.base.url", "http://localhost:8081");
|
||||
if (req.getRequestURI().endsWith("insecure")) {
|
||||
if (System.getProperty("insecure.user.principal.unsupported") == null) Assert.assertNotNull(req.getUserPrincipal());
|
||||
resp.setContentType("text/html");
|
||||
PrintWriter pw = resp.getWriter();
|
||||
pw.printf("<html><head><title>%s</title></head><body>", "Insecure Page");
|
||||
if (req.getUserPrincipal() != null) pw.printf("UserPrincipal: " + req.getUserPrincipal().getName());
|
||||
pw.print("</body></html>");
|
||||
pw.flush();
|
||||
return;
|
||||
}
|
||||
String actionUrl = appBase + "/input-portal/secured/post";
|
||||
|
||||
|
||||
|
|
|
@ -152,6 +152,12 @@ public class SamlAdapterTestStrategy extends ExternalResource {
|
|||
String pageSource = driver.getPageSource();
|
||||
System.out.println(pageSource);
|
||||
Assert.assertTrue(pageSource.contains("parameter=hello"));
|
||||
// test that user principal and KeycloakSecurityContext available
|
||||
driver.navigate().to(APP_SERVER_BASE_URL + "/input-portal/insecure");
|
||||
System.out.println("insecure: ");
|
||||
System.out.println(driver.getPageSource());
|
||||
Assert.assertTrue(driver.getPageSource().contains("Insecure Page"));
|
||||
if (System.getProperty("insecure.user.principal.unsupported") == null) Assert.assertTrue(driver.getPageSource().contains("UserPrincipal"));
|
||||
|
||||
// test logout
|
||||
|
||||
|
|
|
@ -86,7 +86,7 @@
|
|||
"connectionsInfinispan": {
|
||||
"default": {
|
||||
"clustered": "${keycloak.connectionsInfinispan.clustered:false}",
|
||||
"async": "${keycloak.connectionsInfinispan.async:true}",
|
||||
"async": "${keycloak.connectionsInfinispan.async:false}",
|
||||
"sessionsOwners": "${keycloak.connectionsInfinispan.sessionsOwners:2}"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,4 +31,4 @@ idm.test.kerberos.allow.kerberos.authentication=true
|
|||
idm.test.kerberos.realm=KEYCLOAK.ORG
|
||||
idm.test.kerberos.server.principal=HTTP/localhost@KEYCLOAK.ORG
|
||||
idm.test.kerberos.debug=false
|
||||
idm.test.kerberos.use.kerberos.for.password.authentication=false
|
||||
idm.test.kerberos.use.kerberos.for.password.authentication=true
|
|
@ -1,8 +1,8 @@
|
|||
[libdefaults]
|
||||
default_realm = KEYCLOAK.ORG
|
||||
default_tgs_enctypes = des3-cbc-sha1-kd aes256-cts-hmac-sha1-96 rc4-hmac
|
||||
default_tkt_enctypes = des3-cbc-sha1-kd aes256-cts-hmac-sha1-96 rc4-hmac
|
||||
permitted_enctypes = des3-cbc-sha1-kd aes256-cts-hmac-sha1-96 rc4-hmac
|
||||
default_tgs_enctypes = des3-cbc-sha1-kd aes256-cts-hmac-sha1-96 rc4-hmac aes128-cts-hmac-sha1-96
|
||||
default_tkt_enctypes = des3-cbc-sha1-kd aes256-cts-hmac-sha1-96 rc4-hmac aes128-cts-hmac-sha1-96
|
||||
permitted_enctypes = des3-cbc-sha1-kd aes256-cts-hmac-sha1-96 rc4-hmac aes128-cts-hmac-sha1-96
|
||||
kdc_timeout = 30000
|
||||
dns_lookup_realm = false
|
||||
dns_lookup_kdc = false
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIDXTCCAkWgAwIBAgIJAIzE3vQp7EQWMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
|
||||
BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
|
||||
aWRnaXRzIFB0eSBMdGQwHhcNMTYwMjI5MDgzMDU0WhcNNDMwNzE2MDgzMDU0WjBF
|
||||
MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50
|
||||
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
|
||||
CgKCAQEAp1+GzdEkt2FZbISXYO12503FL6Oh8s4+tJ2fE66N8IezhugP8xiySDfW
|
||||
TEMaO5Z2TaTnQQoF9SSZ9Edq1GPxpBX0cdkCOBopEGdlb3hUYDeMaDMs18KGemUc
|
||||
Fj+CWB5VVcbmWMJ36WCz7FC+Oe38tmujR1AJpJL3pwqazyWIZzPqX8rW+rrNPGKP
|
||||
C96oBPZMb4RJWivLBJi/o5MGSpo1sJNtxyF4zUUI00LX0wZAV1HH1XErd1Vz41on
|
||||
nmB+tj9nevVRR4rDV280IELp9Ud0PIb3w843uJtwfSAwVG0pT6hv1VBDrBxTS08N
|
||||
dPU8CtkQAXzCCr8nqfAbUFOhcWRQgQIDAQABo1AwTjAdBgNVHQ4EFgQUFE+uUZAI
|
||||
n57ArEylqhCmHkAenTEwHwYDVR0jBBgwFoAUFE+uUZAIn57ArEylqhCmHkAenTEw
|
||||
DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEApkgD3OCtw3+sk7GR1YaJ
|
||||
xNd8HT+fxXmnLqGnCQWX8lRIg5vj1PDMRev6vlIK3JfQV3zajcpKFfpy96klWsJy
|
||||
ZLYBVW2QOtMzDdQ9I8dS4Pn/SJ/Vo/M/ucfY4ttcuUL3oQCrI/c/u9tcamGMfbwd
|
||||
658MlXrUvt4B6qXY5AbgUvYR25P86uw7hSFMq5tQftNQsLbOh2FEeIiKhpgI7w8S
|
||||
SPajaWjUXsfHc5H7f9MciE2NS1Vd3AViGrVWP1rgQ1Iv0UyQVQrnjmIs12ENJmTd
|
||||
5lDqra5FJhaO7+RUG6er8n8HwXzhHkPmezGqtxWKikjitqvDY9prB3omJSa4Led+
|
||||
AQ==
|
||||
-----END CERTIFICATE-----
|
|
@ -0,0 +1,27 @@
|
|||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEogIBAAKCAQEApnZ/E2BUULHjsRiSnEgZ4vGe15BRqZPdkHR+NcvVYpThc7Jq
|
||||
Y6nZrdrwO9sOjlMC5e2Q18Fypi4KbJpGSe9r0DPgcbPsHSoe2xFO3M8XBE0DyoRb
|
||||
laQFhe6p/sj3ak32k2zn+fMZUmlx/MTNQh1ICki7So0NDCBXt8XGZNnEyvKeXOUZ
|
||||
P5qicP9KxVAQiWJvlkaTjc8rrRTmf+HWw/QfgQC0tzBRpa7T+RpW9O+rnWfOaNfT
|
||||
kTb9itIc+ZOa2Z4iidZ7+ifMOp9cNT641Wb6iYqJ2ufqY+msxI54tYM1tPgGS7r4
|
||||
SnCwmnqTaO383wXUl8TQ7qStmAWIepV3nNyuAQIDAQABAoIBAEj90rDrX21W43Fn
|
||||
RfpTP06dBjqdpMFH/jJ2clUigPnOMKGrzSzQcIvkYczNPC+6RJ4PsqB4yc4GiDmg
|
||||
2EtZOZw88yDIdTNAofELQNpf0Ebpgk0OBp6yIl3dDhuTgbHSZ9mzOnEGYMcbR4k/
|
||||
voVME6e2xrFk8iCsGeqSRXE5cCpQzXLFQGRrGGoJH8hggteyQhei39pxo8IM87A0
|
||||
ptdZj8YJ0YUUpztyshR5KAzZeJPjKRXE1wPiZoqDzQ0OCC+k9IGCeJIAAGVB1Zs7
|
||||
4ga4gEkczYfSbhRRGl7Uls4XJHL1pNsU75sRpQZGuGm9as6PprYJiDH5Eoz+EdzO
|
||||
+oxs0FUCgYEA1NFbnyjlTEB9liVAMSd+BNRL7c78+Tsbfn/y6b1pEcSZ5+R79tNw
|
||||
NTAmk0GSrW64MRQS2gYe871w9QBwpSlNoGMN5Zjzl8WIZ+AFzTCVnEyBt0DOTj/w
|
||||
ZU2/o56C+/CYj6kUjP88HRIX/Grr7rY20uGcDKVLFVeYCBGUI/m0WOsCgYEAyD1F
|
||||
TLZea00ZBl0xb/ju0KKGHxw63BldfoKCXjTarz1M7Nl9vBdGuOBH+H3a2wSPhJfb
|
||||
7Le9LY+tU53aCJql5xshsDNtduTMdHPamKXkHH038Tbn1vb8HWPzAzYcAY3oAPVJ
|
||||
OPPWtTmVeitl0Lo5kPYFbx4yBykOOAokLE/qGcMCgYBZu4CnRkYQday+TyyWzTEM
|
||||
djshpUHzEGISX36b4ZpYvI2sQiGmvBY2xvus4VwoNmQBhZZBSY1pdjoXg7z7VsP9
|
||||
WWa1pV0oZEiUi9fGYbLjeTrEetXCFqGVBUhFhAN0mUiqYj9hCAlftI5ahva96ySI
|
||||
nEoA5v0WnZ1j4Y2V8aaCSwKBgA2bBCtrNM0rpuikymgmTOvGL0DL5T/xRUYETiFi
|
||||
i/1eN+zb9kwidL65ForO2mEJVUsYGmxiK6t92LQWxKrS/zTNxiM3y1dJwo6jFJZM
|
||||
p0w8QeHU7jnP/F9u3CM6uPpuDvaJtBj3kH4t1HdBnaBqFuE/pizfq1yLMJkkL0MH
|
||||
hwuLAoGAEwOTV+mkM6Fv+JGDwRkfFlz95XfQsTXT3xBgGy9sGmtn2b4tPCzO/Fxd
|
||||
eLD9dMeI8M3yB4Vc1gRhK8jmmgvcYhsyLn+DreLGWnwQjQn+vAMCpkcPMGJjp3bz
|
||||
UooIHTCHq7aNirxPrzgi+F5PcGR074qPFrdVaL031CPTAGmMqzE=
|
||||
-----END RSA PRIVATE KEY-----
|
|
@ -0,0 +1,51 @@
|
|||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIJKAIBAAKCAgEAni4pAISeCrzLSEuTJPx51Uwh0LIQuNHRTah1UuWk5NP4W1ct
|
||||
l1PuCoddtZNiPL2tnEw0Js1m7vlI3+miynNy8ze2LCtaA98oPUVsD9C13hKH3nQ1
|
||||
Zf/IYCzHMC7BAJ7pFCgo2TM0S2ic2eHyr+/vQd2ha8mnj6hzwJ3O9KaIINDLA1Eu
|
||||
cJNZDxmlbzJfap5mFH1Uj2RhDiYM4m8445fR7QDv3Zhiv0HMP0dCaVnPot+jT6ar
|
||||
195XDDLSIrK+sc8SFLdlc4BlGitDyZviKVzI5E4nn4hkHLBK6jHzNd+afJLPyZIm
|
||||
t0N9NPj6y3h3gaGAVMWQpK2bgKEdF6417CPGbcMuGsBbpmDQzl9T4EtcN/Pq741t
|
||||
yCEXUWORHOEU+bBiEO1RAWsah13LxhYQ38YclTkr7BGZfQ4sfEZDKEknr7UuQw1e
|
||||
E7eZLmiAb+N72kxO5u50OtAmqUhw8LN5iQ/gD5dcItsdi643nKTFUB+bEIqqWaXB
|
||||
vRsIn3BmNJiPrgUFJoMm94vrAC1WKjKU4QfERSD/SUuFL3FisP3c33VP1VX33rfM
|
||||
6PIWyffIx43XoSp7MwFL3zPvTNLBP33LIBPD83ZT1Af46ky8TIA98CEkNNQdBOQE
|
||||
trSsm0snYuku1xNQrdccw3jFvroVQGGPS4fLb0ZveUzvWlTXfPwcF/I+UX8CAwEA
|
||||
AQKCAgAtBL4uo4/HRowkez3ZnPGfvxcwqj0QHMcQ7wzVFv6jEaDxfhI/q2doH7aP
|
||||
4u5mkyVCAn4lPPg6s0AGWhw2ujzQ894pG/12dKCjL4rdxWy9U1DQAwrfiLiihpv6
|
||||
HpRXdOKpFfTqPCR6AcACUsgbQpI5kLZMUwwZWrv26fLotiywCjx9KTGqYIVMQuRO
|
||||
jWuSkJpE4paaaNsmh7XLDBSI0cCxTz71NlWEUVmr+L/x89mgZT25gZoOyfzEQAKG
|
||||
VZDoUiK3OqrpAHXFyOJ2EqN/WiTitUuZn9u+Pn5TwPuTLZxuL+prV+kk51RPKz1V
|
||||
g2efwTMg7UmJVM+ZAEGjZ7V5C4WBbR4kxpCVe2vs2x6OUkhu84MFrB3WQ5oHbR7C
|
||||
EZc15FVZNux7mHokwBYsosgtNUtdRtWN3mIIjZKQhbQfw5U8LwLGf94Gc5sxAu8O
|
||||
E/KQ2kynqs6OU0PixVMRxkLnVJeT3x4lS8VIuiEx6Wa8+R5774tMmVPPghr2Y2Fx
|
||||
IY/nF0fwU+5wn5Bu2Td5m/+g/lFfWZslTdTRNPtAPHfEuQCVxgtolpS2is9rqJw7
|
||||
0ymmck+XdM18IGVf1e+AH01w8FMlngU7JqO2jkqNE0B+s+FRkYkg/ldTJjI3e8HT
|
||||
kFBIynUfgzNgvulL06Re9TSq5QG9pahpT3FyUILbe808fV+jgQKCAQEAz4bBUHSH
|
||||
TAsgUSvAQ/KDJAaoaEFLAoy1B/u2qk/4e01b1FrEVGO4tPhGR5fsWYew7ktTMHPp
|
||||
ywra1sTdUUKEVm51YHiymT1T5w5S1gYJn6eElhfcf3tTk1zDW+WBQ0lrn5FVe4Kt
|
||||
guFledpGqssGymqrxwKqKnGY5viI+jUmBZATIo6yeNDJuBd2W2kXO40wUn7M5/yE
|
||||
a1jUxc6SPC2Z4CsAZyL5YuwbwW/K2dP97MX2snyPlnLzm9etBsMa52N/ODBThgxE
|
||||
+pwST+0DMbi9RtSCEQfeTEmJvuXkANhLnQZq7oW+WwEpWTjNQMzdaOYyIBk4vlmP
|
||||
8mjI+KdplCtNcQKCAQEAwyC1OH1iASdJ7ihvSx8+nM6vk835Am2xERvcduAh7yX6
|
||||
0S8Uq0AYlFz1qg/7T0HK1+6kWsCSxgaXRnlU6Mit62sEQ89HEs6kPCkjGvT0BLF1
|
||||
dTU1f8xbWG/Ra7ejjuVAV4K2ZhpI2TwHChQt94mkNmeW99SHTVZmsKEo6igfstRx
|
||||
Xk5PwK69YVbR4NEGRWcKjeeokfNML6WKa+G0zzaP6mRpdcIvsHbZJNX9NKAU1IvC
|
||||
+QrrgZNnR0gjFrb77QVK/yED8MidYz1VSOPP1TtozKfyqc7flC28wTd1U1c+Sh+9
|
||||
02nkh2xEKwBRnf3+3qdGYZnZPJTHOnLuOegv84LV7wKCAQEApaE1lNMMUOLobiBv
|
||||
GUvq4sv1iQ/joCtRKQf7KD8fYLnDOt8epwPYHYex/93/Iw2rZuTzhk5dIFKPiq4g
|
||||
vYRLPvh18gMi+C78UgMalfrHn4cByRFOSOjTMV/uA+BOpLdqkDZcdXE8rqLabMCL
|
||||
ejEEQHWWmAVGbw2vLVjbamcU64er3f7p7oclGCqRqi9b/YmYMZ9GRzlBLrP9Tcqq
|
||||
6CC1GTb44VgGlq8/D8n7qpMJrPnrBVVo/HjOeWlPjYAWbur4VI4te2U7gJEkBGp0
|
||||
DDEXz/o3vQP8pgJjT9sHeK0o3DCNE8XmwZdRuwYcu0VGyTxAcWHv6exteNms3Ngw
|
||||
6bMN4QKCAQABFK5MSM2BKiGLsyeip+Kl5bMtQ2fMrqTbbmcTNXyaoYA5JmSb6jf4
|
||||
omct8Pa4Yqnn9kdsxUJK3IB8AHIK9AmakzYr1fsTzJc2ShgKry6m1ADNjGTmd1BO
|
||||
NhhX22WJhhWMJooyGJUstttnH+N9SoLhVkOMzd2N/RuGgO4EFgLO78RM/GwOqikc
|
||||
X+m7sAyz17VEQfM6E7npTaZtoItq1meHqdS3tUKkXJQpUxIa94QGBVwoGvpg9lsN
|
||||
FwYyuwK8NlpK/XjTHZlZkl5lj/V3veN/trJuZFnyrSote8wnkQUkTfa0NBLy+ROL
|
||||
lW3eTSjbPNvz8HE2l7Bez4IoSfPyClh7AoIBAAyWkCZU/57BWfWQVM+74Czjfh9l
|
||||
fYrri16POhlDe7oZn/nAaKxWGoNTOEFkKeM0uag+mDUspxXn/ZREV+xb3WXTNTjV
|
||||
hLM7HiKMx9rMOIQedO4heyJWT5fIzGfxmPVok9cO8XMG+Ox3SLpn1S0N5+5fkvQj
|
||||
S4OX8v4U0AAhfwyxd1zvvsMWj9lPEOpFIWpjhFtnxvevCYCu62JKYKwI0kSqFeWr
|
||||
nm1EgcpZVKvLZjScWiW+f20KOBeg/WFBeqh0BFEM7eCOg1EyGJnHLegYabcpWIYH
|
||||
hLJWQdXkUbXkJBql3Sbd3o3ZTiknahO4bc1Kcxm29Kns8Y02cEd+Ahwuhh4=
|
||||
-----END RSA PRIVATE KEY-----
|
|
@ -0,0 +1,9 @@
|
|||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApnZ/E2BUULHjsRiSnEgZ
|
||||
4vGe15BRqZPdkHR+NcvVYpThc7JqY6nZrdrwO9sOjlMC5e2Q18Fypi4KbJpGSe9r
|
||||
0DPgcbPsHSoe2xFO3M8XBE0DyoRblaQFhe6p/sj3ak32k2zn+fMZUmlx/MTNQh1I
|
||||
Cki7So0NDCBXt8XGZNnEyvKeXOUZP5qicP9KxVAQiWJvlkaTjc8rrRTmf+HWw/Qf
|
||||
gQC0tzBRpa7T+RpW9O+rnWfOaNfTkTb9itIc+ZOa2Z4iidZ7+ifMOp9cNT641Wb6
|
||||
iYqJ2ufqY+msxI54tYM1tPgGS7r4SnCwmnqTaO383wXUl8TQ7qStmAWIepV3nNyu
|
||||
AQIDAQAB
|
||||
-----END PUBLIC KEY-----
|
14
testsuite/integration/src/test/resources/keys/public4096.pem
Normal file
14
testsuite/integration/src/test/resources/keys/public4096.pem
Normal file
|
@ -0,0 +1,14 @@
|
|||
-----BEGIN PUBLIC KEY-----
|
||||
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAni4pAISeCrzLSEuTJPx5
|
||||
1Uwh0LIQuNHRTah1UuWk5NP4W1ctl1PuCoddtZNiPL2tnEw0Js1m7vlI3+miynNy
|
||||
8ze2LCtaA98oPUVsD9C13hKH3nQ1Zf/IYCzHMC7BAJ7pFCgo2TM0S2ic2eHyr+/v
|
||||
Qd2ha8mnj6hzwJ3O9KaIINDLA1EucJNZDxmlbzJfap5mFH1Uj2RhDiYM4m8445fR
|
||||
7QDv3Zhiv0HMP0dCaVnPot+jT6ar195XDDLSIrK+sc8SFLdlc4BlGitDyZviKVzI
|
||||
5E4nn4hkHLBK6jHzNd+afJLPyZImt0N9NPj6y3h3gaGAVMWQpK2bgKEdF6417CPG
|
||||
bcMuGsBbpmDQzl9T4EtcN/Pq741tyCEXUWORHOEU+bBiEO1RAWsah13LxhYQ38Yc
|
||||
lTkr7BGZfQ4sfEZDKEknr7UuQw1eE7eZLmiAb+N72kxO5u50OtAmqUhw8LN5iQ/g
|
||||
D5dcItsdi643nKTFUB+bEIqqWaXBvRsIn3BmNJiPrgUFJoMm94vrAC1WKjKU4QfE
|
||||
RSD/SUuFL3FisP3c33VP1VX33rfM6PIWyffIx43XoSp7MwFL3zPvTNLBP33LIBPD
|
||||
83ZT1Af46ky8TIA98CEkNNQdBOQEtrSsm0snYuku1xNQrdccw3jFvroVQGGPS4fL
|
||||
b0ZveUzvWlTXfPwcF/I+UX8CAwEAAQ==
|
||||
-----END PUBLIC KEY-----
|
|
@ -38,6 +38,7 @@ sslRequired.option.external=external requests
|
|||
sslRequired.option.none=none
|
||||
sslRequired.tooltip=Is HTTPS required? 'None' means HTTPS is not required for any client IP address. 'External requests' means localhost and private IP addresses can access without HTTPS. 'All requests' means HTTPS is required for all IP addresses.
|
||||
publicKey=Public key
|
||||
privateKey=Private key
|
||||
gen-new-keys=Generate new keys
|
||||
certificate=Certificate
|
||||
host=Host
|
||||
|
|
|
@ -1171,17 +1171,8 @@ module.controller('CreateClientCtrl', function($scope, realm, client, templates,
|
|||
|
||||
|
||||
$scope.save = function() {
|
||||
|
||||
$scope.client.protocol = $scope.protocol;
|
||||
|
||||
if ($scope.client.protocol == 'openid-connect' && !$scope.client.rootUrl) {
|
||||
Notifications.error("You must specify the root URL of application");
|
||||
}
|
||||
|
||||
if ($scope.client.protocol == 'saml' && !$scope.client.adminUrl) {
|
||||
Notifications.error("You must specify the SAML Endpoint URL");
|
||||
}
|
||||
|
||||
Client.save({
|
||||
realm: realm.realm,
|
||||
client: ''
|
||||
|
@ -1599,8 +1590,8 @@ module.controller('ClientProtocolMapperListCtrl', function($scope, realm, client
|
|||
});
|
||||
|
||||
module.controller('ClientProtocolMapperCtrl', function($scope, realm, serverInfo, client, mapper, ClientProtocolMapper, Notifications, Dialog, $location) {
|
||||
/*
|
||||
$scope.realm = realm;
|
||||
/*
|
||||
$scope.client = client;
|
||||
$scope.create = false;
|
||||
$scope.protocol = client.protocol;
|
||||
|
@ -1674,12 +1665,13 @@ module.controller('ClientProtocolMapperCtrl', function($scope, realm, serverInfo
|
|||
});
|
||||
|
||||
module.controller('ClientProtocolMapperCreateCtrl', function($scope, realm, serverInfo, client, ClientProtocolMapper, Notifications, Dialog, $location) {
|
||||
$scope.realm = realm;
|
||||
|
||||
if (client.protocol == null) {
|
||||
client.protocol = 'openid-connect';
|
||||
}
|
||||
var protocol = client.protocol;
|
||||
/*
|
||||
$scope.realm = realm;
|
||||
$scope.client = client;
|
||||
$scope.create = true;
|
||||
$scope.protocol = protocol;
|
||||
|
|
|
@ -990,8 +990,21 @@ module.controller('RealmTokenDetailCtrl', function($scope, Realm, realm, $http,
|
|||
};
|
||||
});
|
||||
|
||||
module.controller('RealmKeysDetailCtrl', function($scope, Realm, realm, $http, $location, Dialog, Notifications) {
|
||||
$scope.realm = realm;
|
||||
module.controller('RealmKeysDetailCtrl', function($scope, Realm, realm, $http, $route, $location, Dialog, Notifications) {
|
||||
$scope.realm = angular.copy(realm);
|
||||
$scope.enableUpload = false;
|
||||
|
||||
$scope.$watch('realm', function () {
|
||||
if (!angular.equals($scope.realm, realm)) {
|
||||
if ($scope.realm.privateKey && $scope.realm.publicKey != realm.publicKey) {
|
||||
$scope.enableUpload = true;
|
||||
} else if ($scope.realm.certificate != realm.certificate) {
|
||||
$scope.enableUpload = true;
|
||||
} else {
|
||||
$scope.enableUpload = false;
|
||||
}
|
||||
}
|
||||
}, true);
|
||||
|
||||
$scope.generate = function() {
|
||||
Dialog.confirmGenerateKeys($scope.realm.realm, 'realm', function() {
|
||||
|
@ -1003,6 +1016,34 @@ module.controller('RealmKeysDetailCtrl', function($scope, Realm, realm, $http, $
|
|||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.cancel = function() {
|
||||
$route.reload();
|
||||
}
|
||||
|
||||
$scope.save = function() {
|
||||
var title = 'Upload keys for realm';
|
||||
var msg = 'Are you sure you want to upload keys for ' + $scope.realm.realm + '?';
|
||||
var btns = {
|
||||
ok: {
|
||||
label: 'Upload Keys',
|
||||
cssClass: 'btn btn-danger'
|
||||
},
|
||||
cancel: {
|
||||
label: 'Cancel',
|
||||
cssClass: 'btn btn-default'
|
||||
}
|
||||
};
|
||||
|
||||
Dialog.open(title, msg, btns, function() {
|
||||
Realm.update($scope.realm, function () {
|
||||
Notifications.success('Keys uploaded for realm.');
|
||||
Realm.get({ id : realm.realm }, function(updated) {
|
||||
$scope.realm = updated;
|
||||
})
|
||||
});
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
module.controller('RealmSessionStatsCtrl', function($scope, realm, stats, RealmClientSessionStats, RealmLogoutAll, Notifications) {
|
||||
|
|
|
@ -220,14 +220,14 @@
|
|||
<div class="input-group" ng-repeat="(i, redirectUri) in client.redirectUris track by $index">
|
||||
<input class="form-control" ng-model="client.redirectUris[i]">
|
||||
<div class="input-group-addon">
|
||||
<i class="pficon pficon-remove" style="width: 10px;" data-ng-click="deleteRedirectUri($index)"></i>
|
||||
<i class="fa fa-minus" style="width: 10px;" data-ng-click="deleteRedirectUri($index)"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<input class="form-control" ng-model="newRedirectUri" id="newRedirectUri">
|
||||
<div class="input-group-addon">
|
||||
<i class="pficon pficon-add" style="width: 10px;" data-ng-click="newRedirectUri.length > 0 && addRedirectUri()"></i>
|
||||
<i class="fa fa-plus" style="width: 10px;" data-ng-click="newRedirectUri.length > 0 && addRedirectUri()"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -279,14 +279,14 @@
|
|||
<div class="input-group" ng-repeat="(i, webOrigin) in client.webOrigins track by $index">
|
||||
<input class="form-control" ng-model="client.webOrigins[i]">
|
||||
<div class="input-group-addon">
|
||||
<i class="pficon pficon-remove" style="width: 10px;" data-ng-click="deleteWebOrigin($index)"></i>
|
||||
<i class="fa fa-minus" style="width: 10px;" data-ng-click="deleteWebOrigin($index)"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<input class="form-control" ng-model="newWebOrigin" id="newWebOrigin">
|
||||
<div class="input-group-addon">
|
||||
<i class="pficon pficon-add" style="width: 10px;" data-ng-click="newWebOrigin.length > 0 && addWebOrigin()"></i>
|
||||
<i class="fa fa-plus" style="width: 10px;" data-ng-click="newWebOrigin.length > 0 && addWebOrigin()"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -18,8 +18,8 @@
|
|||
</div>
|
||||
|
||||
<div class="col-md-6" data-ng-show="importing">
|
||||
<button class="btn btn-default" data-ng-click="viewImportDetails()">{{:: 'view-details' | translate}}</button>
|
||||
<button class="btn btn-default" data-ng-click="reset()">{{:: 'clear-import' | translate}}</button>
|
||||
<button class="btn btn-default" type="button" data-ng-click="viewImportDetails()">{{:: 'view-details' | translate}}</button>
|
||||
<button class="btn btn-default" type="button" data-ng-click="reset()">{{:: 'clear-import' | translate}}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -58,14 +58,14 @@
|
|||
<kc-tooltip>{{:: 'client-template.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
<div class="form-group" data-ng-hide="protocol == 'saml'">
|
||||
<label class="col-md-2 control-label" for="rootUrl">{{:: 'root-url' | translate}} <span class="required">*</span></label>
|
||||
<label class="col-md-2 control-label" for="rootUrl">{{:: 'root-url' | translate}}</label>
|
||||
<div class="col-sm-6">
|
||||
<input class="form-control" type="text" name="rootUrl" id="rootUrl" data-ng-model="client.rootUrl">
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'root-url.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
<div class="form-group" data-ng-show="protocol == 'saml'">
|
||||
<label class="col-md-2 control-label" for="masterSamlUrl">{{:: 'client-saml-endpoint' | translate}} <span class="required">*</span></label>
|
||||
<label class="col-md-2 control-label" for="masterSamlUrl">{{:: 'client-saml-endpoint' | translate}}</label>
|
||||
<div class="col-sm-6">
|
||||
<input class="form-control" type="text" name="masterSamlUrl" id="masterSamlUrl"
|
||||
data-ng-model="client.adminUrl">
|
||||
|
|
|
@ -3,26 +3,39 @@
|
|||
|
||||
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
|
||||
<fieldset class="border-top">
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" for="privateKey">{{:: 'privateKey' | translate}}</label>
|
||||
|
||||
<div class="col-md-10">
|
||||
<textarea type="password" id="privateKey" name="privateKey" class="form-control" rows="{{!realm.privateKey ? 1 : 8}}" data-ng-model="realm.privateKey"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" for="publicKey">{{:: 'publicKey' | translate}}</label>
|
||||
|
||||
<div class="col-md-10">
|
||||
<textarea type="text" id="publicKey" name="publicKey" class="form-control" rows="4"
|
||||
kc-select-action="click" readonly>{{realm.publicKey}}</textarea>
|
||||
kc-select-action="click" data-ng-model="realm.publicKey"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" for="certificate">{{:: 'certificate' | translate}}</label>
|
||||
|
||||
<div class="col-md-10">
|
||||
<textarea type="text" id="certificate" name="certificate" class="form-control" rows="8" kc-select-action="click" readonly>{{realm.certificate}}</textarea>
|
||||
<textarea type="text" id="certificate" name="certificate" class="form-control" rows="8" kc-select-action="click" data-ng-model="realm.certificate"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div class="form-group" data-ng-show="access.manageRealm">
|
||||
<div class="col-md-10 col-md-offset-2">
|
||||
<button class="btn btn-danger" type="submit" data-ng-click="generate()">{{:: 'gen-new-keys' | translate}}</button>
|
||||
<button class="btn btn-danger" type="button" data-ng-click="generate()" data-ng-disabled="enableUpload">{{:: 'gen-new-keys' | translate}}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" data-ng-show="access.manageRealm">
|
||||
<div class="col-md-10 col-md-offset-2">
|
||||
<button class="btn btn-danger" type="button" data-ng-click="save()" data-ng-disabled="!enableUpload">{{:: 'upload-keys' | translate}}</button>
|
||||
<button class="btn btn-default" type="button" data-ng-click="cancel()" data-ng-disabled="!enableUpload">{{:: 'cancel' | translate}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
@ -19,8 +19,11 @@ package org.keycloak.util.ldap;
|
|||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
|
||||
|
@ -43,13 +46,7 @@ import org.apache.directory.server.protocol.shared.transport.UdpTransport;
|
|||
import org.apache.directory.shared.kerberos.KerberosTime;
|
||||
import org.apache.directory.shared.kerberos.KerberosUtils;
|
||||
import org.apache.directory.shared.kerberos.codec.types.EncryptionType;
|
||||
import org.ietf.jgss.GSSException;
|
||||
import org.ietf.jgss.GSSManager;
|
||||
import org.ietf.jgss.GSSName;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.util.KerberosSerializationUtils;
|
||||
import sun.security.jgss.GSSNameImpl;
|
||||
import sun.security.jgss.krb5.Krb5NameElement;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
|
@ -113,15 +110,8 @@ public class KerberosEmbeddedServer extends LDAPEmbeddedServer {
|
|||
this.kdcEncryptionTypes = readProperty(PROPERTY_KDC_ENCTYPES, DEFAULT_KDC_ENCRYPTION_TYPES);
|
||||
|
||||
if (ldapSaslPrincipal == null || ldapSaslPrincipal.isEmpty()) {
|
||||
try {
|
||||
// Same algorithm like sun.security.krb5.PrincipalName constructor
|
||||
GSSName gssName = GSSManager.getInstance().createName("ldap@" + bindHost, GSSName.NT_HOSTBASED_SERVICE);
|
||||
GSSNameImpl gssName1 = (GSSNameImpl) gssName;
|
||||
Krb5NameElement krb5NameElement = (Krb5NameElement) gssName1.getElement(KerberosSerializationUtils.KRB5_OID);
|
||||
this.ldapSaslPrincipal = krb5NameElement.getKrb5PrincipalName().toString();
|
||||
} catch (GSSException uhe) {
|
||||
throw new RuntimeException(uhe);
|
||||
}
|
||||
String hostname = getHostnameForSASLPrincipal(bindHost);
|
||||
this.ldapSaslPrincipal = "ldap/" + hostname + "@" + this.kerberosRealm;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -219,6 +209,31 @@ public class KerberosEmbeddedServer extends LDAPEmbeddedServer {
|
|||
}
|
||||
|
||||
|
||||
// Forked from sun.security.krb5.PrincipalName constructor
|
||||
private String getHostnameForSASLPrincipal(String hostName) {
|
||||
try {
|
||||
// RFC4120 does not recommend canonicalizing a hostname.
|
||||
// However, for compatibility reason, we will try
|
||||
// canonicalize it and see if the output looks better.
|
||||
|
||||
String canonicalized = (InetAddress.getByName(hostName)).
|
||||
getCanonicalHostName();
|
||||
|
||||
// Looks if canonicalized is a longer format of hostName,
|
||||
// we accept cases like
|
||||
// bunny -> bunny.rabbit.hole
|
||||
if (canonicalized.toLowerCase(Locale.ENGLISH).startsWith(
|
||||
hostName.toLowerCase(Locale.ENGLISH)+".")) {
|
||||
hostName = canonicalized;
|
||||
}
|
||||
} catch (UnknownHostException | SecurityException e) {
|
||||
// not canonicalized or no permission to do so, use old
|
||||
}
|
||||
return hostName.toLowerCase(Locale.ENGLISH);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Replacement of apacheDS KdcServer class with disabled ticket replay cache.
|
||||
*
|
||||
|
|
Loading…
Reference in a new issue