Search code examples
javaspringazurespring-bootdynamics-crm

401- Unauthorized authentication using REST API Dynamics CRM with Azure AD from a Spring Boot app


I have a Spring Boot application that gets access token from the Azure authentication endpoint successfully. But when I try to use the same token to access Dynamics CRM Online REST API, I am getting - 401 Unauthorized: [no body]

This is how I get my access token

@Bean(name = "oAuth2RestTemplate")
    public OAuth2RestTemplate oAuth2RestTemplate() {
        String tokenUri = "https://login.windows.net/<tenant id>/oauth2/token";

        ClientCredentialsResourceDetails resourceDetails = new ClientCredentialsResourceDetails();
        resourceDetails.setId("1");
        resourceDetails.setAccessTokenUri(tokenUri);
        resourceDetails.setClientId(clientid);
        resourceDetails.setClientSecret(clientsecret);
        resourceDetails.setGrantType("client_credentials");
        resourceDetails.setScope(Arrays.asList("read", "write"));

        AccessTokenRequest atr = new DefaultAccessTokenRequest();
        atr.add("resource", "https://my.api.crm9.dynamics.com");
        OAuth2ClientContext clientContext = new DefaultOAuth2ClientContext(atr);

        OAuth2RestTemplate oauth2RestTemplate = new OAuth2RestTemplate(resourceDetails, clientContext);

        return oauth2RestTemplate;
    }

This is what I get as my accesstoken using this OAuth2RestTemplate -

{"access_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImtnMkxZczJUMENUaklmajRydDZKSXluZW4zOCIsImtpZCI6ImtnMkxZczJUMENUaklmajRydDZKSXluZW4zOCJ9.eyJhdWQiOiIwMDAwMDAwMi0wMDAwLTAwMDAtYzAwMC0wMDAwMDAwMDAwMDAiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC82MDQwOTE1Yi05ZGZmLTRkNDctYmIzNS04YWM5YzlhNWRjMTgvIiwiaWF0IjoxNjAyNjcxNzYyLCJuYmYiOjE2MDI2NzE3NjIsImV4cCI6MTYwMjY3NTY2MiwiYWlvIjoiRTJSZ1lQRGlWbHRZZlYxNHc5TTMxMmFmQ3BUWEFnQT0iLCJhcHBpZCI6IjM2NDlmNTUxLTRlMjEtNDY3OS04YThjLTA4ZDhmNmIwOTA3MSIsImFwcGlkYWNyIjoiMSIsImlkcCI6Imh0dHBzOi8vc3RzLndpbmRvd3MubmV0LzYwNDA5MTViLTlkZmYtNGQ0Ny1iYjM1LThhYzljOWE1ZGMxOC8iLCJvaWQiOiJiM2FmOTcxYi1lMDQ5LTQ1ZDktOTViNy1hMTg3ZjQ4MWQzNWYiLCJyaCI6IjAuQVMwQVc1RkFZUC1kUjAyN05Zckp5YVhjR0ZIMVNUWWhUbmxHaW93STJQYXdrSEV0QUFBLiIsInN1YiI6ImIzYWY5NzFiLWUwNDktNDVkOS05NWI3LWExODdmNDgxZDM1ZiIsInRlbmFudF9yZWdpb25fc2NvcGUiOiJOQSIsInRpZCI6IjYwNDA5MTViLTlkZmYtNGQ0Ny1","token_type":"Bearer","expires_in":3528,"ext_expires_in":"3599","expires_on":"1602675662","not_before":"1602671762","resource":"**00000002-0000-0000-c000-000000000000**"}

As you can see, the resource in the access token is different than my desired resource endpoint - https://my.api.crm9.dynamics.com

I am trying to access my rest service this way -

public List<Contact> findContacts() throws MalformedURLException, IOException {
        System.out.println("getting access token");
        final UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromHttpUrl(
                "https://my.api.crm9.dynamics.com").path("/api/data/v9.1/contacts?$select=_accountid,address_stateorprovince," +
                "contactid,emailaddress,fullname&$filter=startswith(emailaddress,'something')");

        final String url = uriComponentsBuilder.build().toUriString();
        System.out.println("url is :: "+url);
        try {
            ResponseEntity<List<Contact>> response = oAuth2RestTemplate.exchange(url, HttpMethod.GET, createRequestEntity(),
                    new ParameterizedTypeReference<List<Contact>>() {});
            List<Contact> contacts = null;
            if (null != response) {
                contacts = response.getBody();
            }
            return contacts;
        } catch (final Exception e) {
            log.error(e.getMessage());
            log.error("<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
            log.error("Failed to retrieve contacts from Dynamics CRM.", e);
            return new ArrayList<>();
        }

I have looked at OAuth2.0 authentication of Dynamics CRM WebAPIs from a background Java (Spring) application Resource(the web api base url) is being set in the DefaultAccessTokenRequest but that does not seem to have any impact in my case.

My theory is the resource in the access token being different is causing a 401 while trying to access the rest endpoint. But I cannot seem to be able to set the resource explicitly. My questions -

  1. Any other theories for my 401?
  2. How can I explicitly set my resource in my access token? I have tried doing it in my DefaultAccessTokenRequest while creating my OAuth2RestTemplate

Solution

  • I will answer my own question in case its helpful for someone else. My method in my question get the token but did not set the resource property on the token as I had mentioned. Consequently rest endpoint authentication failed.

    The following method worked for me to get the token with required resource -

    public String getToken() throws MalformedURLException, IOException {
        // create headers
        final HttpHeaders headers = new HttpHeaders();
    
        // set `content-type` header
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
    
        // set `accept` header
        headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
    
        // request body parameters
        final MultiValueMap<String, String> map = new LinkedMultiValueMap<String, String>();
        map.add("grant_type", "client_credentials");
        map.add("resource", "https://my.api.crm9.dynamics.com");
        map.add("client_id", clientid);
        map.add("client_secret", clientsecret);
    
        // build the request
        final HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(map, headers);
        // send POST request
        final ResponseEntity<String> response = restTemplate.postForEntity(
                    dynamicsOAuth2ClientProperties.getAccessTokenUri(), entity, String.class);
    
        //Extract the token from the json response
        final ObjectMapper mapper = new ObjectMapper();
        final JsonNode actualObj = mapper.readTree(response.getBody());
        
        return actualObj.get("access_token").asText();
    }