Search code examples
javahttp-redirectoauth-2.0postmanokhttp

How to get Optum sandbox to authenticate with OAuth 2.0 using Java and okhttp


I am trying to get a Java application that uses okhttp to properly authenticate with the Optum API, as documented here. Although I can get a proper response from a test tool, I can not get a proper response from the Java application.

Using the information in the section API Permissions and Scopes in the API documentation, I was able to get the call working in Postman (an application used to test http requests). When I replicate the call in Java, I get a 500 server error when I expect a 302 redirect. The API support desk at Optum was unable to explain why the same call from Postman does not work in Java, so I thought I would ask here, in case someone has discovered how to get past this problem, or has things I might try.

The way I concluded that the call from Postman, and the call from Java were equivalent was to send the request to https://httpbin.org, which echos back what it sees.

My Postman export that includes TestAuthHttpBin and TestAuthOptumSandbox may be found on my github project. That could be imported into Postman if you want to test this yourself. Import the collection, click the Authorization tab, then Get New Access Token on each request. Here is the Postman screen: Postman Screen showing the real and test requests.

Here is the code I'm running (available here) that is designed to be equivalent to the Postman call:

public class FirstGetOptum {

//============================= URL PARAMETERS ================================================================

public static final ArrayList<Pair<String, String>> URL_PARAM_LIST = new ArrayList<>();
{
    URL_PARAM_LIST.add(new Pair<>("response_type", "code"));
    URL_PARAM_LIST.add(new Pair<>("client_id", "55796a71-8104-4625-b259-bb91e9f13a60"));
    URL_PARAM_LIST.add(new Pair<>("state", "0124"));
    URL_PARAM_LIST.add(new Pair<>("scope", "patient/Patient.read"));
    URL_PARAM_LIST.add(new Pair<>("redirect_uri", "https://sites.google.com/sengsational.com/privacy/privacypolicy"));
    URL_PARAM_LIST.add(new Pair<>("code_challenge", "s6kElxScJMXGilr1VTwZYsjlq5XexWCUn94rmO7Y29o")); // optionally replaced in main()
    URL_PARAM_LIST.add(new Pair<>("code_challenge_method", "S256"));
}

//============================= HEADERS ================================================================

public static final ArrayList<Pair<String, String>> HEADER_LIST = new ArrayList<>();
{
    HEADER_LIST.add(new Pair<>("Upgrade-Insecure-Requests", "1"));
    HEADER_LIST.add(new Pair<>("User-Agent", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) PostmanCanary/11.2.14-canary240621-0734 Electron/20.3.11 Safari/537.36"));
    HEADER_LIST.add(new Pair<>("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"));
    HEADER_LIST.add(new Pair<>("Sec-Fetch-Site", "none"));
    HEADER_LIST.add(new Pair<>("Sec-Fetch-Mode", "navigate"));
    HEADER_LIST.add(new Pair<>("Sec-Fetch-User", "?1"));
    HEADER_LIST.add(new Pair<>("Sec-Fetch-Dest", "document"));
    HEADER_LIST.add(new Pair<>("Accept-Encoding", "gzip, deflate, br"));
    HEADER_LIST.add(new Pair<>("Accept-Language", "en-US"));
}

private static String generateCodeChallenge() throws Exception {
    // Construct the code challenge url parameter
    StringBuilder sb = new StringBuilder ();
    String characters = "01234567890abcde";
    Random random = new Random ();
    for (int i = 0; i < 56; i ++) {
        sb.append (characters.charAt (random.nextInt (characters.length ())));
    }
    String randomText = sb.toString();
    
    // Temporarily override the random text with the same text each time
    randomText = "6b890b254542c9de4603278153e1b127d21730d46ac2620e6e35514c";

    byte[] binaryData = null;
    try {
        binaryData = MessageDigest.getInstance("SHA-256").digest(randomText.getBytes(StandardCharsets.UTF_8));
    } catch (NoSuchAlgorithmException e) {
        throw new Exception("Failed SHA-256");
    }
    
    Base64.Encoder encoder = Base64.getUrlEncoder();
    String codeChallenge = encoder.encodeToString(binaryData);
    codeChallenge =  codeChallenge.replaceAll("=", ""); // remove pad
    return codeChallenge;
}


public static void main(String[] args) throws Exception {
    FirstGetOptum fgo = new FirstGetOptum(); // for static initializers
    
    // Use this boolean to run the test or the actual call
    HttpUrl.Builder urlBuilder = null;
    boolean getHttpBinDump = false;
    if (getHttpBinDump) {
        urlBuilder = HttpUrl.parse("https://www.httpbin.org/get").newBuilder();
    } else {
        urlBuilder = HttpUrl.parse("https://sandbox.authz.flex.optum.com/oauth/authorize").newBuilder();
    }
    
    for (Pair<String, String> pair : URL_PARAM_LIST) {
        urlBuilder.addQueryParameter(pair.getFirst(), pair.getSecond());
    }
    
    boolean replaceCodeChallengeValue = true;
    if (replaceCodeChallengeValue) {
        String codeChallenge = generateCodeChallenge();
        System.out.println("generated codeChallenge [" + codeChallenge + "]");
        urlBuilder.setQueryParameter("code_challenge", codeChallenge);
        urlBuilder.setQueryParameter("code_challenge_method", "S256");
    }

    // It's not supposed to make a difference, but this bit rearranges to match the order that Postman has it
    String url = urlBuilder.build().toString();
    url = url.replaceAll("&code_challenge_method=S256", "") + "&code_challenge_method=S256";
    
    System.out.println("Constructed the URL: [" + url + "]");

    Request.Builder requestBuilder = new Request.Builder();
    requestBuilder.url(url);
    
    for (Pair<String, String> pair : HEADER_LIST) {
        requestBuilder.addHeader(pair.getFirst(), pair.getSecond());
    }

    List<String> headerDebug = requestBuilder.getHeaders$okhttp().getNamesAndValues$okhttp();
    for (int i = 0; i < headerDebug.size(); i = i+2) {
        System.out.println("Header item: " + headerDebug.get(i) + ":" + headerDebug.get(i + 1));
    }
    
    Request request = requestBuilder.build();
    
    OkHttpClient.Builder okHttpBuilder = new OkHttpClient.Builder();
    // There seems to be no difference if requestBuilder.addHeader() is used or if the HeaderInterceptor is used.  I left this here in case it's needed later.
    okHttpBuilder.addInterceptor(fgo.new HeaderInterceptor("User-Agent", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) PostmanCanary/11.2.14-canary240621-0734 Electron/20.3.11 Safari/537.36"));
    OkHttpClient client = okHttpBuilder.build();

    Call call = client.newCall(request);
    
    Response response = call.execute();

    System.out.println("response " + response.code() + " (should be 302)");
        
    System.out.println("response body\n" + response.body().string());

}

class HeaderInterceptor implements Interceptor {
    private String mVariableValue;
    private String mVariableName;

    public HeaderInterceptor(String variableName, String variableValue) {
        mVariableName = variableName;
        mVariableValue = variableValue;
    }

    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request()
                .newBuilder()
                .header(mVariableName, mVariableValue)
                .build();
        return chain.proceed(request);
    }
}
}

That code requires the okhhtp library.

When I send the request to URL https://www.httpbin.org/get I get the same output in Postman and in Java, but when I send the request to URL https://sandbox.authz.flex.optum.com/oauth/authorize I get success the same output from httpbin.org but Optum gives me a 500 server error "E0020000".

The same call should return 302 not 500 server error

Comparing the output from httpbin, we see they are identical, irrespective if the originator is Postman or Java (Postman output, Java output). The only difference is the trace ID, which changes every time:

Automated comparison of Postman call and Java/okhttp call showing no differences

Note

If I change the content of the url parameters so they don't align with the expectations of the Optum API, I get a specific error, telling me which parameter is a problem. So the Optum site is properly able to analyze my request. It's just that once the request gets past the validity checks, it seems to be bombing. The fact that the Postman call works, though, allows the API support desk to shrug off my problem.

So, what else can be different between the two methods of making a call on the Optum sandbox URL: https://sandbox.authz.flex.optum.com/oauth/authorize? Why does one call from Postman work and the other call from Java not work? What else can I try?


Solution

