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:
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".
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:
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?
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:
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
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.