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 isn’t asked to log in again for every single action. Stateless authentication presents a way to persist the authentication data on the client side between requests.
Unlike server-side authentication storage solutions — which commonly rely on sessions — stateless authentication doesn’t require you to track sessions on the server.
Using stateless authentication brings benefits in the following use cases:
- Horizontal Scaling of the Backend
-
This helps to avoid the complexity of managing shared or sticky sessions among multiple backend servers.
- Seamless Deployment
-
Backend servers can be restarted without logging out users, and without the need 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.
Hilla provides stateless authentication support in applications using Spring Security. 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 illustrate the steps to enable stateless authentication in a Hilla application that uses Spring Security. They involve adding dependencies, configuring Spring Security, the JWT authentication principal, and verification.
Dependencies
Add the following dependencies to the project’s pom.xml
file:
<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>
Configure Spring Security
Modify the Spring Security configuration and use the VaadinWebSecurity.setStatelessAuthentication()
method to set up stateless authentication, as follows:
@EnableWebSecurity
@Configuration
public class SecurityConfig extends VaadinWebSecurity {
@Value("${my.app.auth.secret}")
private String authSecret;
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
// Disable creating and using sessions in Spring Security
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// Register your login view to the view access checker mechanism
setLoginView(http, "/login");
// Enable stateless authentication
setStatelessAuthentication(http,
new SecretKeySpec(Base64.getDecoder().decode(authSecret), // (1)
JwsAlgorithms.HS256), // (2)
"com.example.application" // (3)
);
}
}
-
Sets the secret key that’s used for signing and verifying the JWT.
-
Sets the JWT signature algorithm. The key length should match the algorithm chosen. For example, the HS256 algorithm used here requires a 32-byte secret key.
-
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 in the development, staging, and production environments. Don’t commit the secret key into the repository. |
The security configuration given here 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 instruct Git to ignore the directory. Here’s how you might do that:
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 manual.
Handle JWT Authentication Principal
When using stateless authentication, the SecurityContext.getAuthentication().getPrincipal()
call returns a Jwt
instance that contains only the username and roles. In your application, you need to verify that the reference returned by Authentication.getPrincipal()
is a Jwt
instance.
In applications that use username-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 via 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();
}
}
JWT Expiration
By default, the JWT and cookies expire thirty minutes after the last server request. You can customize the expiration period by using an additional duration argument for the configuration method like so:
@EnableWebSecurity
@Configuration
public class SecurityConfig extends VaadinWebSecurity {
@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
);
}
}