17 Case Practice Implementing Single Sign on Based on Spring Security and Oauth2

17 Case Practice Implementing Single Sign-On Based on Spring Security and OAuth2 #

Single Sign-On (SSO) is a common problem we often face when designing and implementing web systems. It allows users to use a set of credentials to log in to multiple independent web applications that need to maintain a unified login state. The implementation of SSO requires specific technologies and frameworks, and Spring Security provides its solution. This lesson will build SSO based on the OAuth2 protocol.

What is Single Sign-On? #

Rather than being a technology system, SSO is an application scenario. Therefore, it is necessary to first understand the relationship between SSO and various technology systems introduced in this column.

Single Sign-On and the OAuth2 Protocol #

Suppose there are two independent systems, A and B, that trust each other and are managed and maintained through a centralized SSO system. Whether accessing system A or system B, when a user logs in once on the identity authentication server, they can obtain the permissions to access the other system. This process is fully automated. SSO achieves this goal by implementing a centralized login system, which handles user authentication and shares that authentication information with other applications.

At this point, you may wonder why we need to implement SSO. The reason is simple because it provides many advantages. Let’s analyze them in detail.

  • First, with SSO, we can ensure that the system is more secure. We only need one centralized server to manage user identities, rather than extending user credentials to each service. This reduces the dimensions that can be attacked.
  • Second, it is cumbersome for users to continuously enter usernames and passwords to access different services. SSO combines different services so that users can navigate seamlessly between services, thereby improving user experience.
  • At the same time, SSO can also help us better understand customers, as we have a single view of customer information and can build user profiles better.

So, how to build SSO? Different companies may have different practices. Choosing Spring Security and the OAuth2 protocol is a good choice because the implementation process is straightforward. Although OAuth2 was initially used to allow users to authorize third-party applications to access their resources, that is, its goal is not specifically to implement SSO, we can use its functional features to achieve single sign-on in a disguised manner. This requires using the authorization code mode of the OAuth2 four authorization modes. For more information about the OAuth2 protocol and the authorization code mode, you can refer to the previous lessons on “Open Protocol: What Problems Does the OAuth2 Protocol Solve?” for review. At the same time, when implementing SSO using the OAuth2 protocol, we will also use JWT to generate and manage tokens. Regarding JWT, you can also review the content of the lesson on “Token Extension: How to Use JWT to Implement Customized Tokens?”.

Workflow of Single Sign-On #

Before providing the implementation solution, let’s first expand on the workflow of SSO to understand the design concepts behind a typical SSO system. The diagram below describes the SSO process. We have two applications, App1 and App2, as well as a centralized SSO server.

17.png

SSO Workflow Diagram

Combining the diagram above, let’s first look at the workflow for App1.

  • The user accesses App1 for the first time. Since the user is not logged in, they are redirected to the SSO server.
  • The user enters their credentials on the login page provided by the SSO server. The SSO server verifies the credentials and generates an SSO Token. The SSO server saves this token in a cookie for the user to use for subsequent logins.
  • The SSO server redirects the user to App1. The SSO Token is included as a query parameter in the redirection URL.
  • App1 saves the token in its cookie and changes the current interaction to that of a logged-in user. App1 can query the SSO server or use the token to obtain user-related information. We know that JWT can be customized and extended, so we can use JWT to transfer user information at this time.

Now, let’s take a look at the workflow when the same user attempts to access App2.

  • Since the application can only access cookies from the same source, it does not know that the user is already logged into App2. Therefore, the user is redirected to the SSO server again.
  • The SSO server discovers that the user has already set a cookie, so it immediately redirects the user to App2 and attaches the SSO Token as a query parameter in the URL.
  • App2 also stores the token in its cookie and changes its interaction to that of a logged-in user.

After the entire process is completed, the user’s browser will have three cookies set, one each for the App1, App2, and SSO Server domains.

Regarding the above process, there are various implementation solutions and tools in the industry, including Facebook Connect, OpenID Connect, CAS, Kerberos, SAML, etc. We do not intend to go into detail on these specific tools but instead focus on building SSO server-side and client-side components from scratch using the technologies we have learned so far.

Implementing the SSO Server-Side #

The core work of implementing the SSO server-side with Spring Security is to use a series of familiar configuration systems to configure basic authentication and authorization information and integrate it with the OAuth2 protocol.

Configuring Basic Authentication and Authorization Information #

We also implement custom authentication and authorization information configuration by inheriting the WebSecurityConfigurerAdapter class. This process is relatively simple, and the complete code is shown below:

@Configuration

