I have a spring-boot backend application that authorizes users using our JASIG-CAS server and redirects them to frontend which can now access protected resources from backend. Now I need to add a mobile client. Up until now my configuration had SimpleUrlAuthenticationSuccessHandler with hardcoded url of my frontend in CasAuthenticationFilter like so:
public CasAuthenticationFilter casAuthenticationFilter() throws Exception {
CasAuthenticationFilter authenticationFilter = new CasAuthenticationFilter(); authenticationFilter.setAuthenticationManager(authenticationManager());
authenticationFilter.setServiceProperties(serviceProperties());
authenticationFilter.setFilterProcessesUrl("/auth/cas");
SimpleUrlAuthenticationSuccessHandler simpleUrlAuthenticationSuccessHandler =
new SimpleUrlAuthenticationSuccessHandler(env.getRequiredProperty(CAS_REDIRECT_TARGET));
authenticationFilter.setAuthenticationSuccessHandler(simpleUrlAuthenticationSuccessHandler);
return authenticationFilter;
}//CasAuthenticationFilter
But now my mobile client should open browser, show familiar CAS login page, authenticate user, redirect to backend which will issue a deep-link to mobile application. The problem is the hardcoded redirection target which points to frontend. The request from CAS looks the same regardles if it was triggerd from frontend or mobile because both use browsers, so I can't distinguish them using my own AuthenticationSuccessHandler. In a desperate act I tried constructing two different authentication flows using using the same CAS server but different callback endpoints. Here is this monster:
package com.my.company.config;
import org.jasig.cas.client.session.SingleSignOutFilter;
import org.jasig.cas.client.validation.Cas20ServiceTicketValidator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpMethod;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.cas.ServiceProperties;
import org.springframework.security.cas.authentication.CasAssertionAuthenticationToken;
import org.springframework.security.cas.authentication.CasAuthenticationProvider;
import org.springframework.security.cas.authentication.NullStatelessTicketCache;
import org.springframework.security.cas.web.CasAuthenticationEntryPoint;
import org.springframework.security.cas.web.CasAuthenticationFilter;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration;
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.http.SessionCreationPolicy;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.inject.Inject;
import javax.servlet.*;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
#Configuration
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private final Logger log = LoggerFactory.getLogger(SecurityConfiguration.class);
private static final String CAS_URL_SERVER = "cas.url.server";
private static final String CAS_URL_LOGIN = "cas.url.login";
private static final String CAS_URL_LOGOUT = "cas.url.logout";
private static final String CAS_URL_SERVICE = "cas.url.service";
private static final String CAS_URL_CALLBACK = "cas.url.callback";
private static final String CAS_REDIRECT_TARGET = "cas.redirect.target";
#Inject
private Environment env;
#Inject
private AjaxAuthenticationSuccessHandler ajaxAuthenticationSuccessHandler;
#Inject
private AjaxAuthenticationFailureHandler ajaxAuthenticationFailureHandler;
#Inject
#Qualifier("casUserDetailsService")
private AuthenticationUserDetailsService<CasAssertionAuthenticationToken> casAuthenticationUserDetailsService;
#Inject
#Qualifier("formUserDetailsService")
private UserDetailsService userDetailsService;
#Inject
private Http401UnauthorizedEntryPoint authenticationEntryPoint;
#Bean
public Cas20ServiceTicketValidator cas20ServiceTicketValidator() {
return new Cas20ServiceTicketValidator(env.getRequiredProperty(CAS_URL_SERVER));
}
#Bean(name="webAuthProvider")
public CasAuthenticationProvider webCasAuthenticationProvider() {
CasAuthenticationProvider casAuthenticationProvider = new CasAuthenticationProvider();
casAuthenticationProvider.setTicketValidator(cas20ServiceTicketValidator());
casAuthenticationProvider.setStatelessTicketCache(new NullStatelessTicketCache());
casAuthenticationProvider.setKey("CAS_WEB_AUTHENTICATION_PROVIDER");
casAuthenticationProvider.setAuthenticationUserDetailsService(casAuthenticationUserDetailsService);
casAuthenticationProvider.setMessageSource(new SpringSecurityMessageSource());
casAuthenticationProvider.setServiceProperties(webServiceProperties());
return casAuthenticationProvider;
}//CasAuthenticationProvider
#Bean(name="mobileAuthProvider")
public CasAuthenticationProvider mobileCasAuthenticationProvider() {
CasAuthenticationProvider casAuthenticationProvider = new CasAuthenticationProvider();
casAuthenticationProvider.setTicketValidator(cas20ServiceTicketValidator());
casAuthenticationProvider.setStatelessTicketCache(new NullStatelessTicketCache());
casAuthenticationProvider.setKey("CAS_MOBILE_AUTHENTICATION_PROVIDER");
casAuthenticationProvider.setAuthenticationUserDetailsService(casAuthenticationUserDetailsService);
casAuthenticationProvider.setMessageSource(new SpringSecurityMessageSource());
casAuthenticationProvider.setServiceProperties(mobileServiceProperties());
return casAuthenticationProvider;
}//CasAuthenticationProvider
#Bean
public SingleSignOutFilter singleSignOutFilter() {
SingleSignOutFilter filter = new SingleSignOutFilter();
return filter;
}//SingleSignOutFilter
#Inject
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder())
.and()
.authenticationProvider(mobileCasAuthenticationProvider())
.authenticationProvider(webCasAuthenticationProvider());
}
#Bean(name = "webCasFilter")
public CasAuthenticationFilter webCasAuthenticationFilter() throws Exception {
CasAuthenticationFilter authenticationFilter = new CasAuthenticationFilter();
authenticationFilter.setBeanName("webCasFilter");
authenticationFilter.setAuthenticationManager(authenticationManager());
authenticationFilter.setServiceProperties(webServiceProperties());
authenticationFilter.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/auth/cas"));
//authenticationFilter.setFilterProcessesUrl("/auth/cas");
SimpleUrlAuthenticationSuccessHandler simpleUrlAuthenticationSuccessHandler =
new SimpleUrlAuthenticationSuccessHandler(env.getRequiredProperty(CAS_REDIRECT_TARGET));
authenticationFilter.setAuthenticationSuccessHandler(simpleUrlAuthenticationSuccessHandler);
return authenticationFilter;
}//CasAuthenticationFilter
#Bean(name = "mobileCasFilter")
public CasAuthenticationFilter mobileCasAuthenticationFilter() throws Exception {
CasAuthenticationFilter authenticationFilter = new CasAuthenticationFilter();
authenticationFilter.setBeanName("mobileCasFilter");
authenticationFilter.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/auth/cas/mobile"));
authenticationFilter.setAuthenticationManager(authenticationManager());
authenticationFilter.setServiceProperties(mobileServiceProperties());
//authenticationFilter.setFilterProcessesUrl("/auth/cas/mobile");
SimpleUrlAuthenticationSuccessHandler simpleUrlAuthenticationSuccessHandler =
new SimpleUrlAuthenticationSuccessHandler("/mobile/deep-link");
authenticationFilter.setAuthenticationSuccessHandler(simpleUrlAuthenticationSuccessHandler);
return authenticationFilter;
}//CasAuthenticationFilter
#Bean(name="webCasAuthenticationEntryPoint")
public CasAuthenticationEntryPoint webCasAuthenticationEntryPoint() {
CasAuthenticationEntryPoint entryPoint = new CasAuthenticationEntryPoint();
entryPoint.setLoginUrl(env.getRequiredProperty(CAS_URL_LOGIN));
entryPoint.setServiceProperties(webServiceProperties());
return entryPoint;
}//CasAuthenticationEntryPoint
#Bean(name="mobileCasAuthenticationEntryPoint")
public CasAuthenticationEntryPoint mobileCasAuthenticationEntryPoint() {
CasAuthenticationEntryPoint entryPoint = new CasAuthenticationEntryPoint();
entryPoint.setLoginUrl(env.getRequiredProperty(CAS_URL_LOGIN));
entryPoint.setServiceProperties(mobileServiceProperties());
return entryPoint;
}//CasAuthenticationEntryPoint
#Override
public void configure(WebSecurity web) throws Exception {
web
.ignoring()
.antMatchers("/scripts/**/*.{js,html}")
.antMatchers("/bower_components/**")
.antMatchers("/i18n/**")
.antMatchers("/assets/**")
.antMatchers("/swagger-ui/**")
.antMatchers("/test/**")
.antMatchers("/console/**");
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
private class CasRedirectionFilter implements Filter {
public void init(FilterConfig fConfig) throws ServletException {
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
HttpServletResponse res = (HttpServletResponse) response;
//CasAuthenticationEntryPoint caep = casAuthenticationEntryPoint();
res.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY);
HttpServletRequest req = (HttpServletRequest) request;;
String contextPath = req.getRequestURI();
if(contextPath.equals("/api/login/mobile")){
String redirectUrl = "https://cas.server.com/cas/login?service=http://localhost:8080/auth/cas/mobile";
res.setHeader("Location", redirectUrl);
}else {
String redirectUrl = "https://cas.server.com/cas/login?service=http://localhost:8080/auth/cas";
res.setHeader("Location", redirectUrl);
}
}
public void destroy() {
}
}
#Bean
public FilterChainProxy loginFilter() throws Exception {
List<SecurityFilterChain> chains = new ArrayList<SecurityFilterChain>();
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/api/login/cas"), new CasRedirectionFilter()));
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/api/login/mobile"), new CasRedirectionFilter()));
log.debug("loginFilter {}", chains);
return new FilterChainProxy(chains);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.ALWAYS)
.and()
.csrf()
.disable()
.addFilterBefore(mobileCasAuthenticationFilter(),CasAuthenticationFilter.class)
.addFilterBefore(webCasAuthenticationFilter(),CasAuthenticationFilter.class)
.addFilterBefore(singleSignOutFilter(), CasAuthenticationFilter.class)
.addFilter(loginFilter())
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
.and()
.logout()
.logoutUrl("/api/logout")
.clearAuthentication(true)
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
.logoutSuccessUrl(env.getRequiredProperty(CAS_URL_LOGOUT))
.permitAll()
.and()
.headers()
.frameOptions()
.disable()
.and()
.formLogin()
//.defaultSuccessUrl(env.getRequiredProperty(CAS_REDIRECT_TARGET), true)
.successHandler(ajaxAuthenticationSuccessHandler)
.failureHandler(ajaxAuthenticationFailureHandler)
.loginProcessingUrl("/api/authentication")
.usernameParameter("j_username")
.passwordParameter("j_password")
.permitAll()
.and()
.authorizeRequests()
.antMatchers(org.springframework.http.HttpMethod.OPTIONS, "/api/**").permitAll()
.antMatchers(org.springframework.http.HttpMethod.OPTIONS, "/**").permitAll()
.antMatchers("/app/**").authenticated()
.antMatchers(HttpMethod.GET, "/api/login").authenticated()
.antMatchers("/api/login/mobile").authenticated()
.antMatchers("/api/login/cas").authenticated()
.antMatchers("/api/register").permitAll()
.antMatchers("/api/activate").permitAll()
.antMatchers("/api/authenticate").authenticated()
.antMatchers("/api/logs/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/api/**").authenticated()
.antMatchers("/metrics/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/mobile/**").permitAll()
.antMatchers("/health/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/trace/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/dump/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/shutdown/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/beans/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/configprops/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/info/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/autoconfig/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/env/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/trace/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/api-docs/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/protected/**").authenticated();
}
#EnableGlobalMethodSecurity(prePostEnabled = true, jsr250Enabled = true)
private static class GlobalSecurityConfiguration extends GlobalMethodSecurityConfiguration {
#Inject
ConferenceRepository conferenceRepository;
#Inject
UserRepository userRepository;
public GlobalSecurityConfiguration() {
super();
}
#Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
PermissionChecker permissionEvaluator = new PermissionChecker(conferenceRepository, userRepository);
DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
expressionHandler.setPermissionEvaluator(permissionEvaluator);
return expressionHandler;
}
}
#Bean(name="webServiceProperties")
public ServiceProperties webServiceProperties() {
ServiceProperties serviceProperties = new ServiceProperties();
serviceProperties.setService("http://localhost:8080/auth/cas");
serviceProperties.setSendRenew(true);
serviceProperties.setAuthenticateAllArtifacts(true);
return serviceProperties;
}//serviceProperties
#Bean(name="mobileServiceProperties")
public ServiceProperties mobileServiceProperties() {
ServiceProperties serviceProperties = new ServiceProperties();
serviceProperties.setService("http://localhost:8080/auth/cas/mobile");
serviceProperties.setSendRenew(true);
serviceProperties.setAuthenticateAllArtifacts(true);
return serviceProperties;
}//serviceProperties
}
This works to some degree. When mobile authentication flow is issued it works as intended but when frontend issues /api/login/cas the TicketGrantingTicket from CAS is first checked using mobile filter against service=/auth/cas/mobile but was issued for service=/auth/cas which invalidates TGT and subsequent validation using casWebAuthenticationFilter uses that invalidated ticket which of course.
So now I'm out of ideas how to force CasAuthenticationFilter to process only certain tickets? Perhaps I'm so tangled up in my idea that I can't see simpler solution? Maybe I should do two separate http security configs?
EDIT:
It seems that it all boils down to the order in which I put AuthenticationProvider:
#Inject
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder())
.and()
.authenticationProvider(webCasAuthenticationProvider())
.authenticationProvider(mobileCasAuthenticationProvider());
}
When mobileAuthenticationProider() goes first then the mobile login works and web one doesn't when I switch the order in which they are called then mobile authentication fails and the web one starts to work.
Ok so I got it working, it doesn't look like the best, most robust solution so it probably nees some more investigation and care. Nevertheless here it goes:
#Bean
public AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> authenticationDetailsSource() {
return new AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails>() {
#Override
public WebAuthenticationDetails buildDetails(
HttpServletRequest request) {
return new CustomAuthenticationDetails(request);
}
};
}
#Bean
public AuthenticationProvider customAuthenticationProvider() {
return new AuthenticationProvider() {
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String serviceUrl;
serviceUrl = ((CustomAuthenticationDetails) authentication.getDetails()).getURI();
if (serviceUrl.equals(env.getRequiredProperty(CAS_URL_CALLBACK_MOBILE))) {
return mobileCasAuthenticationProvider().authenticate(authentication);
} else {
return webCasAuthenticationProvider().authenticate(authentication);
}
}
public boolean supports(final Class<?> authentication) {
return (UsernamePasswordAuthenticationToken.class
.isAssignableFrom(authentication))
|| (CasAuthenticationToken.class.isAssignableFrom(authentication))
|| (CasAssertionAuthenticationToken.class
.isAssignableFrom(authentication));
}
};
}
I added my custom AuthenticationProvider which distinguishes between the two that I really needed. It does it using another custom class, namely CustomAuthenticationDetails which stores information about where the request came from.
public class CustomAuthenticationDetails extends WebAuthenticationDetails {
private final Logger log = LoggerFactory.getLogger(CustomAuthenticationDetails.class);
private final String URI;
private final String sessionId;
public CustomAuthenticationDetails(HttpServletRequest request) {
super(request);
this.URI = request.getRequestURI();
HttpSession session = request.getSession(false);
this.sessionId = (session != null) ? session.getId() : null;
}
public String getURI() {
return URI;
}
public String getSessionId() {
return sessionId;
}
}
And all this is wired together in AuthenticationFilter using authenticationFilter.setAuthenticationDetailsSource(authenticationDetailsSource());. Hope it can help someone in future problems or at least lead in the right direction.
Related
I am building a project with Spring Security and Spring Boot, but when I login with ajax request, Spring Security's request matcher can not match the login request /auth/login,
I want to know how I could configure it correctly.
This is my WebSecurity Configuration:
package com.keanu.blog.web.config;
import com.keanu.blog.web.security.GlobalAuthenticationFilter;
import com.keanu.blog.web.security.LoginAuthenticationProvider;
import com.keanu.blog.web.security.JwtAuthenticationFilter;
import com.keanu.blog.web.security.TokenAuthenticationProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.access.AccessDeniedHandler;
/**
*
* #author leiyongqi
* #date 2019/07/27
*/
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private AccessDeniedHandler accessDeniedHandler;
#Autowired
private UserDetailsService userDetailsService;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/auth/**", "/registry").permitAll()
.anyRequest()
.authenticated()
.and()
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/auth/login")
.defaultSuccessUrl("/")
.failureUrl("/login?error=true")
.permitAll()
.and()
.logout()
.logoutSuccessUrl("/login")
.and()
.addFilter(globalAuthenticationFilter());
}
#Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
return new JwtAuthenticationFilter(authenticationManager());
}
#Bean("loginAuthenticationProvider")
public AuthenticationProvider loginAuthenticationProvider() {
return new LoginAuthenticationProvider();
}
#Bean("tokenAuthenticationProvider")
public AuthenticationProvider tokenAuthenticationProvider() {
return new TokenAuthenticationProvider();
}
#Bean
public GlobalAuthenticationFilter globalAuthenticationFilter() throws Exception {
GlobalAuthenticationFilter globalAuthenticationFilter = new GlobalAuthenticationFilter();
globalAuthenticationFilter.setAuthenticationManager(authenticationManager());
return globalAuthenticationFilter;
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(loginAuthenticationProvider())
.authenticationProvider(tokenAuthenticationProvider())
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/assets/**","/druid/**");
}
#Bean
public AccessDeniedHandler accessDeniedHandler() {
return new CustomAccessDeniedHandler();
}
#Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
And this is My GlobalAuthenticationFilter.class, every request can be intercepted by this filter.
package com.keanu.blog.web.security;
import com.keanu.blog.common.constant.AuthConstant;
import com.keanu.blog.common.utils.AesUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
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;
/**
* Custom Authentication Filter (Both Username and Token)
* #author leiyongqi
* #date 2019/08/01
*/
#Slf4j
public class GlobalAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private String keyParameter = "key";
#Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
try {
// Authenticate with username
if (isLoginRequest(request, response)) {
Authentication authentication = processLogin(request, response);
successfulAuthentication(request, response, chain, authentication);
return;
}
// If the request isn't login request, authenticate with jwt token
// TODO Authenticate with jwt token
String token = obtainJwtToken(request);
} catch (AuthenticationException e) {
unsuccessfulAuthentication(request, response, e);
return;
}
chain.doFilter(request, response);
}
private String obtainJwtToken(HttpServletRequest request) {
return request.getHeader(AuthConstant.JWT_TOKEN_HEADER);
}
private boolean isLoginRequest(HttpServletRequest request, HttpServletResponse response) {
return requiresAuthentication(request, response) && "POST".equalsIgnoreCase(request.getMethod());
}
private String obtainSecretKey(HttpServletRequest request) {
return request.getParameter(keyParameter);
}
private Authentication processLogin(HttpServletRequest request, HttpServletResponse response) {
String username = obtainUsername(request);
String encryptPassword = obtainPassword(request);
String key = obtainSecretKey(request);
if (log.isDebugEnabled()) {
log.debug("处理登录请求:username[{}], password[{}], secretKey[{}]", username, encryptPassword, key);
}
// 解密密码以及用 BCryptPasswordEncoder 加密
String password = null;
try {
password = AesUtil.encryptPassword(AesUtil.decryptAes(encryptPassword, key));
} catch (Exception e) {
log.error("解密密码失败:password[{}], key[{}], error[{}]", encryptPassword, key, e.getMessage());
}
Authentication authentication = new UsernamePasswordAuthenticationToken(username, password);
Authentication responseAuth = getAuthenticationManager().authenticate(authentication);
if (responseAuth == null || !responseAuth.isAuthenticated()) {
throw new InternalAuthenticationServiceException("Unable to authenticate User for provided credentials");
}
if (log.isDebugEnabled()) {
log.debug("登录用户 [{}] 验证成功", username);
}
return responseAuth;
}
#Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
response.setStatus(HttpStatus.OK.value());
response.getWriter().write("");
}
}
but if I use ajax to send login request using url /auth/login, Spring Security can not match the login request so I can't handle the login request.
this is my Login Page's ajax code:
$.ajax({
type: "POST",
url: loginUrl, // loginUrl => '/auth/login'
contentType: "application/json;charset=utf-8",
data: JSON.stringify({'username': username, 'password': encryptPwd, 'secretKey': key}),
dataType: "json",
async: true,
success: function (data) {
window.localStorage.setItem("JWT-TOKEN", data);
window.location.href = "/";
},
error: function (e) {
alert("登录失败:" + e.message);
}
})
Through checking the source code, the request can't go into the 'handle login' block because requiresAuthentication(request) can't match the login pattern with '/auth/login', but I don' t know why.
I expected the login request with ajax to be intercepted by Spring Security so the request could go into 'handle login' block I showed above.
I have configured Spring security to work with both LDAP and DB based login. First it tries to login via LDAP and if required permissions is not there then it takes to username/password entry page.
<security:http auto-config="false" entry-point-ref="loginUrlAuthenticationEntryPoint">
<security:custom-filter ref="customPreAuthFilter" position="PRE_AUTH_FILTER"/> // This is for LDAP
<security:custom-filter ref="customAuthFilter" position="FORM_LOGIN_FILTER"/> // This is for DB Based
/** intercept urls
**/
</security:http>
I want to add a new screen on the top and user need to select between the two button LDAP or username/pass. How do I proceed?
The data to be accessed is the same url i.e. /home but both ldap and DB users should be able to access.
If you look at code in UsernamePasswordAuthenticationFilter there is setDetails method.
from docs:
Provided so that subclasses may configure what is put into the
authentication request's details property.
Idea from here
Provision to change ldap/Ad provider url at run time
You can set the details like authtype here and use it authentication provider, But to achieve the things you would lik adds little more work.
Adding details and hope it helps.
CustomUsernamePasswordAuthenticationFilter.java
import javax.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.stereotype.Component;
#Component
public class CustomUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private static final Logger logger = LoggerFactory.getLogger(CustomUsernamePasswordAuthenticationFilter.class);
#Autowired
#Override
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
// TODO Auto-generated method stub
super.setAuthenticationManager(authenticationManager);
}
#Autowired
#Override
public void setAuthenticationDetailsSource(
AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource) {
super.setAuthenticationDetailsSource(authenticationDetailsSource);
}
#Override
protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
String authType = request.getParameter("authType");
logger.info("authType {} ",authType);
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}
}
But this is not sufficient you would need to extend WebAuthenticationDetails.
Reason is WebAuthenticationDetails provides only remote IP address and sessionId so, to add authType we need to extend this class.
You have to extend WebAuthenticationDetailsSource to return CustomAuthenticationDetails as shown below.
CustomAuthenticationDetails.java
public class CustomAuthenticationDetails extends WebAuthenticationDetails{
private final String authType;
public CustomAuthenticationDetails(HttpServletRequest request) {
super(request);
authType = request.getParameter("authType");
}
public String getAuthType() {
return authType;
}
}
CustomWebAuthenticationDetailsSource.java
public class CustomWebAuthenticationDetailsSource extends WebAuthenticationDetailsSource {
#Override
public WebAuthenticationDetails buildDetails(HttpServletRequest context) {
return new CustomAuthenticationDetails(context);
}
}
Please note these classes for demo purpose only.
Need to autowire actual authentication providers in these classes.
import java.util.Arrays;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.ldap.authentication.LdapAuthenticationProvider;
import org.springframework.stereotype.Component;
#Component
public class AuthenicationProviderJdbcLdapImpl implements AuthenticationProvider{
// you need to autowire jdbc auth provider
#Autowired(required = false)
DaoAuthenticationProvider authenticationProvider;
//you need to autowire ldap auth provider
#Autowired(required = false)
LdapAuthenticationProvider ldapAuthenticationProvider;
protected static class User{
private final String userId;
private final String password;
public User(String userId,String password) {
this.userId = userId;
this.password = password;
}
public String getUserId() {
return userId;
}
public String getPassword() {
return password;
}
#Override
public String toString() {
return "User [userId=" + userId + ", password=" + password + "]";
}
}
private final static Logger logger = LoggerFactory.getLogger(AuthenicationProviderJdbcLdapImpl.class);
private static final List<User> users1 = Arrays.asList(new User("admin1", "password"),new User("admin2", "password"));
private static final List<User> users2 = Arrays.asList(new User("admin3", "password"),new User("admin4", "password"));
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
CustomAuthenticationDetails details = (CustomAuthenticationDetails) authentication.getDetails();
String authType = details.getAuthType();
logger.info("authType {}",authType);
if("jdbc".equalsIgnoreCase(authType)) {
logger.info("perfrom jdbc authentication");
//perform your authentication using jdbc
//the below is just for explaination
return performAuthentication(authentication, users1);
}else if("ldap".equalsIgnoreCase(authType)) {
logger.info("perfrom ldap authentication");
//perform your authentication using ldap
//the below is just for explaination
return performAuthentication(authentication, users2);
}
return null;
}
private Authentication performAuthentication(Authentication authentication,List<User> users) {
String credential = (String) authentication.getCredentials();
String userId = authentication.getName();
for(User user: users) {
if(user.getUserId().equals(userId)&& user.getPassword().equals(credential)) {
authentication = new UsernamePasswordAuthenticationToken(authentication.getPrincipal(), authentication.getCredentials(),authentication.getAuthorities());
return authentication;
}
}
return null;
}
#Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
If you would need to redirect different login page (not sure, if you have the requirement) you register AuthenticationFailureHandler shown in security config. Here it is redirected to login and login1 based on condition.
http.failureHandler(new AuthenticationFailureHandler() {
#Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
String whichPage = request.getParameter("whichPage");
System.out.println("inside login failure handler "+whichPage);
if("login1".equals(whichPage)) {
response.sendRedirect(request.getContextPath() +"/login1");
}else {
response.sendRedirect(request.getContextPath() +"/login");
}
}
})
WebSecurityConfig.java
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private UserDetailsService userDetailsService;
#Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean
public AuthenticationManager getAuthenticationManager() throws Exception {
return super.authenticationManagerBean();
}
#Autowired
AuthenticationSuccessHandler authenticationSuccessHandler;
#Autowired()
AuthenicationProviderJdbcImpl authenicationProviderJdbcImpl;
#Autowired()
AuthenicationProviderLdapImpl authenicationProviderLdapImpl;
#Autowired
CustomUsernamePasswordAuthenticationFilter customUsernamePasswordAuthenticationFilter;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterAt(customUsernamePasswordAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
http
.authorizeRequests()
.antMatchers("/resources/**", "/registration","/login1").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()//.successHandler(authenticationSuccessHandler)
.failureHandler(new AuthenticationFailureHandler() {
#Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
String whichPage = request.getParameter("whichPage");
System.out.println("inside login failure handler "+whichPage);
if("login1".equals(whichPage)) {
response.sendRedirect(request.getContextPath() +"/login1");
}else {
response.sendRedirect(request.getContextPath() +"/login");
}
}
})
.and()
.logout()
.permitAll();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenicationProviderLdapImpl).authenticationProvider(authenicationProviderJdbcImpl);
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
/*auth.userDetailsService(userDetailsService)
.passwordEncoder(bCryptPasswordEncoder());*/
}
}
The below is from logs when authType = jdbc or authType=ldap
login called
2018-11-23 17:45:25.606 INFO 7232 --- [nio-8080-exec-6] stomUsernamePasswordAuthenticationFilter : authType jdbc
2018-11-23 17:45:25.606 INFO 7232 --- [nio-8080-exec-6] c.t.a.AuthenicationProviderJdbcLdapImpl : authType jdbc
2018-11-23 17:45:25.606 INFO 7232 --- [nio-8080-exec-6] c.t.a.AuthenicationProviderJdbcLdapImpl : perfrom jdbc authentication
login called
login1 called
login1 called
2018-11-23 17:45:42.435 INFO 7232 --- [nio-8080-exec-5] stomUsernamePasswordAuthenticationFilter : authType ldap
2018-11-23 17:45:42.435 INFO 7232 --- [nio-8080-exec-5] c.t.a.AuthenicationProviderJdbcLdapImpl : authType ldap
2018-11-23 17:45:42.435 INFO 7232 --- [nio-8080-exec-5] c.t.a.AuthenicationProviderJdbcLdapImpl : perfrom ldap authentication returning true in ldap
I have a spring boot app with the following web security configuration:
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/login**", "/signup**").permitAll()
.and()
.csrf().disable()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.addFilterBefore(jwtAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class);
}
The JWTAuthenticationFilter looks like this :
#Component
public class JWTAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
#Autowired
private UserDetailsService customUserDetailsService;
private static Logger logger = LoggerFactory.getLogger(JWTAuthenticationFilter.class);
private final static UrlPathHelper urlPathHelper = new UrlPathHelper();
public JWTAuthenticationFilter() {
super("/greeting");
setAuthenticationManager(new NoOpAuthenticationManager());
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
Authentication authentication = AuthenticationService.getAuthentication(request, customUserDetailsService);
return getAuthenticationManager().authenticate(authentication);
}
#Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
logger.debug("failed authentication while attempting to access " + urlPathHelper.getPathWithinApplication((HttpServletRequest) request));
}
}
1. Authentication is done successfuly, I even see the following log line in the console:
2017-05-19 03:11:42 [https-jsse-nio-8443-exec-4] DEBUG c.b.c.s.a.j.JWTAuthenticationFilter -
Authentication success. Updating SecurityContextHolder to contain: org.springframework.security.authentication.UsernamePasswordAuthenticationToken#f297a5c8: Principal: administrator; Credentials: [PROTECTED]; Authenticated: true; Details: null; Granted Authorities: USER_ROLE
but still the client side, 403 response is recieved.
2. I want to make this authentication filter execute for all endpoints, except those with permitAll in the web security configurer. How should I do that?
Reason for that is it's redirecting to the default redirect URL in AbstractAuthenticationProcessingFilter which is /. To override this behavior you'll need to override successfulAuthentication().
unsuccessfulAuthentication() method needs to send an authentication error.
Here's the implementation of both of these methods.
#Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
SecurityContextHolder.getContext().setAuthentication(authResult);
chain.doFilter(request, response);
}
#Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) throws IOException, ServletException {
logger.debug("failed authentication while attempting to access "
+ urlPathHelper.getPathWithinApplication((HttpServletRequest) request));
//Add more descriptive message
response.sendError(HttpServletResponse.SC_UNAUTHORIZED,
"Authentication Failed");
}
Following is a self contained working example. I have populated dummy authentication object for all requests, you'll need to use your own user details service to validate and then populate authentication object conditionally.
package com.test;
import java.io.IOException;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.util.UrlPathHelper;
#SpringBootApplication
public class TestSpringSecurityCustomApplication {
public static void main(String[] args) {
SpringApplication.run(TestSpringSecurityCustomApplication.class, args);
}
}
#Configuration
class CustomWebSecurity extends WebSecurityConfigurerAdapter {
#Autowired
private JWTAuthenticationFilter jwtAuthenticationFilter;
#Override
protected void configure(HttpSecurity http) throws Exception {
System.out.println("Configuring security");
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
.anyRequest().authenticated()
.and().csrf().disable();
}
#Override
public void configure(WebSecurity web)
throws Exception {
web.ignoring().antMatchers("/login/**", "/signup/**");
}
/* Stopping spring from adding filter by default */
#Bean
public FilterRegistrationBean rolesAuthenticationFilterRegistrationDisable(JWTAuthenticationFilter filter) {
FilterRegistrationBean registration = new FilterRegistrationBean(filter);
registration.setEnabled(false);
return registration;
}
}
#RestController
#RequestMapping("greeting")
class TestService {
#RequestMapping("test")
public String test() {
return "Hello World " + new Date();
}
}
#Component
class JWTAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
#Autowired
private UserDetailsService customUserDetailsService;
private static Logger logger = LoggerFactory.getLogger(JWTAuthenticationFilter.class);
private final static UrlPathHelper urlPathHelper = new UrlPathHelper();
public JWTAuthenticationFilter() {
super("/**");
setAuthenticationManager(new NoOpAuthenticationManager());
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
Authentication authentication = AuthenticationService.getAuthentication(request, customUserDetailsService);
return getAuthenticationManager().authenticate(authentication);
}
#Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
SecurityContextHolder.getContext().setAuthentication(authResult);
chain.doFilter(request, response);
}
#Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) throws IOException, ServletException {
logger.debug("failed authentication while attempting to access "
+ urlPathHelper.getPathWithinApplication((HttpServletRequest) request));
//Add more descriptive message
response.sendError(HttpServletResponse.SC_UNAUTHORIZED,
"Authentication Failed");
}
}
class AuthenticationService {
public static Authentication getAuthentication(HttpServletRequest request, UserDetailsService userDetailsService) {
String username = "TEST_USER";// get this from the token or request
UserDetails user = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(user,
user.getPassword(), user.getAuthorities());
//Use following to indicate that authentication failed, if user not found or role doesn't match
boolean hasAuthenticationFailed = false;
if(hasAuthenticationFailed) {
throw new AuthenticationException(username){};
}
return authentication;
}
}
#Component
class CustomUserDetailsService implements UserDetailsService {
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// Returning dummy user, use your own logic for example load from
// database
List<SimpleGrantedAuthority> authorities = Arrays.asList(new SimpleGrantedAuthority(("ROLE_USER")));
User user = new User("TEST_USER", "NO_PASSWORD", authorities);
System.out.println("user : " + user.getUsername());
return user;
}
}
class NoOpAuthenticationManager implements AuthenticationManager {
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
return authentication;
}
}
Edit
With custom security filters, permitAll() method doesn't seem to have any effect. So following method should be overridden in WebSecurityConfigurerAdapter to ignore URLs
#Override
public void configure(WebSecurity web)
throws Exception {
web.ignoring().antMatchers("/login/**", "/signup/**");
}
Note: I have modified above code to use the same. Also if you want to ignore sub URLs to login ie. login/dafdsf, then you should use /login/** instead of /login**
I have been struggling to get waffle to work with spring 4.2.5 using spring java configuration. And I thought I might as well help others in the same situation.
We use a custom preWaffle and postWaffle filter to authenticate that the user exists in our database after it has been validated via waffles NTLM protocol.
We also have methods for authorization of a user actions using the EnableGlobalMethodSecurity annotation.
To get this working in spring java configuration was trouble some to say the least. You can find our solution in the answer below. I hope it will help.
SpringConfiguration.java
// ... imports
#Configuration
#EnableWebMvc
#EnableScheduling
#PropertySources({
#PropertySource("classpath:app.properties")
// ... Properties sources
})
#EnableTransactionManagement
#ComponentScan(basePackages = "com.our.package")
public class SpringConfiguration extends WebMvcConfigurerAdapter {
// Our Spring configuration ...
}
SecurityConfiguration.java
// ... imports
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, proxyTargetClass = true)
#Order(1)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter{
// Authentication manager configuration
#Autowired
private WindowsAuthenticationProviderWrapper authProvider;
#Autowired
private AuthenticationManagerBuilder auth;
#Override
protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authProvider);
}
#Bean
public AuthenticationManager authenticationManager() throws Exception {
return auth.getObject();
}
// Waffle configuration
#Bean
public Filter customPreAuthSecurityFilter() {
return new CustomPreAuthSecurityFilter();
}
#Bean
public Filter customNegotiateSecurityFilter() {
return new CustomNegotiateSecurityFilter();
}
#Bean
public WindowsAuthProviderImpl waffleAuthProvider(){
return new WindowsAuthProviderImpl();
}
#Bean(name="negotiateSecurityFilterProvider")
#Autowired
public NegotiateSecurityFilterProvider negotiateSecurityFilterProvider(){
NegotiateSecurityFilterProvider bean = new NegotiateSecurityFilterProvider(waffleAuthProvider());
List<String> protocols = new ArrayList<>();
protocols.add("Negotiate");
bean.setProtocols(protocols);
return bean;
}
#Bean
public BasicSecurityFilterProvider basicSecurityFilterProvider(){
return new BasicSecurityFilterProvider(waffleAuthProvider());
}
#Bean(name="waffleSecurityFilterProviderCollection")
#Autowired
public waffle.servlet.spi.SecurityFilterProviderCollection negotiateSecurityFilterProviderCollection() {
final List<SecurityFilterProvider> lsp = new ArrayList<>();
lsp.add(negotiateSecurityFilterProvider());
lsp.add(basicSecurityFilterProvider());
return new waffle.servlet.spi.SecurityFilterProviderCollection(lsp.toArray(new SecurityFilterProvider[]{}));
}
#Bean(name="negotiateSecurityFilterEntryPoint")
#Autowired
public waffle.spring.NegotiateSecurityFilterEntryPoint negotiateSecurityFilterEntryPoint() {
final waffle.spring.NegotiateSecurityFilterEntryPoint ep = new waffle.spring.NegotiateSecurityFilterEntryPoint();
ep.setProvider(negotiateSecurityFilterProviderCollection());
return ep;
}
#Bean(name="negotiateSecurityFilter")
#Autowired
public waffle.spring.NegotiateSecurityFilter waffleNegotiateSecurityFilter(){
waffle.spring.NegotiateSecurityFilter bean = new waffle.spring.NegotiateSecurityFilter();
bean.setRoleFormat("both");
bean.setPrincipalFormat("fqn");
bean.setAllowGuestLogin(false);
bean.setProvider(negotiateSecurityFilterProviderCollection());
return bean;
}
// Static Mappings
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/assets/**");
}
// Security filter chain
// The custom filters can be removed if you only use waffle
// but this is how we added them
#Override
protected void configure(HttpSecurity http) throws Exception {
// A user needs to have the role user and has to be authenticated
http.exceptionHandling()
.authenticationEntryPoint(negotiateSecurityFilterEntryPoint()).and()
.addFilterBefore(customPreAuthSecurityFilter(), BasicAuthenticationFilter.class)
.addFilterAfter(waffleNegotiateSecurityFilter(), BasicAuthenticationFilter.class)
.addFilterAfter(customNegotiateSecurityFilter(), BasicAuthenticationFilter.class)
.authorizeRequests().anyRequest().fullyAuthenticated();
}
}
To be able to autowire the waffle authProvider I created the following wrapperclass.
WindowsAuthenticationProviderWrapper.java
// ... imports
// This class purpose is only to make the Windows authentication provider autowireable in spring.
#Component
public class WindowsAuthenticationProviderWrapper extends WindowsAuthenticationProvider{}
As requested (Some code has been stripped due to security risks).
CustomPreAuthFilter.java
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
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;
/**
* This filter removes the excess negoatiate header sent by IE. If the client
* has already authenticated, strip the Authorization header from the request.
*/
public class CustomPreAuthSecurityFilter extends GenericFilterBean {
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
SecurityContext sec = SecurityContextHolder.getContext();
HttpServletRequest req = (HttpServletRequest) servletRequest;
if(sec != null && sec.getAuthentication() != null) {
req = new CustomServletRequestWrapper(req);
}
try {
filterChain.doFilter(req, servletResponse);
} catch (RuntimeException e) {
sendUnauthorized((HttpServletResponse) servletResponse);
}
}
private void sendUnauthorized(HttpServletResponse response) throws IOException {
logger.warn("error logging in user");
SecurityContextHolder.clearContext();
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
}
CustomNegotiateSecurityFilter.java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.GenericFilterBean;
import waffle.servlet.WindowsPrincipal;
import waffle.spring.WindowsAuthenticationToken;
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.util.Date;
/**
* Handle post NTLM authentication against system database
*/
public class CustomNegotiateSecurityFilter extends GenericFilterBean {
#Autowired
private UserDAO userDAO;
#Autowired
Environment env;
private static final Logger LOGGER = LoggerFactory.getLogger(CustomNegotiateSecurityFilter.class);
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
final HttpServletRequest request = (HttpServletRequest) req;
final HttpServletResponse response = (HttpServletResponse) res;
SecurityContext sec = SecurityContextHolder.getContext();
Authentication authentication = sec.getAuthentication();
// Continue filter chain if we are anonymously authenticated or if DB authentication has already happened.
if (authentication != null && authentication.getClass() == WindowsAuthenticationToken.class) {
// The user is Authenticated with NTLM but needs to be checked against the DB.
User user;
try {
// fetch user from DB ...
} catch (Exception e) {
// The could not be found in the DB.
sendUnauthorized(response);
return;
}
// The user was found in the DB.
WindowsPrincipal principal = (WindowsPrincipal)authentication.getPrincipal();
final CustomAuthenticationToken token = new CustomAuthenticationToken(principal); // This class extends WindowsAuthenticationToken
// add roles to token ...
sec.setAuthentication(token);
}
chain.doFilter(request, response);
}
private void sendUnauthorized(HttpServletResponse response) throws IOException {
logger.warn("Could not log in user");
SecurityContextHolder.clearContext();
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
private void addRoleToAuthentication(WindowsAuthenticationToken authentication, String role) {
for(GrantedAuthority authority : authentication.getAuthorities()) {
if(authority.getAuthority().equals(role)) {
return;
}
}
authentication.getAuthorities().add(new SimpleGrantedAuthority(role));
}
}
EDIT
For those who asked about here is one implementation. CustomServletRequestWrapper:
class CustomServletRequestWrapper extends HttpServletRequestWrapper {
public CustomServletRequestWrapper(HttpServletRequest request) {
super(request);
}
public String getHeader(String name) {
if(name.equals("Authorization"))
return null;
String header = super.getHeader(name);
return (header != null) ? header : super.getParameter(name); // Note: you can't use getParameterValues() here.
}
public Enumeration getHeaderNames() {
List<String> names = Collections.list(super.getHeaderNames());
names.addAll(Collections.list(super.getParameterNames()));
names.remove("Authorization");
return Collections.enumeration(names);
}
}
If you need more information don't hessitate to ask.
I genereted an application using JHipster and I would like to use my instance of jasig CAS as SSO with my application instead of default form login that comes with the app. Ultimately I would like to use custom CAS parameters to assign authorities.
I followed this example. Now I have the app generated with JHipster which I successfully connected to my CAS. When I type localhost:8080/app/login it redirects me to my CAS instance, authenticates me and redirects back to JHipster app but JHipster doesn't let me browse the secured part of my the app and still requires authentication. Below is my SecurityConfiguration.java. Can somone provide any insight as to how proceed from here?
package com.my.company.application.config;
import org.jasig.cas.client.session.SingleSignOutFilter;
import org.jasig.cas.client.validation.Cas20ServiceTicketValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.security.cas.ServiceProperties;
import org.springframework.security.cas.authentication.CasAssertionAuthenticationToken;
import org.springframework.security.cas.web.CasAuthenticationFilter;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration;
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.core.userdetails.AuthenticationUserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.csrf.CsrfFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import pl.edu.uw.dsk.konferator.security.*;
import pl.edu.uw.dsk.konferator.web.filter.CsrfCookieGeneratorFilter;
import javax.inject.Inject;
#Configuration
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private static final String CAS_URL_LOGIN = "cas.url.login";
private static final String CAS_URL_LOGOUT = "cas.url.logout";
private static final String CAS_URL_PREFIX = "cas.url.prefix";
private static final String CAS_SERVICE_URL = "app.service.security";
private static final String CAS_CALLBACK = "/auth/cas";
#Inject
private Environment env;
#Inject
private AjaxLogoutSuccessHandler ajaxLogoutSuccessHandler;
/*#Inject
private AjaxAuthenticationSuccessHandler ajaxAuthenticationSuccessHandler;*/
#Inject
private AjaxAuthenticationFailureHandler ajaxAuthenticationFailureHandler;
#Inject
private AuthenticationUserDetailsService<CasAssertionAuthenticationToken> userDetailsService;
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean
public ServiceProperties serviceProperties() {
ServiceProperties sp = new ServiceProperties();
sp.setService(env.getRequiredProperty(CAS_SERVICE_URL));
sp.setSendRenew(false);
return sp;
}
#Bean
public SimpleUrlAuthenticationSuccessHandler authenticationSuccessHandler() {
SimpleUrlAuthenticationSuccessHandler authenticationSuccessHandler = new SimpleUrlAuthenticationSuccessHandler();
authenticationSuccessHandler.setDefaultTargetUrl("/");
authenticationSuccessHandler.setTargetUrlParameter("spring-security-redirect");
return authenticationSuccessHandler;
}
#Bean
public RememberCasAuthenticationProvider casAuthenticationProvider() {
RememberCasAuthenticationProvider casAuthenticationProvider = new RememberCasAuthenticationProvider();
casAuthenticationProvider.setAuthenticationUserDetailsService(userDetailsService);
casAuthenticationProvider.setServiceProperties(serviceProperties());
casAuthenticationProvider.setTicketValidator(cas20ServiceTicketValidator());
casAuthenticationProvider.setKey("CAS_AUTHENTICATION_PROVIDER");
return casAuthenticationProvider;
}
#Bean
public SessionAuthenticationStrategy sessionStrategy() {
SessionFixationProtectionStrategy sessionStrategy = new SessionFixationProtectionStrategy();
sessionStrategy.setMigrateSessionAttributes(false);
//sessionStrategy.setRetainedAttributes(Arrays.asList("CALLBACKURL"));
return sessionStrategy;
}
/*
#Bean
public Saml11TicketValidator casSamlServiceTicketValidator() {
return new Saml11TicketValidator(env.getRequiredProperty(CAS_URL_PREFIX));
}
*/
#Bean
public Cas20ServiceTicketValidator cas20ServiceTicketValidator() {
return new Cas20ServiceTicketValidator(env.getRequiredProperty(CAS_URL_PREFIX));
}
#Bean
public CasAuthenticationFilter casAuthenticationFilter() throws Exception {
CasAuthenticationFilter casAuthenticationFilter = new CasAuthenticationFilter();
casAuthenticationFilter.setAuthenticationManager(authenticationManager());
casAuthenticationFilter.setAuthenticationDetailsSource(new RememberWebAuthenticationDetailsSource());
casAuthenticationFilter.setSessionAuthenticationStrategy(sessionStrategy());
casAuthenticationFilter.setAuthenticationFailureHandler(ajaxAuthenticationFailureHandler);
casAuthenticationFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler());
casAuthenticationFilter.setFilterProcessesUrl(CAS_CALLBACK);
// casAuthenticationFilter.setRequiresAuthenticationRequestMatcher(new
// AntPathRequestMatcher("/login", "GET"));
return casAuthenticationFilter;
}
#Bean
public RememberCasAuthenticationEntryPoint casAuthenticationEntryPoint() {
RememberCasAuthenticationEntryPoint casAuthenticationEntryPoint = new RememberCasAuthenticationEntryPoint();
casAuthenticationEntryPoint.setLoginUrl(env.getRequiredProperty(CAS_URL_LOGIN));
casAuthenticationEntryPoint.setServiceProperties(serviceProperties());
//move to /app/login due to cachebuster instead of api/authenticate
casAuthenticationEntryPoint.setPathLogin("/app/login");
return casAuthenticationEntryPoint;
}
#Bean
public SingleSignOutFilter singleSignOutFilter() {
SingleSignOutFilter singleSignOutFilter = new SingleSignOutFilter();
singleSignOutFilter.setCasServerUrlPrefix(env.getRequiredProperty(CAS_URL_PREFIX));
return singleSignOutFilter;
}
#Bean
public LogoutFilter requestCasGlobalLogoutFilter() {
LogoutFilter logoutFilter = new LogoutFilter(env.getRequiredProperty(CAS_URL_LOGOUT) + "?service="
+ env.getRequiredProperty(CAS_SERVICE_URL), new SecurityContextLogoutHandler());
// logoutFilter.setFilterProcessesUrl("/logout");
// logoutFilter.setFilterProcessesUrl("/j_spring_cas_security_logout");
logoutFilter.setLogoutRequestMatcher(new AntPathRequestMatcher("/api/logout", "POST"));
return logoutFilter;
}
#Inject
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(casAuthenticationProvider());
}
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring()
.antMatchers("/scripts/**/*.{js,html}")
.antMatchers("/bower_components/**")
.antMatchers("/i18n/**")
.antMatchers("/assets/**")
.antMatchers("/swagger-ui/index.html")
.antMatchers("/test/**")
.antMatchers("/h2-console/**");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterAfter(new CsrfCookieGeneratorFilter(), CsrfFilter.class)
.addFilterBefore(casAuthenticationFilter(), BasicAuthenticationFilter.class)
.addFilterBefore(singleSignOutFilter(), CasAuthenticationFilter.class)
.addFilterBefore(requestCasGlobalLogoutFilter(), LogoutFilter.class)
.exceptionHandling()
.authenticationEntryPoint(casAuthenticationEntryPoint())
// .and()
// .rememberMe()
// .rememberMeServices(rememberMeServices)
// .key(env.getProperty("jhipster.security.rememberme.key"))
// .and()
// .formLogin()
// .loginProcessingUrl("/api/authentication")
// .successHandler(ajaxAuthenticationSuccessHandler)
// .failureHandler(ajaxAuthenticationFailureHandler)
// .usernameParameter("j_username")
// .passwordParameter("j_password")
// .permitAll()
.and()
.logout()
.logoutUrl("/api/logout")
.logoutSuccessHandler(ajaxLogoutSuccessHandler)
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
.permitAll()
.and()
.headers()
.frameOptions()
.disable()
.and()
.authorizeRequests()
.antMatchers("/app/**").authenticated()
.antMatchers("/api/register").permitAll()
.antMatchers("/api/activate").permitAll()
.antMatchers("/api/authenticate").permitAll()
.antMatchers("/api/account/reset_password/init").permitAll()
.antMatchers("/api/account/reset_password/finish").permitAll()
.antMatchers("/api/logs/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/api/audits/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/api/**").authenticated()
.antMatchers("/metrics/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/health/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/trace/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/dump/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/shutdown/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/beans/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/configprops/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/info/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/autoconfig/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/env/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/trace/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/mappings/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/liquibase/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/v2/api-docs/**").permitAll()
.antMatchers("/configuration/security").permitAll()
.antMatchers("/configuration/ui").permitAll()
.antMatchers("/swagger-ui/index.html").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/protected/**").authenticated();
}
#EnableGlobalMethodSecurity(prePostEnabled = true, jsr250Enabled = true)
private static class GlobalSecurityConfiguration extends GlobalMethodSecurityConfiguration {
public GlobalSecurityConfiguration() {
super();
}
}
}
Jhipste doesn't know that you are authenticated, thats the reason it does not let you browse secure pages. If you look closely in your principal.service.js and auth.service.js, they are calling the api/account rest service in AccountResource, and based on this angular determins if you are authenticated or not. So that means we need to return an User from this rest service.
Once you logged in with cas the current user info can be obtained from the AccounResource Rest service. Below is my AccountResource
/**
* GET /account -> get the current user.
*/
#RequestMapping(value = "/account",
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
#Timed
public ResponseEntity<UserDTO> getAccount() {
UserDetails userDetails = (UserDetails)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
List<String> roles = new ArrayList<>();
for (GrantedAuthority authority : userDetails.getGrantedAuthorities()) {
roles.add(authority.getAuthorigy());
}
return new ResponseEntity<>(
new UserDTO(
user.getLogin(),
null,
null,
null,
null,
null,
roles),
HttpStatus.OK);
}
As you can see above, i am getting authentication(casauthentication) token from security context. I have refactored my AccountResource to remove all the reference to user repository as i am no longer using the database to store user info.
Hope this helps.