I have a backend of stateless REST services written in Java Spring Boot.
1- One of the services is "/Login". It receives a username and a password from my own web form (not the form generated by Shibboleth). In this service call, I want to contact Shibboleth (using OAuth2 or SAML or whatever) to authenticate this user and get a token. This should be done synchronously, as the service must return "true" or "false" to the caller. No redirection is permitted: either true of false.
2- This token will be included in the response sent back to the frontend and will be stored in the frontend. It will be resend back to the backend in the following calls to the other REST services (other than /Login). Those other calls must contact Shibboleth by sending the token to it. Shibboleth must return the information about the user, or an error if the token is not correct.
How can I implement points 1- and 2- manually, i.e. without using Spring Security? Just pur Java and maybe other third party libraries.
In my opinion, you can safely use Spring Security for this purpose. All it takes is to just configure it properly.
Let's start from the end, which involves querying Shibboleth after each request to see if the passed token is valid. What you need to do is simply define a class that extends BasicAuthenticationFilter
and use it in your Spring Security configuration. A very simplified implementation might look like this:
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Optional;
public class JWTAuthorizationFilter extends BasicAuthenticationFilter {
public JWTAuthorizationFilter(AuthenticationManager authManager) {
super(authManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {
Optional<CredentialsDto> credentials = getCredentialsFromRequest(request);
if (!credentials.isPresent() || !isUserAuthenticated(credentials.get())) {
chain.doFilter(request, response);
return;
}
UsernamePasswordAuthenticationToken authentication = getAuthenticationObject(credentials.get());
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
}
private Optional<CredentialsDto> getCredentialsFromRequest(HttpServletRequest request) {
//gets passed credentials from web form. Either from headers or body and returns custom dto object.
}
private boolean isUserAuthenticated(CredentialsDto credentials) {
//calls your Shibboleth service to check whether user is authenticated.
}
private UsernamePasswordAuthenticationToken getAuthenticationObject(CredentialsDto credentials) {
//returns org.springframework.security.authentication.UsernamePasswordAuthenticationToken object
//so Spring Security may know that user is authenticated.
return new UsernamePasswordAuthenticationToken(/*TODO*/, /*TODO*/, /*TODO*/);
}
}
Next, you use this class in your Spring Security configuration and set session management to Stateless, so Spring Security does not store sessions, and authentication is required with every single request:
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.config.http.SessionCreationPolicy;
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/login").permitAll()
.anyRequest().authenticated()
.and()
.addFilter(new JWTAuthorizationFilter(authenticationManager()))
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
In the end, all you need to do is define an endpoint that allows logging in and obtaining a token. This will be nothing more than a simple proxy to Shibboleth, that exposes your own form:
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class LoginController {
private final ShibbolethCient shibbolethClient;
public LoginController(ShibbolethCient shibbolethCient) {
this.shibbolethClient = shibbolethCient;
}
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {
ShibbolethTokenRequest tokenRequest = mapIntoTokenRequest(loginRequest);
String token = shibbolethClient.getToken(tokenRequest);
//returns ResponseEntity based on Shibboleth Client response.
}
private ShibbolethTokenRequest mapIntoTokenRequest(LoginRequest loginRequest) {
//returns dto for performing Shibboleth token request
}
}
I would definitely recommend doing this with Spring Security, because that's exactly what this tool is for. It does have a somewhat high barrier to entry, but it will definitely pay off, believe me.