Insecure Storage of Access Tokens

Introduction

Access tokens are used in OAuth 2.0 to grant clients access to protected resources. If these tokens are not stored securely, they can be intercepted or stolen by malicious actors, leading to unauthorized access to user data. Proper storage mechanisms are essential to ensure the security of these tokens.

Here is a comprehensive example of how to use Spring Security, OAuth 2.0, and HashiCorp Vault to build a secure application that protects and manages access tokens.

Detailed Steps and Java Coding

1. Use Secure Storage Mechanisms

Tokens should be stored in a secure manner. In Java, you can use secure libraries to handle token storage. For example, you can use the Java Cryptography Architecture (JCA) to encrypt tokens before storing them.

2. Example Code: Encrypting and Storing Tokens Securely

Let's see how to encrypt and store access tokens securely using JCA and HashiCorp Vault.

Dependencies

Ensure you have the required dependencies in your pom.xml:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.vault</groupId>
    <artifactId>spring-vault-core</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.vault</groupId>
    <artifactId>spring-vault-config</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

Configuration

1. Configure Spring Security for OAuth 2.0

Create a SecurityConfig class to configure Spring Security for OAuth 2.0.

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.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests(authorizeRequests ->
                authorizeRequests
                    .antMatchers("/", "/error").permitAll()
                    .anyRequest().authenticated()
            )
            .oauth2Login(oauth2Login ->
                oauth2Login
                    .userInfoEndpoint(userInfoEndpoint ->
                        userInfoEndpoint.oidcUserService(new OidcUserService())
                    )
            );
    }
}

2. Configure HashiCorp Vault

Configure Spring Vault to connect to your HashiCorp Vault instance.

spring:
  vault:
    uri: http://localhost:8200
    token: s.your_vault_token_here

Token Encryption and Storage

3. Create a Service to Interact with Vault

This service will be used to store and retrieve encrypted tokens.

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.vault.core.VaultTemplate;
import org.springframework.vault.support.VaultResponseSupport;

@Service
public class VaultService {

    private final VaultTemplate vaultTemplate;

    @Value("${spring.vault.uri}")
    private String vaultUri;

    @Value("${spring.vault.token}")
    private String vaultToken;

    public VaultService(VaultTemplate vaultTemplate) {
        this.vaultTemplate = vaultTemplate;
    }

    public void storeToken(String path, String token) {
        vaultTemplate.write(path, new TokenData(token));
    }

    public String retrieveToken(String path) {
        VaultResponseSupport<TokenData> response = vaultTemplate.read(path, TokenData.class);
        return response.getData().getToken();
    }

    public static class TokenData {
        private String token;

        public TokenData(String token) {
            this.token = token;
        }

        public String getToken() {
            return token;
        }

        public void setToken(String token) {
            this.token = token;
        }
    }
}

Web Service Application

4. Create RESTful Endpoints

Enhance the controller to include the getUser(String key) method, which requires OAuth 2.0 authentication before invoking the server API.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api")
public class MyController {

    private final VaultService vaultService;

    @Autowired
    public MyController(VaultService vaultService) {
        this.vaultService = vaultService;
    }

    @GetMapping("/storeToken")
    public String storeToken(@AuthenticationPrincipal OidcUser oidcUser) {
        String token = oidcUser.getIdToken().getTokenValue();
        vaultService.storeToken("secret/my-token", token);
        return "Token stored securely!";
    }

    @GetMapping("/retrieveToken")
    public String retrieveToken() {
        String token = vaultService.retrieveToken("secret/my-token");
        return "Retrieved Token: " + token;
    }

    @GetMapping("/protected")
    public String protectedEndpoint() {
        return "This is a protected resource!";
    }

    @GetMapping("/getUser/{key}")
    public String getUser(@PathVariable String key, @AuthenticationPrincipal OidcUser oidcUser) {
        // Simulate user data retrieval based on the key
        // This method requires the user to be authenticated via OAuth 2.0
        return "User data for key: " + key + " accessed by " + oidcUser.getFullName();
    }
}

Client Program

We'll use Spring's RestTemplate for simplicity, but in a production environment, you might want to consider using WebClient from Spring WebFlux for its reactive capabilities.

1. Add Dependencies

Ensure you have the required dependencies in your pom.xml:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security.oauth2.client</groupId>
    <artifactId>spring-security-oauth2-client</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security.oauth2.core</groupId>
    <artifactId>spring-security-oauth2-core</artifactId>
