OAuth2Strategy

一种用于使用和实现 OAuth2 框架进行身份验证的策略,适用于 Google、Facebook、GitHub 等联合服务。

支持的运行时

运行时 是否支持
Node.js
Cloudflare
Deno

如何使用

安装

npm add remix-auth-oauth2

![警告] 如果你正在使用 Node.js v20 之前的版本,你需要使 WebCrypto API 在全局可用才能使用此包。

import { webcrypto } from "node:crypto";
globalThis.crypto = webcrypto;

或者在运行你的进程时启用实验性标志 --experimental-global-webcrypto。对于 v20 或更高版本,则无需此操作。

直接使用

你可以通过将此策略添加到你的身份验证器实例并配置正确的端点来使用它。

export let authenticator = new Authenticator<User>(sessionStorage);

authenticator.use(
  new OAuth2Strategy<
    User,
    { provider: "provider-name" },
    { id_token: string }
  >(
    {
      clientId: CLIENT_ID,
      clientSecret: CLIENT_SECRET,

      authorizationEndpoint: "https://provider.com/oauth2/authorize",
      tokenEndpoint: "https://provider.com/oauth2/token",
      redirectURI: "https://example.app/auth/callback",

      tokenRevocationEndpoint: "https://provider.com/oauth2/revoke", // optional

      codeChallengeMethod: "S256", // optional
      scopes: ["openid", "email", "profile"], // optional

      authenticateWith: "request_body", // optional
    },
    async ({ tokens, profile, context, request }) => {
      // here you can use the params above to get the user and return it
      // what you do inside this and how you find the user is up to you
      return await getUser(tokens, profile, context, request);
    },
  ),
  // this is optional, but if you setup more than one OAuth2 instance you will
  // need to set a custom name to each one
  "provider-name",
);

使用刷新令牌

该策略公开了一个公共的 refreshToken 方法,你可以使用它来刷新访问令牌。

let strategy = new OAuth2Strategy<User>(options, verify);
let tokens = await strategy.refreshToken(refreshToken);

刷新令牌是验证回调接收到的 tokens 对象的一部分。如何存储它以调用 strategy.refreshToken 以及在刷新后如何处理 tokens 对象,都由你自己决定。

最常见的方法是将刷新令牌存储在用户数据中,然后在刷新令牌后更新会话。

authenticator.use(
  new OAuth2Strategy<User>(
    options,
    async ({ tokens, profile, context, request }) => {
      let user = await getUser(tokens, profile, context, request);
      let { access_token: accessToken, refresh_token: refreshToken } = tokens;
      return { ...user, accessToken, refreshToken };
    },
  ),
);

// later in your code
let user = await authenticator.isAuthenticated(request, {
  failureRedirect: "/login",
});

let session = await sessionStorage.getSession(request.headers.get("cookie"));

let tokens = await strategy.refreshToken(user.refreshToken);

session.set(authenticator.sessionKey, {
  ...user,
  accessToken: tokens.accessToken,
  refreshToken: tokens.refreshToken,
});

// commit the session here

注销用户

如果你想注销用户,除了清除应用程序会话之外,还可以撤销用户在提供商处拥有的访问令牌。

let user = await authenticator.isAuthenticated(request, {
  failureRedirect: "/login",
});

let tokens = await strategy.revokeToken(user.accessToken);

扩展策略

你可以使用此策略作为另一个使用 OAuth2 框架的策略的基类。这样,你就不需要在自定义策略中自己实现整个 OAuth2 流程。

OAuth2Strategy 将为你处理整个流程,并允许你在需要的地方替换部分流程。

让我们看看如何使用 OAuth2Strategy 作为基类来实现 Auth0Strategy

// We need to import from Remix Auth the type of the strategy verify callback
import type { StrategyVerifyCallback } from "remix-auth";

// We need to import the OAuth2Strategy, the verify params and the profile interfaces
import type {
  OAuth2Profile,
  OAuth2StrategyVerifyParams,
  TokenResponseBody,
} from "remix-auth-oauth2";

import { OAuth2Strategy } from "remix-auth-oauth2";

// These are the custom options we need from the developer to use the strategy
export interface Auth0StrategyOptions
  extends Omit<
    OAuth2StrategyOptions,
    "authorizationEndpoint" | "tokenEndpoint" | "tokenRevocationEndpoint"
  > {
  domain: string;
  audience?: string;
}