public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {



    @Override

    protected void configure(AuthenticationManagerBuilder auth) throws Exception {

        auth.userDetailsService(userDetailsServiceBean()).passwordEncoder(passwordEncoder());

    }



    @Override

    public void configure(WebSecurity web) throws Exception {

        web.ignoring().antMatchers("/assets/**", "/css/**", "/images/**");

    }



    @Override

    protected void configure(HttpSecurity http) throws Exception {

        http.formLogin()

                .loginPage("/login")

                .and()

                .authorizeRequests()

                .antMatchers("/login").permitAll()

                .anyRequest()

                .authenticated()

                .and().csrf().disable().cors();

} }

@Bean
@Override
public UserDetailsService userDetailsServiceBean() {
    Collection<UserDetails> users = buildUsers();

    return new InMemoryUserDetailsManager(users);
}

private Collection<UserDetails> buildUsers() {
    String password = passwordEncoder().encode("12345");

    List<UserDetails> users = new ArrayList<>();

    UserDetails user_admin = User.withUsername("admin").password(password).authorities("ADMIN", "USER").build();

    users.add(user_admin);

    return users;
}

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
}

}

In the above code, we have used various features of Spring Security related to authentication, authorization, password management, CSRF, and CORS. We have specified the login page URL on the SSO server using the loginPage() method, and initialized an “admin” user for login purposes.

Configuring the OAuth2 Authorization Server #

Next, we create an AuthorizationServerConfiguration class that extends AuthorizationServerConfigurerAdapter. Note that we need to add the @EnableAuthorizationServer annotation on this class, as shown below:

@EnableAuthorizationServer @Configuration public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {

The main task of configuring the OAuth2 Authorization Server is specifying the clients that need to participate in SSO. In the “Authorization Framework: How to Integrate OAuth2 Protocol in Microservices Architecture?” course, we introduced the ClientDetails interface in Spring Security to describe client details. We also discussed the ClientDetailsService for managing ClientDetails. Based on the ClientDetailsService, we can customize the process of creating ClientDetails. The example code is shown below:

@Bean public ClientDetailsService inMemoryClientDetailsService() throws Exception { return new InMemoryClientDetailsServiceBuilder() // Create client “app1” .withClient(“app1”) .secret(passwordEncoder.encode(“app1_secret”)) .scopes(“all”) .authorizedGrantTypes(“authorization_code”, “refresh_token”) .redirectUris(“http://localhost:8080/app1/login”) .accessTokenValiditySeconds(7200) .autoApprove(true)

.and()

// Create app2 client

.withClient("app2")

.secret(passwordEncoder.encode("app2_secret"))

.scopes("all")

.authorizedGrantTypes("authorization_code", "refresh_token")

.redirectUris("http://localhost:8090/app2/login")

.accessTokenValiditySeconds(7200)

.autoApprove(true)

.and()

.build();

}

Here, we use InMemoryClientDetailsServiceBuilder to create a ClientDetailsService based on in-memory storage. Then, we create two ClientDetails objects corresponding to app1 and app2. Please note that the authorizedGrantTypes specified here is “authorization_code”, which represents the authorization code grant type.

In addition, we also need to add JWT-related settings in the AuthorizationServerConfiguration class:

@Override

public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {

    endpoints.accessTokenConverter(jwtAccessTokenConverter())

            .tokenStore(jwtTokenStore());

}

 
@Bean

public JwtTokenStore jwtTokenStore() {

    return new JwtTokenStore(jwtAccessTokenConverter());

}
 
@Bean

public JwtAccessTokenConverter jwtAccessTokenConverter() {

    JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();

    jwtAccessTokenConverter.setSigningKey("123456");

    return jwtAccessTokenConverter;

}

Here, we use the same configuration methods mentioned in the lesson “Token Extensions: How to Customize Tokens with JWT” to configure JWT-related settings.

Implementing SSO Client #

After discussing the configuration of the SSO server, let’s now discuss the implementation process of the client. In the client, we also create a WebSecurityConfiguration class which extends WebSecurityConfigurerAdapter to set up the authentication and authorization mechanism as shown below:

@EnableOAuth2Sso

@Configuration

public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
 

@Override

public void configure(WebSecurity web) throws Exception {

    super.configure(web);

}
 

@Override

protected void configure(HttpSecurity http) throws Exception {

    http.logout()

            .and()

            .authorizeRequests()

            .anyRequest().authenticated()
@Configuration
@EnableWebSecurity
@EnableOAuth2Sso
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .antMatcher("/**")
                .authorizeRequests()
                .antMatchers("/", "/login**")
                .permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .logout()
                .logoutSuccessUrl("/")
                .permitAll()
                .and()
                .csrf()
                .disable();
    }

}

