React Native基于Auth0社交登录无法获取Refresh Token求助
问题描述
在React Native应用中通过Auth0实现Facebook、Google、Apple社交登录,当前能获取Access Token,但无法获取Refresh Token。已在Auth0 API设置中开启离线访问权限,且scope中添加了offline_access,仍未成功。使用的是机器对机器(M2M)类型的Client ID,未使用Auth0官方SDK。
登录组件代码
const authProviders = { google: { name: "google-oauth2", icon: <GoogleIcon />, color: cs["apple"], }, facebook: { name: "facebook", icon: <FacebookIcon />, color: cs["facebook"], }, apple: { name: "apple", icon: <AppleIcon />, color: cs["apple"], }, }; const onSocialAuth = async (type: auth) => { const connection = type.name; dispatch(setProvider(connection)); const result = await LoginAs(connection); if (result.type === "success" && Platform.OS === "ios") { const accessToken = result.url ? result.url.match(/access_token=([^&]+)/) : null; const token = accessToken ? accessToken[1] : null; const { payload } = await dispatch( userSocialLogin({ access_token: token ?? "" }) ); if (payload?.data?.token) { await storeAsyncData(ENUM.PREVIOUS_AUTH_PROVIDER, type.name); dispatch(completeLogin(payload?.data?.token)); authContext.setAuth({ isUserLoggedIn: !!payload?.data?.token, token: payload?.data?.token, }); } else { alert("Something went wrong while logging with " + connection); } } else { // alert('Failed to log in'); } }; return ( <View style={{ flexDirection: "column", gap: 10, alignItems: "center" }}> {providers?.includes('google-oauth2') && <OutlinedButton loading={loading && provider === authProviders.google.name} buttonStyle={{ gap: 10, paddingVertical: verticalScale(10), flex: 1, }} onPress={() => { onSocialAuth(authProviders.google); }} icon={<GoogleIcon />}> Google </OutlinedButton>} {/* other login methods */} </View> );
登录处理方法
export const LoginAs = async (connection: string) => { const auth0RedirectUri = Platform.OS === "ios" ? `${process.env.EXPO_PUBLIC_AUTH0_REDIRECT_URI}/ios/com.hello.myapp/callback` : `${process.env.EXPO_PUBLIC_AUTH0_REDIRECT_URI}/android/com.hello.myapp/callback`; const auth0Domain = process.env.EXPO_PUBLIC_AUTH0_AUTH_DOMAIN; //env const clientId = process.env.EXPO_PUBLIC_AUTH0_CLIENT_ID; //production const audience = process.env.EXPO_PUBLIC_AUTH0_AUTH_AUDIENCE; const url = `${auth0Domain}/authorize?` + `response_type=token&` + `client_id=${clientId}&` + `redirect_uri=${encodeURIComponent(auth0RedirectUri)}&` + `scope=openid%20profile%20email&` + `audience=${encodeURIComponent(audience || "")}&` + `connection=${connection}`; const browserOptions = { preferEphemeralSession: false, createTask: true, }; const result = await WebBrowser.openAuthSessionAsync( url, auth0RedirectUri, browserOptions ); return result; };
添加offline_access后的返回结果
{ "url": "com.h3llo.fundisapp://hello.us.auth0.com/android/com.hello.myapp/callback#access_token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImFNT05hNEdCd2xQRjFCaURwQWxJRSJ9.eyJpc3MiOiJodHRwczovL2Z1bmRpc3VhdC51cy5hdXRoMC5jb20vIiwic3ViIjoiZ29vZ2xlLW9hdXRoMnwxMTQzMDY4NDQ4MzIxNTkxODA3MjYiLCJhdWQiOlsiaHR0cHM6Ly91YXR2Mi1hcGkuZnVuZGlzLmZpLyIsImh0dHBzOi8vZnVuZGlzdWF0LnVzLmF1dGgwLmNvbS91c2VyaW5mbyJdLCJpYXQiOjE3MzgxMjQ5MjYsImV4cCI6MTczODEzMjEyNiwic2NvcGUiOiJvcGVuaWQgcHJvZmlsZSBlbWFpbCBvZmZsaW5lX2FjY2VzcyIsImF6cCI6IkhQSnl4eUk2ak9CZnVzYmVPZTZXY3pNRk5Cb0xra25YIn0.VHj5hKbFvwhIyMejYA0_Cab01jlMRPg2G5RVRH8lMxkNYugrUmBDqa8JKOjjGxth4waMci6pB_c456XriPKj44Eh2UqjLgcXtVlL3txwCDWNQ-2JY9z74JAnZksGX4gtuG9vKhKR-T80vhQtVnZ3i3YASSuj3h2RF9Rf3Gvm1vRg3QE1lrErZ93HQso_CgLD6lp__XwRewAN_TQJpSVI7VNPOUizpIoKHhkCIl_ki9bHl-uvF63fwXF4X1uMAZT7umqoCgly4B0_04ZBamzyLiMH6pI6qxcFh81BKupDyHuUs7mCbIYHLPQ-bxmz_gabmu_SxhRGk_RzYj_iBkMOkQ&scope=openid%20profile%20email%20offline_access&expires_in=7200&token_type=Bearer", "type": "success" }
解决方案
1. 替换客户端类型:M2M客户端不适合用户登录
机器对机器(M2M)客户端是为服务间通信设计的,不会向用户端请求返回Refresh Token。你需要在Auth0控制台创建一个原生应用类型的客户端,将其Client ID替换当前的M2M Client ID。
2. 切换到授权码流(带PKCE)
当前使用的response_type=token是隐式流,该流程本身不支持返回Refresh Token。必须切换到授权码流+PKCE,这是Auth0推荐的移动端认证方式:
步骤1:生成PKCE的Code Verifier和Code Challenge
PKCE用于防止授权码被拦截,需要在发起认证请求前生成随机的code_verifier,并通过SHA-256哈希生成code_challenge。
import * as Crypto from 'expo-crypto'; // 生成随机Code Verifier const generateCodeVerifier = () => { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~'; let verifier = ''; for (let i = 0; i < 128; i++) { verifier += chars.charAt(Math.floor(Math.random() * chars.length)); } return verifier; }; // 生成Code Challenge(SHA-256哈希后Base64URL编码) const generateCodeChallenge = async (verifier: string) => { const digest = await Crypto.digestStringAsync( Crypto.CryptoDigestAlgorithm.SHA256, verifier ); // 将Hex转为Base64URL return btoa(digest) .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=+$/, ''); };
步骤2:修改LoginAs函数,使用授权码流
更新认证URL,将response_type改为code,添加PKCE参数,并保存code_verifier用于后续交换token:
// 全局或组件内保存code_verifier,用于后续交换token let codeVerifier = ''; export const LoginAs = async (connection: string) => { const auth0RedirectUri = Platform.OS === "ios" ? `${process.env.EXPO_PUBLIC_AUTH0_REDIRECT_URI}/ios/com.hello.myapp/callback` : `${process.env.EXPO_PUBLIC_AUTH0_REDIRECT_URI}/android/com.hello.myapp/callback`; const auth0Domain = process.env.EXPO_PUBLIC_AUTH0_AUTH_DOMAIN; const clientId = process.env.EXPO_PUBLIC_AUTH0_CLIENT_ID; // 替换为原生应用的Client ID const audience = process.env.EXPO_PUBLIC_AUTH0_AUTH_AUDIENCE; // 生成PKCE参数 codeVerifier = generateCodeVerifier(); const codeChallenge = await generateCodeChallenge(codeVerifier); const url = `${auth0Domain}/authorize?` + `response_type=code&` + // 改为授权码流 `client_id=${clientId}&` + `redirect_uri=${encodeURIComponent(auth0RedirectUri)}&` + `scope=openid%20profile%20email%20offline_access&` + // 保留offline_access `audience=${encodeURIComponent(audience || "")}&` + `connection=${connection}&` + `code_challenge=${codeChallenge}&` + `code_challenge_method=S256`; // 指定哈希算法 const browserOptions = { preferEphemeralSession: false, createTask: true, }; const result = await WebBrowser.openAuthSessionAsync( url, auth0RedirectUri, browserOptions ); return result; };
步骤3:交换授权码获取Access Token和Refresh Token
在onSocialAuth中,解析返回的code,然后调用Auth0的/oauth/token接口交换token:
const onSocialAuth = async (type: auth) => { const connection = type.name; dispatch(setProvider(connection)); const result = await LoginAs(connection); if (result.type === "success") { // 解析授权码 const codeMatch = result.url?.match(/code=([^&]+)/); const authCode = codeMatch ? codeMatch[1] : null; if (!authCode) { alert("Failed to retrieve authorization code"); return; } try { // 调用Auth0 token接口交换token const tokenResponse = await fetch(`${process.env.EXPO_PUBLIC_AUTH0_AUTH_DOMAIN}/oauth/token`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ grant_type: 'authorization_code', client_id: process.env.EXPO_PUBLIC_AUTH0_CLIENT_ID!, code: authCode, redirect_uri: Platform.OS === "ios" ? `${process.env.EXPO_PUBLIC_AUTH0_REDIRECT_URI}/ios/com.hello.myapp/callback` : `${process.env.EXPO_PUBLIC_AUTH0_REDIRECT_URI}/android/com.hello.myapp/callback`, code_verifier: codeVerifier, // 之前保存的code_verifier }), }); const tokenData = await tokenResponse.json(); if (tokenData.error) { throw new Error(tokenData.error_description); } // tokenData中会包含access_token、refresh_token、id_token等 const { access_token, refresh_token } = tokenData; // 后续调用你的后端接口,传入access_token const { payload } = await dispatch( userSocialLogin({ access_token }) ); if (payload?.data?.token) { await storeAsyncData(ENUM.PREVIOUS_AUTH_PROVIDER, type.name); // 保存refresh_token到本地存储(注意加密) await storeAsyncData('refresh_token', refresh_token); dispatch(completeLogin(payload?.data?.token)); authContext.setAuth({ isUserLoggedIn: !!payload?.data?.token, token: payload?.data?.token, }); } else { alert("Something went wrong while logging with " + connection); } } catch (error) { alert(`Login error: ${(error as Error).message}`); } } else { alert('Failed to log in'); } };
3. 确认Auth0配置
- 确保原生应用客户端的设置中,允许的回调URL包含你的应用跳转地址。
- 确认Auth0 API的"允许离线访问"已开启(在API设置的"设置"标签页)。
- 确保
offline_accessscope已被添加到请求中,且客户端有权限请求该scope。
内容的提问来源于stack exchange,提问作者psudo
相关产品推荐
相关产品推荐

