Securing Java Application with Spring Security, JWT and?OpenID
Prerequisites
Motivation
The goal of this article is to host a collection of REST APIs in a Java web application and secure it using:
We’ll use Spring Security as the framework to secure our APIs.
Spring Security
For Spring Security Version 3.2.6.
Spring Security is a security framework that provides
It secures incoming requests before they reach the business logic of your Java application. This is achieved by implementing the Chain of Responsibility design pattern through a Filter Chain?—?a sequence of filters that intercept client requests to apply security checks before the request reaches the servlet (or the controller in the case of a REST request).
You can learn more about the Chain of Responsibility design pattern here:
Setting Up Spring?Security
To get started with Spring Security, add the following dependency to pom.xml?
<dependencies>
...
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
...
</dependencies>
After adding the dependency, run the following command to start your application:
./mvnw spring-boot:run
Once the application starts, Spring will secure the application with the default security filter chain. You can read more about the default security filter chain here:
We will no longer use the default security filter chain because it is tightly coupled with the Spring MVC framework, which assumes a server-generated frontend closely integrated with backend REST APIs. This setup relies on server-managed sessions (JSESSIONID) to maintain authentication state, making it unsuitable for hosting REST APIs in a modern web application where the backend is decoupled from the frontend, particularly when using frameworks like React or Vue.
Another significant drawback of the default security filter chain emerges when scaling the application across multiple servers. Since JSESSIONID is stored in memory on a single server, a user who logs in on one server might be routed to another server by a load balancer. If the session is managed in-memory, the new server won’t recognize the user's session, causing authentication failures. This issue highlights the limitations of relying on server-managed sessions in a distributed environment, underscoring the need for a more stateless, scalable approach to authentication.
Lets remove the following filters:?
by adding a SecurityConfig.java file with the following configuration.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@EnableWebSecurity
@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http.build();
}
}
The security filter generated.
HttpSecurity is a class for configuring spring web security, it allows us to add, remove, replace, reorder filters in our Security Filter Chain, whitelist endpoint?…
If you don’t define HttpSecurity, Spring will use the default SecurityFilterChain. If you do define HttpSecurity, Spring will use your custom SecurityFilterChain instead.?
By defining an empty HttpSecurity configuration, Spring will remove the following default filters:
领英推荐
You can read more about this in SpringBootWebSecurityConfiguration.java
class SpringBootWebSecurityConfiguration {
/**
* The default configuration for web security. It relies on Spring Security's
* content-negotiation strategy to determine what sort of authentication to use. If
* the user specifies their own {@link SecurityFilterChain} bean, this will back-off
* completely and the users should specify all the bits that they want to configure as
* part of the custom security configuration.
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnDefaultWebSecurity
static class SecurityFilterChainConfiguration {
Username Email Password Authentication
Lets implement username, email, and password authentication where the user can login by providing a valid username and password combination or a valid email and password combination.
Protecting Endpoints with JWT Authentication
We want to protect our endpoints with JWT authentication because we don’t want to prompt the user to log in every time they access our APIs. Additionally, JWT authentication avoids the drawbacks of the default security filter chain since it is not tightly coupled with the MVC architecture and is scalable. Unlike the JSESSIONID token, JWT tokens do not cause loss of authenticated status in a distributed system.
Integration with?OpenAPI
Add the following dependency to your pom.xml.
<!-- OpenAPI/Swagger -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.0.3</version>
</dependency>
Create the configuration file OpenApiConfig.java.
@Configuration
public class OpenApiConfig implements WebMvcConfigurer {
private static final String API_TITLE = "Payment API";
private static final String API_VERSION = "1.0";
private static final String API_DESCRIPTION = "API for Payment";
private static final String BEARER_ACCESS_TOKEN_SCHEMA = "BearerAccessTokenSchema";
private static final String COOKIE_ACCESS_TOKEN_SCHEMA = "CookieAccessTokenSchema";
private static final String BEARER_ACCESS_TOKEN_FORMAT = "JWT";
private static final String BEARER = "Bearer";
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title(API_TITLE)
.version(API_VERSION)
.description(API_DESCRIPTION))
.components(new Components()
/**
* Add jwt access token to header when we call API from swagger UI
*/
.addSecuritySchemes(BEARER_ACCESS_TOKEN_SCHEMA, new SecurityScheme()
.name(ACCESS_TOKEN)
.type(SecurityScheme.Type.HTTP)
.bearerFormat(BEARER_ACCESS_TOKEN_FORMAT)
.in(SecurityScheme.In.HEADER)
.scheme(BEARER)))
.security(Collections.singletonList(new SecurityRequirement().addList(BEARER_ACCESS_TOKEN_SCHEMA)));
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/swagger-ui.html**").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
}
}
You can access the OpenAPI UI by going to the url https://<application url>/swagger-ui/index.html if you are developing locally https://localhost:8080/swagger-ui/index.html.
Sign into an account to obtain an access token.?
Now lets add the access token to your request header, on OpenAPI UI, click on the Authorize button and add the access token.
Now all your calls on the OpenAPI UI will have access token in the header.
Authentication with?OpenID
We want to provide users with the option to sign into their account using OpenID, specifically through Google Login. The motivation behind this is to offer a convenient alternative to remembering or signing in with a password. If a user forgets their password, they can easily sign in using their Google account. Additionally, if the user is already signed into their Google account on their browser, signing into our application becomes a one-click process.
Bootstrapping with InnoBridge Security
Bootstrap your application InnoBridge Security, you can now protect your application with
with just a few lines of code.
<dependency>
<groupId>io.github.innobridge</groupId>
<artifactId>security</artifactId>
<version>0.0.1</version>
</dependency>
You can read more about InnoBridge Security here: