Spring Boot offers an easier way to create new web applications or web services. The Security
module in the Spring framework enables us to plug in different authentication mechanisms. In some cases, we needed to provide multiple authentication mechanisms for our web service. These authentication mechanisms can be standard or custom.
We had a similar requirement while working on an in-house project to develop a web service for distributing and renewing Kerberos key tabs. The project is named Kite
, and it is a web service built on Spring Boot. We initially added SPNEGO to authenticate users of our Kite
service.
Enabling SPNEGO Authentication using Spring Security
To enable security and add SPNEGO, we needed to make changes to our pom.xml
. The relevant POM changes are shown here:
<dependencies>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>4.0.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>4.0.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security.kerberos</groupId>
<artifactId>spring-security-kerberos-web</artifactId>
<version>1.0.1.RELEASE</version>
</dependency>
</dependencies>
We also needed to add Java code to configure SPENGO authentication. The relevant parts of the Java code related to hooking up SPENGO authentication are shown below:
@Configuration @EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling()
.authenticationEntryPoint(spnegoEntryPoint())
.and().authorizeRequests().anyRequest().authenticated()
.and().formLogin().loginPage("/login").permitAll()
.and().logout().permitAll()
.and().addFilterBefore(spnegoAuthenticationProcessingFilter(authenticationManagerBean()), BasicAuthenticationFilter.class);
}
@Autowired
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(kerberosAuthenticationProvider())
.authenticationProvider(kerberosServiceAuthenticationProvider());
}
@Bean
public KerberosAuthenticationProvider kerberosAuthenticationProvider() {
KerberosAuthenticationProvider provider = new KerberosAuthenticationProvider();
SunJaasKerberosClient client = new SunJaasKerberosClient();
provider.setKerberosClient(client);
provider.setUserDetailsService(dummyUserDetailsService());
return provider;
}
@Bean
public SpnegoEntryPoint spnegoEntryPoint() {
return new SpnegoEntryPoint();
}
@Bean
public SpnegoAuthenticationProcessingFilter spnegoAuthenticationProcessingFilter( AuthenticationManager authenticationManager) {
SpnegoAuthenticationProcessingFilter filter = new SpnegoAuthenticationProcessingFilter();
filter.setAuthenticationManager(authenticationManager);
return filter;
}
@Bean
public KerberosServiceAuthenticationProvider kerberosServiceAuthenticationProvider() {
KerberosServiceAuthenticationProvider provider = new KerberosServiceAuthenticationProvider();
provider.setTicketValidator(sunJaasKerberosTicketValidator()); provider.setUserDetailsService(dummyUserDetailsService());
return provider;
}
@Bean
public SunJaasKerberosTicketValidator sunJaasKerberosTicketValidator() {
SunJaasKerberosTicketValidator ticketValidator = new SunJaasKerberosTicketValidator();
ticketValidator.setServicePrincipal(kiteConfiguration.getSpnegoPrincipal());
ticketValidator.setKeyTabLocation(new FileSystemResource(kiteConfiguration.getKeytab()));
ticketValidator.setDebug(true); return ticketValidator;
}
}
These POM and source code changes enable users to authenticate via Kerberos. Some of our users needed an alternate form of authentication, where the users present a one-time-use token. To hook up our custom token authentication, we took the following steps:
- Implement token authentication logic as
TokenAuthenticationFilter
by extendingAbstractAuthenticationProcessingFilter
. - Plug in
TokenAuthenticationFilter
viaFilterRegistrationBean
.
Custom TokenAuthenticationFilter
The custom token-based authentication filter extends AbstractAuthenticationProcessingFilter
. Here we override the doFilter
to implement our custom authentication logic.
public class TokenAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private static final String SECURITY_TOKEN_KEY = "token";
private TokenManager tokenManager;
public TokenAuthenticationFilter(TokenManager tm) {
super("/");
tokenManager = tm;
}
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
String token = request.getParameter(SECURITY_TOKEN_KEY);
if (token != null) {
Authentication authResult;
try {
authResult = attemptAuthentication(request, response, token);
if (authResult == null) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
} catch (AuthenticationException failed) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
try {
SecurityContextHolder.getContext().setAuthentication(authResult);
} catch (Exception e) {
logger.error(e.getMessage(), e);
if (e.getCause() instanceof AccessDeniedException) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
}
}
chain.doFilter(request, response);// return to others spring security filters
}
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response, String token)
throws AuthenticationException, IOException, ServletException {
AbstractAuthenticationToken userAuthenticationToken = authUserByToken(token);
if (userAuthenticationToken == null)
throw new AuthenticationServiceException(MessageFormat.format("Error | {0}", "Bad Token"));
return userAuthenticationToken;
}
private AbstractAuthenticationToken (String tokenRaw) {
AbstractAuthenticationToken authToken = null;
try {
String user = tokenManager.verifyAndExtractUser(tokenRaw);
if (user != null) {
user = user + "@REALM";
Principal securityUser = new SecurityUser(user);
return new PreAuthenticatedAuthenticationToken(securityUser, null, null);
}
} catch (Exception e) {
logger.error("Error during authUserByToken", e);
}
return authToken;
}
}
Note that in authUserByToken
, we created a SecurityUser
object, and this needs to be of the format user@REALM
.
Adding TokenAuthenticationFilter
To plug in the new authentication mechanism, we can use the FilterRegistrationBean
.
The code snippet below is added to the SecurityConfig
above:
@Bean
public FilterRegistrationBean filterRegistrationBean() {
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
TokenAuthenticationFilter tokenAuthenticationFilter = new TokenAuthenticationFilter();
filterRegistrationBean.setFilter(tokenAuthenticationFilter);
filterRegistrationBean.setEnabled(true);
return filterRegistrationBean;
}
With these code changes, we can add our custom authentication logic in Spring in addition to the existing authentication mechanisms.