Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

remix-auth

Package Overview
Dependencies
Maintainers
0
Versions
34
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

remix-auth - npm Package Compare versions

Comparing version 3.7.0 to 4.0.0

build/index.js.map

51

build/index.d.ts

@@ -1,4 +0,47 @@

export * from "./authenticator";
export * from "./authorizer";
export * from "./error";
export * from "./strategy";
import type { Strategy } from "./strategy.js";
/**
* Create a new instance of the Authenticator.
*
* It receives a instance of a Cookie created using Remix's createCookie.
*
* It optionally receives an object with extra options. The supported options
* are:
* @example
* let auth = new Authenticator();
*/
export declare class Authenticator<User = unknown> {
/**
* A map of the configured strategies, the key is the name of the strategy
* @private
*/
private strategies;
/**
* Call this method with the Strategy, the optional name allows you to setup
* the same strategy multiple times with different names.
* It returns the Authenticator instance for concatenation.
* @example
* auth.use(new SomeStrategy((user) => Promise.resolve(user)));
* auth.use(new SomeStrategy((user) => Promise.resolve(user)), "another");
*/
use(strategy: Strategy<User, never>, name?: string): Authenticator<User>;
/**
* Call this method with the name of the strategy you want to remove.
* It returns the Authenticator instance for concatenation.
* @example
* auth.unuse("another").unuse("some");
*/
unuse(name: string): Authenticator;
/**
* Call this to authenticate a request using some strategy. You pass the name
* of the strategy you want to use and the request to authenticate.
* @example
* async function action({ request }: ActionFunctionArgs) {
* let user = await auth.authenticate("some", request);
* };
* @example
* async function action({ request, context }: ActionFunctionArgs) {
* let user = await auth.authenticate("some", request, { context });
* };
*/
authenticate(strategy: string, request: Request): Promise<User>;
}

76

build/index.js

@@ -1,20 +0,58 @@