// The Auth0Profile extends the OAuth2Profile with the extra params and mark
// some of them as required
export interface Auth0Profile extends OAuth2Profile {
  id: string;
  displayName: string;
  name: {
    familyName: string;
    givenName: string;
    middleName: string;
  };
  emails: Array<{ value: string }>;
  photos: Array<{ value: string }>;
  _json: {
    sub: string;
    name: string;
    given_name: string;
    family_name: string;
    middle_name: string;
    nickname: string;
    preferred_username: string;
    profile: string;
    picture: string;
    website: string;
    email: string;
    email_verified: boolean;
    gender: string;
    birthdate: string;
    zoneinfo: string;
    locale: string;
    phone_number: string;
    phone_number_verified: boolean;
    address: {
      country: string;
    };
    updated_at: string;
  };
}

interface Auth0ExtraParams extends Record<string, unknown> {
  id_token: string;
}

// And we create our strategy extending the OAuth2Strategy, we also need to
// pass the User as we did on the FormStrategy, we pass the Auth0Profile and the
// extra params
export class Auth0Strategy<User> extends OAuth2Strategy<
  User,
  Auth0Profile,
  Auth0ExtraParams
> {
  // The OAuth2Strategy already has a name but we override it to be specific of
  // the service we are using
  name = "auth0";

  private userInfoEndpoint: string;

  // We receive our custom options and our verify callback
  constructor(
    { domain, audience, ...options }: Auth0StrategyOptions,
    // Here we type the verify callback as a StrategyVerifyCallback receiving
    // the User type and the OAuth2StrategyVerifyParams with the Auth0Profile.
    verify: StrategyVerifyCallback<
      User,
      OAuth2StrategyVerifyParams<Auth0Profile, Auth0ExtraParams>
    >,
  ) {
    // And we pass the options to the super constructor using our own options
    // to generate them, this was we can ask less configuration to the developer
    // using our strategy
    super(
      {
        authorizationEndpoint: `https://${domain}/authorize`,
        tokenEndpoint: `https://${domain}/oauth/token`,
        tokenRevocationEndpoint: `https://${domain}/oauth/revoke`
        ...options,
      },
      verify,
    );

    this.userInfoEndpoint = `https://${domain}/userinfo`;
    this.audience = audience;
  }

  // We override the protected authorizationParams method to return a new
  // URLSearchParams with custom params we want to send to the authorizationURL.
  // Here we add the scope so Auth0 can use it, you can pass any extra param
  // you need to send to the authorizationURL here base on your provider.
  // The `request` argument represents the entire Request object, allowing you
  // to access various aspects of the incoming request, such as URL search parameters,
  // headers, or other request-specific data. This flexibility enables you to
  // dynamically set additional URL search parameters based on specific conditions
  // or user input. For example, you might want to include a 'screen_hint' parameter.
  protected authorizationParams(
    params: URLSearchParams,
    request?: Request,
  ): URLSearchParams {
    if (this.audience) params.set("audience", this.audience);
    if (new URL(request.url).searchParams.get('example')) params.set('example', 'example');
    return params;
  }

  // We also override how to use the accessToken to get the profile of the user.
  // Here we fetch a Auth0 specific URL, get the profile data, and build the
  // object based on the Auth0Profile interface.
  protected async userProfile(
    tokens: TokenResponseBody & Auth0ExtraParams,
  ): Promise<Auth0Profile> {
    let response = await fetch(this.userInfoEndpoint, {
      headers: { Authorization: `Bearer ${tokens.access_token}` },
    });

    let data: Auth0Profile["_json"] = await response.json();

    let profile: Auth0Profile = {
      provider: "auth0",
      displayName: data.name,
      id: data.sub,
      name: {
        familyName: data.family_name,
        givenName: data.given_name,
        middleName: data.middle_name,
      },
      emails: [{ value: data.email }],
      photos: [{ value: data.picture }],
      _json: data,
    };

    return profile;
  }
}

就是这样,感谢 OAuth2Strategy,我们不需要自己实现整个 OAuth2 流程,而是可以专注于我们策略的独特部分,即用户配置文件和我们的提供商可能要求我们发送的额外搜索参数。