Remix Auth

RemixReact Router 應用程式提供簡易身份驗證。

功能

  • 完整的伺服器端身份驗證
  • 完整的 TypeScript 支援
  • 基於策略的身份驗證
  • 實作自訂策略

概述

Remix Auth 是一個完整的開放原始碼身份驗證解決方案,適用於 Remix 和 React Router 應用程式。

深受 Passport.js 的啟發,但從頭開始完全重寫,以便在 Web Fetch API 之上運作。Remix Auth 可以輕鬆部署到任何基於 Remix 或 React Router 的應用程式中,並且只需最少的設定。

如同 Passport.js,它使用策略模式來支援不同的身份驗證流程。每個策略都以獨立的 npm 套件發佈。

安裝

若要使用它,請從 npm (或 yarn) 安裝:

npm install remix-auth

此外,請安裝其中一個策略。策略列表可在社群策略討論中找到。

[!TIP] 請檢查策略所支援的 Remix Auth 版本,因為它們可能尚未更新到最新版本。

用法

匯入 Authenticator 類別並使用泛型型別進行實例化,該泛型型別將是您從策略取得的使用者資料的型別。

// Create an instance of the authenticator, pass a generic with what
// strategies will return
export let authenticator = new Authenticator<User>();

User 型別是策略在識別已驗證使用者後將提供給您的任何內容。它可以是完整的使用者資料,或是包含 Token 的字串。這完全取決於您。

之後,請註冊策略。在此範例中,我們將使用 FormStrategy 來檢查您想使用的策略文件,以查看您可能需要的任何設定。

import { FormStrategy } from "remix-auth-form";

// Tell the Authenticator to use the form strategy
authenticator.use(
  new FormStrategy(async ({ form }) => {
    let email = form.get("email");
    let password = form.get("password");
    // the type of this user must match the type you pass to the Authenticator
    // the strategy will automatically inherit the type if you instantiate
    // directly inside the `use` method
    return await login(email, password);
  }),
  // each strategy has a name and can be changed to use another one
  // same strategy multiple times, especially useful for the OAuth2 strategy.
  "user-pass"
);

一旦我們至少註冊了一個策略,就該設定路由了。

首先,建立一個 /login 頁面。在這裡,我們將呈現一個表單以取得使用者的電子郵件和密碼,並使用 Remix Auth 來驗證使用者。

import { Form } from "react-router";
import { authenticator } from "~/services/auth.server";

// Import this from correct place for your route
import type { Route } from "./+types";

// First we create our UI with the form doing a POST and the inputs with the
// names we are going to use in the strategy
export default function Screen() {
  return (
    <Form method="post">
      <input type="email" name="email" required />
      <input
        type="password"
        name="password"
        autoComplete="current-password"
        required
      />
      <button>Sign In</button>
    </Form>
  );
}

// Second, we need to export an action function, here we will use the
// `authenticator.authenticate method`
export async function action({ request }: Route.ActionArgs) {
  // we call the method with the name of the strategy we want to use and the
  // request object
  let user = await authenticator.authenticate("user-pass", request);

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

  throw redirect("/", {
    headers: { "Set-Cookie": await sessionStorage.commitSession(session) },
  });
}

// Finally, we need to export a loader function to check if the user is already
// authenticated and redirect them to the dashboard
export async function loader({ request }: Route.LoaderArgs) {
  let session = await sessionStorage.getSession(request.headers.get("cookie"));
  let user = session.get("user");
  if (user) throw redirect("/dashboard");
  return data(null);
}

可以使用 React Router 的 session storage helper 建立 sessionStorage,由您決定想要使用哪個 session storage 機制,或者您計劃在身份驗證後如何保留使用者資料,也許您只需要一個普通的 Cookie。

進階用法

根據使用者的資料將其重新導向到不同的路由

假設我們有 /dashboard/onboarding 路由,而且使用者驗證後,您需要檢查他們資料中的某些值,以了解他們是否已加入。

export async function action({ request }: Route.ActionArgs) {
  let user = await authenticator.authenticate("user-pass", request);

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

  // commit the session
  let headers = new Headers({ "Set-Cookie": await commitSession(session) });

  // and do your validation to know where to redirect the user
  if (isOnboarded(user)) return redirect("/dashboard", { headers });
  return redirect("/onboarding", { headers });
}

處理錯誤

發生錯誤時,驗證器和策略只會拋出錯誤。您可以捕獲它並根據需要進行處理。

export async function action({ request }: Route.ActionArgs) {
  try {
    return await authenticator.authenticate("user-pass", request);
  } catch (error) {
    if (error instanceof Error) {
      // here the error related to the authentication process
    }

    throw error; // Re-throw other values or unhandled errors
  }
}

[!TIP] 某些策略可能會拋出重新導向回應,這在 OAuth2/OIDC 流程中很常見,因為它們需要將使用者重新導向到身份提供者,然後再返回到應用程式,請確保重新拋出任何未處理的錯誤。在 catch 區塊的開頭使用 if (error instanceof Response) throw error; 來重新拋出任何回應,以防您想要以不同的方式處理它。

