1. Overview

In this tutorial, we'll discuss how to implement SSO – Single Sign On – using Spring Security OAuth and Spring Boot, using Keycloak as the Authorization Server.

We'll use 4 separate applications:

  • An Authorization Server – which is the central authentication mechanism
  • A Resource Server – the provider of Foos
  • Two Client Applications – the applications using SSO

Very simply put, when a user tries to access a resource via one Client app, they'll be redirected to authenticate first, through the Authorization Server. Keycloak will sign the user in, and while still being logged in the first app, if the second Client app is accessed using the same browser, the user will not need to enter their credentials again.

We're going to use the Authorization Code grant type out of OAuth2 to drive the delegation of authentication.

We'll use the OAuth stack in Spring Security 5. If you want to use the Spring Security OAuth legacy stack, have a look at this previous article: Simple Single Sign-On with Spring Security OAuth2 (legacy stack)

As per the migration guide:

Spring Security refers to this feature as OAuth 2.0 Login while Spring Security OAuth refers to it as SSO

Further reading:

Spring Security 5 – OAuth2 Login

Learn how to authenticate users with Facebook, Google or other credentials using OAuth2 in Spring Security 5.

New in Spring Security OAuth2 – Verify Claims

Quick practical intro to the new Claim verification support in Spring Security OAuth.

A Secondary Facebook Login with Spring Social

A quick look at implementing a Facebook driven authentication next to a standard form-login Spring app.

Alright, let's jump right in.

2. The Authorization Server

Previously, the Spring Security OAuth stack offered the possibility of setting up an Authorization Server as a Spring Application.

However, the OAuth stack has been deprecated by Spring and now we'll be using Keycloak as our Authorization Server.

So this time, we'll set up our Authorization Server as an embedded Keycloak server in a Spring Boot app.

In our pre-configuration, we'll define two clients, ssoClient-1 and ssoClient-2, one for each Client Application.

3. The Resource Server

Next, we need a Resource Server, or the REST API which will provide us the Foos our Client App will consume.

It's essentially the same as we used for our Angular Client Apps previously.

4. The Client Applications

Now let's look at our Thymeleaf Client Application; we'll, of course, use Spring Boot to minimize the configuration.

Do keep in mind that we'll need to have 2 of these to demonstrate Single Sign-On functionality.

4.1. Maven Dependencies

First, we will need the following dependencies in our pom.xml:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webflux</artifactId>
</dependency>
<dependency>
    <groupId>io.projectreactor.netty</groupId>
    <artifactId>reactor-netty</artifactId>
</dependency>

To include all the client support we'll require, including security, we just need to add spring-boot-starter-oauth2-client. Also, since the old RestTemplate is going to be deprecated, we're going to use WebClient, and that's why we added spring-webflux and reactor-netty.

4.2. Security Configuration

Next, the most important part, the security configuration of our first client application:

@EnableWebSecurity
public class UiSecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.antMatcher("/**")
          .authorizeRequests()
          .antMatchers("/")
          .permitAll()
          .anyRequest()
          .authenticated()
          .and()
          .oauth2Login();
    }

    @Bean
    WebClient webClient(ClientRegistrationRepository clientRegistrationRepository, 
      OAuth2AuthorizedClientRepository authorizedClientRepository) {
        ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2 = 
          new ServletOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrationRepository, 
          authorizedClientRepository);
        oauth2.setDefaultOAuth2AuthorizedClient(true);
        return WebClient.builder().apply(oauth2.oauth2Configuration()).build();
    }
}

The core part of this configuration is the oauth2Login() method, which is used to enable Spring Security’s OAuth 2.0 Login support. Since we're using Keycloak, which is by default a single sign-on solution for web apps and RESTful web services, we do not need to add any further configuration for SSO.

Finally, we also defined a WebClient bean to act as a simple HTTP Client to handle requests to be sent to our Resource Server.

And here's the application.yml:

spring:
  security:
    oauth2:
      client:
        registration:
          custom:
            client-id: ssoClient-1
            client-secret: ssoClientSecret-1
            scope: read,write
            authorization-grant-type: authorization_code
            redirect-uri: http://localhost:8082/ui-one/login/oauth2/code/custom
        provider:
          custom:
            authorization-uri: http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/auth
            token-uri: http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/token
            user-info-uri: http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/userinfo
            user-name-attribute: preferred_username
  thymeleaf:
    cache: false
    
server: 
  port: 8082
  servlet: 
    context-path: /ui-one

resourceserver:
  api:
    project:
      url: http://localhost:8081/sso-resource-server/api/foos/        

Here, spring.security.oauth2.client.registration is the root namespace for registering a client. We defined a client with registration id custom. Then we defined its client-id, client-secret, scope, authorization-grant-type and redirect-uri, which of course, should be the same as that defined for our Authorization Server.

