Spring Security makes it easy to implement OAuth2 as your protocol for authentication. In this article, we are going to implement an authentication server using Spring Security OAuth2. We are also going to implement a very basic client which will make use of the authentication server.



Prerequisites

The following is required in order to follow along the article.

Java 8

Maven (Gradle is fine too, but examples will use Maven)

Creating the projects

We are going to create two projects. The first one will be the authentication server, and the other one will be the client which will have a basic user interface and use the authentication server. It’s possible to have these both on the same server but I believe it’s easier to understand the examples if they are implemented separately, this also means that it will be much easier to switch to another authentication server in the future, for example, facebook or google.

Start by heading to Spring Initializr. I am creating a Maven application with Java and Spring Boot 1.5.9.

I uploaded the two projects to GitHub if you would like to inspect the final code; Authorization Server and Authorization Client.

Authentication Server

For the authentication add the following dependencies:

Cloud OAuth2

Web

I am also going to go ahead and add Couchbase as a dependency as it’s my preferable database, but you can, of course, use any database that you would like.

Authentication Client

For the authentication client you will want to have the following dependencies:

Cloud OAuth2

Web

Thymeleaf

Authentication Server

Configuring Spring Security OAuth2 Authentication Server

We are going to start with implementing the Spring Security OAuth2 Authentication Server.

Under a package named config create a class AuthorizationServerConfig .

package org.thecuriousdev.authserver.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; @Configuration @EnableAuthorizationServer public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { private AuthenticationManager authenticationManager; @Autowired public AuthorizationServerConfig(AuthenticationManager authenticationManager) { this.authenticationManager = authenticationManager; } @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { security.tokenKeyAccess("permitAll()") .checkTokenAccess("isAuthenticated()"); } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() .withClient("ClientId") .secret("secret") .authorizedGrantTypes("authorization_code") .scopes("user_info") .autoApprove(true); } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.authenticationManager(authenticationManager); } }

The annotation @EnableAuthorizationServer enables an AuthorizationEndpoint and a TokenEndpoint, and we also extend AuthorizationServerConfigurerAdapter to be able to override some configuration that we need. In configure(ClientDetailsServiceConfigurer) we specify that we want to store the necessary data for creating sessions in memory. We choose a pretty basic clientId and secret in the example, but if you are taking this into production then I recommend setting it to something more appropriate. People should not be able to guess this value. There are some different authorizedGrantTypes that we can specify, but authorization_code is considered a “classic” OAuth2 flow. If you are interested in reading about different grant types, then I recommend checking out Authorization Grant. Additionally, we configure autoApprove to be true in order to disable the user approval process and do a redirect directly.

ResourceServerConfig

Our authentication server is also going to act as a resource server and therefore we need to create that configuration.

package org.thecuriousdev.authserver.config; import org.springframework.beans.factory.annotation.Autowired; 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.WebSecurityConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; import org.thecuriousdev.authserver.service.CustomUserDetailsService; @Configuration @EnableResourceServer public class ResourceServerConfig extends WebSecurityConfigurerAdapter { @Autowired private AuthenticationManager authenticationManager; @Autowired private CustomUserDetailsService customUserDetailsService; @Autowired public ResourceServerConfig(AuthenticationManager authenticationManager, CustomUserDetailsService customUserDetailsService) { this.authenticationManager = authenticationManager; this.customUserDetailsService = customUserDetailsService; } @Override protected void configure(HttpSecurity http) throws Exception { http.requestMatchers() .antMatchers("/login", "/oauth/authorize") .and() .authorizeRequests() .anyRequest() .authenticated() .and() .formLogin() .permitAll(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.parentAuthenticationManager(authenticationManager) .userDetailsService(customUserDetailsService); } }

Pretty standard stuff, we declare two antMatchers “/login” and “/oauth/authorize”, and we also let Spring generate a login page that everyone is permitted to visit. For the AuthenticationManager we pass in a CustomUserDetailsService which is a class we created in order to make it possible to read the user from the database. The CustomUserDetailsService can make use of any database. The implementation is very dependant on which type of database that you use, but I will give you mine as an example and it should be very straightforward for you to retrieve the user from any database of your choice.

package org.thecuriousdev.authserver.service; import java.util.Arrays; import javax.annotation.PostConstruct; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import org.thecuriousdev.authserver.entity.User; import org.thecuriousdev.authserver.repository.UserRepository; import org.thecuriousdev.authserver.util.CustomUserDetails; @Service public class CustomUserDetailsService implements UserDetailsService { private UserRepository userRepository; @Autowired public CustomUserDetailsService(UserRepository userRepository) { this.userRepository = userRepository; } @PostConstruct public void test() { User user = new User(); user.setUsername("planevik"); user.setPassword("password"); user.setRoles(Arrays.asList("ADMIN", "USER")); if (!userRepository.findById("planevik").isPresent()) { userRepository.create(user); } } @Override public UserDetails loadUserByUsername(String id) throws UsernameNotFoundException { return new CustomUserDetails(userRepository.findById(id) .orElseThrow(() -> new UsernameNotFoundException("User not found"))); } }

Our class implements the UserDetailsService which forces us to implement the loadUserByUsername method where we have to return UserDetails for the user. Because of this, we also create a CustomerUserDetails class which can look like the following. If you are interested in implementing a Couchbase as your database, I recommend checking out my previous article, NOSQL with Couchbase: Getting Started, where I basically implement a very similar repository. Of course, you can also check out the GitHub source code for this article.

package org.thecuriousdev.authserver.util; import java.util.Collection; import java.util.stream.Collectors; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.thecuriousdev.authserver.entity.User; public class CustomUserDetails implements UserDetails { private String username; private String password; private Collection authorities; public CustomUserDetails(User user) { this.username = user.getUsername(); this.password = user.getPassword(); this.authorities = user.getRoles().stream() .map(SimpleGrantedAuthority::new) .collect(Collectors.toList()); } @Override public Collection getAuthorities() { return authorities; } @Override public String getPassword() { return password; } @Override public String getUsername() { return username; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }

Our class implements the UserDetails interface and implements the necessary methods.

We have a User entity which we create a convenient constructor for that will construct our CustomUserDetails class from it. We leave out the implementation of account expiration, locked and enabled by simply returning true.

Returning a resource

When a user has authenticated we want the resource server to return a resource so that we can check if it worked. We create a class ResourceController .

package org.thecuriousdev.authserver; import java.security.Principal; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/rest/resource") public class ResourceController { @GetMapping public Principal user(Principal principal) { return principal; } }

We return the principal which also makes it possible to inspect the name of the authenticated user.

Application configuration

The last thing we need is some application configuration. I created the application.yml under our resources folder.

server: port: 12050 context-path: /auth security.basic.enable: false

We specify the context-path /auth to make it more clear that it’s the authentication server.

Spring Security OAuth2 Authentication Client

We move on to our second server which will make use of the Spring Security OAuth2 authentication server that we have created. The client application will be a very basic UI application with an index page instructing people to press a link to log in where they get redirected to the login form of the authentication server to retrieve an authorization token. This token will be stored as a cookie and used when retrieving resources from the resource server.

OAuthConfig

In order to enable OAuth2 as single sign on we can use the @EnableOAuth2Sso. We also extend the WebSecurityConfigurerAdapter in order to override configure(HttpSecurity) .

package org.thecuriousdev.authclient.config; import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @Configuration @EnableOAuth2Sso public class OAuthConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.antMatcher("/**") .authorizeRequests() .antMatchers("/", "/login**") .permitAll() .anyRequest() .authenticated(); } }