</dependency>

2. Configure OAuth 2.0 Client

First, ensure you have the environment variables set up for the client ID and client secret. You can set these in your operating system or through a configuration file.

For example, in your .env file:

OAUTH_CLIENT_ID=your-client-id
OAUTH_CLIENT_SECRET=your-client-secret

Create a configuration class to set up the OAuth 2.0 client.

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientProviderBuilder;

@Configuration
public class OAuth2ClientConfig {

    @Value("${OAUTH_CLIENT_ID}")
    private String clientId;

    @Value("${OAUTH_CLIENT_SECRET}")
    private String clientSecret;

    @Bean
    public ClientRegistrationRepository clientRegistrationRepository() {
        return new InMemoryClientRegistrationRepository(this.googleClientRegistration());
    }

    private ClientRegistration googleClientRegistration() {
        return ClientRegistration.withRegistrationId("google")
                .clientId(clientId)
                .clientSecret(clientSecret)
                .redirectUri("http://localhost:8080/login/oauth2/code/google")
                .authorizationUri("https://accounts.google.com/o/oauth2/auth")
                .tokenUri("https://oauth2.googleapis.com/token")
                .userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo")
                .scope("openid", "profile", "email")
                .build();
    }

    @Bean
    public OAuth2AuthorizedClientManager authorizedClientManager(ClientRegistrationRepository clientRegistrationRepository) {
        OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
                .authorizationCode()
                .refreshToken()
                .build();

        DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
                clientRegistrationRepository, new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository));
        authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

        return authorizedClientManager;
    }
}

3. Create a Service to Consume the API

Create a service that will obtain an authorized token and use it to invoke the getUser service.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizeRequest;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.net.URI;

@Service
public class ApiService {

    private final OAuth2AuthorizedClientManager authorizedClientManager;
    private final OAuth2AuthorizedClientService authorizedClientService;

    @Autowired
    public ApiService(OAuth2AuthorizedClientManager authorizedClientManager, OAuth2AuthorizedClientService authorizedClientService) {
        this.authorizedClientManager = authorizedClientManager;
        this.authorizedClientService = authorizedClientService;
    }

    public String getUserData(String key) {
        OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("google")
                .principal("principalName")
                .build();

        OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest);
        String accessToken = authorizedClient.getAccessToken().getTokenValue();

        RestTemplate restTemplate = new RestTemplate();
        HttpHeaders headers = new HttpHeaders();
        headers.setBearerAuth(accessToken);
        RequestEntity<Void> request = new RequestEntity<>(headers, HttpMethod.GET, URI.create("http://localhost:8080/api/getUser/" + key));

        ResponseEntity<String> response = restTemplate.exchange(request, String.class);
        return response.getBody();
    }
}

4. Create a Controller to Invoke the Service

Create a controller to demonstrate how the client can securely call the getUser service.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ClientController {

    private final ApiService apiService;

    @Autowired
    public ClientController(ApiService apiService) {
        this.apiService = apiService;
    }

    @GetMapping("/fetchUserData")
    public String fetchUserData(@RequestParam String key) {
        return apiService.getUserData(key);
    }
}

How It Works

  1. Client Initiates OAuth 2.0 Flow: The client application initiates the OAuth 2.0 authorization flow to obtain an access token.

  2. User Authentication: The user authenticates with the OAuth 2.0 provider (e.g., Google).

  3. Access Token Retrieval: The client application receives an access token upon successful authentication.

  4. Token Storage: The access token is securely stored in HashiCorp Vault.

  5. Accessing Protected Resource: The client uses the access token to make an authenticated request to the server's protected endpoint.

  6. Server Authentication and Authorization: The server validates the access token and grants access to the protected resource if the token is valid.

Best Practices for Secure Token Storage

  • Use Secure Storage: Use secure services like HashiCorp Vault for storing sensitive information.

  • Use Strong Encryption: Ensure that tokens are encrypted using strong algorithms.

  • Implement Token Rotation: Regularly rotate tokens to minimize the impact of a token compromise.

  • Secure Communication: Always use HTTPS to communicate with external services to prevent token interception.

Summary and Key Takeaways

By following these best practices, you can ensure that your client applications interact securely with your OAuth 2.0 protected web services.

Last updated