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:
Bill Burke 2016-02-27 17:22:23 -05:00
parent a0696fcb97
commit 37584a24e0
87 changed files with 1684 additions and 263 deletions

View file

@ -18,6 +18,8 @@
package org.keycloak.adapters.jetty.core; package org.keycloak.adapters.jetty.core;
import org.eclipse.jetty.security.DefaultUserIdentity; 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.ServerAuthException;
import org.eclipse.jetty.security.UserAuthentication; import org.eclipse.jetty.security.UserAuthentication;
import org.eclipse.jetty.security.authentication.DeferredAuthentication; import org.eclipse.jetty.security.authentication.DeferredAuthentication;
@ -135,10 +137,44 @@ public abstract class AbstractKeycloakJettyAuthenticator extends LoginAuthentica
return new DefaultUserIdentity(theSubject, principal, theRoles); 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 @Override
public void setConfiguration(AuthConfiguration configuration) { public void setConfiguration(AuthConfiguration configuration) {
//super.setConfiguration(configuration); //super.setConfiguration(configuration);
initializeKeycloak(); initializeKeycloak();
// need this so that getUserPrincipal does not throw NPE
_loginService = new DummyLoginService();
String error = configuration.getInitParameter(FormAuthenticator.__FORM_ERROR_PAGE); String error = configuration.getInitParameter(FormAuthenticator.__FORM_ERROR_PAGE);
setErrorPage(error); setErrorPage(error);
} }

View file

@ -89,6 +89,7 @@ public class OIDCFilterSessionStore extends FilterSessionStore implements Adapte
protected void cleanSession(HttpSession session) { protected void cleanSession(HttpSession session) {
session.removeAttribute(KeycloakAccount.class.getName()); session.removeAttribute(KeycloakAccount.class.getName());
session.removeAttribute(KeycloakSecurityContext.class.getName());
clearSavedRequest(session); clearSavedRequest(session);
} }
@ -160,6 +161,7 @@ public class OIDCFilterSessionStore extends FilterSessionStore implements Adapte
SerializableKeycloakAccount sAccount = new SerializableKeycloakAccount(roles, account.getPrincipal(), securityContext); SerializableKeycloakAccount sAccount = new SerializableKeycloakAccount(roles, account.getPrincipal(), securityContext);
HttpSession httpSession = request.getSession(); HttpSession httpSession = request.getSession();
httpSession.setAttribute(KeycloakAccount.class.getName(), sAccount); 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()); if (idMapper != null) idMapper.map(account.getKeycloakSecurityContext().getToken().getClientSession(), account.getPrincipal().getName(), httpSession.getId());
//String username = securityContext.getToken().getSubject(); //String username = securityContext.getToken().getSubject();
//log.fine("userSessionManagement.login: " + username); //log.fine("userSessionManagement.login: " + username);

View file

@ -69,12 +69,22 @@ public class CatalinaSessionTokenStore extends CatalinaAdapterSessionStore imple
// just in case session got serialized // just in case session got serialized
if (session.getDeployment() == null) session.setCurrentRequestInfo(deployment, this); 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 // FYI: A refresh requires same scope, so same roles will be set. Otherwise, refresh will fail and token will
// not be updated // not be updated
boolean success = session.refreshExpiredToken(false); 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 // 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"); 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) { protected void cleanSession(Session catalinaSession) {
catalinaSession.getSession().removeAttribute(KeycloakSecurityContext.class.getName());
catalinaSession.getSession().removeAttribute(SerializableKeycloakAccount.class.getName());
catalinaSession.getSession().removeAttribute(OidcKeycloakAccount.class.getName()); catalinaSession.getSession().removeAttribute(OidcKeycloakAccount.class.getName());
catalinaSession.setPrincipal(null); catalinaSession.setPrincipal(null);
catalinaSession.setAuthType(null); catalinaSession.setAuthType(null);
@ -164,6 +176,7 @@ public class CatalinaSessionTokenStore extends CatalinaAdapterSessionStore imple
session.setPrincipal(principal); session.setPrincipal(principal);
session.setAuthType("KEYCLOAK"); session.setAuthType("KEYCLOAK");
session.getSession().setAttribute(SerializableKeycloakAccount.class.getName(), sAccount); session.getSession().setAttribute(SerializableKeycloakAccount.class.getName(), sAccount);
session.getSession().setAttribute(KeycloakSecurityContext.class.getName(), account.getKeycloakSecurityContext());
String username = securityContext.getToken().getSubject(); String username = securityContext.getToken().getSubject();
log.fine("userSessionManagement.login: " + username); log.fine("userSessionManagement.login: " + username);
this.sessionManagement.login(session); this.sessionManagement.login(session);

View file

@ -92,7 +92,8 @@ public class ServletSessionTokenStore implements AdapterTokenStore {
} else { } else {
log.debug("Refresh failed. Account was not active. Returning null and invalidating Http session"); log.debug("Refresh failed. Account was not active. Returning null and invalidating Http session");
try { try {
session.setAttribute(KeycloakUndertowAccount.class.getName(), null); session.removeAttribute(KeycloakUndertowAccount.class.getName());
session.removeAttribute(KeycloakSecurityContext.class.getName());
session.invalidate(); session.invalidate();
} catch (Exception e) { } catch (Exception e) {
log.debug("Failed to invalidate session, might already be invalidated"); 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); final ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY);
HttpSession session = getSession(true); HttpSession session = getSession(true);
session.setAttribute(KeycloakUndertowAccount.class.getName(), account); session.setAttribute(KeycloakUndertowAccount.class.getName(), account);
session.setAttribute(KeycloakSecurityContext.class.getName(), account.getKeycloakSecurityContext());
sessionManagement.login(servletRequestContext.getDeployment().getSessionManager()); sessionManagement.login(servletRequestContext.getDeployment().getSessionManager());
} }

View file

@ -22,6 +22,7 @@ import io.undertow.server.HttpServerExchange;
import io.undertow.server.session.Session; import io.undertow.server.session.Session;
import io.undertow.util.Sessions; import io.undertow.util.Sessions;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.adapters.AdapterTokenStore; import org.keycloak.adapters.AdapterTokenStore;
import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.OidcKeycloakAccount; import org.keycloak.adapters.OidcKeycloakAccount;
@ -101,6 +102,7 @@ public class UndertowSessionTokenStore implements AdapterTokenStore {
public void saveAccountInfo(OidcKeycloakAccount account) { public void saveAccountInfo(OidcKeycloakAccount account) {
Session session = Sessions.getOrCreateSession(exchange); Session session = Sessions.getOrCreateSession(exchange);
session.setAttribute(KeycloakUndertowAccount.class.getName(), account); session.setAttribute(KeycloakUndertowAccount.class.getName(), account);
session.setAttribute(KeycloakSecurityContext.class.getName(), account.getKeycloakSecurityContext());
sessionManagement.login(session.getSessionManager()); sessionManagement.login(session.getSessionManager());
} }
@ -111,6 +113,7 @@ public class UndertowSessionTokenStore implements AdapterTokenStore {
KeycloakUndertowAccount account = (KeycloakUndertowAccount)session.getAttribute(KeycloakUndertowAccount.class.getName()); KeycloakUndertowAccount account = (KeycloakUndertowAccount)session.getAttribute(KeycloakUndertowAccount.class.getName());
if (account == null) return; if (account == null) return;
session.removeAttribute(KeycloakUndertowAccount.class.getName()); session.removeAttribute(KeycloakUndertowAccount.class.getName());
session.removeAttribute(KeycloakSecurityContext.class.getName());
} }
@Override @Override

View file

@ -18,6 +18,8 @@
package org.keycloak.adapters.saml.jetty; package org.keycloak.adapters.saml.jetty;
import org.eclipse.jetty.security.DefaultUserIdentity; 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.ServerAuthException;
import org.eclipse.jetty.security.UserAuthentication; import org.eclipse.jetty.security.UserAuthentication;
import org.eclipse.jetty.security.authentication.DeferredAuthentication; 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 @Override
public void setConfiguration(AuthConfiguration configuration) { public void setConfiguration(AuthConfiguration configuration) {
//super.setConfiguration(configuration); //super.setConfiguration(configuration);
initializeKeycloak(); initializeKeycloak();
// need this so that getUserPrincipal does not throw NPE
_loginService = new DummyLoginService();
String error = configuration.getInitParameter(FormAuthenticator.__FORM_ERROR_PAGE); String error = configuration.getInitParameter(FormAuthenticator.__FORM_ERROR_PAGE);
setErrorPage(error); setErrorPage(error);
} }

View file

@ -167,9 +167,9 @@ public abstract class AbstractSamlAuthenticatorValve extends FormAuthenticator i
@Override @Override
public void invoke(Request request, Response response) throws IOException, ServletException { public void invoke(Request request, Response response) throws IOException, ServletException {
log.fine("*********************** SAML ************"); log.fine("*********************** SAML ************");
CatalinaHttpFacade facade = new CatalinaHttpFacade(response, request);
SamlDeployment deployment = deploymentContext.resolveDeployment(facade);
if (request.getRequestURI().substring(request.getContextPath().length()).endsWith("/saml")) { if (request.getRequestURI().substring(request.getContextPath().length()).endsWith("/saml")) {
CatalinaHttpFacade facade = new CatalinaHttpFacade(response, request);
SamlDeployment deployment = deploymentContext.resolveDeployment(facade);
if (deployment != null && deployment.isConfigured()) { if (deployment != null && deployment.isConfigured()) {
SamlSessionStore tokenStore = getSessionStore(request, facade, deployment); SamlSessionStore tokenStore = getSessionStore(request, facade, deployment);
SamlAuthenticator authenticator = new CatalinaSamlEndpoint(facade, deployment, tokenStore); SamlAuthenticator authenticator = new CatalinaSamlEndpoint(facade, deployment, tokenStore);
@ -180,6 +180,7 @@ public abstract class AbstractSamlAuthenticatorValve extends FormAuthenticator i
} }
try { 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); super.invoke(request, response);
} finally { } finally {
} }

View file

@ -17,6 +17,9 @@
package org.keycloak.common.constants; 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> * @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 * 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 * 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 * 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);
}
}
/** /**

View file

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

View file

@ -25,22 +25,13 @@ import java.io.ObjectInputStream;
import java.io.ObjectOutput; import java.io.ObjectOutput;
import java.io.ObjectOutputStream; import java.io.ObjectOutputStream;
import java.io.Serializable; 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 javax.security.auth.kerberos.KerberosTicket;
import org.ietf.jgss.GSSCredential; 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 * 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 class KerberosSerializationUtils {
public static final Oid KRB5_OID;
public static final Oid KRB5_NAME_OID;
public static final String JAVA_INFO; public static final String JAVA_INFO;
static { 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 javaVersion = System.getProperty("java.version");
String javaRuntimeVersion = System.getProperty("java.runtime.version"); String javaRuntimeVersion = System.getProperty("java.runtime.version");
String javaVendor = System.getProperty("java.vendor"); String javaVendor = System.getProperty("java.vendor");
@ -72,50 +54,17 @@ public class KerberosSerializationUtils {
private KerberosSerializationUtils() { private KerberosSerializationUtils() {
} }
public static String serializeCredential(GSSCredential gssCredential) throws KerberosSerializationException { public static String serializeCredential(KerberosTicket kerberosTicket, GSSCredential gssCredential) throws KerberosSerializationException {
try { try {
if (gssCredential == null) { if (gssCredential == null) {
throw new KerberosSerializationException("Null credential given as input"); throw new KerberosSerializationException("Null credential given as input");
} }
if (!(gssCredential instanceof GSSCredentialImpl)) { kerberosTicket = KerberosJdkProvider.getProvider().gssCredentialToKerberosTicket(kerberosTicket, gssCredential);
throw new KerberosSerializationException("Unknown credential type: " + gssCredential.getClass());
}
GSSCredentialImpl gssCredImpl = (GSSCredentialImpl) gssCredential; return serialize(kerberosTicket);
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) { } catch (IOException e) {
throw new KerberosSerializationException("Exception occured", e); throw new KerberosSerializationException("Unexpected exception when serialize GSSCredential", e);
} catch (GSSException e) {
throw new KerberosSerializationException("Exception occured", e);
} }
} }
@ -132,32 +81,12 @@ public class KerberosSerializationUtils {
} }
KerberosTicket ticket = (KerberosTicket) deserializedCred; KerberosTicket ticket = (KerberosTicket) deserializedCred;
String fullName = ticket.getClient().getName();
Method getInstance = Reflections.findDeclaredMethod(Krb5NameElement.class, "getInstance", String.class, Oid.class); return KerberosJdkProvider.getProvider().kerberosTicketToGSSCredential(ticket);
Krb5NameElement krb5Name = Reflections.invokeMethod(true, getInstance, Krb5NameElement.class, null, fullName, KRB5_NAME_OID); } catch (KerberosSerializationException ke) {
throw ke;
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);
} catch (Exception ioe) { } catch (Exception ioe) {
throw new KerberosSerializationException("Exception occured", ioe); throw new KerberosSerializationException("Unexpected exception when deserialize GSSCredential", ioe);
} }
} }

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

View file

@ -28,6 +28,9 @@ import java.io.ObjectOutputStream;
import java.io.Serializable; 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> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */

View 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) {
}
}
}

View file

@ -28,7 +28,6 @@
<module name="javax.api"/> <module name="javax.api"/>
<module name="javax.activation.api"/> <module name="javax.activation.api"/>
<module name="sun.jdk" optional="true" /> <module name="sun.jdk" optional="true" />
<module name="sun.jdk.jgss" optional="true" />
</dependencies> </dependencies>
</module> </module>

View file

@ -33,7 +33,6 @@
<module name="javax.api"/> <module name="javax.api"/>
<module name="javax.activation.api"/> <module name="javax.activation.api"/>
<module name="sun.jdk" optional="true" /> <module name="sun.jdk" optional="true" />
<module name="sun.jdk.jgss" optional="true" />
</dependencies> </dependencies>
</module> </module>

View file

@ -28,7 +28,6 @@
<module name="javax.api"/> <module name="javax.api"/>
<module name="javax.activation.api"/> <module name="javax.activation.api"/>
<module name="sun.jdk" optional="true" /> <module name="sun.jdk" optional="true" />
<module name="sun.jdk.jgss" optional="true" />
</dependencies> </dependencies>
</module> </module>

View file

@ -33,7 +33,6 @@
<module name="javax.api"/> <module name="javax.api"/>
<module name="javax.activation.api"/> <module name="javax.activation.api"/>
<module name="sun.jdk" optional="true" /> <module name="sun.jdk" optional="true" />
<module name="sun.jdk.jgss" optional="true" />
</dependencies> </dependencies>
</module> </module>

View file

@ -28,7 +28,6 @@
<module name="javax.api"/> <module name="javax.api"/>
<module name="javax.activation.api"/> <module name="javax.activation.api"/>
<module name="sun.jdk" optional="true" /> <module name="sun.jdk" optional="true" />
<module name="sun.jdk.jgss" optional="true" />
</dependencies> </dependencies>
</module> </module>

View file

@ -33,7 +33,6 @@
<module name="javax.api"/> <module name="javax.api"/>
<module name="javax.activation.api"/> <module name="javax.activation.api"/>
<module name="sun.jdk" optional="true" /> <module name="sun.jdk" optional="true" />
<module name="sun.jdk.jgss" optional="true" />
</dependencies> </dependencies>
</module> </module>

View file

@ -28,7 +28,6 @@
<module name="javax.api"/> <module name="javax.api"/>
<module name="javax.activation.api"/> <module name="javax.activation.api"/>
<module name="sun.jdk" optional="true" /> <module name="sun.jdk" optional="true" />
<module name="sun.jdk.jgss" optional="true" />
</dependencies> </dependencies>
</module> </module>

View file

@ -33,7 +33,6 @@
<module name="javax.api"/> <module name="javax.api"/>
<module name="javax.activation.api"/> <module name="javax.activation.api"/>
<module name="sun.jdk" optional="true" /> <module name="sun.jdk" optional="true" />
<module name="sun.jdk.jgss" optional="true" />
</dependencies> </dependencies>
</module> </module>

View file

@ -28,7 +28,6 @@
<module name="javax.api"/> <module name="javax.api"/>
<module name="javax.activation.api"/> <module name="javax.activation.api"/>
<module name="sun.jdk" optional="true" /> <module name="sun.jdk" optional="true" />
<module name="sun.jdk.jgss" optional="true" />
</dependencies> </dependencies>
</module> </module>

View file

@ -33,7 +33,6 @@
<module name="javax.api"/> <module name="javax.api"/>
<module name="javax.activation.api"/> <module name="javax.activation.api"/>
<module name="sun.jdk" optional="true" /> <module name="sun.jdk" optional="true" />
<module name="sun.jdk.jgss" optional="true" />
</dependencies> </dependencies>
</module> </module>

View file

@ -28,7 +28,6 @@
<module name="javax.api"/> <module name="javax.api"/>
<module name="javax.activation.api"/> <module name="javax.activation.api"/>
<module name="sun.jdk" optional="true" /> <module name="sun.jdk" optional="true" />
<module name="sun.jdk.jgss" optional="true" />
</dependencies> </dependencies>
</module> </module>

View file

@ -28,7 +28,6 @@
<module name="javax.api"/> <module name="javax.api"/>
<module name="javax.activation.api"/> <module name="javax.activation.api"/>
<module name="sun.jdk" optional="true" /> <module name="sun.jdk" optional="true" />
<module name="sun.jdk.jgss" optional="true" />
</dependencies> </dependencies>
</module> </module>

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

View file

@ -155,7 +155,7 @@ Authorization: basic BASE64(client-id + ':' + client-secret)
</para> </para>
<para> <para>
To retrieve the Adapter Configuration then do a HTTP GET to: To retrieve the Adapter Configuration then do a HTTP GET to:
<literal>&lt;KEYCLOAK URL&gt;//realms/&lt;realm&gt;/clients-registrations/installation/&lt;client id&gt;</literal> <literal>&lt;KEYCLOAK URL&gt;//realms/&lt;realm&gt;/clients-registrations/install/&lt;client id&gt;</literal>
</para> </para>
<para> <para>
No authentication is required for public clients. This means that for the JavaScript adapter you can No authentication is required for public clients. This means that for the JavaScript adapter you can

View file

@ -52,7 +52,7 @@ public abstract class CommonKerberosConfig {
return getConfig().get(KerberosConstants.KEYTAB); return getConfig().get(KerberosConstants.KEYTAB);
} }
public boolean getDebug() { public boolean isDebug() {
return Boolean.valueOf(getConfig().get(KerberosConstants.DEBUG)); return Boolean.valueOf(getConfig().get(KerberosConstants.DEBUG));
} }

View file

@ -17,16 +17,18 @@
package org.keycloak.federation.kerberos.impl; package org.keycloak.federation.kerberos.impl;
import java.util.HashMap; import java.io.IOException;
import java.util.Map;
import javax.security.auth.Subject; 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.Configuration;
import javax.security.auth.login.LoginContext; import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException; import javax.security.auth.login.LoginException;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.common.util.KerberosJdkProvider;
import org.keycloak.federation.kerberos.CommonKerberosConfig; 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 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 final CommonKerberosConfig config;
private LoginContext loginContext; private LoginContext loginContext;
public KerberosServerSubjectAuthenticator(CommonKerberosConfig config) { public KerberosServerSubjectAuthenticator(CommonKerberosConfig config) {
this.config = config; this.config = config;
} }
public Subject authenticateServerSubject() throws LoginException { public Subject authenticateServerSubject() throws LoginException {
Configuration config = createJaasConfiguration(); 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(); loginContext.login();
return loginContext.getSubject(); return loginContext.getSubject();
} }
public void logoutServerSubject() { public void logoutServerSubject() {
if (loginContext != null) { if (loginContext != null) {
try { try {
@ -60,24 +74,9 @@ public class KerberosServerSubjectAuthenticator {
} }
} }
protected Configuration createJaasConfiguration() { protected Configuration createJaasConfiguration() {
return new Configuration() { return KerberosJdkProvider.getProvider().createJaasConfigurationForServer(config.getKeyTab(), config.getServerPrincipal(), config.isDebug());
@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 };
}
};
} }
} }

View file

@ -33,6 +33,7 @@ import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException; import javax.security.auth.login.LoginException;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.common.util.KerberosJdkProvider;
import org.keycloak.federation.kerberos.CommonKerberosConfig; import org.keycloak.federation.kerberos.CommonKerberosConfig;
import org.keycloak.models.ModelException; import org.keycloak.models.ModelException;
@ -58,7 +59,7 @@ public class KerberosUsernamePasswordAuthenticator {
* @return true if user available * @return true if user available
*/ */
public boolean isUserAvailable(String username) { public boolean isUserAvailable(String username) {
logger.debug("Checking existence of user: " + username); logger.debugf("Checking existence of user: %s", username);
try { try {
String principal = getKerberosPrincipal(username); String principal = getKerberosPrincipal(username);
loginContext = new LoginContext("does-not-matter", null, loginContext = new LoginContext("does-not-matter", null,
@ -70,7 +71,7 @@ public class KerberosUsernamePasswordAuthenticator {
throw new IllegalStateException("Didn't expect to end here"); throw new IllegalStateException("Didn't expect to end here");
} catch (LoginException le) { } catch (LoginException le) {
String message = le.getMessage(); String message = le.getMessage();
logger.debug("Message from kerberos: " + message); logger.debugf("Message from kerberos: %s", message);
checkKerberosServerAvailable(le); checkKerberosServerAvailable(le);
@ -128,6 +129,7 @@ public class KerberosUsernamePasswordAuthenticator {
return loginContext.getSubject(); return loginContext.getSubject();
} }
public void logoutSubject() { public void logoutSubject() {
if (loginContext != null) { if (loginContext != null) {
try { try {
@ -139,7 +141,6 @@ public class KerberosUsernamePasswordAuthenticator {
} }
protected String getKerberosPrincipal(String username) throws LoginException { protected String getKerberosPrincipal(String username) throws LoginException {
if (username.contains("@")) { if (username.contains("@")) {
String[] tokens = username.split("@"); String[] tokens = username.split("@");
@ -156,6 +157,7 @@ public class KerberosUsernamePasswordAuthenticator {
return username + "@" + config.getKerberosRealm(); return username + "@" + config.getKerberosRealm();
} }
protected CallbackHandler createJaasCallbackHandler(final String principal, final String password) { protected CallbackHandler createJaasCallbackHandler(final String principal, final String password) {
return new CallbackHandler() { return new CallbackHandler() {
@ -176,17 +178,8 @@ public class KerberosUsernamePasswordAuthenticator {
}; };
} }
protected Configuration createJaasConfiguration() {
return new Configuration() {
@Override protected Configuration createJaasConfiguration() {
public AppConfigurationEntry[] getAppConfigurationEntry(String name) { return KerberosJdkProvider.getProvider().createJaasConfigurationForUsernamePasswordLogin(config.isDebug());
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 };
}
};
} }
} }

View file

@ -19,15 +19,21 @@ package org.keycloak.federation.kerberos.impl;
import java.io.IOException; import java.io.IOException;
import java.security.PrivilegedExceptionAction; import java.security.PrivilegedExceptionAction;
import java.util.Iterator;
import java.util.Set;
import javax.security.auth.Subject; 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.keycloak.common.util.Base64;
import org.ietf.jgss.GSSContext; import org.ietf.jgss.GSSContext;
import org.ietf.jgss.GSSCredential; import org.ietf.jgss.GSSCredential;
import org.ietf.jgss.GSSException; import org.ietf.jgss.GSSException;
import org.ietf.jgss.GSSManager; import org.ietf.jgss.GSSManager;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.common.util.KerberosJdkProvider;
import org.keycloak.federation.kerberos.CommonKerberosConfig; import org.keycloak.federation.kerberos.CommonKerberosConfig;
import org.keycloak.common.util.KerberosSerializationUtils; import org.keycloak.common.util.KerberosSerializationUtils;
@ -45,6 +51,7 @@ public class SPNEGOAuthenticator {
private boolean authenticated = false; private boolean authenticated = false;
private String authenticatedKerberosPrincipal = null; private String authenticatedKerberosPrincipal = null;
private GSSCredential delegationCredential; private GSSCredential delegationCredential;
private KerberosTicket kerberosTicket;
private String responseToken = null; private String responseToken = null;
public SPNEGOAuthenticator(CommonKerberosConfig kerberosConfig, KerberosServerSubjectAuthenticator kerberosSubjectAuthenticator, String spnegoToken) { public SPNEGOAuthenticator(CommonKerberosConfig kerberosConfig, KerberosServerSubjectAuthenticator kerberosSubjectAuthenticator, String spnegoToken) {
@ -61,6 +68,14 @@ public class SPNEGOAuthenticator {
try { try {
Subject serverSubject = kerberosSubjectAuthenticator.authenticateServerSubject(); Subject serverSubject = kerberosSubjectAuthenticator.authenticateServerSubject();
authenticated = Subject.doAs(serverSubject, new AcceptSecContext()); 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) { } catch (Exception e) {
log.warn("SPNEGO login failed", e); log.warn("SPNEGO login failed", e);
} finally { } finally {
@ -89,7 +104,7 @@ public class SPNEGOAuthenticator {
if (log.isTraceEnabled()) { if (log.isTraceEnabled()) {
log.trace("Serializing credential " + delegationCredential); log.trace("Serializing credential " + delegationCredential);
} }
return KerberosSerializationUtils.serializeCredential(delegationCredential); return KerberosSerializationUtils.serializeCredential(kerberosTicket, delegationCredential);
} catch (KerberosSerializationUtils.KerberosSerializationException kse) { } catch (KerberosSerializationUtils.KerberosSerializationException kse) {
log.warn("Couldn't serialize credential: " + delegationCredential, kse); log.warn("Couldn't serialize credential: " + delegationCredential, kse);
return null; return null;
@ -150,7 +165,10 @@ public class SPNEGOAuthenticator {
protected GSSContext establishContext() throws GSSException, IOException { protected GSSContext establishContext() throws GSSException, IOException {
GSSManager manager = GSSManager.getInstance(); 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[] inputToken = Base64.decode(spnegoToken);
byte[] respToken = gssContext.acceptSecContext(inputToken, 0, inputToken.length); byte[] respToken = gssContext.acceptSecContext(inputToken, 0, inputToken.length);

View file

@ -22,6 +22,7 @@ import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
import org.jboss.resteasy.client.jaxrs.ResteasyWebTarget; import org.jboss.resteasy.client.jaxrs.ResteasyWebTarget;
import org.keycloak.admin.client.Config; import org.keycloak.admin.client.Config;
import org.keycloak.admin.client.resource.BasicAuthFilter; import org.keycloak.admin.client.resource.BasicAuthFilter;
import org.keycloak.common.util.Time;
import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.AccessTokenResponse;
import javax.ws.rs.BadRequestException; import javax.ws.rs.BadRequestException;
@ -34,8 +35,11 @@ import java.util.Date;
*/ */
public class TokenManager { public class TokenManager {
private static final long DEFAULT_MIN_VALIDITY = 30;
private AccessTokenResponse currentToken; private AccessTokenResponse currentToken;
private Date expirationTime; private long expirationTime;
private long minTokenValidity = DEFAULT_MIN_VALIDITY;
private final Config config; private final Config config;
private final ResteasyClient client; private final ResteasyClient client;
@ -73,10 +77,11 @@ public class TokenManager {
TokenService tokenService = target.proxy(TokenService.class); 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 currentToken;
return response;
} }
public AccessTokenResponse refreshToken(){ public AccessTokenResponse refreshToken(){
@ -95,27 +100,22 @@ public class TokenManager {
TokenService tokenService = target.proxy(TokenService.class); TokenService tokenService = target.proxy(TokenService.class);
try { try {
AccessTokenResponse response = tokenService.refreshToken(config.getRealm(), form.asMap()); int requestTime = Time.currentTime();
defineCurrentToken(response); currentToken = tokenService.refreshToken(config.getRealm(), form.asMap());
return response; expirationTime = requestTime + currentToken.getExpiresIn();
return currentToken;
} catch (BadRequestException e) { } catch (BadRequestException e) {
return grantToken(); return grantToken();
} }
} }
private void setExpirationTime() { public void setMinTokenValidity(long minTokenValidity) {
Calendar cal = Calendar.getInstance(); this.minTokenValidity = minTokenValidity;
cal.add(Calendar.SECOND, (int) currentToken.getExpiresIn());
expirationTime = cal.getTime();
} }
private boolean tokenExpired() { private boolean tokenExpired() {
return new Date().after(expirationTime); return (Time.currentTime() + minTokenValidity) >= expirationTime;
}
private void defineCurrentToken(AccessTokenResponse accessTokenResponse){
currentToken = accessTokenResponse;
setExpirationTime();
} }
} }

View file

@ -36,10 +36,10 @@ public interface TokenService {
@POST @POST
@Path("/realms/{realm}/protocol/openid-connect/token") @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 @POST
@Path("/realms/{realm}/protocol/openid-connect/token") @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);
} }

View file

@ -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 ~ Copyright 2016 Red Hat, Inc. and/or its affiliates
~ and other contributors as indicated by the @author tags. ~ and other contributors as indicated by the @author tags.
@ -19,17 +16,8 @@
~ limitations under the License. ~ limitations under the License.
--> -->
<module xmlns="urn:jboss:module:1.3" name="sun.jdk.jgss"> <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">
<resources> <changeSet author="keycloak" id="1.9.1">
</resources> <modifyDataType tableName="REALM" columnName="PRIVATE_KEY" newDataType="VARCHAR(4096)"/>
<dependencies> </changeSet>
<system export="true"> </databaseChangeLog>
<paths>
<path name="sun/security/jgss" />
<path name="sun/security/jgss/spi" />
<path name="sun/security/jgss/krb5" />
</paths>
</system>
</dependencies>
</module>

View file

@ -30,4 +30,5 @@
<include file="META-INF/jpa-changelog-1.7.0.xml"/> <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.8.0.xml"/>
<include file="META-INF/jpa-changelog-1.9.0.xml"/> <include file="META-INF/jpa-changelog-1.9.0.xml"/>
<include file="META-INF/jpa-changelog-1.9.1.xml"/>
</databaseChangeLog> </databaseChangeLog>

View file

@ -86,7 +86,7 @@ public class Pbkdf2PasswordHashProvider implements PasswordHashProviderFactory,
byte[] key = getSecretKeyFactory().generateSecret(spec).getEncoded(); byte[] key = getSecretKeyFactory().generateSecret(spec).getEncoded();
return Base64.encodeBytes(key); return Base64.encodeBytes(key);
} catch (InvalidKeySpecException e) { } 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 { try {
return SecretKeyFactory.getInstance(PBKDF2_ALGORITHM); return SecretKeyFactory.getInstance(PBKDF2_ALGORITHM);
} catch (NoSuchAlgorithmException e) { } catch (NoSuchAlgorithmException e) {
throw new RuntimeException("PBKDF2 algorithm not found"); throw new RuntimeException("PBKDF2 algorithm not found", e);
} }
} }

View file

@ -71,7 +71,7 @@ public class CredentialValidation {
public static boolean validateHashedCredential(KeycloakSession session, RealmModel realm, UserModel user, String unhashedCredValue, UserCredentialValueModel credential) { public static boolean validateHashedCredential(KeycloakSession session, RealmModel realm, UserModel user, String unhashedCredValue, UserCredentialValueModel credential) {
if(unhashedCredValue == null){ if (unhashedCredValue == null || unhashedCredValue.isEmpty()) {
return false; return false;
} }

View file

@ -695,6 +695,16 @@ public class RepresentationToModel {
if ("GENERATE".equals(rep.getPublicKey())) { if ("GENERATE".equals(rep.getPublicKey())) {
KeycloakModelUtils.generateRealmKeys(realm); 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){ if(rep.isInternationalizationEnabled() != null){

View file

@ -621,7 +621,7 @@ public class AccountService extends AbstractSecuredLocalService {
} }
} }
if (Validation.isEmpty(passwordNew)) { if (Validation.isBlank(passwordNew)) {
setReferrerOnPage(); setReferrerOnPage();
return account.setError(Messages.MISSING_PASSWORD).createResponse(AccountPages.PASSWORD); return account.setError(Messages.MISSING_PASSWORD).createResponse(AccountPages.PASSWORD);
} }

View file

@ -20,7 +20,10 @@ import org.jboss.resteasy.annotations.cache.NoCache;
import org.jboss.resteasy.spi.BadRequestException; import org.jboss.resteasy.spi.BadRequestException;
import org.jboss.resteasy.spi.NotFoundException; import org.jboss.resteasy.spi.NotFoundException;
import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.KeyPairVerifier;
import org.keycloak.common.ClientConnection; 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.Event;
import org.keycloak.events.EventQuery; import org.keycloak.events.EventQuery;
import org.keycloak.events.EventStoreProvider; import org.keycloak.events.EventStoreProvider;
@ -30,6 +33,8 @@ import org.keycloak.events.admin.AdminEventQuery;
import org.keycloak.events.admin.OperationType; import org.keycloak.events.admin.OperationType;
import org.keycloak.exportimport.ClientDescriptionConverter; import org.keycloak.exportimport.ClientDescriptionConverter;
import org.keycloak.exportimport.ClientDescriptionConverterFactory; 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.ClientModel;
import org.keycloak.models.GroupModel; import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession; 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.HttpHeaders;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriInfo; import javax.ws.rs.core.UriInfo;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.text.ParseException; import java.text.ParseException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
@ -236,6 +244,14 @@ public class RealmAdminResource {
logger.debug("updating realm: " + realm.getName()); logger.debug("updating realm: " + realm.getName());
try { 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); RepresentationToModel.updateRealm(rep, realm);
// Refresh periodic sync tasks for configured federationProviders // Refresh periodic sync tasks for configured federationProviders
@ -253,7 +269,7 @@ public class RealmAdminResource {
throw e; throw e;
} catch (Exception e) { } catch (Exception e) {
logger.error(e.getMessage(), 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);
} }
} }

View file

@ -93,6 +93,7 @@ import org.keycloak.services.managers.BruteForceProtector;
import org.keycloak.services.managers.UserSessionManager; import org.keycloak.services.managers.UserSessionManager;
import org.keycloak.services.resources.AccountService; import org.keycloak.services.resources.AccountService;
import org.keycloak.common.util.Time; import org.keycloak.common.util.Time;
import org.keycloak.services.validation.Validation;
/** /**
* Base resource for managing users * Base resource for managing users
@ -707,6 +708,9 @@ public class UsersResource {
if (pass == null || pass.getValue() == null || !CredentialRepresentation.PASSWORD.equals(pass.getType())) { if (pass == null || pass.getValue() == null || !CredentialRepresentation.PASSWORD.equals(pass.getType())) {
throw new BadRequestException("No password provided"); throw new BadRequestException("No password provided");
} }
if (Validation.isBlank(pass.getValue())) {
throw new BadRequestException("Empty password not allowed");
}
UserCredentialModel cred = RepresentationToModel.convertCredential(pass); UserCredentialModel cred = RepresentationToModel.convertCredential(pass);
try { try {

View file

@ -152,6 +152,7 @@ integration-arquillian
├──console (activated by -Pconsole-ui-tests) ├──console (activated by -Pconsole-ui-tests)
├──mod_auth_mellon (activated by -Pmod_auth_mellon) ├──mod_auth_mellon (activated by -Pmod_auth_mellon)
├──console_no_users (activated by -Pconsole-ui-no-users-tests)
└──... └──...
``` ```

View file

@ -17,7 +17,9 @@
package org.keycloak.testsuite.arquillian; package org.keycloak.testsuite.arquillian;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* *
@ -33,6 +35,8 @@ public final class TestContext {
private final List<ContainerInfo> appServerBackendsInfo = new ArrayList<>(); private final List<ContainerInfo> appServerBackendsInfo = new ArrayList<>();
private boolean adminLoggedIn; private boolean adminLoggedIn;
private final Map customContext = new HashMap<>();
public TestContext(SuiteContext suiteContext, Class testClass) { public TestContext(SuiteContext suiteContext, Class testClass) {
this.suiteContext = suiteContext; this.suiteContext = suiteContext;
@ -88,4 +92,12 @@ public final class TestContext {
+ (isAdapterTest() ? "App server container: " + getAppServerInfo() + "\n" : ""); + (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);
}
} }

View file

@ -17,6 +17,7 @@
package org.keycloak.testsuite.auth.page; package org.keycloak.testsuite.auth.page;
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement; import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy; import org.openqa.selenium.support.FindBy;
@ -38,7 +39,8 @@ public class WelcomePage extends AuthServer {
private WebElement createButton; private WebElement createButton;
public boolean isPasswordSet() { 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) { 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();
}
} }

View file

@ -40,7 +40,7 @@ public abstract class Login extends AuthRealm {
@Override @Override
public UriBuilder createUriBuilder() { public UriBuilder createUriBuilder() {
return super.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) { public void setProtocol(String protocol) {

View file

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

View file

@ -43,6 +43,8 @@ public class IOUtil {
private static final Logger log = Logger.getLogger(IOUtil.class); 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) { public static <T> T loadJson(InputStream is, Class<T> type) {
try { try {
return JsonSerialization.readValue(is, type); return JsonSerialization.readValue(is, type);

View file

@ -14,15 +14,28 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.testsuite.util; 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.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; 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 { 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() { protected static final File DATA_DIR = new File(PROJECT_BUILD_DIRECTORY, "stats/data");
time = new Date().getTime(); protected static final File CHARTS_DIR = new File(PROJECT_BUILD_DIRECTORY, "stats/charts");
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) {
} else {
elapsedTime = new Date().getTime() - time;
}
return elapsedTime;
} }
public static void time(String operation) { public void reset() {
if (time == null) { reset(operation); // log last operation
System.out.println(MessageFormat.format("Starting timer for operation {0}", operation)); }
time();
} else { public void reset(String operation) {
long timeOrig = time; reset(operation, true);
time(); }
logOperation(operation, time - timeOrig);
System.out.println(MessageFormat.format("Operation {0} took {1} ms", operation, time - timeOrig)); 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)) { if (!stats.containsKey(operation)) {
stats.put(operation, new ArrayList<Long>()); 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() { public void clearStats() {
if (!stats.isEmpty()) { clearStats(true, true, true);
System.out.println("OPERATION STATS:"); }
}
for (String op : stats.keySet()) { public void clearStats(boolean logStats, boolean saveData, boolean saveCharts) {
long sum = 0; if (logStats) {
for (Long t : stats.get(op)) { log.info("Timer Statistics:");
sum += t; for (String op : stats.keySet()) {
long sum = 0;
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(); 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 + "'.");
}
}
} }

View file

@ -16,6 +16,7 @@
*/ */
package org.keycloak.testsuite; package org.keycloak.testsuite;
import java.io.File;
import org.keycloak.testsuite.arquillian.TestContext; import org.keycloak.testsuite.arquillian.TestContext;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -36,7 +37,6 @@ import org.keycloak.admin.client.resource.RealmsResource;
import org.keycloak.models.Constants; import org.keycloak.models.Constants;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation; 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.AuthServerTestEnricher;
import org.keycloak.testsuite.arquillian.SuiteContext; import org.keycloak.testsuite.arquillian.SuiteContext;
import org.keycloak.testsuite.auth.page.WelcomePage; 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.auth.page.login.UpdatePassword;
import org.keycloak.testsuite.util.Timer; import org.keycloak.testsuite.util.Timer;
import org.keycloak.testsuite.util.WaitUtils; 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() { public void afterAbstractKeycloakTest() {
// removeTestRealms(); // keeping test realms after test to be able to inspect failures, instead deleting existing realms before import // removeTestRealms(); // keeping test realms after test to be able to inspect failures, instead deleting existing realms before import
// keycloak.close(); // keeping admin connection open // keycloak.close(); // keeping admin connection open
Timer.printStats();
} }
private void updateMasterAdminPassword() { private void updateMasterAdminPassword() {

View file

@ -62,6 +62,9 @@ public class ChangePasswordTest extends AbstractAccountManagementTest {
testRealmChangePasswordPage.changePasswords(correctPassword, NEW_PASSWORD, NEW_PASSWORD + "-mismatch"); testRealmChangePasswordPage.changePasswords(correctPassword, NEW_PASSWORD, NEW_PASSWORD + "-mismatch");
assertAlertError(); assertAlertError();
testRealmChangePasswordPage.changePasswords(correctPassword, " ", " ");
assertAlertError();
} }
@Test @Test

View file

@ -27,6 +27,7 @@ import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.adapter.AbstractServletsAdapterTest; import org.keycloak.testsuite.adapter.AbstractServletsAdapterTest;
import org.keycloak.testsuite.adapter.page.*; import org.keycloak.testsuite.adapter.page.*;
import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.auth.page.login.SAMLIDPInitiatedLogin;
import org.keycloak.testsuite.util.IOUtil; import org.keycloak.testsuite.util.IOUtil;
import org.w3c.dom.Document; import org.w3c.dom.Document;
@ -81,6 +82,9 @@ public abstract class AbstractSAMLServletsAdapterTest extends AbstractServletsAd
@Page @Page
private SalesPostSigTransientServlet salesPostSigTransientServletPage; private SalesPostSigTransientServlet salesPostSigTransientServletPage;
@Page
private SAMLIDPInitiatedLogin samlidpInitiatedLogin;
@Deployment(name = BadClientSalesPostSigServlet.DEPLOYMENT_NAME) @Deployment(name = BadClientSalesPostSigServlet.DEPLOYMENT_NAME)
protected static WebArchive badClientSalesPostSig() { protected static WebArchive badClientSalesPostSig() {
return samlServletDeployment(BadClientSalesPostSigServlet.DEPLOYMENT_NAME, SendUsernameServlet.class); 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")); assertTrue(driver.getPageSource().contains("Forbidden") || driver.getPageSource().contains("Status 403"));
salesPostSigTransientServletPage.logout(); 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();
}
} }

View file

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

View file

@ -299,7 +299,8 @@
], ],
"adminUrl": "http://localhost:8080/employee2", "adminUrl": "http://localhost:8080/employee2",
"attributes": { "attributes": {
"saml.authnstatement": "true" "saml.authnstatement": "true",
"saml_idp_initiated_sso_url_name" : "employee2"
}, },
"protocolMappers": [ "protocolMappers": [
{ {

View file

@ -60,7 +60,7 @@
<property name="enabled">${auth.server.wildfly}</property> <property name="enabled">${auth.server.wildfly}</property>
<property name="adapterImplClass">org.jboss.as.arquillian.container.managed.ManagedDeployableContainer</property> <property name="adapterImplClass">org.jboss.as.arquillian.container.managed.ManagedDeployableContainer</property>
<property name="jbossHome">${keycloak.home}</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="managementPort">${auth.server.management.port}</property>
<property name="startupTimeoutInSeconds">${startup.timeout.sec}</property> <property name="startupTimeoutInSeconds">${startup.timeout.sec}</property>
</configuration> </configuration>
@ -131,7 +131,7 @@
<property name="enabled">${auth.server.eap7}</property> <property name="enabled">${auth.server.eap7}</property>
<property name="adapterImplClass">org.jboss.as.arquillian.container.managed.ManagedDeployableContainer</property> <property name="adapterImplClass">org.jboss.as.arquillian.container.managed.ManagedDeployableContainer</property>
<property name="jbossHome">${keycloak.home}</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="startupTimeoutInSeconds">${startup.timeout.sec}</property>
<property name="managementPort">${auth.server.management.port}</property> <property name="managementPort">${auth.server.management.port}</property>
</configuration> </configuration>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -134,12 +134,24 @@
<module>console</module> <module>console</module>
</modules> </modules>
</profile> </profile>
<profile>
<id>console-ui-no-users-tests</id>
<modules>
<module>console_no_users</module>
</modules>
</profile>
<profile> <profile>
<id>mod_auth_mellon</id> <id>mod_auth_mellon</id>
<modules> <modules>
<module>mod_auth_mellon</module> <module>mod_auth_mellon</module>
</modules> </modules>
</profile> </profile>
<profile>
<id>jpa-performance</id>
<modules>
<module>jpa-performance</module>
</modules>
</profile>
</profiles> </profiles>
</project> </project>

View file

@ -108,6 +108,7 @@
<artifactId>maven-surefire-plugin</artifactId> <artifactId>maven-surefire-plugin</artifactId>
<configuration> <configuration>
<systemPropertyVariables> <systemPropertyVariables>
<project.build.directory>${project.build.directory}</project.build.directory>
<browser>${browser}</browser> <browser>${browser}</browser>
<firefox_binary>${firefox_binary}</firefox_binary> <firefox_binary>${firefox_binary}</firefox_binary>
<shouldDeploy>false</shouldDeploy> <shouldDeploy>false</shouldDeploy>
@ -223,6 +224,12 @@
<version>2.1.0.Alpha3</version><!-- TODO upgrade <arquillian-graphene.version> and use ${arquillian-graphene.version} --> <version>2.1.0.Alpha3</version><!-- TODO upgrade <arquillian-graphene.version> and use ${arquillian-graphene.version} -->
</dependency> </dependency>
<dependency>
<groupId>jfree</groupId>
<artifactId>jfreechart</artifactId>
<version>1.0.13</version>
</dependency>
<!-- <dependency> <!-- <dependency>
<groupId>org.arquillian.extension</groupId> <groupId>org.arquillian.extension</groupId>
<artifactId>arquillian-recorder-reporter-impl</artifactId> <artifactId>arquillian-recorder-reporter-impl</artifactId>
@ -411,10 +418,83 @@
<groupId>org.codehaus.mojo</groupId> <groupId>org.codehaus.mojo</groupId>
<artifactId>xml-maven-plugin</artifactId> <artifactId>xml-maven-plugin</artifactId>
</plugin> </plugin>
<plugin>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-maven-plugin</artifactId>
</plugin>
</plugins> </plugins>
</build> </build>
</profile> </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> <profile>
<id>auth-server-wildfly</id> <id>auth-server-wildfly</id>
<properties> <properties>

View file

@ -138,6 +138,12 @@ public class AdapterTestStrategy extends ExternalResource {
String pageSource = driver.getPageSource(); String pageSource = driver.getPageSource();
System.out.println(pageSource); System.out.println(pageSource);
Assert.assertTrue(pageSource.contains("parameter=hello")); 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 // test logout

View file

@ -100,6 +100,7 @@ public class FilterAdapterTest {
@Test @Test
public void testSavedPostRequest() throws Exception { public void testSavedPostRequest() throws Exception {
System.setProperty("insecure.user.principal.unsupported", "true");
testStrategy.testSavedPostRequest(); testStrategy.testSavedPostRequest();
} }

View file

@ -17,6 +17,9 @@
package org.keycloak.testsuite.adapter; package org.keycloak.testsuite.adapter;
import org.junit.Assert;
import org.keycloak.KeycloakSecurityContext;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
@ -35,6 +38,17 @@ public class InputServlet extends HttpServlet {
@Override @Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String appBase = System.getProperty("app.server.base.url", "http://localhost:8081"); 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"; String actionUrl = appBase + "/input-portal/secured/post";

View file

@ -48,8 +48,8 @@ public class ConcurrencyTest extends AbstractClientTest {
private static final Logger log = Logger.getLogger(ConcurrencyTest.class); private static final Logger log = Logger.getLogger(ConcurrencyTest.class);
private static final int DEFAULT_THREADS = 10; private static final int DEFAULT_THREADS = 5;
private static final int DEFAULT_ITERATIONS = 100; private static final int DEFAULT_ITERATIONS = 20;
// If enabled only one request is allowed at the time. Useful for checking that test is working. // If enabled only one request is allowed at the time. Useful for checking that test is working.
private static final boolean SYNCHRONIZED = false; private static final boolean SYNCHRONIZED = false;

View file

@ -25,6 +25,7 @@ import org.keycloak.admin.client.resource.ServerInfoResource;
import org.keycloak.models.Constants; import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.RoleRepresentation;
@ -32,6 +33,7 @@ import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.KeycloakServer; import org.keycloak.testsuite.KeycloakServer;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.NotFoundException; import javax.ws.rs.NotFoundException;
import java.io.IOException; import java.io.IOException;
import java.util.Collections; import java.util.Collections;
@ -51,6 +53,10 @@ import static org.junit.Assert.fail;
*/ */
public class RealmTest extends AbstractClientTest { 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 @Test
public void getRealms() { public void getRealms() {
List<RealmRepresentation> realms = keycloak.realms().findAll(); 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());
}
} }

View file

@ -646,6 +646,23 @@ public class UserTest extends AbstractClientTest {
assertEquals("Keycloak Account Management", driver.getTitle()); 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() { private void switchEditUsernameAllowedOn() {
RealmRepresentation rep = realm.toRepresentation(); RealmRepresentation rep = realm.toRepresentation();
rep.setEditUsernameAllowed(true); rep.setEditUsernameAllowed(true);

View file

@ -192,6 +192,10 @@ public abstract class AbstractKerberosTest {
loginPage.login("jduke", "theduke"); loginPage.login("jduke", "theduke");
changePasswordPage.assertCurrent(); 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 // Change password is not possible as editMode is READ_ONLY
changePasswordPage.changePassword("theduke", "newPass", "newPass"); changePasswordPage.changePassword("theduke", "newPass", "newPass");
Assert.assertTrue(driver.getPageSource().contains("You can't update your password as your account is read only")); Assert.assertTrue(driver.getPageSource().contains("You can't update your password as your account is read only"));

View file

@ -17,6 +17,9 @@
package org.keycloak.testsuite.keycloaksaml; package org.keycloak.testsuite.keycloaksaml;
import org.junit.Assert;
import org.keycloak.KeycloakSecurityContext;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
@ -34,6 +37,16 @@ public class InputServlet extends HttpServlet {
@Override @Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String appBase = System.getProperty("app.server.base.url", "http://localhost:8081"); 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"; String actionUrl = appBase + "/input-portal/secured/post";

View file

@ -152,6 +152,12 @@ public class SamlAdapterTestStrategy extends ExternalResource {
String pageSource = driver.getPageSource(); String pageSource = driver.getPageSource();
System.out.println(pageSource); System.out.println(pageSource);
Assert.assertTrue(pageSource.contains("parameter=hello")); 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 // test logout

View file

@ -86,7 +86,7 @@
"connectionsInfinispan": { "connectionsInfinispan": {
"default": { "default": {
"clustered": "${keycloak.connectionsInfinispan.clustered:false}", "clustered": "${keycloak.connectionsInfinispan.clustered:false}",
"async": "${keycloak.connectionsInfinispan.async:true}", "async": "${keycloak.connectionsInfinispan.async:false}",
"sessionsOwners": "${keycloak.connectionsInfinispan.sessionsOwners:2}" "sessionsOwners": "${keycloak.connectionsInfinispan.sessionsOwners:2}"
} }
} }

View file

@ -31,4 +31,4 @@ idm.test.kerberos.allow.kerberos.authentication=true
idm.test.kerberos.realm=KEYCLOAK.ORG idm.test.kerberos.realm=KEYCLOAK.ORG
idm.test.kerberos.server.principal=HTTP/localhost@KEYCLOAK.ORG idm.test.kerberos.server.principal=HTTP/localhost@KEYCLOAK.ORG
idm.test.kerberos.debug=false idm.test.kerberos.debug=false
idm.test.kerberos.use.kerberos.for.password.authentication=false idm.test.kerberos.use.kerberos.for.password.authentication=true

View file

@ -1,8 +1,8 @@
[libdefaults] [libdefaults]
default_realm = KEYCLOAK.ORG default_realm = KEYCLOAK.ORG
default_tgs_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 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 permitted_enctypes = des3-cbc-sha1-kd aes256-cts-hmac-sha1-96 rc4-hmac aes128-cts-hmac-sha1-96
kdc_timeout = 30000 kdc_timeout = 30000
dns_lookup_realm = false dns_lookup_realm = false
dns_lookup_kdc = false dns_lookup_kdc = false

View file

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

View file

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

View file

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

View file

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

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

View file

@ -38,6 +38,7 @@ sslRequired.option.external=external requests
sslRequired.option.none=none 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. 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 publicKey=Public key
privateKey=Private key
gen-new-keys=Generate new keys gen-new-keys=Generate new keys
certificate=Certificate certificate=Certificate
host=Host host=Host

View file

@ -1171,17 +1171,8 @@ module.controller('CreateClientCtrl', function($scope, realm, client, templates,
$scope.save = function() { $scope.save = function() {
$scope.client.protocol = $scope.protocol; $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({ Client.save({
realm: realm.realm, realm: realm.realm,
client: '' 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) { module.controller('ClientProtocolMapperCtrl', function($scope, realm, serverInfo, client, mapper, ClientProtocolMapper, Notifications, Dialog, $location) {
/*
$scope.realm = realm; $scope.realm = realm;
/*
$scope.client = client; $scope.client = client;
$scope.create = false; $scope.create = false;
$scope.protocol = client.protocol; $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) { module.controller('ClientProtocolMapperCreateCtrl', function($scope, realm, serverInfo, client, ClientProtocolMapper, Notifications, Dialog, $location) {
$scope.realm = realm;
if (client.protocol == null) { if (client.protocol == null) {
client.protocol = 'openid-connect'; client.protocol = 'openid-connect';
} }
var protocol = client.protocol; var protocol = client.protocol;
/* /*
$scope.realm = realm;
$scope.client = client; $scope.client = client;
$scope.create = true; $scope.create = true;
$scope.protocol = protocol; $scope.protocol = protocol;

View file

@ -990,8 +990,21 @@ module.controller('RealmTokenDetailCtrl', function($scope, Realm, realm, $http,
}; };
}); });
module.controller('RealmKeysDetailCtrl', function($scope, Realm, realm, $http, $location, Dialog, Notifications) { module.controller('RealmKeysDetailCtrl', function($scope, Realm, realm, $http, $route, $location, Dialog, Notifications) {
$scope.realm = realm; $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() { $scope.generate = function() {
Dialog.confirmGenerateKeys($scope.realm.realm, 'realm', 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) { module.controller('RealmSessionStatsCtrl', function($scope, realm, stats, RealmClientSessionStats, RealmLogoutAll, Notifications) {

View file

@ -220,14 +220,14 @@
<div class="input-group" ng-repeat="(i, redirectUri) in client.redirectUris track by $index"> <div class="input-group" ng-repeat="(i, redirectUri) in client.redirectUris track by $index">
<input class="form-control" ng-model="client.redirectUris[i]"> <input class="form-control" ng-model="client.redirectUris[i]">
<div class="input-group-addon"> <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> </div>
<div class="input-group"> <div class="input-group">
<input class="form-control" ng-model="newRedirectUri" id="newRedirectUri"> <input class="form-control" ng-model="newRedirectUri" id="newRedirectUri">
<div class="input-group-addon"> <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> </div>
</div> </div>
@ -279,14 +279,14 @@
<div class="input-group" ng-repeat="(i, webOrigin) in client.webOrigins track by $index"> <div class="input-group" ng-repeat="(i, webOrigin) in client.webOrigins track by $index">
<input class="form-control" ng-model="client.webOrigins[i]"> <input class="form-control" ng-model="client.webOrigins[i]">
<div class="input-group-addon"> <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> </div>
<div class="input-group"> <div class="input-group">
<input class="form-control" ng-model="newWebOrigin" id="newWebOrigin"> <input class="form-control" ng-model="newWebOrigin" id="newWebOrigin">
<div class="input-group-addon"> <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> </div>
</div> </div>

View file

@ -18,8 +18,8 @@
</div> </div>
<div class="col-md-6" data-ng-show="importing"> <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" type="button" 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="reset()">{{:: 'clear-import' | translate}}</button>
</div> </div>
</div> </div>
@ -58,14 +58,14 @@
<kc-tooltip>{{:: 'client-template.tooltip' | translate}}</kc-tooltip> <kc-tooltip>{{:: 'client-template.tooltip' | translate}}</kc-tooltip>
</div> </div>
<div class="form-group" data-ng-hide="protocol == 'saml'"> <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"> <div class="col-sm-6">
<input class="form-control" type="text" name="rootUrl" id="rootUrl" data-ng-model="client.rootUrl"> <input class="form-control" type="text" name="rootUrl" id="rootUrl" data-ng-model="client.rootUrl">
</div> </div>
<kc-tooltip>{{:: 'root-url.tooltip' | translate}}</kc-tooltip> <kc-tooltip>{{:: 'root-url.tooltip' | translate}}</kc-tooltip>
</div> </div>
<div class="form-group" data-ng-show="protocol == 'saml'"> <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"> <div class="col-sm-6">
<input class="form-control" type="text" name="masterSamlUrl" id="masterSamlUrl" <input class="form-control" type="text" name="masterSamlUrl" id="masterSamlUrl"
data-ng-model="client.adminUrl"> data-ng-model="client.adminUrl">

View file

@ -3,26 +3,39 @@
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm"> <form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
<fieldset class="border-top"> <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"> <div class="form-group">
<label class="col-md-2 control-label" for="publicKey">{{:: 'publicKey' | translate}}</label> <label class="col-md-2 control-label" for="publicKey">{{:: 'publicKey' | translate}}</label>
<div class="col-md-10"> <div class="col-md-10">
<textarea type="text" id="publicKey" name="publicKey" class="form-control" rows="4" <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> </div>
<div class="form-group"> <div class="form-group">
<label class="col-md-2 control-label" for="certificate">{{:: 'certificate' | translate}}</label> <label class="col-md-2 control-label" for="certificate">{{:: 'certificate' | translate}}</label>
<div class="col-md-10"> <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>
</div> </div>
</fieldset> </fieldset>
<div class="form-group" data-ng-show="access.manageRealm"> <div class="form-group" data-ng-show="access.manageRealm">
<div class="col-md-10 col-md-offset-2"> <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>
</div> </div>
</form> </form>

View file

@ -19,8 +19,11 @@ package org.keycloak.util.ldap;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.Locale;
import java.util.Properties; import java.util.Properties;
import java.util.Set; 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.KerberosTime;
import org.apache.directory.shared.kerberos.KerberosUtils; import org.apache.directory.shared.kerberos.KerberosUtils;
import org.apache.directory.shared.kerberos.codec.types.EncryptionType; 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.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> * @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); this.kdcEncryptionTypes = readProperty(PROPERTY_KDC_ENCTYPES, DEFAULT_KDC_ENCRYPTION_TYPES);
if (ldapSaslPrincipal == null || ldapSaslPrincipal.isEmpty()) { if (ldapSaslPrincipal == null || ldapSaslPrincipal.isEmpty()) {
try { String hostname = getHostnameForSASLPrincipal(bindHost);
// Same algorithm like sun.security.krb5.PrincipalName constructor this.ldapSaslPrincipal = "ldap/" + hostname + "@" + this.kerberosRealm;
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);
}
} }
} }
@ -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. * Replacement of apacheDS KdcServer class with disabled ticket replay cache.
* *