Creating templates

We are going to create two templates, one index template which will be open to everyone. The second one, will be a secured template which requires a valid authentication token.

Under resources/templates, create the two following templates.

index.html

<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>Spring Security OAuth2 Example</title> </head> <body> <h1>Spring Security OAuth2 Example</h1> <a href="secure">Login via OAuth2 here</a> </body> </html>

secure.html

<html xmlns:th="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>Spring Security OAuth2 Example</title> </head> <body> <h1>Spring Security OAuth2 Example - Secured</h1> <p>Welcome <span th:text="${#authentication.name}"></span> to the secured page!</p> </body> </html>

Creating the controller for displaying the templates

Create a controller like the following in order to forward the request to the appropriate template that we created.

package org.thecuriousdev.authclient.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; @Controller public class BasicController { @GetMapping public String index() { return "index"; } @GetMapping("/secure") public String secure() { return "secure"; } }

Application configuration

Our Spring Security OAuth2 client application also needs some configuration.

server: port: 12051 context-path: /ui session: cookie: name: UISESSION security: basic: enabled: false oauth2: client: clientId: ClientId clientSecret: secret accessTokenUri: http://localhost:12050/auth/oauth/token userAuthorizationUri: http://localhost:12050/auth/oauth/authorize resource: userInfoUri: http://localhost:12050/auth/rest/resource spring: thymeleaf: cache: false

We assign a context-path of /ui so that it is more clear that it is the user interface. We also give the name “UISESSION” to our session cookie. In this cookie, the authentication token will be stored that we retrieved from the authentication server. In our OAuth2 client configuration, the clientId and secret have to match to what we created in the authentication server, and we also assign the URI for retrieving the token and on what URI the user should perform the authorization. Additionally, we specify what URL should be used to retrieve the user information. All of this has to match with what we created in the authentication server.

Thymeleaf-Extras-SpringSecurity4

In order to inspect the authoriation object (and use more nice funtionalities with Spring Security via Thymeleaf, the following dependency should be added to both the server and the client.

<dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity4</artifactId> </dependency>

Trying it out

If we now boot both servers up and hit the client application on http://localhost:12051/ui we should be presented with the index page where we can press to log in with OAuth2. Log in with OAuth2 should redirect us to http://localhost:12050/auth/login where we can enter our user credentials. Assuming that you have implemented a CustomerDetailService with a database of your choice, you should be able to login with the details that we created in our @PostConstruct in that service. Doing that redirects us back to our client application http://localhost:12051/ui/secure and we have now accessed a secured template.

If we inspect our cookies in the browser you should now see a UISESSION cookie that was created which stores the authentication token that we received from our Spring Security OAuth2 authentication server.

Final words

We have created an Authentication Server which implements both an AuthorizationServer and a ResourceServer. Additionally, we created an Authentication Client application which uses the Authentication Server as the security mechanism. The Authentication Server that we created is basically the same type of service that Facebook and Google provides (but of course a lot primitive). Because we developed a separate client application, it is also very easy to switch it to use an existing trusted authentication server (such as Facebook or Google).

Like this: Like Loading...