The only thing that needs to be emphasized here is the @EnableOAuth2Sso annotation, which is the entry point for the automatic configuration related to single sign-on. The definition is as follows:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@EnableOAuth2Client
@EnableConfigurationProperties(OAuth2SsoProperties.class)
@Import({ OAuth2SsoDefaultConfiguration.class, OAuth2SsoCustomConfiguration.class,
        ResourceServerTokenServicesConfiguration.class })
public @interface EnableOAuth2Sso {
 
}

In the @EnableOAuth2Sso annotation, we can find the @EnableOAuth2Client annotation, which indicates that the OAuth2Client client is enabled. At the same time, OAuth2SsoDefaultConfiguration and OAuth2SsoCustomConfiguration are used to configure OAuth2-based SSO behaviors, and ResourceServerTokenServicesConfiguration configures token-related operations based on JWT.

Next, we add the following configuration items to the application.yml configuration file of the app1 client:

server:
  port: 8080
  servlet:
    context-path: /app1

Here, we use the server.servlet.context-path configuration item to set the application’s context path, which is equivalent to adding a prefix to the complete URL address. This means that the original address that accessed <http://localhost:8080/login> will become <http://localhost:8080/app1/login>, which is a common technique when using SSO.

Then, we add the following configuration items to the configuration file:

security:
  oauth2:
    client:
      client-id: app1
      client-secret: app1_secret
      access-token-uri: http://localhost:8888/oauth/token
      user-authorization-uri: http://localhost:8888/oauth/authorize
    resource:
      jwt:
        key-uri: http://localhost:8888/oauth/token_key

These configuration items are dedicated to the OAuth2 protocol. We can see the “client” configuration section used to set client information. In addition to the client ID and password, it also specifies the “access-token-uri” address used to obtain the token and the “user-authorization-uri” address used for authorization, both of which should point to the SSO server address created earlier.

On the other hand, once the “security.oauth2.resource.jwt” configuration item is added to the configuration file, the validation of the token will use JwtTokenStore, which corresponds to the JwtTokenStore created by the SSO server.

So far, we have created an SSO client application app1, and the process of creating app2 is exactly the same, which will not be discussed here. You can refer to the complete code at https://github.com/lagouEdAnna/SpringSecurity-jianxiang/tree/main/SpringSsoDemo.

Demonstration #

Finally, let’s demonstrate the entire single sign-on process. Start the SSO server, app1, and app2 in order, and then access the app1 address http://localhost:8080/app1/system/profile in the browser. The browser will be redirected to the SSO server login page.

Please note that if we open the “Network” tab of the browser and view its access paths when accessing the above address, we can see that it indeed first redirects to the login page of app1 (http://localhost:8080/app1/login) and then redirects to the SSO server. Since the user is not logged in, it will be redirected to the login page of the SSO server (http://localhost:8888/login). The entire request redirection process is shown in the following figure:

17-2.png

Network request redirection flow diagram when accessing app1 in an unauthenticated state

After successfully entering the correct username and password on the login page of the SSO server, we can authenticate successfully. Now let’s take a look at the network request process, shown below:

17-3png.png

Network request redirection flow diagram during the app1 login process

As can be seen, after successfully logging in, the authorization system redirects to the callback URL configured in app1 (http://localhost:8080/app1/login). At the same time, we also find two new parameters, code and state, in the request URL. The app1 client will use this code to access the /oauth/token interface of the SSO server to request a token. After a successful request, it will be redirected to the callback URL configured in app1.

Now, if you visit app2, similar to the first time you accessed app1, the browser first redirects to the login page of app2, and then redirects to the authorization link of the SSO server, and finally redirects directly to the login page of app2. The difference is that this time there is no need to redirect to the SSO server for login, but to access the authorization interface of the SSO server successfully and redirect to the callback path of app2 with the code parameter. Then app2 uses this code to access the /oauth/token interface to obtain the token, so it can access protected resources normally.

Summary and Preview #

This lesson is a relatively independent part of the content, focusing on the design and implementation of the single sign-on (SSO) scenario that is often encountered in daily development. We can treat each independent system as a client and implement SSO based on the OAuth2 protocol. In addition, this lesson provides a detailed introduction to how to build an SSO server-side and client-side components, as well as the interaction process between the two.

Here’s a question for you to think about: can you describe the overall workflow of SSO based on the OAuth2 protocol?

After introducing the OAuth2 protocol and its application scenarios, we will introduce a completely new topic, reactive programming, which is a technology trend. In the next lesson, we will discuss how to add reactive programming features to Spring Security.