使用 OAuth 或 OpenID 提供商进行身份验证

了解如何利用 expo-auth-session 库通过 OAuth 或 OpenID 提供商实现身份验证。


expo-auth-session 提供了统一的 API,用于在 Android、iOS 和 Web 上实现 OAuth 和 OpenID Connect 提供程序。本指南将通过几个示例向你展示如何使用 AuthSession API。

¥expo-auth-session provides a unified API for implementing OAuth and OpenID Connect providers on Android, iOS, and web. This guide will show you how to use the AuthSession API using a few examples.

所有身份验证提供程序的规则

¥Rules for all authentication providers

使用 AuthSession API 时,以下规则适用于所有身份验证提供程序:

¥When using the AuthSession API, the following rules apply to all authentication providers:

  • 使用 WebBrowser.maybeCompleteAuthSession() 关闭 Web 弹出窗口。如果你忘记添加此项,则弹出窗口将不会关闭。

    ¥Use WebBrowser.maybeCompleteAuthSession() to dismiss the web popup. If you forget to add this then the popup window will not close.

  • 使用 AuthSession.makeRedirectUri() 创建重定向,这完成了与通用平台支持相关的大量繁重工作。在幕后,它使用 expo-linking

    ¥Create redirects with AuthSession.makeRedirectUri() this does a lot of the heavy lifting involved with universal platform support. Behind the scenes, it uses expo-linking.

  • 使用 AuthSession.useAuthRequest() 构建请求,该钩子允许异步设置,这意味着移动浏览器不会阻止身份验证。

    ¥Build requests using AuthSession.useAuthRequest(), the hook allows for async setup which means mobile browsers won't block the authentication.

  • 确保在定义 request 之前禁用提示。

    ¥Be sure to disable the prompt until request is defined.

  • 你只能在 Web 上的用户交互中调用 promptAsync

    ¥You can only invoke promptAsync in user interaction on the web.

  • 由于无法自定义你的应用方案,Expo Go 无法用于本地开发和测试支持 OAuth 或 OpenID Connect 的应用。你可以改为使用 开发构建,它可以实现类似于 Expo Go 的开发体验,并支持登录后 OAuth 重定向回你的应用,其工作方式与生产中的工作方式相同。

    ¥Expo Go cannot be used for local development and testing of OAuth or OpenID Connect-enabled apps due to the inability to customize your app scheme. You can instead use a Development Build, which enables an Expo Go-like development experience and supports OAuth redirects back to your app after login in a manner that works just like it would in production.

获取访问令牌

¥Obtaining access tokens

大多数提供程序使用 OAuth 2 标准进行安全身份验证和授权。在授权代码授予中,身份提供者返回一次性代码。然后将此代码交换为用户的访问令牌。

¥Most providers use the OAuth 2 standard for secure authentication and authorization. In the authorization code grant, the identity provider returns a one-time code. This code is then exchanged for the user's access token.

由于 你的客户端应用代码不是存储密钥的安全场所,有必要在服务器中交换授权代码,例如与 API 路由反应服务器组件。这将允许你安全地存储和使用客户端密钥来访问提供商的令牌端点。

¥Since your client application code is not a secure place to store secrets, it is necessary to exchange the authorization code in a server such as with API routes or React Server Components. This will allow you to securely store and use a client secret to access the provider's token endpoint.

示例

¥Examples

以下示例展示了如何使用 AuthSession API 与一些常用提供商进行身份验证。

¥The following examples show how to use the AuthSession API to authenticate with a few popular providers.

