spring-bootspring-securityauth0okta

calling the Auth0 API with Spring Boot using Oauth2


I have a Spring Boot 3 service that uses Auth0/Okta to secure its API. The users get a token and can call my endpoints. I am using these parameters in my application.yaml :

okta:
  oauth2:
    audience: 
    issuer: 

My service also needs to be able to call Auth0, to be able to create a user for instance, using OAuth2 token

Things are properly setup on the Auth0 side, because I am able to make the call with Postman, after retrieving a token using clientId and clientSecret that were issued for my service.

I assumed it would be straightforward to do with the SDK and starter and embed this in my service, but I am not finding the way to achieve that :

enter image description here

and through a private key :

enter image description here

enter image description here

I could not find any resource on how to achieve something similar, with Oauth2.

When I try it "manually", by setting up more code myself, I am failing to get a token, because Auth0 requires an audience parameter, that is not supported out of the box by Spring Security (https://github.com/spring-projects/spring-security/issues/6569)

I was expecting this to be very easy given how Spring Boot and Auth0 are popular, but it turns out I am not finding a single example of this working more or less out of the box with Spring Boot 3 / Spring Security 6..

In the end, these were useful resources :

Did I miss anything in the docs, or is there truly no better way of calling the Auth0 API with OAuth2 ?


Solution

  • I just went through this exercise, so allow me to help.

    First, let me say that I went down the route of using the okta SDK but I didn't have a good experience with it.

    <dependency>
        <groupId>com.okta</groupId>
        <artifactId>okta-spring-boot-starter</artifactId>
        <version>3.0.5</version>
    </dependency>
    
    1. It didn't integrate well with the Spring Framework.
    2. Why not use openID-Connect and OAuth2 libraries since they are more compatible with, well, openid-connect and oauth2. Makes it much easier to switch to different OpenId/OAuth2 providers, KeyCloak.

    I also used Auth0 by Okta oauth2 service since it is a free developer oriented platform.

    My project used Spring security dependencies:

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

    Since you are only interested in the Resource Server made a quick project for that.

    Add this to your application.yml.

    spring:
      security:
        oauth2:
          resourceserver:
            jwt:
              issuer-uri: ${auth0audience-issuer-uri}
              audiences: ${auth0audience-audience}
          client:
            registration:
              auth0:
                client-id: ${auth0audience-client-id}
                client-secret: ${auth0audience-client-secret}
                authorization-grant-type: client_credentials
                provider: auth0
            provider:
              auth0:
                issuer-uri: ${auth0audience-issuer-uri}
    

    Then configure the SecurityFilterChain to require authentication and use JWT access tokens for authentication.

    @Configuration
    public class SecurityConfiguration {
    
        @Bean
        public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
            return http.authorizeHttpRequests(auth -> auth
                            .anyRequest()
                            .authenticated())
                    .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults())).build();
        }
    }
    

    As you have found out though, the difficulty really is in accessing it through WebClient. Spring security must be configured to add the audience parameter to the request headers when requesting an access token to use with the API. This is important because the access token must have the aud property when it is issued. This really means configurating the OAuth2AuthorizedClientManager to have a DefaultClientCredentialsTokenResponseClient that is configured to add the audience parameter to the request for an access token. This client is invoked auto-magically in the background when there is a need for a new access token.

    @Configuration
    public class WebClientConfiguration {
        @Value("${spring.security.oauth2.resourceserver.jwt.audiences}")
        private String audience;
    
        @Bean
        public WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
            ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
                    new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
    
            return WebClient.builder()
                    .apply(oauth2Client.oauth2Configuration())
                    .baseUrl("http://localhost:8080/")
                    .build();
        }
    
        @Bean
        public OAuth2AuthorizedClientManager authorizedClientManager(
                ClientRegistrationRepository clientRegistrationRepository,
                OAuth2AuthorizedClientService authorizedClientService) {
    
            OAuth2AuthorizedClientProvider authorizedClientProvider =
                    OAuth2AuthorizedClientProviderBuilder.builder()
                            .clientCredentials((builder) ->
                                    builder.accessTokenResponseClient(clientCredentialsAccessTokenResponseClient())
                                            .build())
                            .build();
    
            AuthorizedClientServiceOAuth2AuthorizedClientManager authorizedClientManager =
                    new AuthorizedClientServiceOAuth2AuthorizedClientManager(
                            clientRegistrationRepository, authorizedClientService);
            authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
    
            return authorizedClientManager;
        }
    
        private OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> clientCredentialsAccessTokenResponseClient() {
            DefaultClientCredentialsTokenResponseClient accessTokenResponseClient =
                    new DefaultClientCredentialsTokenResponseClient();
    
            OAuth2ClientCredentialsGrantRequestEntityConverter requestEntityConverter =
                    new OAuth2ClientCredentialsGrantRequestEntityConverter();
            requestEntityConverter.addParametersConverter(source -> CollectionUtils.toMultiValueMap(Collections.singletonMap("audience", Collections.singletonList(audience))));
            accessTokenResponseClient.setRequestEntityConverter(requestEntityConverter);
    
            return accessTokenResponseClient;
        }
    
    }
    

    Then you can use it.

    @SpringBootApplication
    public class Auth0WebClientAudienceApplication implements ApplicationRunner {
    
        public static void main(String[] args) {
            SpringApplication.run(Auth0WebClientAudienceApplication.class, args);
        }
    
        @Autowired
        WebClient webClient;
        @Override
        public void run(ApplicationArguments args) throws Exception {
            List<String> r = webClient.get().uri("movies")
                    .attributes(clientRegistrationId("auth0"))
                    .retrieve()
                    .bodyToMono(new ParameterizedTypeReference<List<String>>() {
                    }).block();
            System.out.println("movies = " + r);
        }
    }
    

    The special need here is the line .attributes(clientRegistrationId("auth0")) which tells the webclient which client the client manager should use to get the access token before making the call to the API. It's all very well hidden with minimal documentation.

    The code for the API is nothing special. This example does not attempt to demonstrate authorization.

    @RestController
    @RequestMapping
    public class MovieApi {
        @GetMapping("movies")
        public List<String> getMovies() {
            SecurityContext securityContext = SecurityContextHolder.getContext();
            if (!(securityContext instanceof AnonymousAuthenticationToken)) {
                return List.of("movie1", "movie2");
            }
            return Collections.emptyList();
        }
    }
    

    Git repo of this code at Auth0WebClientAudience.

    Spring Security Framework has some documenation at OAuth 2.0.