Stateless Authentication With Spring Security

After a user logs in, the web application has to persist the authentication data over server requests, so that the user is not asked to log in again for every single action. Stateless authentication presents a way to persist the authentication data between the requests on the client-side.

Unlike server-side authentication storage solutions, which commonly rely on sessions, stateless authentication does not require keeping track of sessions on the server.

When to Use Stateless Authentication

Using stateless authentication gives benefits in the following use cases:

Horizontal scaling of the backend

Helps to avoid the complexity of managing shared or sticky sessions between multiple backend servers.

Seamless deployment

Backend servers can be restarted without logging out users, without requirement for session persistence.

Offline logout for client-side applications

Users can log off and have their authentication data destroyed on the client without requesting a logout from the server.

Fusion provides the stateless authentication support in applications using Spring Security. Under the hood, it uses a signed JSON Web Token (JWT) stored in a cookie pair: the token content in the JS-accessible cookie, and the signature in the HTTP-only cookie.

Enabling Stateless Authentication

The following examples describe the steps to enable stateless authentication in a Fusion application that uses Spring Security.

Step 1: Dependencies

Add the following dependencies to the project’s pom.xml:

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-resource-server</artifactId>
</dependency>

Step 2: Configure Spring Security

Modify the Spring Security configuration and use the VaadinWebSecurityConfigurerAdapter.setStatelessAuthentication() method to set up stateless authentication as follows:

@EnableWebSecurity
@Configuration
public class SecurityConfig extends VaadinWebSecurityConfigurerAdapter {

    @Value("${my.app.auth.secret}")
    private String authSecret;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);

        http.sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        setLoginView(http, "/login");

        setStatelessAuthentication(http,
                new SecretKeySpec(Base64.getDecoder().decode(authSecret), 1
                        JwsAlgorithms.HS256), 2
                "com.example.application" 3
        );
    }
}
  1. Sets the secret key that is used for signing and verifying the JWT.

  2. Sets the JWT signature algorithm. Note that the key length should match the algorithm chosen. For example, the HS256 algorithm used above requires a 32-byte secret key.

  3. Sets the issuer JWT claim — a string or a URL that identifies your application.

Caution
Secret Key Considerations
  • The secret key must be unique to your application.

  • Use different keys on the development, staging, and production environments.

  • Do not commit the secret key into the repository.

The security configuration above gets the secret key from the Base64-encoded my.app.auth.secret string property. You should configure the property value in your environment accordingly.

To avoid hard-coding the value and committing it into the repository, you can create a separate application.properties file in the config/local/ subdirectory and make Git ignore the directory.

For example:

mkdir -p config/local/

echo "
# Contains secrets that shouldn’t go into the repository
config/local/" >> .gitignore

echo "my.app.auth.secret=$(openssl rand -base64 32)" > config/local/application.properties

Spring Boot supports many ways of configuring properties. See the Externalized Configuration feature section in the Spring Boot Reference.

Step 3: Handle the JWT Authentication Principal

Note
The Authentication Principal is a JWT

When using stateless authentication, the SecurityContext.getAuthentication().getPrincipal() call returns a Jwt instance that only contains the username and roles.

In your application, you need to verify that the reference returned by Authentication.getPrincipal() and is a Jwt instance.

In applications that use a username and password authentication, you may need to access the full UserDetails instance for the current user. You can use the UserDetailsService to load the user details using the username from the JWT:

@Component
public class SecurityUtils  {

    @Autowired
    private UserDetailsService userDetailsService;

    public Optional<UserDetails> getAuthenticatedUser() {
        SecurityContext context = SecurityContextHolder.getContext();
        Object principal = context.getAuthentication().getPrincipal();
        if (principal instanceof Jwt) {
            String username = ((Jwt) principal).getSubject();
            return Optional.of(userDetailsService.loadUserByUsername(username));
        }
        // Anonymous or no authentication.
        return Optional.empty();
    }

}

Step 4: Verify

After the step 3, your application should be using stateless authentication.

To verify that:

  1. Start the development server and open your application.

  2. Log in.

  3. Restart the development server.

You should remain logged in after the restart.

JWT Expiration

By default, the JWT and cookies expire after 30 minutes after the last server request. You can customize the expiration period by using an additional duration argument for the configuration method.

For example:

@EnableWebSecurity
@Configuration
public class SecurityConfig extends VaadinWebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        ...
        setStatelessAuthentication(http,
            new SecretKeySpec(Base64.getDecoder().decode("..."),
                JwsAlgorithms.HS256),
            "com.example.application",
            3600 // The JWT lifetime in seconds
        );
    }
}