网站提供者PKCE自动发现
获取你的配置OAuth 2.0支持的无法使用
  • 提供商仅允许每个应用使用一个重定向 URI。对于要使用的每种方法,你都需要一个单独的应用:

    ¥Provider only allows one redirect URI per app. You'll need an individual app for every method you want to use:

    • 独立/开发构建:com.your.app://*

      ¥Standalone / development build: com.your.app://*

    • 网址:https://yourwebsite.com/*

      ¥Web: https://yourwebsite.com/*

  • redirectUri 需要两个斜杠(://)。

    ¥The redirectUri requires two slashes (://).

  • revocationEndpoint 是动态的,需要你的 config.clientId

    ¥revocationEndpoint is dynamic and requires your config.clientId.

GitHub Auth Example
import { useEffect } from 'react';
import * as WebBrowser from 'expo-web-browser';
import { makeRedirectUri, useAuthRequest } from 'expo-auth-session';
import { Button } from 'react-native';

WebBrowser.maybeCompleteAuthSession();

// Endpoint
const discovery = {
  authorizationEndpoint: 'https://github.com/login/oauth/authorize',
  tokenEndpoint: 'https://github.com/login/oauth/access_token',
  revocationEndpoint: 'https://github.com/settings/connections/applications/<CLIENT_ID>',
};

export default function App() {
  const [request, response, promptAsync] = useAuthRequest(
    {
      clientId: 'CLIENT_ID',
      scopes: ['identity'],
      redirectUri: makeRedirectUri({
        scheme: 'your.app'
      }),
    },
    discovery
  );

  useEffect(() => {
    if (response?.type === 'success') {
      const { code } = response.params;
    }
  }, [response]);

  return (
    <Button
      disabled={!request}
      title="Login"
      onPress={() => {
        promptAsync();
      }}
    />
  );
}
网站提供者PKCE自动发现
报名 > 应用OpenID支持的可用的
  • 你无法定义自定义 redirectUri,Okta 将为你提供一个。

    ¥You cannot define a custom redirectUri, Okta will provide you with one.

Okta Auth Example
import { useEffect } from 'react';
import * as WebBrowser from 'expo-web-browser';
import { makeRedirectUri, useAuthRequest, useAutoDiscovery } from 'expo-auth-session';
import { Button, Platform } from 'react-native';

WebBrowser.maybeCompleteAuthSession();

export default function App() {
  // Endpoint
  const discovery = useAutoDiscovery('https://<OKTA_DOMAIN>.com/oauth2/default');
  // Request
  const [request, response, promptAsync] = useAuthRequest(
    {
      clientId: 'CLIENT_ID',
      scopes: ['openid', 'profile'],
      redirectUri: makeRedirectUri({
        native: 'com.okta.<OKTA_DOMAIN>:/callback',
      }),
    },
    discovery
  );

  useEffect(() => {
    if (response?.type === 'success') {
      const { code } = response.params;
    }
  }, [response]);

  return (
    <Button
      disabled={!request}
      title="Login"
      onPress={() => {
        promptAsync();
      }}
    />
  );
}

重定向 URI 模式

¥Redirect URI patterns

以下是你可能最终使用的一些常见重定向 URI 模式的示例。

¥Here are a few examples of some common redirect URI patterns you may end up using.

独立/开发构建

¥Standalone/development build

yourscheme://path

在某些情况下,会有 1 到 3 个斜杠 (/)。

¥In some cases there will be anywhere between 1 to 3 slashes (/).

  • 环境:

    ¥Environment:

    • 裸工作流程

      ¥Bare workflow

      • npx expo prebuild
    • 在 App 或 Play Store 中独立构建或在本地测试

      ¥Standalone builds in the App or Play Store or testing locally

      • 安卓:eas buildnpx expo run:android

        ¥Android: eas build or npx expo run:android

      • iOS:eas buildnpx expo run:ios

        ¥iOS: eas build or npx expo run:ios

  • 创造:在正确的环境中运行时使用 AuthSession.makeRedirectUri({ native: '<YOUR_URI>' }) 选择原生。

    ¥Create: Use AuthSession.makeRedirectUri({ native: '<YOUR_URI>' }) to select native when running in the correct environment.

    • your.app://redirect -> makeRedirectUri({ scheme: 'your.app', path: 'redirect' })

    • your.app:/// -> makeRedirectUri({ scheme: 'your.app', isTripleSlashed: true })

    • your.app:/authorize -> makeRedirectUri({ native: 'your.app:/authorize' })

    • your.app://auth?foo=bar -> makeRedirectUri({ scheme: 'your.app', path: 'auth', queryParams: { foo: 'bar' } })

    • exp://u.expo.dev/[project-id]?channel-name=[channel-name]&runtime-version=[runtime-version] -> makeRedirectUri()

    • 此链接通常可以自动创建,但我们建议你至少定义 scheme 属性。可以通过传递 native 属性在应用中覆盖整个 URL。通常,这将用于 Google 或 Okta 等要求你使用自定义原生 URI 重定向的提供商。你可以使用 npx uri-scheme 添加、列出和打开 URI 方案。

      ¥This link can often be created automatically but we recommend you define the scheme property at least. The entire URL can be overridden in apps by passing the native property. Often this will be used for providers like Google or Okta which require you to use a custom native URI redirect. You can add, list, and open URI schemes using npx uri-scheme.

    • 如果在弹出后更改 expo.scheme,则需要使用 expo apply 命令将更改应用到原生项目,然后重建它们(yarn iosyarn android)。

      ¥If you change the expo.scheme after ejecting then you'll need to use the expo apply command to apply the changes to your native project, then rebuild them (yarn ios, yarn android).

  • 用法:promptAsync({ redirectUri })

    ¥Usage: promptAsync({ redirectUri })

改善用户体验

¥Improving user experience

"登录流程" 是一件需要做好的重要事情,在很多情况下,这是用户 promise 再次使用你的应用的地方。糟糕的体验可能会导致用户在真正开始使用你的应用之前就放弃它。

¥The "login flow" is an important thing to get right, in a lot of cases this is where the user will commit to using your app again. A bad experience can cause users to give up on your app before they've really gotten to use it.

以下是一些技巧,可帮助你快速、轻松、安全地进行用户身份验证:

¥Here are a few tips you can use to make authentication quick, easy, and secure for your users:

预热浏览器

¥Warming the browser

在 Android 上,你可以选择在使用网络浏览器之前对其进行预热。这允许浏览器应用在后台预先初始化自身。这样做可以显着加快提示用户进行身份验证的速度。

¥On Android you can optionally warm up the web browser before it's used. This allows the browser app to pre-initialize itself in the background. Doing this can significantly speed up prompting the user for authentication.

import { useEffect } from 'react';
import * as WebBrowser from 'expo-web-browser';

function App() {
  useEffect(() => {
    WebBrowser.warmUpAsync();

    return () => {
      WebBrowser.coolDownAsync();
    };
  }, []);

  // Do authentication ...
}

隐式登录

¥Implicit login

由于没有安全的方法来将客户端密钥存储在应用包中,因此历史上许多提供商都提供了 "隐式流",它使你能够在没有客户端密钥的情况下请求访问令牌。由于固有的安全风险(包括访问令牌注入的风险),不再建议这样做。相反,大多数提供商现在支持带有 PKCE(代码交换证明密钥)扩展的授权代码,以便在客户端应用代码中安全地交换授权代码以获取访问令牌。了解有关 从隐式流程过渡到使用 PKCE 的授权代码 的更多信息。

¥Because there was no secure way to do this to store client secrets in your app bundle, historically, many providers have offered an "Implicit flow" which enables you to request an access token without the client secret. This is no longer recommended due to inherent security risks, including the risk of access token injection. Instead, most providers now support the authorization code with PKCE (Proof Key for Code Exchange) extension to securely exchange an authorization code for an access token within your client app code. Learn more about transitioning from Implicit flow to authorization code with PKCE.

expo-auth-session 仍然支持用于旧版代码目的的隐式流。下面是隐式流程的示例实现。

¥expo-auth-session still supports Implicit flow for legacy code purposes. Below is an example implementation of the Implicit flow.

import { useEffect } from 'react';
import * as WebBrowser from 'expo-web-browser';
import { makeRedirectUri, useAuthRequest, ResponseType } from 'expo-auth-session';

WebBrowser.maybeCompleteAuthSession();

// Endpoint
const discovery = {
  authorizationEndpoint: 'https://accounts.spotify.com/authorize',
};

function App() {
  const [request, response, promptAsync] = useAuthRequest(
    {
      responseType: ResponseType.Token,
      clientId: 'CLIENT_ID',
      scopes: ['user-read-email', 'playlist-modify-public'],
      redirectUri: makeRedirectUri({
        scheme: 'your.app'
      }),
    },
    discovery
  );

  useEffect(() => {
    if (response && response.type === 'success') {
      const token = response.params.access_token;
    }
  }, [response]);

  return <Button disabled={!request} onPress={() => promptAsync()} title="Login" />;
}

存储数据

¥Storing data

在 Android 和 iOS 等原生平台上,你可以使用名为 expo-secure-store 的库在本地保护访问令牌等内容(这与不安全的 AsyncStorage 不同)。Android 上支持原生访问加密的 SharedPreferences,iOS 上支持 密钥扣服务。目前没有与此功能等效的 Web 应用。

¥On native platforms such as Android and iOS, you can secure things like access tokens locally using a library called expo-secure-store (This is different to AsyncStorage which is not secure). It provides native access to encrypted SharedPreferences on Android and keychain services on iOS and . There is no web equivalent to this functionality.

你可以存储身份验证结果并在以后重新水化它们,以避免提示用户再次登录。

¥You can store your authentication results and rehydrate them later to avoid having to prompt the user to login again.

import * as SecureStore from 'expo-secure-store';

const MY_SECURE_AUTH_STATE_KEY = 'MySecureAuthStateKey';

function App() {
  const [, response] = useAuthRequest({});

  useEffect(() => {
    if (response && response.type === 'success') {
      const auth = response.params;
      const storageValue = JSON.stringify(auth);

      if (Platform.OS !== 'web') {
        // Securely store the auth on your device
        SecureStore.setItemAsync(MY_SECURE_AUTH_STATE_KEY, storageValue);
      }
    }
  }, [response]);

  // More login code...
}