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

编辑页面

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


For the complete documentation index, see llms.txt. Use this file to discover all available pages.

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

所有身份验证提供方的规则

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

  • 使用 WebBrowser.maybeCompleteAuthSession() 来关闭 web 弹出窗口。如果忘记添加这一行,弹窗将不会关闭。
  • 使用 AuthSession.makeRedirectUri() 创建重定向 URI,这会处理跨平台支持中的许多繁琐工作。在底层,它使用了 expo-linking
  • 使用 AuthSession.useAuthRequest() 构建请求,该 hook 支持异步初始化,这意味着移动浏览器不会阻塞身份验证。
  • request 定义之前,请确保禁用提示。
  • 在 web 上只能在用户交互中调用 promptAsync
  • 由于无法自定义应用 scheme,Expo Go 不能用于 OAuth 或启用 OpenID Connect 的应用的本地开发和测试。你可以改用 Development Build,它提供类似 Expo Go 的开发体验,并支持登录后通过 OAuth 重定向回你的应用,其工作方式与生产环境完全一致。

获取访问令牌

大多数提供方使用 OAuth 2 标准进行安全身份验证和授权。在授权码授权流程中,身份提供方会返回一个一次性代码。然后使用该代码换取用户的访问令牌。

由于你的客户端应用代码不是安全存储密钥的地方,因此有必要在服务器中交换授权码,例如通过 API 路由React Server Components。这将允许你安全地存储和使用客户端密钥,以访问提供方的令牌端点。

示例

以下示例展示了如何使用 AuthSession API 与几个常见提供方进行身份验证。

网站提供方PKCE自动发现
获取你的配置OAuth 2.0支持不可用
  • 提供方每个应用只允许一个重定向 URI。你需要为每种想使用的方法分别创建一个独立的应用:
    • 独立版 / 开发构建:com.your.app://*
    • Web:https://yourwebsite.com/*
  • redirectUri 需要两个斜杠(://)。
  • revocationEndpoint 是动态的,并且需要你的 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(); // 端点 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 会为你提供一个。
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() { // 端点 const discovery = useAutoDiscovery('https://<OKTA_DOMAIN>.com/oauth2/default'); // 请求 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(); }} /> ); }

Redirect URI Patterns

Here are some common examples of redirect URI patterns you might end up using.

Standalone/Development Builds

yourscheme://path

In some cases, there may be 1 to 3 slashes (/).

  • Environment:

    • Existing React Native app
      • npx expo prebuild
    • Standalone build in the App or Play Store, or local testing
      • Android: eas build or npx expo run:android
      • iOS: eas build or npx expo run:ios
  • Creation: When running in the correct environment, use AuthSession.makeRedirectUri({ native: '<YOUR_URI>' }) to select native.

    • 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()
    • This link is usually created automatically, but we recommend that you at least define the scheme property. The entire URL can be overridden in the app by passing the native property. This is usually used by providers like Google or Okta that require you to use a custom native URI redirect. You can use npx uri-scheme to add, list, and open URI schemes.
    • If you change expo.scheme, run npx expo prebuild --clean to regenerate the native project with the new scheme, then rebuild with npx expo run:android and npx expo run:ios.
  • Usage: promptAsync({ redirectUri })

Improving User Experience

The “login flow” is a very important part of the process, and in many cases, it’s where users will once again commit to using your app. A poor experience may cause users to abandon it before they even really start using it.

Here are some tips that can make user authentication faster, simpler, and more secure:

Warming Up the Browser

On Android, you can optionally warm up the web browser before using it. This allows the browser app to pre-initialize itself in the background. Doing so can significantly speed up the authentication prompt shown to the user.

import { useEffect } from 'react'; import * as WebBrowser from 'expo-web-browser'; function App() { useEffect(() => { WebBrowser.warmUpAsync(); return () => { WebBrowser.coolDownAsync(); }; }, []); // 执行身份验证 ... }

隐式登录

由于没有安全的方式将客户端密钥存储在你的应用包中,历史上许多提供商都提供了“隐式流程”,使你无需客户端密钥即可请求访问令牌。由于存在固有的安全风险,包括访问令牌注入的风险,这种方式已不再推荐。 取而代之的是,大多数提供商现在都支持带有 PKCE(Proof Key for Code Exchange)扩展的授权码流程,以便在你的客户端应用代码中安全地将授权码交换为访问令牌。了解更多关于从隐式流程迁移到带有 PKCE 的授权码流程

expo-auth-session 仍然支持隐式流程,以用于兼容旧代码。下面是一个隐式流程的示例实现。

import { useEffect } from 'react'; import * as WebBrowser from 'expo-web-browser'; import { makeRedirectUri, useAuthRequest, ResponseType } from 'expo-auth-session'; WebBrowser.maybeCompleteAuthSession(); // 端点 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="登录" />; }

存储数据

在 Android 和 iOS 等原生平台上,你可以使用名为 expo-secure-store 的库在本地安全地存储诸如访问令牌之类的数据(这不同于不安全的 AsyncStorage)。它在 Android 上提供对加密 SharedPreferences 的原生访问,并在 iOS 上提供对 keychain services 的原生访问。Web 上没有与此功能对应的实现。

你可以存储你的身份验证结果,并在之后重新恢复它们,从而避免再次提示用户登录。

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') { // 将认证安全地存储在你的设备上 SecureStore.setItemAsync(MY_SECURE_AUTH_STATE_KEY, storageValue); } } }, [response]); // 更多登录代码... }