Search code examples
javaoauth-2.0

Retrieve access token from server with Java HttpClient using Client Credentials grant


I am trying to use Dell's Warranty API. To do this you first need to get an access token (that expires after 1 hour) and then make API requests using that token. They have a tutorial for postman, which works fine, but I am trying to make something more automated since we have 1000+ assets to lookup with this API.

I am trying to use java.net.http even though numerous examples exist for the older APIs, I would prefer not to use external libraries or older APIs.

To get the token you send a request to their API with your client ID and client secret.

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpClient.Redirect;
import java.net.http.HttpClient.Version;
import java.net.http.HttpRequest;
import java.net.http.HttpRequest.BodyPublishers;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.time.Duration;
import java.util.Base64;

public class Main {

    public static void main(String[] args) throws IOException, InterruptedException {
        
        String tokenURL = "https://apigtwb2c.us.dell.com/auth/oauth/v2/token";
        String clientID = "{redacted}";
        String clientSecret = "{redacted}";
        String formatted = clientID + ":" + clientSecret;
        String encoded = Base64.getEncoder().encodeToString((formatted).getBytes());
        
        HttpClient client = HttpClient.newBuilder().version(Version.HTTP_1_1).followRedirects(Redirect.NORMAL).connectTimeout(Duration.ofSeconds(10)).build();
        HttpRequest request = HttpRequest.newBuilder().uri(URI.create(tokenURL)).headers("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8", "Accept", "application/json", "grant_type", "client_credentials", "Authorization", "Basic " + encoded)
                .POST(BodyPublishers.noBody()).build();
        
        System.out.println(request.headers());
        
        HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
        
        System.out.println(response.statusCode());
        System.out.println(response.body());
    }

}

The server is responding with

400
{
  "error":"invalid_request",
  "error_description":"Missing or duplicate parameters"
}

According to their docs, 400 is a bad request. I am currently sending an empty body, but am not sure if that's what the issue is.

And I am not entirely sure what is missing. I have tried multiple combinations of headers without success. The same works just fine with cURL which is what led me to as far as I am now.

Here is the cURL request just in case..

curl -v https://apigtwb2c.us.dell.com/auth/oauth/v2/token -H "Accept: application/json" -u "{redacted}:{redacted}" -d "grant_type=client_credentials"
*   Trying 143.166.28.87:443...
* Connected to apigtwb2c.us.dell.com (143.166.28.87) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*  CAfile: /etc/ssl/certs/ca-certificates.crt
*  CApath: none
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Request CERT (13):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Certificate (11):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES256-GCM-SHA384
* ALPN, server did not agree to a protocol
* Server certificate:
*  subject: C=US; ST=Texas; L=Round Rock; O=Dell; CN=*.apis.dell.com
*  start date: Jul 26 19:18:16 2021 GMT
*  expire date: Jul 20 19:18:15 2022 GMT
*  subjectAltName: host "apigtwb2c.us.dell.com" matched cert's "apigtwb2c.us.dell.com"
*  issuer: C=US; O=Entrust, Inc.; OU=See www.entrust.net/legal-terms; OU=(c) 2012 Entrust, Inc. - for authorized use only; CN=Entrust Certification Authority - L1K
*  SSL certificate verify ok.
* Server auth using Basic with user '{redacted}'
> POST /auth/oauth/v2/token HTTP/1.1
> Host: apigtwb2c.us.dell.com
> Authorization: Basic {redacted}
> User-Agent: curl/7.79.1
> Accept: application/json
> Content-Length: 29
> Content-Type: application/x-www-form-urlencoded
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Pragma: no-cache
< Cache-Control: no-store
< X-Correlation-ID: {redacted}
< Content-Type: application/json;charset=UTF-8
< Content-Length: 127
< Date: Thu, 14 Oct 2021 18:56:28 GMT
< Server: dell
<
{
  "access_token":"{redacted}",
  "token_type":"Bearer",
  "expires_in":3600,
  "scope":"oob"
* Connection #0 to host apigtwb2c.us.dell.com left intact

Thanks!


Solution

  • Body is required

    According to your curl log, the request should have this content-type application/x-www-form-urlencoded

    This kind of content-type needs a body, so that is your first error:

    .POST(BodyPublishers.noBody())

    Second error is that in oauth2 protocol, grant_type=client_credentials is not a header, it is a form parameter in the body. Also you curl snippet confirms that: curl ... -d "grant_type=client_credentials". Check this: curl -d

    The Client Credentials grant

    If Dell platform implements the oauth2 protocol strictly, they should implement the oauth2 spec:

    https://datatracker.ietf.org/doc/html/rfc6749#section-4.4

    POST /token HTTP/1.1
    Host: server.example.com
    Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
    Content-Type: application/x-www-form-urlencoded
    
    grant_type=client_credentials
    

    In which we can see that body just need:

    • the grant_type in application/x-www-form-urlencoded style (not json)
    • content-type header
    • Authorization header which should be like your String encoded = Base64 ...

    If Dell implements another kind of this grant, you should read in the docs, something like this:

    https://www.oauth.com/oauth2-servers/access-tokens/client-credentials/

    POST /token HTTP/1.1
    Host: authorization-server.com
     
    grant_type=client_credentials
    &client_id=xxxxxxxxxx
    &client_secret=xxxxxxxxxx
    

    In which we can see that body needs the client_id and client_secret, unlike the spec in which credential are sent as basic auth header.

    Java request with x-www-form-urlencoded body

    If Dell implements this spec and assuming that you use Java 11, this untested code should work:

    String url = "https://apigtwb2c.us.dell.com/auth/oauth/v2/token";
    Map<String, String> parameters = new HashMap<>();
    parameters.put("grant_type", "client_credentials");
    parameters.put("client_id", "****");
    parameters.put("client_secret", "****");
    
    String form = parameters.keySet().stream()
    .map(key -> key + "=" + URLEncoder.encode(parameters.get(key), StandardCharsets.UTF_8))
    .collect(Collectors.joining("&"));
    
    HttpClient client = HttpClient.newHttpClient();
    
    HttpRequest request = HttpRequest.newBuilder().uri(URI.create(url))
    .headers("Content-Type", "application/x-www-form-urlencoded")
    .POST(BodyPublishers.ofString(form)).build();
    
    HttpResponse<?> response = client.send(request, BodyHandlers.ofString());
    System.out.println(response.statusCode() + response.body().toString());
    

    Some providers, supports several types of request, so the previous snippet should work. If Dell implements strictly the main spec, just delete this part:

    parameters.put("client_id", "****");
    parameters.put("client_secret", "****");
    

    and add your authorization header

    String formatted = clientID + ":" + clientSecret;
    String encoded = Base64.getEncoder().encodeToString((formatted).getBytes());
    ...
    .headers("Content-Type", "application/x-www-form-urlencoded", "Authorization", "Basic " + encoded)