Securing Modern Web Applications with OAuth 2.0

Everwell Engineering
Pulse by Everwell
Published in
6 min readNov 28, 2023

In the dynamic landscape of modern web applications, security is not merely a feature; it’s an absolute necessity. As digital ecosystems expand and user interactions become increasingly intricate, safeguarding sensitive data and preserving user privacy has become paramount. This is where OAuth 2.0 comes into play as an authorization framework.

Introduction

OAuth 2.0 provides detailed control over third-party access in our apps, ensuring user data security and supporting efficient growth with external services. It balances strong security with the flexibility needed for modern web applications, essential for protecting user information and enabling smooth third-party integration.

Advantages of Oauth2

The following advantages that led us to implement Oauth2 was

  1. Speed and Efficiency: OAuth 2.0 is fast. It uses tokens instead of passwords, making logging in quicker and safer.
  2. Security: OAuth 2.0 is more secure. It uses temporary access tokens, reducing the risk of someone getting unauthorized access.
  3. Third-Party Integration: OAuth 2.0 lets other apps safely access your information with your permission, helping them work together better.
  4. Role-Based Permissions: OAuth 2.0 controls who can do what in an app, making sure people only access what they need to.

Components of the System:

  • Resource Server: Responsible for managing and storing the data that clients need to access.
  • Authorization Server: Handles the OAuth 2.0 authentication and authorization process.
  • Clients: Send requests to the resource server to access data after obtaining an OAuth2 token from the authorization server.

Steps in the System:

  1. A Client sends a request to the Authorization Server to authenticate itself and request an access token.
  2. The Authorization Server authenticates the Client and issues a token.
  3. The Client sends a request to the Resource Server, including the access token in the request.
  4. The Resource Server uses the token to authenticate the client and determine whether it has permission to access the requested data.
  5. If the client is Authenticated and Authorized, the resource server returns the requested data to the client.

Authorization Server Implementation

First, we need to configure the Authorization Server. We are using Spring Boot 3.0.1 and oauth2 version 1.0.0. The following dependency for oauth2 needs to be added in pom.xml file :

<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-authorization-server</artifactId>
<version>1.0.0</version>
</dependency>

Now we’ll configure the port that our auth server will run on by setting the server.port property in the application.properties file:

server.port = 8072

In order to allow client configuration from DB, we have created an implementation of the RegisteredClientRepository which is the Oauth central component where new clients can be registered and existing clients can be queried. We will put this in a new file CustomRegisteredClientRepository.java

@Configuration
public class CustomRegisteredClientRepository implements RegisteredClientRepository {

public Map<String, RegisteredClient> idRegistrationMap;
public Map<String, RegisteredClient> clientIdRegistrationMap;
@Autowired
private ClientsAuthListStore clientsAuthListStore;
@Autowired
private ClientsListStore clientsListStore;

@PostConstruct
public void customRegisteredClientRepositoryStore() {
List<ClientsAuth> clientsAuthList = clientsAuthListStore.getClientsAuthList();
List<Clients> clientsList = clientsListStore.getClientsList();
Map<UUID, Clients> clientsMap = clientsList.stream().collect(Collectors.toMap(Clients::getId, e -> e));
ConcurrentHashMap<String, RegisteredClient> idRegistrationMapResult = new ConcurrentHashMap<>();
ConcurrentHashMap<String, RegisteredClient> clientIdRegistrationMapResult = new ConcurrentHashMap<>();
for (ClientsAuth clientsAuth : clientsAuthList) {
RegisteredClient registeredClient = RegisteredClient.withId(clientsAuth.getId().toString())
.clientId(clientsAuth.getClientId().toString())
.clientSecret(clientsAuth.getSecret())
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.redirectUri(clientsMap.get(clientsAuth.getClientId()).getWebsite())
.scope(OidcScopes.OPENID)
.build();
idRegistrationMapResult.put(registeredClient.getId(), registeredClient);
clientIdRegistrationMapResult.put(registeredClient.getClientId(), registeredClient);
}
this.idRegistrationMap = idRegistrationMapResult;
this.clientIdRegistrationMap = clientIdRegistrationMapResult;
}

The attributes we’re setting up include:

  1. Client ID — Spring will utilize this to identify the requesting client accessing the resource. It enforces security by preventing unauthorized clients to gain access
  2. Client secret code — a confidential code shared between the client and server, establishing trust between them.
  3. Authentication method — we’ll employ authentication type of client secret post, this ensures that potentially sensitive information such as the client secret it not present in URL or headers reducing risk of Interception
  4. Authorization grant type — This controls how a client can request token. We aim to permit the client to generate using client credentials, authorization code and a refresh token.
  5. Redirect URI — the client will use this in a redirect-driven process. It prevents open redirect attacks where a client is redirected to a potentially malicious site
  6. Scope — this parameter defines the authorizations that the client is allowed. In our instance, we will have the mandatory `OidcScopes.OPENID`. We can further use scopes such as `read` or `write` to limit client access

We also need to implement the following interface methods in the same file

    @Override
public void save(RegisteredClient registeredClient) {
this.idRegistrationMap.put(registeredClient.getId(), registeredClient);
this.clientIdRegistrationMap.put(registeredClient.getClientId(), registeredClient);
}

@Override
public RegisteredClient findById(String id) {
return this.idRegistrationMap.get(id);
}
@Override
public RegisteredClient findByClientId(String clientId) {
return this.clientIdRegistrationMap.get(clientId);
}

Following that, we will create a bean to implement the standard OAuth security and create a standard form login page by default in AuthorizationServerConfig.java

@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
return http.formLogin(Customizer.withDefaults()).build();
}