  • Found the issue. It's not the header problem.

    You need to disable followRedirects in OkHttpClient. By default, OkHttpClient was following the redirects which was causing 500 error in your case.

    Fix: Add this okHttpBuilder.followRedirects(false) in your code. You will get result as 302 which is expected.

    Tested code:

    public class FirstGetOptum {
    
        //============================= URL PARAMETERS ================================================================
    
        public static final ArrayList<Pair<String, String>> URL_PARAM_LIST = new ArrayList<>();
    
        static {
            URL_PARAM_LIST.add(new Pair<>("response_type", "code"));
            URL_PARAM_LIST.add(new Pair<>("client_id", "55796a71-8104-4625-b259-bb91e9f13a60"));
            URL_PARAM_LIST.add(new Pair<>("state", "0124"));
            URL_PARAM_LIST.add(new Pair<>("scope", "patient/Patient.read"));
            URL_PARAM_LIST.add(new Pair<>("redirect_uri", "https://sites.google.com/sengsational.com/privacy/privacypolicy"));
            URL_PARAM_LIST.add(new Pair<>("code_challenge", "s6kElxScJMXGilr1VTwZYsjlq5XexWCUn94rmO7Y29o")); // optionally replaced in main()
            URL_PARAM_LIST.add(new Pair<>("code_challenge_method", "S256"));
        }
    