After that, we defined our service provider or the Authorization Server, again with the same id of custom, and listed down its different URI's for Spring Security to use. That's all we need to define, and the framework does the entire logging-in process, including redirection to Keycloak, seamlessly for us.

Also note that, in our example here, we rolled out our Authorization Server, but of course we can also use other, third-party providers such as Facebook or GitHub.

4.3. The Controller

Let’s now implement our controller in the Client App to ask for Foos from our Resource Server:

@Controller
public class FooClientController {

    @Value("${resourceserver.api.url}")
    private String fooApiUrl;

    @Autowired
    private WebClient webClient;

    @GetMapping("/foos")
    public String getFoos(Model model) {
        List<FooModel> foos = this.webClient.get()
            .uri(fooApiUrl)
            .retrieve()
            .bodyToMono(new ParameterizedTypeReference<List<FooModel>>() {
            })
            .block();
        model.addAttribute("foos", foos);
        return "foos";
    }
}

As we can see, we have only one method here that'll dish out the resources to the foos template. We did not have to add any code for login.

4.4. Front End

Now, let's take a look at the front-end configuration of our client application. We're not going to focus on that here, mainly because we already covered in on the site.

Our client application here has a very simple front-end; here's the index.html:

<a class="navbar-brand" th:href="@{/foos/}">Spring OAuth Client Thymeleaf - 1</a>
<label>Welcome !</label> <br /> <a th:href="@{/foos/}">Login</a>

And the foos.html:

<a class="navbar-brand" th:href="@{/foos/}">Spring OAuth Client Thymeleaf -1</a>
Hi, <span sec:authentication="name">preferred_username</span>   
    
<h1>All Foos:</h1>
<table>
  <thead>
    <tr>
      <td>ID</td>
      <td>Name</td>                    
    </tr>
  </thead>
  <tbody>
    <tr th:if="${foos.empty}">
      <td colspan="4">No foos</td>
    </tr>
    <tr th:each="foo : ${foos}">
      <td><span th:text="${foo.id}"> ID </span></td>
      <td><span th:text="${foo.name}"> Name </span></td>                    
    </tr>
  </tbody>
</table>

The foos.html page needs the users to be authenticated. If a non-authenticated user tries to access foos.html, they'll be redirected to Keycloak's login page first.

4.5. The Second Client Application

We'll configure a second application, Spring OAuth Client Thymeleaf -2 using another client_id ssoClient-2.

It'll mostly be the same as the first application we just described.

The application.yml will differ to include a different client_id, client_secret and redirect_uri in its spring.security.oauth2.client.registration:

spring:
  security:
    oauth2:
      client:
        registration:
          custom:
            client-id: ssoClient-2
            client-secret: ssoClientSecret-2
            scope: read,write
            authorization-grant-type: authorization_code
            redirect-uri: http://localhost:8084/ui-two/login/oauth2/code/custom

And, of course, we need to have a different server port for it as well, so that we can run them in parallel:

server: 
  port: 8084
  servlet: 
    context-path: /ui-two

Finally, we'll tweak the front end HTMLs to have a title as Spring OAuth Client Thymeleaf – 2 instead of – 1 so that we can distinguish between the two.

5. Testing SSO Behavior

To test SSO behavior, let's run our Applications.

We'll need all our 4 Boot Apps – the Authorization Server, the Resource Server and both Client Applications – to be up and running for this.

Now let's open up a browser, say Chrome, and log in to Client-1 using the credentials [email protected]/123. Next, in another window or tab, hit the URL for Client-2. On clicking the login button, we'll be redirected to the Foos page straightaway, bypassing the authentication step.

Similarly, if the user logs in to Client-2 first, they need not enter their username/password for Client-1.

6. Conclusion

In this tutorial, we focused on implementing Single Sign-On using Spring Security OAuth2 and Spring Boot using Keycloak as the identity provider.

As always, the full source code can be found over on GitHub.

Security bottom

I just announced the new Learn Spring Security course, including the full material focused on the new OAuth2 stack in Spring Security 5:

>> CHECK OUT THE COURSE
17 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Stefano Cazzola
Stefano Cazzola
3 years ago

Very good tutorial, as usual. But what about not using SpringBoot? I mean, it can be very convenient, I don’t deny that, but it also hides what goes on under the wood giving a sense of automagical (or informagical…) that personally makes me get lost. And it’s days now that I’m trying to have this very same scenarion working without Spring Boot

Grzegorz Piwowarek
Grzegorz Piwowarek
3 years ago

In this case, I would investigate what @EnableOAuth2Sso annotation actually does by looking what annotations it aggregates and then have a look at their implementations. After all, Spring Boot’s magic is simply default configuration classes that fire up when no other configs are present.

http://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/autoconfigure/security/oauth2/client/EnableOAuth2Sso.html

