Following on from the previous post about documenting MicroServices with Swagger, we also wanted to have a uniform authorisation/authentication model for access to our services.
Our basic requirements were as follows:
- They must support client-side authorisation (e.g. via Javascript calls in browsers)
- They should have a single authorisation point
- Session authorisation timeout must be controllable
- They should be multi-domain ready (e.g. authentication from <user>@ed.ac.uk or <another-user>@another.ac.uk
After reviewing our options, OAuth2 was the obvious contender. We already have a web single sign-on solution called EASE which uses Cosign, so we need to use that for any web-based user authentication.
The remainder of this article shows how we went about setting up an OAuth2 service using Spring Boot.
Look before you leap
Firstly, I’m not going to pretend OAuth2 can immediately be understood with no prior reading. You need at least a basic grasp of what OAuth2 is, and how it can be used.
The main OAuth2 site is an invaluable resource, which links to the spec covering details of the main concepts of OAuth, such as Authorisation Grants, Clients, and Tokens.
Also for someone new to OAuth, this basic article is a good introduction to OAuth in general:
Spring Boot for OAuth2
The great thing is that Spring Boot has an OAuth2 server which is very easy to set up. I would read the Spring.io tutorial on SSO with OAuth2, especially the section on Creating an Authorisation server first. If you run through this and refer to the GitHub samples you should become comfortable with the basics in creating an Authorisation server.
Database setup
Firstly we change the default settings so that the OAuth2 application talks to a back end database. Make sure you set up a datasource as standard in your application.properties. Then in your @EnableAuthorizationServer configuration class, add the following to tell the Authorisation server to use JDBC for authorisation code, token store and client details:
@Autowired private DataSource dataSource; @Bean public TokenStore getTokenStore() { return new JdbcTokenStore(dataSource); } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.jdbc(dataSource).passwordEncoder(passwordEncoder); } @Bean protected AuthorizationCodeServices authorizationCodeServices() { return new JdbcAuthorizationCodeServices(dataSource); }
Integration with Single Sign-On
We use Cosign for Single Sign-On, so we then configured the OAuth2 server to expect the RemoteUser to be set when the authorise URL is called. Cosign on the application server will be set to protect that URL, so that only authenticated users can access.
In code, we then map in a RemoteUserAuthenticationFilter and PreAuthenticatedAuthenticationProvider which handle creating a basic user principal based on the RemoteUser in a LoginConfig @Configuration class:
@Configuration protected static class LoginConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.addFilterBefore(remoteUserAuthenticationFilter(), RequestHeaderAuthenticationFilter.class) .authenticationProvider( preauthAuthProvider()) .authorizeRequests().anyRequest().authenticated(); } @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider(preauthAuthProvider()); } @Bean public UserDetailsByNameServiceWrapper<PreAuthenticatedAuthenticationToken> userDetailsServiceWrapper() { UserDetailsByNameServiceWrapper<PreAuthenticatedAuthenticationToken> wrapper = new UserDetailsByNameServiceWrapper<PreAuthenticatedAuthenticationToken>(); wrapper.setUserDetailsService(new RemoteUserDetailsService()); return wrapper; } @Bean public PreAuthenticatedAuthenticationProvider preauthAuthProvider() { PreAuthenticatedAuthenticationProvider preauthAuthProvider = new PreAuthenticatedAuthenticationProvider(); preauthAuthProvider.setPreAuthenticatedUserDetailsService(userDetailsServiceWrapper()); return preauthAuthProvider; } @Bean public RemoteUserAuthenticationFilter remoteUserAuthenticationFilter() throws Exception { RemoteUserAuthenticationFilter filter = new RemoteUserAuthenticationFilter(); filter.setAuthenticationManager(authenticationManager()); return filter; } @Autowired private AuthenticationManager authenticationManager; }
Custom authorise screen
Finally, we wanted to make the authorise screen look more like our standard UI here. I wrote a post recently about building a WebJar for our Edinburgh GEL, so we add that to the pom file, along with some standard WebJars which provide jQuery.
Secondly, we override the standard template with one of our own. Firstly define a WebMvcConfigurerAdapter which overrides the specific view controller handling access confirmations:
@Controller @Configuration public class ConfigurerAdapter extends WebMvcConfigurerAdapter{ @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/oauth/confirm_access").setViewName("authorize"); } }
Then you can add a template in src/main/resources/templates/authorize.ftl which implements the custom look/feel you need, using the exposed model variables. I’ve include a snippet of the form we use here so you can see the exposed variables and how we’re handling them in FreeMarker:
<form id="confirmationForm" name="confirmationForm" action="authorize" method="post"> <input name="user_oauth_approval" value="true" type="hidden" /> <p> Do you authorize "${authorizationRequest.clientId}" at "${authorizationRequest.redirectUri}" to access your protected resources: <ul> <#list authorizationRequest.scope as scope> <li> <div class="form-group"> ${scope} : <input type="radio" name="scope.${scope}" value="true" checked> Approve <input type="radio" name="scope.${scope}" value="false"> Deny </div> </li> </#list> </ul> </p> <button class="btn btn-primary" type="submit">Approve</button> </form>
Our end result looks like this:
Next steps
Next steps will be to give a more rich set of client information to the authorisation screen, so the user sees a more friendly authorisation page which tells them more about which application is asking for which dataset.
And finally
In the next article I’ll describe how to configure a Swagger documented Spring Boot Microservice to support OAuth2.