        private static String generateCodeChallenge() throws Exception {
            // Construct the code challenge url parameter
            StringBuilder sb = new StringBuilder();
            String characters = "01234567890abcde";
            Random random = new Random();
            for (int i = 0; i < 56; i++) {
                sb.append(characters.charAt(random.nextInt(characters.length())));
            }
            String randomText = sb.toString();
    
            // Temporarily override the random text with the same text each time
            randomText = "6b890b254542c9de4603278153e1b127d21730d46ac2620e6e35514c";
    
            byte[] binaryData = null;
            try {
                binaryData = MessageDigest.getInstance("SHA-256").digest(randomText.getBytes(StandardCharsets.UTF_8));
            } catch (NoSuchAlgorithmException e) {
                throw new Exception("Failed SHA-256");
            }
    
            Base64.Encoder encoder = Base64.getUrlEncoder();
            String codeChallenge = encoder.encodeToString(binaryData);
            codeChallenge = codeChallenge.replaceAll("=", ""); // remove pad
            return codeChallenge;
        }
    
    
        public static void main(String[] args) throws Exception {
            FirstGetOptum fgo = new FirstGetOptum(); // for static initializers
    
            // Use this boolean to run the test or the actual call
            HttpUrl.Builder urlBuilder = null;
    
            boolean getHttpBinDump = false;
    
            if (getHttpBinDump) {
                urlBuilder = HttpUrl.parse("https://www.httpbin.org/get").newBuilder();
            } else {
                urlBuilder = HttpUrl.parse("https://sandbox.authz.flex.optum.com/oauth/authorize").newBuilder();
            }
    
            for (Pair<String, String> pair : URL_PARAM_LIST) {
                urlBuilder.addQueryParameter(pair.getFirst(), pair.getSecond());
            }
    
            boolean replaceCodeChallengeValue = true;
            if (replaceCodeChallengeValue) {
                String codeChallenge = generateCodeChallenge();
                System.out.println("generated codeChallenge [" + codeChallenge + "]");
                urlBuilder.setQueryParameter("code_challenge", codeChallenge);
                urlBuilder.setQueryParameter("code_challenge_method", "S256");
            }
    
            // It's not supposed to make a difference, but this bit rearranges to match the order that Postman has it
            String url = urlBuilder.build().toString();
            url = url.replaceAll("&code_challenge_method=S256", "") + "&code_challenge_method=S256";
    
            System.out.println("Constructed the URL: [" + url + "]");
    
            OkHttpClient.Builder okHttpBuilder = new OkHttpClient.Builder();
    
            // There seems to be no difference if requestBuilder.addHeader() is used or if the HeaderInterceptor is used.  I left this here in case it's needed later.
            okHttpBuilder.addInterceptor(fgo.new HeaderInterceptor("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"));
            okHttpBuilder.addInterceptor(fgo.new HeaderInterceptor("Upgrade-Insecure-Requests", "1"));
            okHttpBuilder.addInterceptor(fgo.new HeaderInterceptor("Sec-Fetch-Site", "none"));
            okHttpBuilder.addInterceptor(fgo.new HeaderInterceptor("Sec-Fetch-Mode", "navigate"));
            okHttpBuilder.addInterceptor(fgo.new HeaderInterceptor("Sec-Fetch-User", "?1"));
            okHttpBuilder.addInterceptor(fgo.new HeaderInterceptor("Sec-Fetch-Dest", "document"));
            okHttpBuilder.addInterceptor(fgo.new HeaderInterceptor("Accept-Encoding", "gzip, deflate, br"));
            okHttpBuilder.addInterceptor(fgo.new HeaderInterceptor("Accept-Language", "en-US"));
            okHttpBuilder.addInterceptor(fgo.new HeaderInterceptor("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"));
            okHttpBuilder.followRedirects(false);
    
            Call call = okHttpBuilder.build().newCall(new Request.Builder().url(url).build());
            Response response = call.execute();
    
            System.out.println("response " + response.code() + " (should be 302)");
            System.out.println("response body\n" + response.body().string());
        }
    
        class HeaderInterceptor implements Interceptor {
            private String mVariableValue;
            private String mVariableName;
    
            public HeaderInterceptor(String variableName, String variableValue) {
                mVariableName = variableName;
                mVariableValue = variableValue;
            }
    
            @Override
            public Response intercept(Chain chain) throws IOException {
                Request request = chain.request()
                        .newBuilder()
                        .header(mVariableName, mVariableValue)
                        .build();
                return chain.proceed(request);
            }
        }
    }
    

    Output:

    • When getHttpBinDump is false (TestAuthOptumSandbox):
    generated codeChallenge [EKhdok_1ZKtBrevixgZqXxEHxn5pQjKkFA4bTVOmyH4]
    Constructed the URL: [https://sandbox.authz.flex.optum.com/oauth/authorize?response_type=code&client_id=55796a71-8104-4625-b259-bb91e9f13a60&state=0124&scope=patient%2FPatient.read&redirect_uri=https%3A%2F%2Fsites.google.com%2Fsengsational.com%2Fprivacy%2Fprivacypolicy&code_challenge=EKhdok_1ZKtBrevixgZqXxEHxn5pQjKkFA4bTVOmyH4&code_challenge_method=S256]
    response 302 (should be 302)
    response body
    
    • When getHttpBinDump is true (TestAuthHttpBin):
    generated codeChallenge [EKhdok_1ZKtBrevixgZqXxEHxn5pQjKkFA4bTVOmyH4]
    Constructed the URL: [https://www.httpbin.org/get?response_type=code&client_id=55796a71-8104-4625-b259-bb91e9f13a60&state=0124&scope=patient%2FPatient.read&redirect_uri=https%3A%2F%2Fsites.google.com%2Fsengsational.com%2Fprivacy%2Fprivacypolicy&code_challenge=EKhdok_1ZKtBrevixgZqXxEHxn5pQjKkFA4bTVOmyH4&code_challenge_method=S256]
    response 200 (should be 302)
    response body
    {
      "args": {
        "client_id": "55796a71-8104-4625-b259-bb91e9f13a60", 
        "code_challenge": "EKhdok_1ZKtBrevixgZqXxEHxn5pQjKkFA4bTVOmyH4", 
        "code_challenge_method": "S256", 
        "redirect_uri": "https://sites.google.com/sengsational.com/privacy/privacypolicy", 
        "response_type": "code", 
        "scope": "patient/Patient.read", 
        "state": "0124"
      }, 
      "headers": {
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", 
        "Accept-Encoding": "gzip, deflate, br", 
        "Accept-Language": "en-US", 
        "Host": "www.httpbin.org", 
        "Sec-Fetch-Dest": "document", 
        "Sec-Fetch-Mode": "navigate", 
        "Sec-Fetch-Site": "none", 
        "Sec-Fetch-User": "?1", 
        "Upgrade-Insecure-Requests": "1", 
        "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36", 
        "X-Amzn-Trace-Id": "Root=1-66daa962-4a7c521c2b0e18a461abed67"
      }, 
      "origin": "49.37.250.89", 
      "url": "https://www.httpbin.org/get?response_type=code&client_id=55796a71-8104-4625-b259-bb91e9f13a60&state=0124&scope=patient%2FPatient.read&redirect_uri=https%3A%2F%2Fsites.google.com%2Fsengsational.com%2Fprivacy%2Fprivacypolicy&code_challenge=EKhdok_1ZKtBrevixgZqXxEHxn5pQjKkFA4bTVOmyH4&code_challenge_method=S256"
    }
    

    See if this helps.