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

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_access scope已被添加到请求中,且客户端有权限请求该scope。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 18:37:01