Stefano Cazzola
Stefano Cazzola
3 years ago

Thanks for the hint.

Jack
Jack
3 years ago

I have a question about `http.requestMatchers().antMatchers(“/login”, “/oauth/authorize”)`. When I include this it works fine, however I want to support both regular form login in addition to oAuth login. If I remove the above or change it to `”/**”` the sp gets an `org.springframework.security.oauth2.common.exceptions.InvalidTokenException`. Do you know how I can fix this?

Eugen Paraschiv
3 years ago
Reply to  Jack

Hey @disqus_tFKAIUkKXT:disqus – it sounds like you need to support multiple authentication providers – have a look at this writeup.
Of course that’s not the only option to implement multiple logins, as you’re describing, but it’s a good, standard way to go.

Jack
Jack
3 years ago

I may be wrong but does the above link show how to use different user data stores, LDAP, database etc. In my case I want the user to use normal form login (all endpoints secured) or acting as an idP by using oauth for another SP. In both scenarios the same login form should be used and the same authentication provider (a database).

Eugen Paraschiv
3 years ago
Reply to  Jack

Hey @disqus_tFKAIUkKXT:disqus – so the focus of that writeup is to show how you can use multiple auth providers. But, it sounds like that might not be your exact scenario.
So, in order for me to understand, you’ll have to explain your scenario in a lot more detail. Feel free to do that over email or simply in a StackOverflow question (follow up with the link here).

Cheers,
Eugen.

Jack
Jack
3 years ago

Hi @baeldung:disqus . Actually I solved the problem. I simply needed to add my apis to the matcher. (along with other stuff I wanted secured) http.requestMatchers().antMatchers("/login", "/oauth/authorize", "/apis/*") The key was the following statement on the documentation http://projects.spring.io/spring-security-oauth/docs/oauth2.html “Note: if your Authorization Server is also a Resource Server then there is another security filter chain with lower priority controlling the API resources. Fo those requests to be protected by access tokens you need their paths not to be matched by the ones in the main user-facing filter chain, so be sure to include a request matcher that picks out only… Read more »

Povilas
Povilas
3 years ago

I don’t understand… I log in and access the securedPage.html successfully, when I open other browser and application asks me to log in again. I thought this is called “single sign on” and the second time client application should not ask me to log in in the same computer. Am I missing something?

Eugen Paraschiv
3 years ago
Reply to  Povilas

So, this is a very simple example, which is why there’s no automatic redirect. What you’re seeing is the standard login page. But – if, on your second visit (when you’re already logged in) – you try to log in again, you’ll noticed you’re not promoted to log in, you’re simply redirected to where you need to go.
Hope that clears things up.
Cheers,
Eugen.

Ruslan Stelmachenko
Ruslan Stelmachenko
3 years ago
Reply to  Povilas

No, you understand SSO incorrectly. Single Sign On in web-page context is not means your computer remember you. It means your browser remember you and if you then log in into another application using same authorization server (and the same browser!), you will be logged in automatically (not asking login and password again). But it not means your another browser also logs you in. Another browser knows nothing about cookies of your first browser. And cookies is how it works. When your frist restricted resource redirects you to authorization server, and then you log in successfully, this authorization server creates… Read more »

Dinesh
Dinesh
3 years ago

Hi,
I am newbie for spring security & OAuth2. I download the project & run on my local. But I am unable to understand control flow of the application. For me thr is no login page when click on the login on the ui ‘http://localhost:8082/ui/’. Error page appear when click on the Longin button.

Please let me know where I am doing wrong.

Whitelabel Error Page

This application has no explicit mapping for /error, so you are seeing this as a fallback.
Sun Aug 13 23:12:29 IST 2017
There was an unexpected error (type=Forbidden, status=403).
Access Denied

Grzegorz Piwowarek
3 years ago
Reply to  Dinesh

Dinesh, it looks like it’s our fault – please pull the newest changes and see if it works now

Diego
Diego
3 years ago
Reply to  Dinesh

I have the same problem…. someone can solve this problem

Grzegorz Piwowarek
3 years ago
Reply to  Diego

I believe it’s fixed now – check the latest version of the code and article

Mike
Mike
3 years ago

I am getting following error after login page.
This application has no explicit mapping for /error, so you are seeing this as a fallback.
Wed Aug 16 11:54:43 CDT 2017
There was an unexpected error (type=Forbidden, status=403).
Access Denied

Once login I am expecting to see secured page however not getting that.
Any Idea what it could be ?

I have all three applications running. Is there anything else I need to have ?

Thanks
Mike

Grzegorz Piwowarek
3 years ago
Reply to  Mike

Mike, I believe we might have misplaced one configuration during the refactor. Please pull the newest changes and see if it helps

Comments are closed on this article!