/*
* 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.adapters.springsecurity.filter;
import org.keycloak.adapters.AdapterDeploymentContext;
import org.keycloak.adapters.AdapterTokenStore;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.RequestAuthenticator;
import org.keycloak.adapters.spi.AuthChallenge;
import org.keycloak.adapters.spi.AuthOutcome;
import org.keycloak.adapters.spi.HttpFacade;
import org.keycloak.adapters.springsecurity.KeycloakAuthenticationException;
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.AdapterTokenStoreFactory;
import org.keycloak.adapters.springsecurity.token.SpringSecurityAdapterTokenStoreFactory;
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.authentication.event.InteractiveAuthenticationSuccessEvent;
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.authentication.SimpleUrlAuthenticationFailureHandler;
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.FilterChain;
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 {
public static final String DEFAULT_LOGIN_URL = "/sso/login";
public static final String AUTHORIZATION_HEADER = "Authorization";
public static final String SCHEME_BEARER = "bearer ";
public static final String SCHEME_BASIC = "basic ";
/**
* 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(DEFAULT_LOGIN_URL), new RequestHeaderRequestMatcher(AUTHORIZATION_HEADER));
private static final Logger log = LoggerFactory.getLogger(KeycloakAuthenticationProcessingFilter.class);
private ApplicationContext applicationContext;
private AdapterDeploymentContext adapterDeploymentContext;
private AdapterTokenStoreFactory adapterTokenStoreFactory = new SpringSecurityAdapterTokenStoreFactory();
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);
setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler(DEFAULT_LOGIN_URL));
}
/**
* 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);
super.setAllowSessionCreation(false);
super.setContinueChainBeforeSuccessfulAuthentication(false);
}
@Override
public void afterPropertiesSet() {
adapterDeploymentContext = applicationContext.getBean(AdapterDeploymentContext.class);
super.afterPropertiesSet();
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
log.debug("Attempting Keycloak authentication");
HttpFacade facade = new SimpleHttpFacade(request, response);
KeycloakDeployment deployment = adapterDeploymentContext.resolveDeployment(facade);
AdapterTokenStore tokenStore = adapterTokenStoreFactory.createAdapterTokenStore(deployment, request);
RequestAuthenticator authenticator
= new SpringSecurityRequestAuthenticator(facade, request, deployment, tokenStore, -1);
AuthOutcome result = authenticator.authenticate();
log.debug("Auth outcome: {}", result);
if (AuthOutcome.FAILED.equals(result)) {
throw new KeycloakAuthenticationException("Auth outcome: " + result);
}
else if (AuthOutcome.AUTHENTICATED.equals(result)) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Assert.notNull(authentication, "Authentication SecurityContextHolder was null");
return authenticationManager.authenticate(authentication);
}
else {
AuthChallenge challenge = authenticator.getChallenge();
if (challenge != null) {
challenge.challenge(facade);
}
return null;
}
}
/**
* Returns true if the request was made with a bearer token authorization header.
*
* @param request the current <code>HttpServletRequest</code>
*
* @return <code>true</code> if the <code>request</code> was made with a bearer token authorization header;
* <code>false</code> otherwise.
*/
protected boolean isBearerTokenRequest(HttpServletRequest request) {
String authValue = request.getHeader(AUTHORIZATION_HEADER);
return authValue != null && authValue.toLowerCase().startsWith(SCHEME_BEARER);
}
/**
* Returns true if the request was made with a Basic authentication authorization header.
*
* @param request the current <code>HttpServletRequest</code>
* @return <code>true</code> if the <code>request</code> was made with a Basic authentication authorization header;
* <code>false</code> otherwise.
*/
protected boolean isBasicAuthRequest(HttpServletRequest request) {
String authValue = request.getHeader(AUTHORIZATION_HEADER);
return authValue != null && authValue.toLowerCase().startsWith(SCHEME_BASIC);
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
if (!(this.isBearerTokenRequest(request) || this.isBasicAuthRequest(request))) {
super.successfulAuthentication(request, response, chain, authResult);
return;
}
if (log.isDebugEnabled()) {
log.debug("Authentication success using bearer token/basic authentication. Updating SecurityContextHolder to contain: {}", authResult);
}
SecurityContextHolder.getContext().setAuthentication(authResult);
// Fire event
if (this.eventPublisher != null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
try {
chain.doFilter(request, response);
} finally {
SecurityContextHolder.clearContext();
}
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) throws IOException, ServletException {
if (this.isBearerTokenRequest(request)) {
SecurityContextHolder.clearContext();
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unable to authenticate bearer token");
return;
}
else if (this.isBasicAuthRequest(request)) {
SecurityContextHolder.clearContext();
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unable to authenticate with basic authentication");
return;
}
super.unsuccessfulAuthentication(request, response, failed);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
/**
* Sets the adapter token store factory to use when creating per-request adapter token stores.
*
* @param adapterTokenStoreFactory the <code>AdapterTokenStoreFactory</code> to use
*/
public void setAdapterTokenStoreFactory(AdapterTokenStoreFactory adapterTokenStoreFactory) {
Assert.notNull(adapterTokenStoreFactory, "AdapterTokenStoreFactory cannot be null");
this.adapterTokenStoreFactory = adapterTokenStoreFactory;
}
/**
* This filter does not support explicitly enabling session creation.
*
* @throws UnsupportedOperationException this filter does not support explicitly enabling session creation.
*/
@Override
public final void setAllowSessionCreation(boolean allowSessionCreation) {
throw new UnsupportedOperationException("This filter does not support explicitly setting a session creation policy");
}
/**
* This filter does not support explicitly setting a continue chain before success policy
*
* @throws UnsupportedOperationException this filter does not support explicitly setting a continue chain before success policy
*/
@Override
public final void setContinueChainBeforeSuccessfulAuthentication(boolean continueChainBeforeSuccessfulAuthentication) {
throw new UnsupportedOperationException("This filter does not support explicitly setting a continue chain before success policy");
}
}