You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Expo AuthSession对接Let's Encrypt证书Keycloak,Android换JWT报错

Expo React Native + Keycloak (Let's Encrypt) 认证问题:Android端exchangeCodeAsync报错

问题背景

用Expo开发React Native应用,通过Let's Encrypt签发SSL证书的Keycloak实现认证。Web浏览器、Postman、iOS端均能正常获取Token,但Android端调用exchangeCodeAsync换取JWT时一直返回Error: An unknown error occurred,卡了一个月未解决。

核心代码如下:

import React from "react";
import {
  makeRedirectUri,
  useAuthRequest,
  exchangeCodeAsync,
  CodeChallengeMethod,
} from "expo-auth-session";
import { Button, View, StyleSheet } from "react-native";

// WebBrowser.maybeCompleteAuthSession();

const discovery = {
  authorizationEndpoint:
    "https://<MY DOMAIN SIGNED BY LETSENCRYPT>/protocol/openid-connect/auth",
  tokenEndpoint:
    "https://<MY DOMAIN SIGNED BY LETSENCRYPT>/protocol/openid-connect/token",
};

export default function App() {
  const [secretCode, setSecretCode] = React.useState("");

  const [request, response, promptAsync] = useAuthRequest(
    {
      clientId: "<MY CLIENT ID>",
      redirectUri: makeRedirectUri({}),
      usePKCE: true,
      codeChallenge: "",
      codeChallengeMethod: CodeChallengeMethod.S256,
      responseType: "code",
      // state: code_state,
    },
    discovery
  );

  React.useEffect(() => {
    if (response?.type === "success") {
      const { code } = response.params;
      setSecretCode(code);
      console.log("code :>> ", code);
    }
  }, [response]);

  const getToken = async () => {
    try {
      const requestToken = exchangeCodeAsync(
        {
          code: secretCode,
          redirectUri: makeRedirectUri({}),
          clientId: "<MY CLIENT ID>",
          clientSecret: "<MY CLIENT SECRET>",
          extraParams: {
            code_verifier: request?.codeVerifier ? request?.codeVerifier : "",
          },
        },
        discovery
      );
      console.log("requestToken :>> ", requestToken);
    } catch (err) {
      console.log(err);
    }
  };

  return (
    <View style={styles.container}>
      <View>
        <Button
          disabled={!request}
          title="Login"
          onPress={() => {
            promptAsync();
          }}
        />
      </View>
      {response && <Button title="Obtain Token" onPress={() => getToken()} />}
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    alignItems: "center",
    justifyContent: "center",
  },
});

排查与修复方案

1. 修复PKCE参数错误

手动设置codeChallenge: ""会直接破坏PKCE流程,Expo的useAuthRequest会自动生成合法的code challenge和verifier,无需手动赋值。删除以下两行:

codeChallenge: "",
codeChallengeMethod: CodeChallengeMethod.S256,

修正后的useAuthRequest配置:

const [request, response, promptAsync] = useAuthRequest(
  {
    clientId: "<MY CLIENT ID>",
    redirectUri: makeRedirectUri({}),
    usePKCE: true,
    responseType: "code",
  },
  discovery
);

2. 给exchangeCodeAsync添加await

当前代码未加await,直接打印Promise对象,错误捕获也不完整。修改getToken函数:

const getToken = async () => {
  try {
    const requestToken = await exchangeCodeAsync(
      {
        code: secretCode,
        redirectUri: makeRedirectUri({}),
        clientId: "<MY CLIENT ID>",
        clientSecret: "<MY CLIENT SECRET>",
        extraParams: {
          code_verifier: request?.codeVerifier || "",
        },
      },
      discovery
    );
    console.log("requestToken :>> ", requestToken);
  } catch (err) {
    // 打印完整错误信息,方便定位问题
    console.error("获取Token失败:", err);
    console.error("错误详情:", JSON.stringify(err, null, 2));
  }
};

3. 恢复WebBrowser.maybeCompleteAuthSession

注释掉的WebBrowser.maybeCompleteAuthSession()是Android端处理认证回调的关键,取消注释并导入对应模块:

import { WebBrowser } from 'expo-web-browser';

WebBrowser.maybeCompleteAuthSession();

4. 处理Android对Let's Encrypt证书的兼容性

Android 7.0及以下系统默认不包含Let's Encrypt的ISRG Root X1根证书,会导致SSL握手失败。

  • 若需兼容旧设备,将ISRG Root X1证书放到应用res/raw目录,然后在app.json中配置网络安全规则:
    {
      "expo": {
        "android": {
          "networkSecurityConfig": {
            "domainConfigs": [
              {
                "domain": "<MY DOMAIN>",
                "trustAnchors": [
                  {
                    "certificates": ["isrgrootx1.pem"]
                  }
                ]
              }
            ]
          }
        }
      }
    }
    

5. 验证redirectUri一致性

打印makeRedirectUri()的结果,确保与Keycloak客户端配置的Valid Redirect URIs完全一致:

console.log("当前Redirect URI:", makeRedirectUri({}));

Android端的redirectUri格式通常为exp://<你的IP>:<端口>/--/,需准确添加到Keycloak的对应配置项中。

6. 抓包查看真实请求错误

用Charles或Fiddler抓取Android端的Token请求,查看是否存在SSL错误或HTTP状态码异常(如400、401)——“未知错误”往往是被封装的具体请求问题。

7. 检查Keycloak客户端配置

  • 确认客户端Access Type设置为confidential(因使用了clientSecret)
  • 确保Standard Flow Enabled处于开启状态
  • 检查Token Endpoint是否被防火墙或IP限制拦截

内容的提问来源于stack exchange,提问作者Walid Bahhou

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.19 12:40:29