{"id":13581178,"url":"https://github.com/sergiodxa/remix-auth","last_synced_at":"2025-05-12T13:29:12.007Z","repository":{"id":37485210,"uuid":"388624643","full_name":"sergiodxa/remix-auth","owner":"sergiodxa","description":"Simple Authentication for Remix","archived":false,"fork":false,"pushed_at":"2025-05-06T00:07:27.000Z","size":1187,"stargazers_count":2160,"open_issues_count":0,"forks_count":114,"subscribers_count":8,"default_branch":"main","last_synced_at":"2025-05-06T00:36:32.333Z","etag":null,"topics":["auth","auth0","authentication","oauth2","remix","strategies"],"latest_commit_sha":null,"homepage":"https://sergiodxa.github.io/remix-auth/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sergiodxa.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null},"funding":{"github":"sergiodxa"}},"created_at":"2021-07-22T23:32:29.000Z","updated_at":"2025-05-06T00:07:30.000Z","dependencies_parsed_at":"2023-11-09T23:57:27.534Z","dependency_job_id":"7264c586-7776-4585-817a-f2172a8b5bdd","html_url":"https://github.com/sergiodxa/remix-auth","commit_stats":{"total_commits":172,"total_committers":48,"mean_commits":"3.5833333333333335","dds":"0.36046511627906974","last_synced_commit":"220f93984a3234b38c4842ad7c767ce3c0505175"},"previous_names":[],"tags_count":36,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sergiodxa%2Fremix-auth","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sergiodxa%2Fremix-auth/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sergiodxa%2Fremix-auth/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sergiodxa%2Fremix-auth/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sergiodxa","download_url":"https://codeload.github.com/sergiodxa/remix-auth/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253747445,"owners_count":21957758,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["auth","auth0","authentication","oauth2","remix","strategies"],"created_at":"2024-08-01T15:01:58.893Z","updated_at":"2025-05-12T13:29:11.948Z","avatar_url":"https://github.com/sergiodxa.png","language":"TypeScript","funding_links":["https://github.com/sponsors/sergiodxa"],"categories":["TypeScript","Utility","plugin","Packages"],"sub_categories":[],"readme":"![](/assets/header.png)\n\n# Remix Auth\n\n### Simple Authentication for [Remix](https://remix.run) and [React Router](https://reactrouter.com) apps.\n\n## Support the Project\n\nIf you find Remix Auth useful, please consider [sponsoring the project](https://github.com/sponsors/sergiodxa). Your support helps maintain this stable, production-ready library!\n\n## Features\n\n- Full **Server-Side** Authentication\n- Complete **TypeScript** Support\n- **Strategy**-based Authentication\n- Implement **custom** strategies\n\n## Overview\n\nRemix Auth is a complete open-source authentication solution for Remix and React Router applications.\n\nHeavily inspired by [Passport.js](https://passportjs.org), but completely rewrote it from scratch to work on top of the [Web Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). Remix Auth can be dropped in to any Remix or React Router based application with minimal setup.\n\nAs with Passport.js, it uses the strategy pattern to support the different authentication flows. Each strategy is published individually as a separate npm package.\n\n## Installation\n\n```bash\nnpm install remix-auth\n```\n\n## Supported Authentication Methods\n\nRemix Auth supports various authentication methods through its strategy system. Here are some popular strategies:\n\n- [Form Strategy](https://github.com/sergiodxa/remix-auth-form) - Username/password form-based authentication\n- [OAuth 2.0](https://github.com/sergiodxa/remix-auth-oauth2) - Generic OAuth 2.0 authentication\n- [GitHub](https://github.com/sergiodxa/remix-auth-github) - GitHub authentication\n\nFor a complete list of community-maintained strategies, check the [Community Strategies discussion](https://github.com/sergiodxa/remix-auth/discussions/111).\n\n\u003e [!TIP]\n\u003e Check in the strategies what versions of Remix Auth they support, as they may not be updated to the latest version.\n\n## Usage\n\nImport the `Authenticator` class and instantiate with a generic type that will be the type of the user data you will get from the strategies.\n\n```ts\n// app/services/auth.server.ts\nimport { Authenticator } from \"remix-auth\";\nimport { createCookieSessionStorage } from \"react-router\";\n\n// Define your user type\ntype User = {\n  id: string;\n  email: string;\n  name: string;\n  // ... other user properties\n};\n\n// Create a session storage\nexport const sessionStorage = createCookieSessionStorage({\n  cookie: {\n    name: \"__session\",\n    httpOnly: true,\n    path: \"/\",\n    sameSite: \"lax\",\n    secrets: [\"s3cr3t\"], // replace this with an actual secret\n    secure: process.env.NODE_ENV === \"production\",\n  },\n});\n\n// Create an instance of the authenticator, pass a generic with what\n// strategies will return\nexport const authenticator = new Authenticator\u003cUser\u003e();\n```\n\nThe `User` type is whatever your strategies will give you after identifying the authenticated user. It can be the complete user data, or a string with a token. It is completely up to you.\n\nAfter that, register the strategies. In this example, we will use the [FormStrategy](https://github.com/sergiodxa/remix-auth-form) to check the documentation of the strategy you want to use to see any configuration you may need.\n\n```ts\n// app/services/auth.server.ts\nimport { FormStrategy } from \"remix-auth-form\";\nimport { Authenticator } from \"remix-auth\";\n\n// Your authentication logic (replace with your actual DB/API calls)\nasync function login(email: string, password: string): Promise\u003cUser\u003e {\n  // Verify credentials\n  // Return user data or throw an error\n}\n\n// Tell the Authenticator to use the form strategy\nauthenticator.use(\n  new FormStrategy(async ({ form }) =\u003e {\n    const email = form.get(\"email\") as string;\n    const password = form.get(\"password\") as string;\n\n    if (!email || !password) {\n      throw new Error(\"Email and password are required\");\n    }\n\n    // the type of this user must match the type you pass to the\n    // Authenticator the strategy will automatically inherit the type if\n    // you instantiate directly inside the `use` method\n    return await login(email, password);\n  }),\n  // each strategy has a name and can be changed to use the same strategy\n  // multiple times, especially useful for the OAuth2 strategy.\n  \"user-pass\"\n);\n```\n\nOnce we have at least one strategy registered, it is time to set up the routes.\n\nFirst, create a `/login` page. Here we will render a form to get the email and password of the user and use Remix Auth to authenticate the user.\n\n```tsx\n// app/routes/login.tsx or equivalent route file\nimport { Form, useActionData, data, redirect } from \"react-router\";\nimport { authenticator, sessionStorage } from \"~/services/auth.server\";\n\n// Import this from correct place for your route\nimport type { Route } from \"./+types\";\n\n// First we create our UI with the form doing a POST and the inputs with\n// the names we are going to use in the strategy\nexport default function Component({ actionData }: Route.ComponentProps) {\n  return (\n    \u003cdiv\u003e\n      \u003ch1\u003eLogin\u003c/h1\u003e\n\n      {actionData?.error ? (\n        \u003cdiv className=\"error\"\u003e{actionData.error}\u003c/div\u003e\n      ) : null}\n\n      \u003cForm method=\"post\"\u003e\n        \u003cdiv\u003e\n          \u003clabel htmlFor=\"email\"\u003eEmail\u003c/label\u003e\n          \u003cinput type=\"email\" name=\"email\" id=\"email\" required /\u003e\n        \u003c/div\u003e\n\n        \u003cdiv\u003e\n          \u003clabel htmlFor=\"password\"\u003ePassword\u003c/label\u003e\n          \u003cinput\n            type=\"password\"\n            name=\"password\"\n            id=\"password\"\n            autoComplete=\"current-password\"\n            required\n          /\u003e\n        \u003c/div\u003e\n\n        \u003cbutton type=\"submit\"\u003eSign In\u003c/button\u003e\n      \u003c/Form\u003e\n    \u003c/div\u003e\n  );\n}\n\n// Second, we need to export an action function, here we will use the\n// `authenticator.authenticate` method\nexport async function action({ request }: Route.ActionArgs) {\n  try {\n    // we call the method with the name of the strategy we want to use and the\n    // request object\n    let user = await authenticator.authenticate(\"user-pass\", request);\n\n    let session = await sessionStorage.getSession(\n      request.headers.get(\"cookie\")\n    );\n\n    session.set(\"user\", user);\n\n    // Redirect to the home page after successful login\n    return redirect(\"/\", {\n      headers: {\n        \"Set-Cookie\": await sessionStorage.commitSession(session),\n      },\n    });\n  } catch (error) {\n    // Return validation errors or authentication errors\n    if (error instanceof Error) {\n      return json({ error: error.message });\n    }\n\n    // Re-throw any other errors (including redirects)\n    throw error;\n  }\n}\n\n// Finally, we need to export a loader function to check if the user is already\n// authenticated and redirect them to the dashboard\nexport async function loader({ request }: Route.LoaderArgs) {\n  let session = await sessionStorage.getSession(request.headers.get(\"cookie\"));\n  let user = session.get(\"user\");\n\n  // If the user is already authenticated redirect to the dashboard\n  if (user) return redirect(\"/dashboard\");\n\n  // Otherwise return null to render the login page\n  return json(null);\n}\n```\n\nThe sessionStorage can be created using React Router's session storage hepler, is up to you to decide what session storage mechanism you want to use, or how you plan to keep the user data after authentication, maybe you just need a plain cookie.\n\n## Advanced Usage\n\n### Redirect the user to different routes based on their data\n\nSay we have `/dashboard` and `/onboarding` routes, and after the user authenticates, you need to check some value in their data to know if they are onboarded or not.\n\n```ts\nexport async function action({ request }: Route.ActionArgs) {\n  let user = await authenticator.authenticate(\"user-pass\", request);\n\n  let session = await sessionStorage.getSession(request.headers.get(\"cookie\"));\n  session.set(\"user\", user);\n\n  // commit the session\n  let headers = new Headers({ \"Set-Cookie\": await commitSession(session) });\n\n  // and do your validation to know where to redirect the user\n  if (isOnboarded(user)) return redirect(\"/dashboard\", { headers });\n  return redirect(\"/onboarding\", { headers });\n}\n```\n\n### Handle errors\n\nIn case of error, the authenticator and the strategy will simply throw an error. You can catch it and handle it as you wish.\n\n```ts\nexport async function action({ request }: Route.ActionArgs) {\n  try {\n    return await authenticator.authenticate(\"user-pass\", request);\n  } catch (error) {\n    if (error instanceof Error) {\n      // here the error related to the authentication process\n    }\n\n    throw error; // Re-throw other values or unhandled errors\n  }\n}\n```\n\n\u003e [!TIP]\n\u003e Some strategies may throw a redirect response, this is common on OAuth2/OIDC flows as they need to redirect the user to the identity provider and then back to the application, ensure you re-throw anything that's not a handled error\n\u003e Use `if (error instanceof Response) throw error;` at the beginning of the catch block to re-throw any response first in case you want to handle it differently.\n\n### Logout the user\n\nBecause you're in charge of keeping the user data after login, how you handle the logout will depend on that. You can simply remove the user data from the session, or you can create a new session, or you can even invalidate the session.\n\n```ts\nexport async function action({ request }: Route.ActionArgs) {\n  let session = await sessionStorage.getSession(request.headers.get(\"cookie\"));\n  return redirect(\"/login\", {\n    headers: { \"Set-Cookie\": await sessionStorage.destroySession(session) },\n  });\n}\n```\n\n### Protect a route\n\nTo protect a route, you can use the `loader` function to check if the user is authenticated. If not, you can redirect them to the login page.\n\n```ts\nexport async function loader({ request }: Route.LoaderArgs) {\n  let session = await sessionStorage.getSession(request.headers.get(\"cookie\"));\n  let user = session.get(\"user\");\n  if (!user) throw redirect(\"/login\");\n  return null;\n}\n```\n\nThis is outside the scope of Remix Auth as where you store the user data depends on your application.\n\nA simple way could be to create an `authenticate` helper.\n\n```ts\nexport async function authenticate(request: Request, returnTo?: string) {\n  let session = await sessionStorage.getSession(request.headers.get(\"cookie\"));\n  let user = session.get(\"user\");\n  if (user) return user;\n  if (returnTo) session.set(\"returnTo\", returnTo);\n  throw redirect(\"/login\", {\n    headers: { \"Set-Cookie\": await sessionStorage.commitSession(session) },\n  });\n}\n```\n\nThen in your loaders and actions call that:\n\n```ts\nexport async function loader({ request }: Route.LoaderArgs) {\n  let user = await authenticate(request, \"/dashboard\");\n  // use the user data here\n}\n```\n\n### Create a strategy\n\nAll strategies extends the `Strategy` abstract class exported by Remix Auth. You can create your own strategies by extending this class and implementing the `authenticate` method.\n\n```ts\nimport { Strategy } from \"remix-auth/strategy\";\n\nexport namespace MyStrategy {\n  export interface ConstructorOptions {\n    // The values you will pass to the constructor\n  }\n\n  export interface VerifyOptions {\n    // The values you will pass to the verify function\n  }\n}\n\nexport class MyStrategy\u003cUser\u003e extends Strategy\u003cUser, MyStrategy.VerifyOptions\u003e {\n  name = \"my-strategy\";\n\n  constructor(\n    protected options: MyStrategy.ConstructorOptions,\n    verify: Strategy.VerifyFunction\u003cUser, MyStrategy.VerifyOptions\u003e\n  ) {\n    super(verify);\n  }\n\n  async authenticate(request: Request): Promise\u003cUser\u003e {\n    // Your logic here, you can use `this.options` to get constructor options\n  }\n}\n```\n\nAt some point of your `authenticate` method, you will need to call `this.verify(options)` to call the `verify` function the application defined.\n\n```ts\nexport class MyStrategy\u003cUser\u003e extends Strategy\u003cUser, MyStrategy.VerifyOptions\u003e {\n  name = \"my-strategy\";\n\n  constructor(\n    protected options: MyStrategy.ConstructorOptions,\n    verify: Strategy.VerifyFunction\u003cUser, MyStrategy.VerifyOptions\u003e\n  ) {\n    super(verify);\n  }\n\n  async authenticate(request: Request): Promise\u003cUser\u003e {\n    return await this.verify({\n      /* your verify options here */\n    });\n  }\n}\n```\n\nThe options will depend on the second generic you pass to the `Strategy` class.\n\nWhat you want to pass to the `verify` method is up to you and what your authentication flow needs.\n\n#### Store intermediate state\n\nIf your strategy needs to store intermediate state, you can override the `contructor` method to expect a `Cookie` object, or even a `SessionStorage` object.\n\n```ts\nimport { SetCookie } from \"@mjackson/headers\";\n\nexport class MyStrategy\u003cUser\u003e extends Strategy\u003cUser, MyStrategy.VerifyOptions\u003e {\n  name = \"my-strategy\";\n\n  constructor(\n    protected cookieName: string,\n    verify: Strategy.VerifyFunction\u003cUser, MyStrategy.VerifyOptions\u003e\n  ) {\n    super(verify);\n  }\n\n  async authenticate(\n    request: Request,\n    options: Strategy.AuthenticateOptions\n  ): Promise\u003cUser\u003e {\n    let header = new SetCookie({\n      name: this.cookieName,\n      value: \"some value\",\n      // more options\n    });\n    // More code\n  }\n}\n```\n\nThe result of `header.toString()` will be a string you have to send to the browser using the `Set-Cookie` header, this can be done by throwing a redirect with the header.\n\n```ts\nexport class MyStrategy\u003cUser\u003e extends Strategy\u003cUser, MyStrategy.VerifyOptions\u003e {\n  name = \"my-strategy\";\n\n  constructor(\n    protected cookieName: string,\n    verify: Strategy.VerifyFunction\u003cUser, MyStrategy.VerifyOptions\u003e\n  ) {\n    super(verify);\n  }\n\n  async authenticate(request: Request): Promise\u003cUser\u003e {\n    let header = new SetCookie({\n      name: this.cookieName,\n      value: \"some value\",\n      // more options\n    });\n    throw redirect(\"/some-route\", {\n      headers: { \"Set-Cookie\": header.toString() },\n    });\n  }\n}\n```\n\nThen you can read the value in the next request using the `Cookie` object from the `@mjackson/headers` package.\n\n```ts\nimport { Cookie } from \"@mjackson/headers\";\n\nexport class MyStrategy\u003cUser\u003e extends Strategy\u003cUser, MyStrategy.VerifyOptions\u003e {\n  name = \"my-strategy\";\n\n  constructor(\n    protected cookieName: string,\n    verify: Strategy.VerifyFunction\u003cUser, MyStrategy.VerifyOptions\u003e\n  ) {\n    super(verify);\n  }\n\n  async authenticate(request: Request): Promise\u003cUser\u003e {\n    let cookie = new Cookie(request.headers.get(\"cookie\") ?? \"\");\n    let value = cookie.get(this.cookieName);\n    // More code\n  }\n}\n```\n\n#### Use AsyncLocalStorage to pass extra data to authenticate\n\nIf you need more than the request object to authenticate the user, you can use the `AsyncLocalStorage` API to pass data to the `authenticate` method.\n\n```ts\nimport { AsyncLocalStorage } from \"async_hooks\";\n\nexport const asyncLocalStorage = new AsyncLocalStorage\u003c{\n  someValue: string;\n  // more values\n}\u003e();\n\nexport class MyStrategy\u003cUser\u003e extends Strategy\u003cUser, MyStrategy.VerifyOptions\u003e {\n  name = \"my-strategy\";\n\n  constructor(\n    protected cookieName: string,\n    verify: Strategy.VerifyFunction\u003cUser, MyStrategy.VerifyOptions\u003e\n  ) {\n    super(verify);\n  }\n\n  async authenticate(request: Request): Promise\u003cUser\u003e {\n    let store = asyncLocalStorage.getStore();\n    if (!store) throw new Error(\"Failed to get AsyncLocalStorage store\");\n    let { someValue } = store;\n    // More code\n  }\n}\n```\n\nThen you can set the value in the `authenticate` method.\n\n```ts\nexport async function action({ request }: Route.ActionArgs) {\n  // Set the value in the AsyncLocalStorage\n  let user = await asyncLocalStorage.run({ someValue: \"some value\" }, () =\u003e\n    authenticator.authenticate(\"user-pass\", request)\n  );\n\n  let session = await sessionStorage.getSession(request.headers.get(\"cookie\"));\n\n  session.set(\"user\", user);\n\n  return redirect(\"/dashboard\", {\n    headers: { \"Set-Cookie\": await commitSession(session) },\n  });\n}\n```\n\n## License\n\nSee [LICENSE](./LICENSE).\n\n## Author\n\n- [Sergio Xalambrí](https://sergiodxa.com)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsergiodxa%2Fremix-auth","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsergiodxa%2Fremix-auth","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsergiodxa%2Fremix-auth/lists"}