登出使用者

因為您負責在登入後保留使用者資料,所以如何處理登出將取決於此。您可以簡單地從 Session 中移除使用者資料,或者您可以建立新的 Session,甚至可以使 Session 無效。

export async function action({ request }: Route.ActionArgs) {
  let session = await sessionStorage.getSession(request.headers.get("cookie"));
  return redirect("/login", {
    headers: { "Set-Cookie": await sessionStorage.destroySession(session) },
  });
}

保護路由

若要保護路由,您可以使用 loader 函式來檢查使用者是否已驗證。如果沒有,您可以將其重新導向到登入頁面。

export async function loader({ request }: Route.LoaderArgs) {
  let session = await sessionStorage.getSession(request.headers.get("cookie"));
  let user = session.get("user");
  if (!user) throw redirect("/login");
  return null;
}

這不屬於 Remix Auth 的範圍,因為您儲存使用者資料的位置取決於您的應用程式。

一個簡單的方法是建立一個 authenticate helper。

export async function authenticate(request: Request, returnTo?: string) {
  let session = await sessionStorage.getSession(request.headers.get("cookie"));
  let user = session.get("user");
  if (user) return user;
  if (returnTo) session.set("returnTo", returnTo);
  throw redirect("/login", {
    headers: { "Set-Cookie": await sessionStorage.commitSession(session) },
  });
}

然後在您的 loaders 和 actions 中呼叫它:

export async function loader({ request }: Route.LoaderArgs) {
  let user = await authenticate(request, "/dashboard");
  // use the user data here
}

建立策略

所有策略都擴充了 Remix Auth 所匯出的 Strategy 抽象類別。您可以透過擴充此類別並實作 authenticate 方法來建立自己的策略。

import { Strategy } from "remix-auth/strategy";

export namespace MyStrategy {
  export interface VerifyOptions {
    // The values you will pass to the verify function
  }
}

export class MyStrategy<User> extends Strategy<User, MyStrategy.VerifyOptions> {
  name = "my-strategy";

  async authenticate(
    request: Request,
    options: Strategy.AuthenticateOptions
  ): Promise<User> {
    // Your logic here
  }
}

在您的 authenticate 方法的某個時刻,您需要呼叫 this.verify(options) 來呼叫應用程式定義的 verify 函式。

export class MyStrategy<User> extends Strategy<User, MyStrategy.VerifyOptions> {
  name = "my-strategy";

  async authenticate(
    request: Request,
    options: Strategy.AuthenticateOptions
  ): Promise<User> {
    return await this.verify({
      /* your options here */
    });
  }
}

選項將取決於您傳遞給 Strategy 類別的第二個泛型。

您想要傳遞給 verify 方法的內容取決於您和您的身份驗證流程的需求。

儲存中間狀態

如果您的策略需要儲存中間狀態,您可以覆寫 contructor 方法,以預期一個 Cookie 物件,甚至是一個 SessionStorage 物件。

import { SetCookie } from "@mjackson/headers";

export class MyStrategy<User> extends Strategy<User, MyStrategy.VerifyOptions> {
  name = "my-strategy";

  constructor(
    protected cookieName: string,
    verify: Strategy.VerifyFunction<User, MyStrategy.VerifyOptions>
  ) {
    super(verify);
  }

  async authenticate(
    request: Request,
    options: Strategy.AuthenticateOptions
  ): Promise<User> {
    let header = new SetCookie({
      name: this.cookieName,
      value: "some value",
      // more options
    });
    // More code
  }
}

header.toString() 的結果將是一個您必須使用 Set-Cookie 標頭傳送到瀏覽器的字串,這可以透過使用標頭拋出重新導向來完成。

export class MyStrategy<User> extends Strategy<User, MyStrategy.VerifyOptions> {
  name = "my-strategy";

  constructor(
    protected cookieName: string,
    verify: Strategy.VerifyFunction<User, MyStrategy.VerifyOptions>
  ) {
    super(verify);
  }

  async authenticate(
    request: Request,
    options: Strategy.AuthenticateOptions
  ): Promise<User> {
    let header = new SetCookie({
      name: this.cookieName,
      value: "some value",
      // more options
    });
    throw redirect("/some-route", {
      headers: { "Set-Cookie": header.toString() },
    });
  }
}

然後,您可以使用 @mjackson/headers 套件中的 Cookie 物件來讀取下一個請求中的值。

import { Cookie } from "@mjackson/headers";

export class MyStrategy<User> extends Strategy<User, MyStrategy.VerifyOptions> {
  name = "my-strategy";

  constructor(
    protected cookieName: string,
    verify: Strategy.VerifyFunction<User, MyStrategy.VerifyOptions>
  ) {
    super(verify);
  }

  async authenticate(
    request: Request,
    options: Strategy.AuthenticateOptions
  ): Promise<User> {
    let cookie = new Cookie(request.headers.get("cookie") ?? "");
    let value = cookie.get(this.cookieName);
    // More code
  }
}

授權

請參閱 LICENSE

作者