Expo AuthSession对接Let's Encrypt证书Keycloak,Android换JWT报错
问题背景
用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