"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
/**
* Create a new instance of the Authenticator.
*
* It receives a instance of a Cookie created using Remix's createCookie.
*
* It optionally receives an object with extra options. The supported options
* are:
* @example
* let auth = new Authenticator();
*/
export class Authenticator {
/**
* A map of the configured strategies, the key is the name of the strategy
* @private
*/
strategies = new Map();
/**
* Call this method with the Strategy, the optional name allows you to setup
* the same strategy multiple times with different names.
* It returns the Authenticator instance for concatenation.
* @example
* auth.use(new SomeStrategy((user) => Promise.resolve(user)));
* auth.use(new SomeStrategy((user) => Promise.resolve(user)), "another");
*/
use(strategy, name) {
this.strategies.set(name ?? strategy.name, strategy);
return this;
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
__exportStar(require("./authenticator"), exports);
__exportStar(require("./authorizer"), exports);
__exportStar(require("./error"), exports);
__exportStar(require("./strategy"), exports);
/**
* Call this method with the name of the strategy you want to remove.
* It returns the Authenticator instance for concatenation.
* @example
* auth.unuse("another").unuse("some");
*/
unuse(name) {
this.strategies.delete(name);
return this;
}
/**
* Call this to authenticate a request using some strategy. You pass the name
* of the strategy you want to use and the request to authenticate.
* @example
* async function action({ request }: ActionFunctionArgs) {
* let user = await auth.authenticate("some", request);
* };
* @example
* async function action({ request, context }: ActionFunctionArgs) {
* let user = await auth.authenticate("some", request, { context });
* };
*/
authenticate(strategy, request) {
let instance = this.strategies.get(strategy);
if (!instance)
throw new ReferenceError(`Strategy ${strategy} not found.`);
return instance.authenticate(new Request(request.url, request));
}
}
//# sourceMappingURL=index.js.map

@@ -1,58 +0,2 @@

import { AppLoadContext, SessionStorage } from "@remix-run/server-runtime";
/**
* Extra information from the Authenticator to the strategy
*/
export interface AuthenticateOptions {
/**
* The key of the session used to set the user data.
*/
sessionKey: string;
/**
* In what key of the session the errors will be set.
* @default "auth:error"
*/
sessionErrorKey: string;
/**
* The key of the session used to set the strategy used to authenticate the
* user.
*/
sessionStrategyKey: string;
/**
* The name used to register the strategy
*/
name: string;
/**
* To what URL redirect in case of a successful authentication.
* If not defined, it will return the user data.
*/
successRedirect?: string;
/**
* To what URL redirect in case of a failed authentication.
* If not defined, it will return null
*/
failureRedirect?: string;
/**
* Set if the strategy should throw an error instead of a Reponse in case of
* a failed authentication.
* @default false
*/
throwOnError?: boolean;
/**
* The context object received by the loader or action.
* This can be used by the strategy if needed.
*/
context?: AppLoadContext;
}
/**
* A function which will be called to find the user using the information the
* strategy got from the request.
*
* @param params The params from the strategy.
* @returns The user data.
* @throws {AuthorizationError} If the user was not found. Any other error will be ignored and thrown again by the strategy.
*/
export interface StrategyVerifyCallback<User, VerifyParams> {
(params: VerifyParams): Promise<User>;
}
/**
* The Strategy class is the base class every strategy should extend.

@@ -63,11 +7,5 @@ *

* - VerifyParams is the type of the params the verify callback will receive from the strategy.
*
* This class also defines as protected two methods, `success` and `failure`.
* - `success` is called when the authentication was successful.
* - `failure` is called when the authentication failed.
* These methods helps you return or throw the correct value, response or error
* from within the strategy `authenticate` method.
*/
export declare abstract class Strategy<User, VerifyOptions> {
protected verify: StrategyVerifyCallback<User, VerifyOptions>;
protected verify: Strategy.VerifyFunction<User, VerifyOptions>;
/**

@@ -79,34 +17,24 @@ * The name of the strategy.

abstract name: string;
constructor(verify: StrategyVerifyCallback<User, VerifyOptions>);
constructor(verify: Strategy.VerifyFunction<User, VerifyOptions>);
/**
* The authentication flow of the strategy.
*
* This method receives the Request to authenticator and the session storage
* to use from the Authenticator. It may receive a custom callback.
* This method receives the Request from the authenticator we want to
* authenticate.
*
* At the end of the flow, it will return a Response to be used by the
* At the end of the flow, it will return a the User data to be used by the
* application.
*/
abstract authenticate(request: Request, sessionStorage: SessionStorage, options: AuthenticateOptions): Promise<User>;
abstract authenticate(request: Request): Promise<User>;
}
export declare namespace Strategy {
/**
* Throw an AuthorizationError or a redirect to the failureRedirect.
* @param message The error message to set in the session.
* @param request The request to get the cookie out of.
* @param sessionStorage The session storage to retrieve the session from.
* @param options The strategy options.
* @throws {AuthorizationError} If the throwOnError is set to true.
* @throws {Response} If the failureRedirect is set or throwOnError is false.
* @returns {Promise<never>}
* A function which will be called to find the user using the information the
* strategy got from the request.
*
* @param params The params from the strategy.
* @returns The user data.
* @throws {AuthorizationError} If the user was not found. Any other error will be ignored and thrown again by the strategy.
*/
protected failure(message: string, request: Request, sessionStorage: SessionStorage, options: AuthenticateOptions, cause?: Error): Promise<never>;
/**
* Returns the user data or throw a redirect to the successRedirect.
* @param user The user data to set in the session.
* @param request The request to get the cookie out of.
* @param sessionStorage The session storage to retrieve the session from.
* @param options The strategy options.
* @returns {Promise<User>} The user data.
* @throws {Response} If the successRedirect is set, it will redirect to it.
*/
protected success(user: User, request: Request, sessionStorage: SessionStorage, options: AuthenticateOptions): Promise<User>;
type VerifyFunction<User, VerifyParams> = (params: VerifyParams) => Promise<User>;
}

@@ -1,6 +0,1 @@

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Strategy = void 0;
const server_runtime_1 = require("@remix-run/server-runtime");
const error_1 = require("./error");
/**

@@ -12,62 +7,9 @@ * The Strategy class is the base class every strategy should extend.

* - VerifyParams is the type of the params the verify callback will receive from the strategy.
*
* This class also defines as protected two methods, `success` and `failure`.
* - `success` is called when the authentication was successful.
* - `failure` is called when the authentication failed.
* These methods helps you return or throw the correct value, response or error
* from within the strategy `authenticate` method.
*/
class Strategy {
export class Strategy {
verify;
constructor(verify) {
this.verify = verify;
}
/**
* Throw an AuthorizationError or a redirect to the failureRedirect.
* @param message The error message to set in the session.
* @param request The request to get the cookie out of.
* @param sessionStorage The session storage to retrieve the session from.
* @param options The strategy options.
* @throws {AuthorizationError} If the throwOnError is set to true.
* @throws {Response} If the failureRedirect is set or throwOnError is false.
* @returns {Promise<never>}
*/
async failure(message, request, sessionStorage, options, cause) {
// if a failureRedirect is not set, we throw a 401 Response or an error
if (!options.failureRedirect) {
if (options.throwOnError)
throw new error_1.AuthorizationError(message, cause);
throw (0, server_runtime_1.json)({ message }, 401);
}
let session = await sessionStorage.getSession(request.headers.get("Cookie"));
// if we do have a failureRedirect, we redirect to it and set the error
// in the session errorKey
session.flash(options.sessionErrorKey, { message });
throw (0, server_runtime_1.redirect)(options.failureRedirect, {
headers: { "Set-Cookie": await sessionStorage.commitSession(session) },
});
}
/**
* Returns the user data or throw a redirect to the successRedirect.
* @param user The user data to set in the session.
* @param request The request to get the cookie out of.
* @param sessionStorage The session storage to retrieve the session from.
* @param options The strategy options.
* @returns {Promise<User>} The user data.
* @throws {Response} If the successRedirect is set, it will redirect to it.
*/
async success(user, request, sessionStorage, options) {
var _a;
// if a successRedirect is not set, we return the user
if (!options.successRedirect)
return user;
let session = await sessionStorage.getSession(request.headers.get("Cookie"));
// if we do have a successRedirect, we redirect to it and set the user
// in the session sessionKey
session.set(options.sessionKey, user);
session.set(options.sessionStrategyKey, (_a = options.name) !== null && _a !== void 0 ? _a : this.name);
throw (0, server_runtime_1.redirect)(options.successRedirect, {
headers: { "Set-Cookie": await sessionStorage.commitSession(session) },
});
}
}
exports.Strategy = Strategy;
//# sourceMappingURL=strategy.js.map
{
"name": "remix-auth",
"version": "3.7.0",
"description": "Simple Authentication for Remix",
"main": "./build/index.js",
"types": "./build/index.d.ts",
"scripts": {
"build": "tsc --project tsconfig.json",
"typecheck": "tsc --project tsconfig.json --noEmit",
"lint": "eslint --ext .ts,.tsx src/",
"test": "jest --config=config/jest.config.ts --passWithNoTests",
"coverage": "npm run test -- --coverage"
},
"keywords": [
"remix",
"auth",
"authentication",
"local",
"auth0",
"oauth2",
"strategies"
],
"author": {
"name": "Sergio Xalambrí",
"email": "hello@sergiodxa.com",
"url": "https://sergiodxa.com"
},
"repository": {
"url": "https://github.com/sergiodxa/remix-auth",
"type": "git"
},
"homepage": "https://github.com/sergiodxa/remix-auth#readme",
"license": "MIT",
"files": [
"build",
"package.json",
"README.md"
],
"peerDependencies": {
"@remix-run/react": "^1.0.0 || ^2.0.0",
"@remix-run/server-runtime": "^1.0.0 || ^2.0.0"
},
"devDependencies": {
"@babel/core": "^7.14.2",
"@babel/preset-env": "^7.14.1",
"@babel/preset-react": "^7.13.13",
"@babel/preset-typescript": "^7.13.0",
"@remix-run/node": "^2.0.1",
"@remix-run/react": "^2.0.1",
"@remix-run/serve": "^2.0.1",
"@remix-run/server-runtime": "^2.0.1",
"@types/jest": "^29.5.5",
"@types/prop-types": "^15.7.4",
"@types/react": "^18.2.20",
"@types/uuid": "^8.3.3",
"@typescript-eslint/eslint-plugin": "^6.7.3",
"@typescript-eslint/parser": "^6.7.3",
"babel-jest": "^26.6.3",
"eslint": "^7.26.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-cypress": "^2.11.3",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-jest": "^24.3.6",
"eslint-plugin-jest-dom": "^3.9.0",
"eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-testing-library": "^4.3.0",
"eslint-plugin-unicorn": "^32.0.1",
"jest": "^29.7.0",
"jest-fetch-mock": "^3.0.3",
"prettier": "^2.3.2",
"react": "^18.2.0",
"ts-node": "^9.1.1",
"typescript": "^5.1.6"
},
"dependencies": {
"uuid": "^8.3.2"
}
"name": "remix-auth",
"version": "4.0.0",
"author": {
"name": "Sergio Xalambrí",
"email": "hello+oss@sergiodxa.com",
"url": "https://sergiodxa.com"
},
"repository": {
"url": "https://github.com/sergiodxa/remix-auth",
"type": "git"
},
"devDependencies": {
"@arethetypeswrong/cli": "^0.17.0",
"@biomejs/biome": "^1.8.3",
"@mjackson/headers": "^0.8.0",
"@total-typescript/tsconfig": "^1.0.4",
"@types/bun": "^1.1.6",
"typedoc": "^0.26.5",
"typedoc-plugin-mdn-links": "^3.2.6",
"typescript": "^5.5.4"
},
"exports": {
".": "./build/index.js",
"./strategy": "./build/strategy.js",
"./package.json": "./package.json"
},
"bugs": {
"url": "https://github.com/sergiodxa/remix-auth/issues"
},
"description": "Simple Authentication for Remix and React Router",
"engines": {
"node": ">=20.0.0"
},
"files": [
"build",
"src",
"package.json",
"README.md"
],
"funding": [
"https://github.com/sponsors/sergiodxa"
],
"homepage": "https://github.com/sergiodxa/remix-auth",
"license": "MIT",
"scripts": {
"build": "tsc",
"typecheck": "tsc --noEmit",
"quality": "biome check .",
"quality:fix": "biome check . --write --unsafe",
"exports": "bun run ./scripts/exports.ts"
},
"sideEffects": false,
"type": "module"
}

@@ -5,3 +5,3 @@ ![](/assets/header.png)

### Simple Authentication for [Remix](https://remix.run/)
### Simple Authentication for [Remix](https://remix.run) and [React Router](https://reactrouter.com) apps.

@@ -13,11 +13,9 @@ ## Features

- **Strategy**-based Authentication
- Easily handle **success and failure**
- Implement **custom** strategies
- Supports persistent **sessions**
## Overview
Remix Auth is a complete open-source authentication solution for Remix.run applications.
Remix Auth is a complete open-source authentication solution for Remix and React Router applications.
Heavily 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-based application with minimal setup.
Heavily 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.

@@ -36,41 +34,16 @@ As with Passport.js, it uses the strategy pattern to support the different authentication flows. Each strategy is published individually as a separate npm package.

> [!TIP]
> Check in the strategies what versions of Remix Auth they support, as they may not be updated to the latest version.
## Usage
Remix Auth needs a session storage object to store the user session. It can be any object that implements the [SessionStorage interface from Remix](https://remix.run/docs/en/main/utils/sessions#createsessionstorage).
Import the `Authenticator` class and instantiate with a generic type that will be the type of the user data you will get from the strategies.
In this example I'm using the [createCookieSessionStorage](https://remix.run/docs/en/main/utils/sessions#createcookiesessionstorage) function.
```ts
// app/services/session.server.ts
import { createCookieSessionStorage } from "@remix-run/node";
// export the whole sessionStorage object
export let sessionStorage = createCookieSessionStorage({
cookie: {
name: "_session", // use any name you want here
sameSite: "lax", // this helps with CSRF
path: "/", // remember to add this so the cookie will work in all routes
httpOnly: true, // for security reasons, make this cookie http only
secrets: ["s3cr3t"], // replace this with an actual secret
secure: process.env.NODE_ENV === "production", // enable this in prod only
},
});
// you can also export the methods individually for your own usage
export let { getSession, commitSession, destroySession } = sessionStorage;
```
Now, create a file for the Remix Auth configuration. Here import the `Authenticator` class and your `sessionStorage` object.
```ts
// app/services/auth.server.ts
import { Authenticator } from "remix-auth";
import { sessionStorage } from "~/services/session.server";
// Create an instance of the authenticator, pass a generic with what
// strategies will return and will store in the session
export let authenticator = new Authenticator<User>(sessionStorage);
// strategies will return
export let authenticator = new Authenticator<User>();
```
The `User` type is whatever you will store in the session storage to identify the authenticated user. It can be the complete user data or a string with a token. It is completely configurable.
The `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.

@@ -87,7 +60,6 @@ After 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.

let password = form.get("password");
let user = await login(email, 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 user;
return await login(email, password);
}),

@@ -100,3 +72,3 @@ // each strategy has a name and can be changed to use another one

Now that at least one strategy is registered, it is time to set up the routes.
Once we have at least one strategy registered, it is time to set up the routes.

@@ -106,7 +78,8 @@ First, 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.

```tsx
// app/routes/login.tsx
import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node";
import { Form } from "@remix-run/react";
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

@@ -131,169 +104,260 @@ // names we are going to use in the strategy

// `authenticator.authenticate method`
export async function action({ request }: ActionFunctionArgs) {
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, optionally we pass an object with the URLs we want the user
// to be redirected to after a success or a failure
return await authenticator.authenticate("user-pass", request, {
successRedirect: "/dashboard",
failureRedirect: "/login",
// 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 can export a loader function where we check if the user is
// authenticated with `authenticator.isAuthenticated` and redirect to the
// dashboard if it is or return null if it's not
export async function loader({ request }: LoaderFunctionArgs) {
// If the user is already authenticated redirect to /dashboard directly
return await authenticator.isAuthenticated(request, {
successRedirect: "/dashboard",
});
};
// 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);
}
```
With this, we have our login page. If we need to get the user data in another route of the application, we can use the `authenticator.isAuthenticated` method passing the request this way:
The 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.
## Advanced Usage
### Redirect the user to different routes based on their data
Say 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.
```ts
// get the user data or redirect to /login if it failed
let user = await authenticator.isAuthenticated(request, {
failureRedirect: "/login",
});
export async function action({ request }: Route.ActionArgs) {
let user = await authenticator.authenticate("user-pass", request);
// if the user is authenticated, redirect to /dashboard
await authenticator.isAuthenticated(request, {
successRedirect: "/dashboard",
});
let session = await sessionStorage.getSession(request.headers.get("cookie"));
session.set("user", user);
// get the user or null, and do different things in your loader/action based on
// the result
let user = await authenticator.isAuthenticated(request);
if (user) {
// here the user is authenticated
} else {
// here the user is not authenticated
// 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 });
}
```
Once the user is ready to leave the application, we can call the `logout` method inside an action.
### Handle errors
In case of error, the authenticator and the strategy will simply throw an error. You can catch it and handle it as you wish.
```ts
export async function action({ request }: ActionFunctionArgs) {
await authenticator.logout(request, { redirectTo: "/login" });
};
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
}
}
```
## Advanced Usage
> [!TIP]
> 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
> 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.
### Custom redirect URL based on the user
### Logout the user
Say 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.
Because 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.
If we do not pass the `successRedirect` option to the `authenticator.authenticate` method, it will return the user data.
Note that we will need to store the user data in the session this way. To ensure we use the correct session key, the authenticator has a `sessionKey` property.
```ts
export async function action({ request }: ActionFunctionArgs) {
let user = await authenticator.authenticate("user-pass", request, {
failureRedirect: "/login",
let session = await sessionStorage.getSession(request.headers.get("cookie"));
return redirect("/login", {
headers: { "Set-Cookie": await sessionStorage.destroySession(session) },
});
}
```
// manually get the session
let session = await getSession(request.headers.get("cookie"));
// and store the user data
session.set(authenticator.sessionKey, user);
### Protect a route
// commit the session
let headers = new Headers({ "Set-Cookie": await commitSession(session) });
To 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.
// and do your validation to know where to redirect the user
if (isOnboarded(user)) return redirect("/dashboard", { headers });
return redirect("/onboarding", { headers });
};
```ts
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;
}
```
### Changing the session key
This is outside the scope of Remix Auth as where you store the user data depends on your application.
If we want to change the session key used by Remix Auth to store the user data, we can customize it when creating the `Authenticator` instance.
A simple way could be to create an `authenticate` helper.
```ts
export let authenticator = new Authenticator<AccessToken>(sessionStorage, {
sessionKey: "accessToken",
});
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) },
});
}
```
With this, both `authenticate` and `isAuthenticated` will use that key to read or write the user data (in this case, the access token).
Then in your loaders and actions call that:
If we need to read or write from the session manually, remember always to use the `authenticator.sessionKey` property. If we change the key in the `Authenticator` instance, we will not need to change it in the code.
```ts
export async function loader({ request }: Route.LoaderArgs) {
let user = await authenticate(request, "/dashboard");
// use the user data here
}
```
### Reading authentication errors
### Create a strategy
When the user cannot authenticate, the error will be set in the session using the `authenticator.sessionErrorKey` property.
All 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.
We can customize the name of the key when creating the `Authenticator` instance.
```ts
import { Strategy } from "remix-auth/strategy";
```ts
export let authenticator = new Authenticator<User>(sessionStorage, {
sessionErrorKey: "my-error-key",
});
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
}
}
```
Furthermore, we can read the error using that key after a failed authentication.
At some point of your `authenticate` method, you will need to call `this.verify(options)` to call the `verify` function the application defined.
```ts
// in the loader of the login route
export async function loader({ request }: LoaderFunctionArgs) {
await authenticator.isAuthenticated(request, {
successRedirect: "/dashboard",
});
let session = await getSession(request.headers.get("cookie"));
let error = session.get(authenticator.sessionErrorKey);
return json({ error }, {
headers:{
'Set-Cookie': await commitSession(session) // You must commit the session whenever you read a flash
}
});
};
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 */
});
}
}
```
Remember always to use the `authenticator.sessionErrorKey` property. If we change the key in the `Authenticator` instance, we will not need to change it in the code.
The options will depend on the second generic you pass to the `Strategy` class.
### Errors Handling
What you want to pass to the `verify` method is up to you and what your authentication flow needs.
By default, any error in the authentication process will throw a Response object. If `failureRedirect` is specified, this will always be a redirect response with the error message on the `sessionErrorKey`.
#### Store intermediate state
If a `failureRedirect` is not defined, Remix Auth will throw a 401 Unauthorized response with a JSON body containing the error message. This way, we can use the CatchBoundary component of the route to render any error message.
If your strategy needs to store intermediate state, you can use override the `contructor` method to expect a `Cookie` object, or even a `SessionStorage` object.
If we want to get an error object inside the action instead of throwing a Response, we can configure the `throwOnError` option to `true`. We can do this when instantiating the `Authenticator` or calling `authenticate`.
```ts
import { SetCookie } from "@mjackson/headers";
If we do it in the `Authenticator,` it will be the default behavior for all the `authenticate` calls.
export class MyStrategy<User> extends Strategy<User, MyStrategy.VerifyOptions> {
name = "my-strategy";
```ts
export let authenticator = new Authenticator<User>(sessionStorage, {
throwOnError: true,
});
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
}
}
```
Alternatively, we can do it on the action itself.
The 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.
```ts
import { AuthorizationError } from "remix-auth";
export class MyStrategy<User> extends Strategy<User, MyStrategy.VerifyOptions> {
name = "my-strategy";
export async function action({ request }: ActionFunctionArgs) {
try {
return await authenticator.authenticate("user-pass", request, {
successRedirect: "/dashboard",
throwOnError: true,
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
});
} catch (error) {
// Because redirects work by throwing a Response, you need to check if the
// caught error is a response and return it or throw it again
if (error instanceof Response) return error;
if (error instanceof AuthorizationError) {
// here the error is related to the authentication process
}
// here the error is a generic error that another reason may throw
throw redirect("/some-route", {
headers: { "Set-Cookie": header.toString() },
});
}
};
}
```
If we define both `failureRedirect` and `throwOnError`, the redirect will happen instead of throwing an error.
Then you can read the value in the next request using the `Cookie` object from the `@mjackson/headers` package.
```ts
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
See [LICENSE](./LICENSE).
## Author
- [Sergio Xalambrí](https://sergiodxa.com)
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc