Merge pull request #1191 from Smartling/KEYCLOAK-1238

Add Spring Security adapter
This commit is contained in:
Bill Burke 2015-04-28 21:49:33 -04:00
commit 4a89fe93b5
32 changed files with 1926 additions and 0 deletions

View file

@ -31,5 +31,6 @@
<module>admin-client</module>
<module>osgi-adapter</module>
<module>spring-boot</module>
<module>spring-security</module>
</modules>
</project>

View file

@ -0,0 +1,135 @@
<?xml version="1.0"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>1.2.0.RC1-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>keycloak-spring-security-adapter</artifactId>
<name>Keycloak Spring Security Integration</name>
<description/>
<properties>
<spring.version>3.2.7.RELEASE</spring.version>
<spring-security.version>3.2.7.RELEASE</spring-security.version>
<mockito.version>1.9.5</mockito.version>
<apache-httpcomponents.version>4.3.6</apache-httpcomponents.version>
</properties>
<dependencies>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-adapter-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.jboss.spec.javax.servlet</groupId>
<artifactId>jboss-servlet-api_3.0_spec</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>${spring-security.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>${spring-security.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>${apache-httpcomponents.version}</version>
</dependency>
<dependency>
<groupId>net.iharder</groupId>
<artifactId>base64</artifactId>
<version>${base64.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>${bouncycastle.crypto.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-core-asl</artifactId>
<version>${jackson.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-mapper-asl</artifactId>
<version>${jackson.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-xc</artifactId>
<version>${jackson.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>${slf4j.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
<version>${jboss.logging.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.6</source>
<target>1.6</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

View file

@ -0,0 +1,58 @@
package org.keycloak.adapters.springsecurity;
import org.keycloak.adapters.AdapterDeploymentContext;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.KeycloakDeploymentBuilder;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.io.Resource;
import java.io.InputStream;
/**
* Bean holding the {@link KeycloakDeployment} and {@link AdapterDeploymentContext} for this
* Spring application context. The Keycloak deployment is loaded from the required
* <code>WEB-INF/keycloak.json</code> file generated by Keycloak.
*
* @author <a href="mailto:srossillo@smartling.com">Scott Rossillo</a>
* @version $Revision: 1 $
*/
public class AdapterDeploymentContextBean implements ApplicationContextAware, InitializingBean {
private ApplicationContext applicationContext;
private AdapterDeploymentContext deploymentContext;
private KeycloakDeployment deployment;
@Override
public void afterPropertiesSet() throws Exception {
Resource resource = applicationContext.getResource("WEB-INF/keycloak.json");
InputStream is = resource.getInputStream();
this.deployment = KeycloakDeploymentBuilder.build(is);
this.deploymentContext = new AdapterDeploymentContext(deployment);
}
/**
* Returns the Keycloak {@link AdapterDeploymentContext} for this application context.
*
* @return the Keycloak {@link AdapterDeploymentContext} for this application context
*/
public AdapterDeploymentContext getDeploymentContext() {
return deploymentContext;
}
/**
* Returns the {@link KeycloakDeployment} for this application context.
*
* @return the {@link KeycloakDeployment} for this application context
*/
public KeycloakDeployment getDeployment() {
return deployment;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}

View file

@ -0,0 +1,10 @@
package org.keycloak.adapters.springsecurity;
/**
* Locator interface for Spring context component scanning.
*
* @author <a href="mailto:srossillo@smartling.com">Scott Rossillo</a>
* @version $Revision: 1 $
*/
public interface KeycloakSecurityComponents {
}

View file

@ -0,0 +1,62 @@
package org.keycloak.adapters.springsecurity.account;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.util.Assert;
/**
* Represents an authority granted to an {@link Authentication} by the Keycloak server.
*
* @author <a href="mailto:srossillo@smartling.com">Scott Rossillo</a>
* @version $Revision: 1 $
*/
public class KeycloakRole implements GrantedAuthority {
private String role;
/**
* Creates a new granted authority from the given Keycloak role.
*
* @param role the name of this granted authority
*/
public KeycloakRole(String role) {
Assert.notNull(role, "role cannot be null");
this.role = role;
}
@Override
public String getAuthority() {
return role;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof KeycloakRole)) {
return false;
}
KeycloakRole that = (KeycloakRole) o;
if (!role.equals(that.role)) {
return false;
}
return true;
}
@Override
public int hashCode() {
return 3 * role.hashCode();
}
@Override
public String toString() {
return "KeycloakRole{" +
"role='" + role + '\'' +
'}';
}
}

View file

@ -0,0 +1,42 @@
package org.keycloak.adapters.springsecurity.account;
import org.keycloak.adapters.KeycloakAccount;
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
import java.io.Serializable;
import java.security.Principal;
import java.util.Set;
/**
* Concrete, serializable {@link KeycloakAccount} implementation.
*
* @author <a href="mailto:srossillo@smartling.com">Scott Rossillo</a>
* @version $Revision: 1 $
*/
public class SimpleKeycloakAccount implements KeycloakAccount, Serializable {
private Set<String> roles;
private Principal principal;
private RefreshableKeycloakSecurityContext securityContext;
public SimpleKeycloakAccount(Principal principal, Set<String> roles, RefreshableKeycloakSecurityContext securityContext) {
this.principal = principal;
this.roles = roles;
this.securityContext = securityContext;
}
@Override
public Principal getPrincipal() {
return principal;
}
@Override
public Set<String> getRoles() {
return roles;
}
@Override
public RefreshableKeycloakSecurityContext getKeycloakSecurityContext() {
return securityContext;
}
}

View file

@ -0,0 +1,45 @@
package org.keycloak.adapters.springsecurity.authentication;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.util.Assert;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* Provides a Keycloak {@link AuthenticationEntryPoint authentication entry point}.
*
* @author <a href="mailto:srossillo@smartling.com">Scott Rossillo</a>
* @version $Revision: 1 $
*/
public class KeycloakAuthenticationEntryPoint implements AuthenticationEntryPoint {
/**
* Default Keycloak authentication login URI
*/
public static final String DEFAULT_LOGIN_URI = "/sso/login";
private final static Logger log = LoggerFactory.getLogger(KeycloakAuthenticationEntryPoint.class);
private String loginUri = DEFAULT_LOGIN_URI;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
throws IOException, ServletException {
String contextAwareLoginUri = request.getContextPath() + loginUri;
log.debug("Redirecting to login URI {}", contextAwareLoginUri);
response.sendRedirect(contextAwareLoginUri);
}
public void setLoginUri(String loginUri) {
Assert.notNull(loginUri, "loginUri cannot be null");
this.loginUri = loginUri;
}
}

View file

@ -0,0 +1,38 @@
package org.keycloak.adapters.springsecurity.authentication;
import org.keycloak.adapters.springsecurity.account.KeycloakRole;
import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import java.util.ArrayList;
import java.util.List;
/**
* Performs authentication on a {@link KeycloakAuthenticationToken}.
*
* @author <a href="mailto:srossillo@smartling.com">Scott Rossillo</a>
* @version $Revision: 1 $
*/
public class KeycloakAuthenticationProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
KeycloakAuthenticationToken token = (KeycloakAuthenticationToken) authentication;
List<GrantedAuthority> grantedAuthorities = new ArrayList<GrantedAuthority>();
for (String role : token.getAccount().getRoles()) {
grantedAuthorities.add(new KeycloakRole(role));
}
return new KeycloakAuthenticationToken(token.getAccount(), grantedAuthorities);
}
@Override
public boolean supports(Class<?> aClass) {
return KeycloakAuthenticationToken.class.isAssignableFrom(aClass);
}
}

View file

@ -0,0 +1,69 @@
package org.keycloak.adapters.springsecurity.authentication;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.springsecurity.AdapterDeploymentContextBean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.util.Assert;
import org.springframework.web.util.UriComponentsBuilder;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* Logs the current user out of Keycloak.
*
* @author <a href="mailto:srossillo@smartling.com">Scott Rossillo</a>
* @version $Revision: 1 $
*/
public class KeycloakLogoutHandler implements LogoutHandler {
public static final String SSO_LOGOUT_COMPLETE_PARAM = "sso_complete";
private static final Logger log = LoggerFactory.getLogger(KeycloakLogoutHandler.class);
private AdapterDeploymentContextBean deploymentContextBean;
public KeycloakLogoutHandler(AdapterDeploymentContextBean deploymentContextBean) {
Assert.notNull(deploymentContextBean);
this.deploymentContextBean = deploymentContextBean;
}
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
if (authentication instanceof AnonymousAuthenticationToken) {
log.warn("Attempt to log out an anonymous authentication");
return;
}
if (Boolean.valueOf(request.getParameter(SSO_LOGOUT_COMPLETE_PARAM))) {
// already logged out
return;
}
try {
handleSingleSignOut(request, response);
} catch (IOException e) {
throw new IllegalStateException("Unable to redirect to SSO url!", e);
}
}
protected String createRedirectUrl(HttpServletRequest request) {
return UriComponentsBuilder.fromHttpUrl(request.getRequestURL().toString())
.replaceQueryParam(SSO_LOGOUT_COMPLETE_PARAM, true).build().toUriString();
}
protected void handleSingleSignOut(HttpServletRequest request, HttpServletResponse response) throws IOException {
KeycloakDeployment deployment = deploymentContextBean.getDeployment();
String redirectUrl = createRedirectUrl(request);
response.sendRedirect(deployment.getLogoutUrl().queryParam("redirect_uri", redirectUrl).build().toASCIIString());
}
}

View file

@ -0,0 +1,91 @@
package org.keycloak.adapters.springsecurity.authentication;
import org.keycloak.KeycloakPrincipal;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.adapters.AdapterTokenStore;
import org.keycloak.adapters.AdapterUtils;
import org.keycloak.adapters.HttpFacade;
import org.keycloak.adapters.KeycloakAccount;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.OAuthRequestAuthenticator;
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
import org.keycloak.adapters.RequestAuthenticator;
import org.keycloak.adapters.springsecurity.account.SimpleKeycloakAccount;
import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.security.Principal;
import java.util.Set;
import java.util.logging.Level;
/**
* Request authenticator adapter for Spring Security.
*
* @author <a href="mailto:srossillo@smartling.com">Scott Rossillo</a>
* @version $Revision: 1 $
*/
public class SpringSecurityRequestAuthenticator extends RequestAuthenticator {
private static final Logger logger = LoggerFactory.getLogger(SpringSecurityRequestAuthenticator.class);
private final HttpServletRequest request;
/**
* Creates a new Spring Security request authenticator.
*
* @param facade the current <code>HttpFacade</code> (required)
* @param request the current <code>HttpServletRequest</code> (required)
* @param deployment the <code>KeycloakDeployment</code> (required)
* @param tokenStore the <cdoe>AdapterTokenStore</cdoe> (required)
* @param sslRedirectPort the SSL redirect port (required)
*/
public SpringSecurityRequestAuthenticator(
HttpFacade facade,
HttpServletRequest request,
KeycloakDeployment deployment,
AdapterTokenStore tokenStore,
int sslRedirectPort) {
super(facade, deployment, tokenStore, sslRedirectPort);
this.request = request;
}
@Override
protected OAuthRequestAuthenticator createOAuthAuthenticator() {
return new OAuthRequestAuthenticator(this, facade, deployment, sslRedirectPort, tokenStore);
}
@Override
protected void completeOAuthAuthentication(final KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal) {
final RefreshableKeycloakSecurityContext securityContext = principal.getKeycloakSecurityContext();
final Set<String> roles = AdapterUtils.getRolesFromSecurityContext(securityContext);
final KeycloakAccount account = new SimpleKeycloakAccount(principal, roles, securityContext);
request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext);
this.tokenStore.saveAccountInfo(account);
}
@Override
protected void completeBearerAuthentication(KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal, String method) {
RefreshableKeycloakSecurityContext securityContext = principal.getKeycloakSecurityContext();
Set<String> roles = AdapterUtils.getRolesFromSecurityContext(securityContext);
final KeycloakAccount account = new SimpleKeycloakAccount(principal, roles, securityContext);
logger.warn("Completing bearer authentication. Bearer roles: {} ",roles);
SecurityContextHolder.getContext().setAuthentication(new KeycloakAuthenticationToken(account));
request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext);
}
@Override
protected String getHttpSessionId(boolean create) {
HttpSession session = request.getSession(create);
return session != null ? session.getId() : null;
}
}

View file

@ -0,0 +1,45 @@
package org.keycloak.adapters.springsecurity.client;
import org.apache.http.HttpHost;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.impl.client.HttpClients;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
/**
* Factory for {@link ClientHttpRequest} objects created for server to server secured
* communication using OAuth2 bearer tokens issued by Keycloak.
*
* @author <a href="mailto:srossillo@smartling.com">Scott Rossillo</a>
* @version $Revision: 1 $
*/
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class KeycloakClientRequestFactory extends HttpComponentsClientHttpRequestFactory implements ClientHttpRequestFactory {
public static final String AUTHORIZATION_HEADER = "Authorization";
public KeycloakClientRequestFactory() {
super(HttpClients.custom()
.disableCookieManagement()
.build()
);
}
@Override
protected void postProcessHttpRequest(HttpUriRequest request) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
KeycloakAuthenticationToken token = (KeycloakAuthenticationToken) authentication;
KeycloakSecurityContext context = token.getAccount().getKeycloakSecurityContext();
request.setHeader(AUTHORIZATION_HEADER, "Bearer " + context.getTokenString());
}
}

View file

@ -0,0 +1,17 @@
package org.keycloak.adapters.springsecurity.client;
import org.keycloak.adapters.springsecurity.client.KeycloakClientRequestFactory;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;
/**
* Created by scott on 4/22/15.
*/
//@Service
//@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class KeycloakRestTemplate extends RestTemplate implements RestOperations {
public KeycloakRestTemplate(KeycloakClientRequestFactory factory) {
super(factory);
}
}

View file

@ -0,0 +1,84 @@
package org.keycloak.adapters.springsecurity.config;
import org.keycloak.adapters.springsecurity.AdapterDeploymentContextBean;
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationEntryPoint;
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider;
import org.keycloak.adapters.springsecurity.authentication.KeycloakLogoutHandler;
import org.keycloak.adapters.springsecurity.filter.KeycloakAuthenticationProcessingFilter;
import org.keycloak.adapters.springsecurity.filter.KeycloakPreAuthActionsFilter;
import org.keycloak.adapters.springsecurity.management.HttpSessionManager;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.WebSecurityConfigurer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
/**
* Provides a convenient base class for creating a {@link WebSecurityConfigurer}
* instance secured by Keycloak. This implementation allows customization by overriding methods.
*
* @author <a href="mailto:srossillo@smartling.com">Scott Rossillo</a>
* @version $Revision: 1 $
*
* @see EnableWebSecurity
* @see EnableWebMvcSecurity
*/
public abstract class KeycloakWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter implements WebSecurityConfigurer<WebSecurity> {
@Bean
protected AdapterDeploymentContextBean adapterDeploymentContextBean() {
return new AdapterDeploymentContextBean();
}
@Bean
protected AuthenticationEntryPoint authenticationEntryPoint()
{
return new KeycloakAuthenticationEntryPoint();
}
@Bean
protected KeycloakAuthenticationProvider keycloakAuthenticationProvider() {
return new KeycloakAuthenticationProvider();
}
@Bean
protected KeycloakAuthenticationProcessingFilter keycloakAuthenticationProcessingFilter() throws Exception {
KeycloakAuthenticationProcessingFilter filter = new KeycloakAuthenticationProcessingFilter(authenticationManagerBean());
filter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy());
return filter;
}
@Bean
protected KeycloakPreAuthActionsFilter keycloakPreAuthActionsFilter() {
return new KeycloakPreAuthActionsFilter(httpSessionManager());
}
@Bean
protected HttpSessionManager httpSessionManager() {
return new HttpSessionManager();
}
@Bean
protected KeycloakLogoutHandler keycloakLogoutHandler() {
return new KeycloakLogoutHandler(adapterDeploymentContextBean());
}
protected abstract SessionAuthenticationStrategy sessionAuthenticationStrategy();
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement()
.sessionAuthenticationStrategy(sessionAuthenticationStrategy())
.and()
.addFilterBefore(keycloakPreAuthActionsFilter(), LogoutFilter.class)
.addFilterBefore(keycloakAuthenticationProcessingFilter(), X509AuthenticationFilter.class)
.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint());
}
}

View file

@ -0,0 +1,66 @@
package org.keycloak.adapters.springsecurity.facade;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.adapters.HttpFacade;
import org.keycloak.adapters.springsecurity.facade.WrappedHttpServletRequest;
import org.keycloak.adapters.springsecurity.facade.WrappedHttpServletResponse;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.Assert;
import javax.security.cert.X509Certificate;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Simple {@link HttpFacade} wrapping an {@link HttpServletRequest} and {@link HttpServletResponse}.
*
* @author <a href="mailto:srossillo@smartling.com">Scott Rossillo</a>
* @version $Revision: 1 $
*/
public class SimpleHttpFacade implements HttpFacade {
private final HttpServletRequest request;
private final HttpServletResponse response;
/**
* Creates a new simple HTTP facade for the given request and response.
*
* @param request the current <code>HttpServletRequest</code> (required)
* @param response the current <code>HttpServletResponse</code> (required)
*/
public SimpleHttpFacade(HttpServletRequest request, HttpServletResponse response) {
Assert.notNull(request, "HttpServletRequest required");
Assert.notNull(response, "HttpServletResponse required");
this.request = request;
this.response = response;
}
@Override
public KeycloakSecurityContext getSecurityContext() {
SecurityContext context = SecurityContextHolder.getContext();
if (context != null && context.getAuthentication() != null) {
return (KeycloakSecurityContext) context.getAuthentication().getDetails();
}
return null;
}
@Override
public Request getRequest() {
return new WrappedHttpServletRequest(request);
}
@Override
public Response getResponse() {
return new WrappedHttpServletResponse(response);
}
@Override
public X509Certificate[] getCertificateChain() {
// TODO: implement me
return new X509Certificate[0];
}
}

View file

@ -0,0 +1,101 @@
package org.keycloak.adapters.springsecurity.facade;
import org.keycloak.adapters.HttpFacade.Cookie;
import org.keycloak.adapters.HttpFacade.Request;
import org.springframework.util.Assert;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
/**
* Concrete Keycloak {@link Request request} implementation wrapping an {@link HttpServletRequest}.
*
* @author <a href="mailto:srossillo@smartling.com">Scott Rossillo</a>
* @version $Revision: 1 $
*/
class WrappedHttpServletRequest implements Request {
private final HttpServletRequest request;
/**
* Creates a new request for the given <code>HttpServletRequest</code>
*
* @param request the current <code>HttpServletRequest</code> (required)
*/
public WrappedHttpServletRequest(HttpServletRequest request) {
Assert.notNull(request, "HttpServletRequest required");
this.request = request;
}
@Override
public String getMethod() {
return request.getMethod();
}
@Override
public String getURI() {
StringBuffer buf = request.getRequestURL();
if (request.getQueryString() != null) {
buf.append('?').append(request.getQueryString());
}
return buf.toString();
}
@Override
public boolean isSecure() {
return request.isSecure();
}
@Override
public String getQueryParamValue(String param) {
return request.getParameter(param);
}
@Override
public Cookie getCookie(String cookieName) {
for (javax.servlet.http.Cookie cookie : request.getCookies()) {
if (cookie.getName().equals(cookieName)) {
return new Cookie(cookie.getName(), cookie.getValue(), cookie.getVersion(), cookie.getDomain(), cookie.getPath());
}
}
return null;
}
@Override
public String getHeader(String name) {
return request.getHeader(name);
}
@Override
public List<String> getHeaders(String name) {
Enumeration<String> values = request.getHeaders(name);
List<String> array = new ArrayList<String>();
while (values.hasMoreElements()) {
array.add(values.nextElement());
}
return Collections.unmodifiableList(array);
}
@Override
public InputStream getInputStream() {
try {
return request.getInputStream();
} catch (IOException e) {
throw new RuntimeException("Unable to get request input stream", e);
}
}
@Override
public String getRemoteAddr() {
return request.getRemoteAddr();
}
}

View file

@ -0,0 +1,95 @@
package org.keycloak.adapters.springsecurity.facade;
import org.keycloak.adapters.HttpFacade.Response;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
/**
* Concrete Keycloak {@link Response response} implementation wrapping an {@link HttpServletResponse}.
*
* @author <a href="mailto:srossillo@smartling.com">Scott Rossillo</a>
* @version $Revision: 1 $
*/
class WrappedHttpServletResponse implements Response {
private final HttpServletResponse response;
/**
* Creates a new response for the given <code>HttpServletResponse</code>.
*
* @param response the current <code>HttpServletResponse</code> (required)
*/
public WrappedHttpServletResponse(HttpServletResponse response) {
this.response = response;
}
@Override
public void resetCookie(String name, String path) {
Cookie cookie = new Cookie(name, "");
cookie.setMaxAge(0);
if (path != null) {
cookie.setPath(path);
}
response.addCookie(cookie);
}
@Override
public void setCookie(String name, String value, String path, String domain, int maxAge, boolean secure, boolean httpOnly) {
Cookie cookie = new Cookie(name, value);
if (path != null) {
cookie.setPath(path);
}
if (domain != null) {
cookie.setDomain(domain);
}
cookie.setMaxAge(maxAge);
cookie.setSecure(secure);
cookie.setHttpOnly(httpOnly);
response.addCookie(cookie);
}
@Override
public void setStatus(int status) {
response.setStatus(status);
}
@Override
public void addHeader(String name, String value) {
response.addHeader(name, value);
}
@Override
public void setHeader(String name, String value) {
response.setHeader(name, value);
}
@Override
public OutputStream getOutputStream() {
try {
return response.getOutputStream();
} catch (IOException e) {
throw new RuntimeException("Unable to return response output stream", e);
}
}
@Override
public void sendError(int code, String message) {
try {
response.sendError(code, message);
} catch (IOException e) {
throw new RuntimeException("Unable to set HTTP status", e);
}
}
@Override
public void end() {
// TODO: do we need this?
}
}

View file

@ -0,0 +1,4 @@
/**
* Provides an {@link org.keycloak.adapters.HttpFacade} implementation.
*/
package org.keycloak.adapters.springsecurity.facade;

View file

@ -0,0 +1,128 @@
package org.keycloak.adapters.springsecurity.filter;
import org.keycloak.adapters.AuthChallenge;
import org.keycloak.adapters.AuthOutcome;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.springsecurity.AdapterDeploymentContextBean;
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationEntryPoint;
import org.keycloak.adapters.springsecurity.authentication.SpringSecurityRequestAuthenticator;
import org.keycloak.adapters.springsecurity.facade.SimpleHttpFacade;
import org.keycloak.adapters.springsecurity.token.SpringSecurityTokenStore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestHeaderRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* Provides a Keycloak authentication processing filter.
*
* @author <a href="mailto:srossillo@smartling.com">Scott Rossillo</a>
* @version $Revision: 1 $
*/
public class KeycloakAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter implements ApplicationContextAware {
/**
* Request matcher that matches requests to the {@link KeycloakAuthenticationEntryPoint#DEFAULT_LOGIN_URI default login URI}
* and any request with a <code>Authorization</code> header.
*/
public static final RequestMatcher DEFAULT_REQUEST_MATCHER =
new OrRequestMatcher(new AntPathRequestMatcher("/sso/login"), new RequestHeaderRequestMatcher("Authorization"));
private static final Logger log = LoggerFactory.getLogger(KeycloakAuthenticationProcessingFilter.class);
private ApplicationContext applicationContext;
private AdapterDeploymentContextBean adapterDeploymentContextBean;
private AuthenticationManager authenticationManager;
/**
* Creates a new Keycloak authentication processing filter with given {@link AuthenticationManager} and the
* {@link KeycloakAuthenticationProcessingFilter#DEFAULT_REQUEST_MATCHER default request matcher}.
*
* @param authenticationManager the {@link AuthenticationManager} to authenticate requests (cannot be null)
* @see KeycloakAuthenticationProcessingFilter#DEFAULT_REQUEST_MATCHER
*/
public KeycloakAuthenticationProcessingFilter(AuthenticationManager authenticationManager) {
this(authenticationManager, DEFAULT_REQUEST_MATCHER);
}
/**
* Creates a new Keycloak authentication processing filter with given {@link AuthenticationManager} and
* {@link RequestMatcher}.
* <p>
* Note: the given request matcher must support matching the <code>Authorization</code> header if
* bearer token authentication is to be accepted.
* </p>
*
* @param authenticationManager the {@link AuthenticationManager} to authenticate requests (cannot be null)
* @param requiresAuthenticationRequestMatcher the {@link RequestMatcher} used to determine if authentication
* is required (cannot be null)
*
* @see RequestHeaderRequestMatcher
* @see OrRequestMatcher
*
*/
public KeycloakAuthenticationProcessingFilter(AuthenticationManager authenticationManager, RequestMatcher
requiresAuthenticationRequestMatcher) {
super(requiresAuthenticationRequestMatcher);
Assert.notNull(authenticationManager, "authenticationManager cannot be null");
this.authenticationManager = authenticationManager;
super.setAuthenticationManager(authenticationManager);
}
@Override
public void afterPropertiesSet() {
adapterDeploymentContextBean= applicationContext.getBean(AdapterDeploymentContextBean.class);
super.afterPropertiesSet();
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
log.debug("Attempting Keycloak authentication");
KeycloakDeployment deployment = adapterDeploymentContextBean.getDeployment();
SimpleHttpFacade facade = new SimpleHttpFacade(request, response);
SpringSecurityTokenStore tokenStore = new SpringSecurityTokenStore(deployment, request);
SpringSecurityRequestAuthenticator authenticator
= new SpringSecurityRequestAuthenticator(facade, request, deployment, tokenStore, -1);
AuthOutcome result = authenticator.authenticate();
AuthChallenge challenge = authenticator.getChallenge();
log.debug("Auth outcome: {}", result);
if (challenge != null) {
challenge.challenge(facade);
}
if (AuthOutcome.AUTHENTICATED.equals(result)) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Assert.notNull(authentication, "Authentication SecurityContextHolder was null");
return authenticationManager.authenticate(authentication);
}
return null;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}

View file

@ -0,0 +1,77 @@
package org.keycloak.adapters.springsecurity.filter;
import org.keycloak.adapters.AdapterDeploymentContext;
import org.keycloak.adapters.HttpFacade;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.KeycloakDeploymentBuilder;
import org.keycloak.adapters.PreAuthActionsHandler;
import org.keycloak.adapters.UserSessionManagement;
import org.keycloak.adapters.springsecurity.AdapterDeploymentContextBean;
import org.keycloak.adapters.springsecurity.facade.SimpleHttpFacade;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.web.filter.GenericFilterBean;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
/**
* Exposes a Keycloak adapter {@link PreAuthActionsHandler} as a Spring Security filter.
*
* @author <a href="mailto:srossillo@smartling.com">Scott Rossillo</a>
* @version $Revision: 1 $
*/
public class KeycloakPreAuthActionsFilter extends GenericFilterBean implements ApplicationContextAware {
private static final Logger log = LoggerFactory.getLogger(KeycloakPreAuthActionsFilter.class);
private ApplicationContext applicationContext;
private AdapterDeploymentContext deploymentContext;
private UserSessionManagement userSessionManagement;
public KeycloakPreAuthActionsFilter() {
super();
}
public KeycloakPreAuthActionsFilter(UserSessionManagement userSessionManagement) {
this.userSessionManagement = userSessionManagement;
}
@Override
protected void initFilterBean() throws ServletException {
AdapterDeploymentContextBean contextBean = applicationContext.getBean(AdapterDeploymentContextBean.class);
deploymentContext = contextBean.getDeploymentContext();
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpFacade facade = new SimpleHttpFacade((HttpServletRequest)request, (HttpServletResponse)response);
PreAuthActionsHandler handler = new PreAuthActionsHandler(userSessionManagement, deploymentContext, facade);
if (handler.handleRequest()) {
log.info("Pre-auth filter handled request: {}", ((HttpServletRequest) request).getRequestURI());
} else {
chain.doFilter(request, response);
}
}
public void setUserSessionManagement(UserSessionManagement userSessionManagement) {
this.userSessionManagement = userSessionManagement;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}

View file

@ -0,0 +1,4 @@
/**
* Provides Spring Security filters for Keycloak.
*/
package org.keycloak.adapters.springsecurity.filter;

View file

@ -0,0 +1,57 @@
package org.keycloak.adapters.springsecurity.management;
import org.keycloak.adapters.UserSessionManagement;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
import java.util.List;
/**
* User session manager for handling logout of Spring Secured sessions.
*
* @author <a href="mailto:srossillo@smartling.com">Scott Rossillo</a>
* @version $Revision: 1 $
*/
@Component
public class HttpSessionManager implements HttpSessionListener, UserSessionManagement {
private static final Logger log = LoggerFactory.getLogger(HttpSessionManager.class);
private SessionManagementStrategy sessions = new LocalSessionManagementStrategy();
@Override
public void sessionCreated(HttpSessionEvent event) {
log.debug("Session created: {}", event.getSession().getId());
HttpSession session = event.getSession();
sessions.store(session);
}
@Override
public void sessionDestroyed(HttpSessionEvent event) {
sessions.remove(event.getSession().getId());
}
@Override
public void logoutAll() {
log.info("Received request to log out all users.");
for (HttpSession session : sessions.getAll()) {
session.invalidate();
}
sessions.clear();
}
@Override
public void logoutHttpSessions(List<String> ids) {
log.info("Received request to log out {} session(s): {}", ids.size(), ids);
for (String id : ids) {
HttpSession session = sessions.remove(id);
if (session != null) {
session.invalidate();
}
}
sessions.clear();
}
}

View file

@ -0,0 +1,34 @@
package org.keycloak.adapters.springsecurity.management;
import javax.servlet.http.HttpSession;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Created by scott on 4/24/15.
*/
public class LocalSessionManagementStrategy implements SessionManagementStrategy {
private final Map<String, HttpSession> sessions = new ConcurrentHashMap<String, HttpSession>();
@Override
public void clear() {
sessions.clear();
}
@Override
public Collection<HttpSession> getAll() {
return sessions.values();
}
@Override
public void store(HttpSession session) {
sessions.put(session.getId(), session);
}
@Override
public HttpSession remove(String id) {
return sessions.remove(id);
}
}

View file

@ -0,0 +1,41 @@
package org.keycloak.adapters.springsecurity.management;
import javax.servlet.http.HttpSession;
import java.util.Collection;
/**
* Defines a session management strategy.
*
* @author <a href="mailto:srossillo@smartling.com">Scott Rossillo</a>
* @version $Revision: 1 $
*/
public interface SessionManagementStrategy {
/**
* Removes all sessions.
*/
void clear();
/**
* Returns a collection containing all sessions.
*
* @return a <code>Collection</code> of all known <code>HttpSession</code>s, if any;
* an empty <code>Collection</code> otherwise
*/
Collection<HttpSession> getAll();
/**
* Stores the given session.
*
* @param session the <code>HttpSession</code> to store (required)
*/
void store(HttpSession session);
/**
* The unique identifier for the session to remove.
*
* @param id the unique identifier for the session to remove (required)
* @return the <code>HttpSession</code> if it exists; <code>null</code> otherwise
*/
HttpSession remove(String id);
}

View file

@ -0,0 +1,4 @@
/**
* Provides a Keycloak adapter for Spring Security.
*/
package org.keycloak.adapters.springsecurity;

View file

@ -0,0 +1,27 @@
package org.keycloak.adapters.springsecurity.registration;
import org.keycloak.adapters.KeycloakDeployment;
/**
* Manages registration of application nodes.
*
* @author <a href="mailto:srossillo@smartling.com">Scott Rossillo</a>
* @version $Revision: 1 $
*/
public interface NodeManager {
/**
* Registers the given deployment with the Keycloak server.
*
* @param deployment the deployment to register (required)
*/
void register(KeycloakDeployment deployment);
/**
* Unregisters the give deployment from the Keycloak server
* .
* @param deployment the deployment to unregister (required)
*/
void unregister(KeycloakDeployment deployment);
}

View file

@ -0,0 +1,55 @@
package org.keycloak.adapters.springsecurity.token;
import org.keycloak.adapters.KeycloakAccount;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.util.Assert;
import java.security.Principal;
import java.util.Collection;
/**
* Represents the token for a Keycloak authentication request or for an authenticated principal once the request has been
* processed by the {@link AuthenticationManager#authenticate(Authentication)}.
*
* @author <a href="mailto:srossillo@smartling.com">Scott Rossillo</a>
* @version $Revision: 1 $
*/
public class KeycloakAuthenticationToken extends AbstractAuthenticationToken implements Authentication {
private Principal principal;
/**
* Creates a new, unauthenticated Keycloak security token for the given account.
*/
public KeycloakAuthenticationToken(KeycloakAccount account) {
super(null);
Assert.notNull(account, "KeycloakAccount cannot be null");
Assert.notNull(account.getPrincipal(), "KeycloakAccount.getPrincipal() cannot be null");
this.principal = account.getPrincipal();
this.setDetails(account);
}
public KeycloakAuthenticationToken(KeycloakAccount account, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = account.getPrincipal();
this.setDetails(account);
setAuthenticated(true);
}
@Override
public Object getCredentials() {
return this.getAccount().getKeycloakSecurityContext();
}
@Override
public Object getPrincipal() {
return principal;
}
public KeycloakAccount getAccount() {
return (KeycloakAccount) this.getDetails();
}
}

View file

@ -0,0 +1,122 @@
package org.keycloak.adapters.springsecurity.token;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.adapters.AdapterTokenStore;
import org.keycloak.adapters.KeycloakAccount;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
import org.keycloak.adapters.RequestAuthenticator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.Assert;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
/**
* Simple Spring {@link SecurityContext security context} aware {@link AdapterTokenStore adapter token store}.
*
* @author <a href="mailto:srossillo@smartling.com">Scott Rossillo</a>
* @version $Revision: 1 $
*/
public class SpringSecurityTokenStore implements AdapterTokenStore {
private final Logger logger = LoggerFactory.getLogger(SpringSecurityTokenStore.class);
private final KeycloakDeployment deployment;
private final HttpServletRequest request;
public SpringSecurityTokenStore(KeycloakDeployment deployment, HttpServletRequest request) {
Assert.notNull(deployment, "KeycloakDeployment is required");
Assert.notNull(request, "HttpServletRequest is required");
this.deployment = deployment;
this.request = request;
}
@Override
public void checkCurrentToken() {
// no-op
}
@Override
public boolean isCached(RequestAuthenticator authenticator) {
logger.debug("Checking if {} is cached", authenticator);
SecurityContext context = SecurityContextHolder.getContext();
KeycloakAuthenticationToken token;
KeycloakSecurityContext keycloakSecurityContext;
if (context == null || context.getAuthentication() == null) {
return false;
}
if (KeycloakAuthenticationToken.class.isAssignableFrom(context.getAuthentication().getClass())) {
logger.warn("Expected a KeycloakAuthenticationToken, but found {}", context.getAuthentication());
return false;
}
logger.info("Remote logged in already. Establishing state from security context.");
token = (KeycloakAuthenticationToken) context.getAuthentication();
keycloakSecurityContext = token.getAccount().getKeycloakSecurityContext();
if (!deployment.getRealm().equals(keycloakSecurityContext.getRealm())) {
logger.info("Account from security context is from a different realm than for the request.");
logout();
return false;
}
if (keycloakSecurityContext.getToken().isExpired()) {
logger.warn("Security token expired ... not returning from cache");
return false;
}
request.setAttribute(KeycloakSecurityContext.class.getName(), keycloakSecurityContext);
return true;
}
@Override
public void saveAccountInfo(KeycloakAccount account) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null) {
throw new IllegalStateException("Went to save Keycloak account {}, but already have {}");
}
logger.debug("Saving account info {}", account);
SecurityContextHolder.getContext().setAuthentication(new KeycloakAuthenticationToken(account));
}
@Override
public void logout() {
logger.debug("Handling logout request");
HttpSession session = request.getSession(false);
if (session != null) {
session.setAttribute(KeycloakSecurityContext.class.getName(), null);
}
SecurityContextHolder.clearContext();
}
@Override
public void refreshCallback(RefreshableKeycloakSecurityContext securityContext) {
// no-op
}
@Override
public void saveRequest() {
// no-op, Spring Security will handle this
}
@Override
public boolean restoreRequest() {
// no-op, Spring Security will handle this
return false;
}
}

View file

@ -0,0 +1,51 @@
package org.keycloak.adapters.springsecurity.authentication;
import org.junit.Before;
import org.junit.Test;
import org.springframework.http.HttpStatus;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import static org.junit.Assert.*;
/**
* Keycloak authentication entry point tests.
*/
public class KeycloakAuthenticationEntryPointTest {
private KeycloakAuthenticationEntryPoint authenticationEntryPoint;
private MockHttpServletRequest request;
private MockHttpServletResponse response;
@Before
public void setUp() throws Exception {
authenticationEntryPoint = new KeycloakAuthenticationEntryPoint();
request = new MockHttpServletRequest();
response = new MockHttpServletResponse();
}
@Test
public void testCommence() throws Exception {
authenticationEntryPoint.commence(request, response, null);
assertEquals(HttpStatus.FOUND.value(), response.getStatus());
assertEquals(KeycloakAuthenticationEntryPoint.DEFAULT_LOGIN_URI, response.getHeader("Location"));
}
@Test
public void testCommenceNotRootContext() throws Exception {
String contextPath = "/foo";
request.setContextPath(contextPath);
authenticationEntryPoint.commence(request, response, null);
assertEquals(HttpStatus.FOUND.value(), response.getStatus());
assertEquals(contextPath + KeycloakAuthenticationEntryPoint.DEFAULT_LOGIN_URI, response.getHeader("Location"));
}
@Test
public void testSetLoginUri() throws Exception {
final String logoutUri = "/foo";
authenticationEntryPoint.setLoginUri(logoutUri);
authenticationEntryPoint.commence(request, response, null);
assertEquals(HttpStatus.FOUND.value(), response.getStatus());
assertEquals(logoutUri, response.getHeader("Location"));
}
}

View file

@ -0,0 +1,54 @@
package org.keycloak.adapters.springsecurity.authentication;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.adapters.KeycloakAccount;
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
import org.keycloak.adapters.springsecurity.account.SimpleKeycloakAccount;
import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken;
import org.mockito.internal.util.collections.Sets;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
import java.security.Principal;
import java.util.Set;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
/**
* Keycloak authentication provider tests.
*/
public class KeycloakAuthenticationProviderTest {
private KeycloakAuthenticationProvider provider = new KeycloakAuthenticationProvider();
private KeycloakAuthenticationToken token;
private Set<String> roles = Sets.newSet("user", "admin");
@Before
public void setUp() throws Exception {
Principal principal = mock(Principal.class);
RefreshableKeycloakSecurityContext securityContext = mock(RefreshableKeycloakSecurityContext.class);
KeycloakAccount account = new SimpleKeycloakAccount(principal, roles, securityContext);
token = new KeycloakAuthenticationToken(account);
}
@Test
public void testAuthenticate() throws Exception {
Authentication result = provider.authenticate(token);
assertNotNull(result);
assertEquals(roles.size(), result.getAuthorities().size());
assertTrue(result.isAuthenticated());
assertNotNull(result.getPrincipal());
assertNotNull(result.getCredentials());
assertNotNull(result.getDetails());
}
@Test
public void testSupports() throws Exception {
assertTrue(provider.supports(KeycloakAuthenticationToken.class));
assertFalse(provider.supports(PreAuthenticatedAuthenticationToken.class));
}
}

View file

@ -0,0 +1,107 @@
package org.keycloak.adapters.springsecurity.authentication;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.KeycloakPrincipal;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.adapters.AdapterTokenStore;
import org.keycloak.adapters.HttpFacade;
import org.keycloak.adapters.KeycloakAccount;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.OAuthRequestAuthenticator;
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
import org.keycloak.adapters.springsecurity.facade.SimpleHttpFacade;
import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken;
import org.keycloak.representations.AccessToken;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.internal.util.collections.Sets;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.core.context.SecurityContextHolder;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
/**
* Spring Security request authenticator tests.
*/
public class SpringSecurityRequestAuthenticatorTest {
private SpringSecurityRequestAuthenticator authenticator;
private MockHttpServletRequest request;
private MockHttpServletResponse response;
@Mock
private KeycloakDeployment deployment;
@Mock
private AdapterTokenStore tokenStore;
@Mock
private KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal;
@Mock
private AccessToken accessToken;
@Mock
private AccessToken.Access access;
@Mock
private RefreshableKeycloakSecurityContext refreshableKeycloakSecurityContext;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
request = spy(new MockHttpServletRequest());
response = new MockHttpServletResponse();
HttpFacade facade = new SimpleHttpFacade(request, response);
authenticator = new SpringSecurityRequestAuthenticator(facade, request, deployment, tokenStore, 443);
// mocks
when(principal.getKeycloakSecurityContext()).thenReturn(refreshableKeycloakSecurityContext);
when(refreshableKeycloakSecurityContext.getDeployment()).thenReturn(deployment);
when(refreshableKeycloakSecurityContext.getToken()).thenReturn(accessToken);
when(accessToken.getRealmAccess()).thenReturn(access);
when(access.getRoles()).thenReturn(Sets.newSet("user", "admin"));
when(deployment.isUseResourceRoleMappings()).thenReturn(false);
}
@Test
public void testCreateOAuthAuthenticator() throws Exception {
OAuthRequestAuthenticator oathAuthenticator = authenticator.createOAuthAuthenticator();
assertNotNull(oathAuthenticator);
}
@Test
public void testCompleteOAuthAuthentication() throws Exception {
authenticator.completeOAuthAuthentication(principal);
verify(request).setAttribute(eq(KeycloakSecurityContext.class.getName()), eq(refreshableKeycloakSecurityContext));
verify(tokenStore).saveAccountInfo(any(KeycloakAccount.class)); // FIXME: should verify account
}
@Test
public void testCompleteBearerAuthentication() throws Exception {
authenticator.completeBearerAuthentication(principal, "foo");
verify(request).setAttribute(eq(KeycloakSecurityContext.class.getName()), eq(refreshableKeycloakSecurityContext));
assertNotNull(SecurityContextHolder.getContext().getAuthentication());
assertTrue(KeycloakAuthenticationToken.class.isAssignableFrom(SecurityContextHolder.getContext().getAuthentication().getClass()));
}
@Test
public void testGetHttpSessionIdTrue() throws Exception {
String sessionId = authenticator.getHttpSessionId(true);
assertNotNull(sessionId);
}
@Test
public void testGetHttpSessionIdFalse() throws Exception {
String sessionId = authenticator.getHttpSessionId(false);
assertNull(sessionId);
}
}

View file

@ -0,0 +1,103 @@
package org.keycloak.adapters.springsecurity.facade;
import org.junit.Before;
import org.junit.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.servlet.http.Cookie;
import java.util.List;
import static org.junit.Assert.*;
/**
* Wrapped HTTP servlet request tests.
*/
public class WrappedHttpServletRequestTest {
private static final String COOKIE_NAME = "oreo";
private static final String HEADER_MULTI_VALUE = "Multi";
private static final String HEADER_SINGLE_VALUE = "Single";
private static final String REQUEST_METHOD = RequestMethod.GET.name();
private static final String REQUEST_URI = "/foo/bar";
private static final String QUERY_PARM_1 = "code";
private static final String QUERY_PARM_2 = "code2";
private WrappedHttpServletRequest request;
@Before
public void setUp() throws Exception {
MockHttpServletRequest mockHttpServletRequest = new MockHttpServletRequest();
request = new WrappedHttpServletRequest(mockHttpServletRequest);
mockHttpServletRequest.setMethod(REQUEST_METHOD);
mockHttpServletRequest.setRequestURI(REQUEST_URI);
mockHttpServletRequest.setSecure(true);
mockHttpServletRequest.setScheme("https");
mockHttpServletRequest.addHeader(HEADER_SINGLE_VALUE, "baz");
mockHttpServletRequest.addHeader(HEADER_MULTI_VALUE, "foo");
mockHttpServletRequest.addHeader(HEADER_MULTI_VALUE, "bar");
mockHttpServletRequest.addParameter(QUERY_PARM_1, "java");
mockHttpServletRequest.addParameter(QUERY_PARM_2, "groovy");
mockHttpServletRequest.setQueryString(String.format("%s=%s&%s=%s", QUERY_PARM_1, "java", QUERY_PARM_2, "groovy"));
mockHttpServletRequest.setCookies(new Cookie(COOKIE_NAME, "yum"));
mockHttpServletRequest.setContent("All work and no play makes Jack a dull boy".getBytes());
}
@Test
public void testGetMethod() throws Exception {
assertNotNull(request.getMethod());
assertEquals(REQUEST_METHOD, request.getMethod());
}
@Test
public void testGetURI() throws Exception {
assertEquals("https://localhost:80" + REQUEST_URI + "?code=java&code2=groovy" , request.getURI());
}
@Test
public void testIsSecure() throws Exception {
assertTrue(request.isSecure());
}
@Test
public void testGetQueryParamValue() throws Exception {
assertNotNull(request.getQueryParamValue(QUERY_PARM_1));
assertNotNull(request.getQueryParamValue(QUERY_PARM_2));
}
@Test
public void testGetCookie() throws Exception {
assertNotNull(request.getCookie(COOKIE_NAME));
}
@Test
public void testGetHeader() throws Exception {
String header = request.getHeader(HEADER_SINGLE_VALUE);
assertNotNull(header);
assertEquals("baz", header);
}
@Test
public void testGetHeaders() throws Exception {
List<String> headers = request.getHeaders(HEADER_MULTI_VALUE);
assertNotNull(headers);
assertEquals(2, headers.size());
assertTrue(headers.contains("foo"));
assertTrue(headers.contains("bar"));
}
@Test
public void testGetInputStream() throws Exception {
assertNotNull(request.getInputStream());
}
@Test
public void testGetRemoteAddr() throws Exception {
assertNotNull(request.getRemoteAddr());
}
}

View file

@ -0,0 +1,99 @@
package org.keycloak.adapters.springsecurity.facade;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.springframework.http.HttpStatus;
import org.springframework.mock.web.MockHttpServletResponse;
import javax.servlet.http.Cookie;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
public class WrappedHttpServletResponseTest {
private static final String COOKIE_DOMAIN = ".keycloak.org";
private static final String COOKIE_NAME = "foo";
private static final String COOKIE_PATH = "/bar";
private static final String COOKIE_VALUE = "onegreatcookie";
private static final String HEADER = "Test";
private WrappedHttpServletResponse response;
private MockHttpServletResponse mockResponse;
@Before
public void setUp() throws Exception {
mockResponse = spy(new MockHttpServletResponse());
response = new WrappedHttpServletResponse(mockResponse);
}
@Test
public void testResetCookie() throws Exception {
response.resetCookie(COOKIE_NAME, COOKIE_PATH);
verify(mockResponse).addCookie(any(Cookie.class));
assertEquals(COOKIE_NAME, mockResponse.getCookie(COOKIE_NAME).getName());
assertEquals(COOKIE_PATH, mockResponse.getCookie(COOKIE_NAME).getPath());
assertEquals(0, mockResponse.getCookie(COOKIE_NAME).getMaxAge());
assertEquals("", mockResponse.getCookie(COOKIE_NAME).getValue());
}
@Test
public void testSetCookie() throws Exception {
int maxAge = 300;
response.setCookie(COOKIE_NAME, COOKIE_VALUE, COOKIE_PATH, COOKIE_DOMAIN, maxAge, false, true);
verify(mockResponse).addCookie(any(Cookie.class));
assertEquals(COOKIE_NAME, mockResponse.getCookie(COOKIE_NAME).getName());
assertEquals(COOKIE_PATH, mockResponse.getCookie(COOKIE_NAME).getPath());
assertEquals(COOKIE_DOMAIN, mockResponse.getCookie(COOKIE_NAME).getDomain());
assertEquals(maxAge, mockResponse.getCookie(COOKIE_NAME).getMaxAge());
assertEquals(COOKIE_VALUE, mockResponse.getCookie(COOKIE_NAME).getValue());
}
@Test
public void testSetStatus() throws Exception {
int status = HttpStatus.OK.value();
response.setStatus(status);
verify(mockResponse).setStatus(eq(status));
assertEquals(status, mockResponse.getStatus());
}
@Test
public void testAddHeader() throws Exception {
String headerValue = "foo";
response.addHeader(HEADER, headerValue);
verify(mockResponse).addHeader(eq(HEADER), eq(headerValue));
assertTrue(mockResponse.containsHeader(HEADER));
}
@Test
public void testSetHeader() throws Exception {
String headerValue = "foo";
response.setHeader(HEADER, headerValue);
verify(mockResponse).setHeader(eq(HEADER), eq(headerValue));
assertTrue(mockResponse.containsHeader(HEADER));
}
@Test
public void testGetOutputStream() throws Exception {
assertNotNull(response.getOutputStream());
verify(mockResponse).getOutputStream();
}
@Test
public void testSendError() throws Exception {
int status = HttpStatus.UNAUTHORIZED.value();
String reason = HttpStatus.UNAUTHORIZED.getReasonPhrase();
response.sendError(status, reason);
verify(mockResponse).sendError(eq(status), eq(reason));
assertEquals(status, mockResponse.getStatus());
assertEquals(reason, mockResponse.getErrorMessage());
}
@Test
@Ignore
public void testEnd() throws Exception {
// TODO: what is an ended response, one that's committed?
}
}