Every authorization server requires its own unique signing key for tokens to maintain a clear separation between security domains. We will create a 2048-byte RSA key for this purpose in the same file (AuthorizationServerConfig.java).

@Bean
public JWKSource<SecurityContext> jwkSource() {
RSAKey rsaKey = generateRsa();
JWKSet jwkSet = new JWKSet(rsaKey);
return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}

private static RSAKey generateRsa() {
KeyPair keyPair = generateRsaKey();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
return new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
}

private static KeyPair generateRsaKey() {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
return keyPairGenerator.generateKeyPair();
}

Next, we will activate the Spring web security module by using a configuration class annotated with @EnableWebSecurity. We will put it under a new file DefaultSecurityConfig.java

@EnableWebSecurity
@Configuration
public class DefaultSecurityConfig {

@Autowired
private UserDetailsService userDetailsService;

@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}

@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}

@Bean
public JWTAuthorizationFilter jwtAuthenticationFilter() {
return new JWTAuthorizationFilter();
}

@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeHttpRequests(authorizeRequests ->
{
try {
authorizeRequests
.requestMatchers("/oauth2/token")
.permitAll().anyRequest().authenticated()
.and().addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
} catch (Exception e) {
throw new RuntimeException(e);
}
});
return http.build();
}

The provided code represents a Java configuration class for Spring Security in a web application. Let’s break down its key elements:

  1. @EnableWebSecurity is an annotation that indicates this class is used to configure web security for the application.
  2. @Configuration is a Spring annotation that denotes this class as a configuration class for the application context.
  3. userDetailsService is a dependency injected via @Autowired and refers to a custom user details service (UserDetailsServiceImpl) used for authenticating users.
  4. BCryptPasswordEncoder is a Spring Bean that provides a BCrypt password encoder, commonly used for securely hashing passwords.
  5. authenticationManager is another Bean used to obtain the AuthenticationManager from an AuthenticationConfiguration. The AuthenticationManager is a key component for handling user authentication.
  6. jwtAuthenticationFilter is a Bean representing a custom filter called JWTAuthorizationFilter, used for processing JSON Web Tokens (JWT) and handling authorization.
  7. defaultSecurityFilterChain is a Bean that configures the security filters and policies for the application. It defines the security rules as follows:
  • .csrf().disable(): Disables CSRF protection.
  • .authorizeHttpRequests(authorizeRequests -> { ... }): Specifies authorization rules for different endpoints.
  • .requestMatchers("/oauth2/token").permitAll(): Permits unrestricted access to the "/oauth2/token" endpoint.
  • .anyRequest().authenticated(): Requires authentication for all other requests.
  • .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class): Adds the custom JWTAuthorizationFilter before the standard UsernamePasswordAuthenticationFilter.
  • .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS): Sets the session creation policy to be stateless, typically used for token-based authentication.

Resource Server

Next, we need to configure the Resource Server. We are using Spring Boot 3.0.1 and oauth2 version 3.0.0. The following dependency for oauth2 needs to be added in pom.xml.

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
<version>3.0.0</version>
</dependency>

Next, we will specify the JWT Issuer URI which in our case is the Authorization Server. We can do so in application.properties file

spring.security.oauth2.resourceserver.jwt.issuer-uri = http://localhost:8072

Now we can set up our web security configuration in ResourceServerConfig.java

@EnableWebSecurity
@Configuration
public class ResourceServerConfig {

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeHttpRequests(authorizeRequests ->
{
try {
authorizeRequests
.anyRequest().authenticated()
.and()
.oauth2ResourceServer()
.jwt();
} catch (Exception e) {
throw new RuntimeException(e);
}
});
return http.build();
}
}

The provided code is a Spring Security configuration class for a resource server. It disables Cross-Site Request Forgery (CSRF) protection and mandates that all incoming HTTP requests must be authenticated. Additionally, it configures the application to act as a resource server, utilizing OAuth 2.0 and JSON Web Tokens (JWT) for authentication. Incoming requests are expected to contain OAuth 2.0 access tokens in JWT format, which will be validated to ensure secure access to the protected resources.

API Testing

  1. Generating oauth2 token from the web server
curl --request POST \
--url http://localhost:8072/oauth2/token \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data grant_type=client_credentials \
--data client_id=<<enter_your_client_id>> \
--data client_secret=<<enter_your_client_secret>

This returns the following response

{
"access_token": "xx508xx63817x752xx74004x30705xx92x58349x5x78f5xx34xxxxx51",
"token_type": "Bearer",
"expires_in": 300
}

2. We can use the token above to access the resource server

curl --request GET \
--url http://localhost:7020/v1/resource \
--header 'Authorization: Bearer xx508xx63817x752xx74004x30705xx92x58349x5x78f5xx34xxxxx51' \

Conclusion

In this guide, we’ve covered the setup, configuration, and practical use of the Spring Security OAuth Authorization Server to implement secure and efficient authorization mechanisms in Spring applications, ensuring the protection of resources and streamlined user access.

Credits: Bharat Agarwal

--

--