如何使用 Java 和 okhttp 让 Optum 沙箱通过 OAuth 2.0 进行身份验证

问题描述 投票:0回答:1

我正在尝试获取一个使用 okhttp 正确使用 Optum API 进行身份验证的 Java 应用程序,如此处所述。 虽然我可以从测试工具获得正确的响应,但无法从 Java 应用程序获得正确的响应。

使用 API 文档中API 权限和范围部分中的信息,我能够在 Postman(用于测试 http 请求的应用程序)中进行调用。 当我在 Java 中复制调用时,当我期望 302 重定向时,我收到 500 服务器错误。 Optum 的 API 支持台无法解释为什么来自 Postman 的相同调用在 Java 中不起作用,所以我想我应该在这里问,以防有人发现如何解决这个问题,或者有我可以尝试的事情。

我得出的结论是,来自 Postman 的调用和来自 Java 的调用是等效的,即将请求发送到 https://httpbin.org,它会回显它所看到的内容。

我的 Postman 导出(包括

TestAuthHttpBin
TestAuthOptumSandbox
)可以在我的 github 项目上找到。如果您想自己测试的话,可以将其导入 Postman。导入集合,单击
Authorization
选项卡,然后针对每个请求单击
Get New Access Token
。这是邮递员屏幕: Postman Screen showing the real and test requests.

这是我正在运行的代码(可在此处获取),其设计相当于 Postman 调用:

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);
    }
}
}

该代码需要

okhhtp
库。

当我将请求发送到 URL

https://www.httpbin.org/get
时,我在 Postman 和 Java 中得到相同的输出,但是当我将请求发送到 URL
https://sandbox.authz.flex.optum.com/oauth/authorize
时,我从
httpbin.org
获得成功,但 Optum 给了我一个 500 服务器错误“E0020000”。

The same call should return 302 not 500 server error

比较 httpbin 的输出,我们发现它们是相同的,无论发起者是 Postman 还是 Java(Postman 输出Java 输出)。 唯一的区别是跟踪 ID,它每次都会改变:

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

注意

如果我更改了 url 参数的内容,使其与 Optum API 的期望不符,我会收到一个特定错误,告诉我哪个参数有问题。 所以 Optum 网站能够正确分析我的请求。 只是一旦请求通过了有效性检查,它看起来就像是轰炸。 不过,Postman 调用有效这一事实让 API 支持台不再理会我的问题。

那么,调用 Optum 沙箱 URL 的两种方法之间还有什么不同:

https://sandbox.authz.flex.optum.com/oauth/authorize
? 为什么 Postman 的一个调用有效,而 Java 的另一个调用无效? 我还能尝试什么?

java http-redirect oauth-2.0 postman okhttp
1个回答
0
投票

发现问题了。这不是标题问题。

您需要禁用

followRedirects
中的
OkHttpClient
。默认情况下,
OkHttpClient
遵循重定向,这在您的情况下导致
500
错误。

修复: 在您的代码中添加此

okHttpBuilder.followRedirects(false)
。 您将得到预期结果
302

更新代码:

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);
        }
    }
}

输出:

  • getHttpBinDump
    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
    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"
}

看看这是否有帮助。

© www.soinside.com 2019 - 2024. All rights reserved.