Remix Authentication Using Remix-Auth Package
Tags: remix, webdev, react, video
Published: 2022-03-16 16:37:13

This is additional information related to a video walkthrough of a sample [Remix](https://remix.run/docs/en/v1) application using the [remix-auth package](https://github.com/sergiodxa/remix-auth) which is a passport-like framework for simplifying authentication of your remix application using specific packaged strategies.

{% youtube 5O_uufXA8xE %}

In this example, I am using the [Form Strategy](https://github.com/sergiodxa/remix-auth-form) to show a simple login flow.

After creating your Remix application, install the required npm packages
```
npm install remix-auth remix-auth-form
```
Create the `app/services/session.server.ts` file to manage the session and to hold the type that defines the shape of the session information.
>This code is directly from the documentation except for the type `User`; see documentation linked below for additional information
```javascript
// app/services/session.server.ts
import { createCookieSessionStorage } from 'remix';

// 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;

// define the user model
export type User = {
  name: string;
  token: string;
};
```

Create an authentication instance in a new file `app/services/auth.server.ts`. The type `User` will be introduced when we create the file to manage the session.
```javascript
import { Authenticator, AuthorizationError } from 'remix-auth';
import { FormStrategy } from 'remix-auth-form';
import { sessionStorage, User } from '~/services/session.server';

// Create an instance of the authenticator, pass a Type, User,  with what
// strategies will return and will store in the session
const authenticator = new Authenticator<User | Error | null>(sessionStorage, {
  sessionKey: "sessionKey", // keep in sync
  sessionErrorKey: "sessionErrorKey", // keep in sync
});
```
In the same file we will define the strategy that will be used with this authenticator and return the authenticator object from the module.

> the anonymous function related to the strategy could be extracted into a separate function for additional clarity.

We can do some verification inside the function or do it before calling the authenticator. If you are validating in the authenticator, to return errors you must throw them as the type `AuthorizationError`. These errors can be retrieved from the session using the `sessionErrorKey` defined when initializing the `Authenticator`.

If there are no errors then we return whatever information we want stored in the session; in this case it is defined by the type `User` 

```javascript
// Tell the Authenticator to use the form strategy
authenticator.use(
  new FormStrategy(async ({ form }) => {

    // get the data from the form...
    let email = form.get('email') as string;
    let password = form.get('password') as string;

    // initialize the user here
    let user = null;

    // do some validation, errors are in the sessionErrorKey
    if (!email || email?.length === 0) throw new AuthorizationError('Bad Credentials: Email is required')
    if (typeof email !== 'string')
      throw new AuthorizationError('Bad Credentials: Email must be a string')

    if (!password || password?.length === 0) throw new AuthorizationError('Bad Credentials: Password is required')
    if (typeof password !== 'string')
      throw new AuthorizationError('Bad Credentials: Password must be a string')

    // login the user, this could be whatever process you want
    if (email === 'aaron@mail.com' && password === 'password') {
      user = {
        name: email,
        token: `${password}-${new Date().getTime()}`,
      };

      // 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 Promise.resolve({ ...user });

    } else {
      // if problem with user throw error AuthorizationError
      throw new AuthorizationError("Bad Credentials")
    }

  }),
);

export default authenticator
```
###Application Routes
There are two routes in this application, the index route which is protected and the login route which is not; we will start with the index route in a file called `app/routes/index.ts`

included are the necessary imports
```javascript
// app/routes/index.ts
import { ActionFunction, Form, LoaderFunction, useLoaderData } from "remix";
import authenticator from "~/services/auth.server";
```
Next we need to check before the route is loaded if there is an authenticated user we can load the route, otherwise use redirect to the login route. We can do that using the `LoaderFunction` and calling the `authenticator.isAuthenticated` method. If there is an authenticated session then the `authenticator.isAuthenticated` method will return the session information which we are then passing to the page as loader data.
```javascript
// app/routes/index.ts
/**
 * check the user to see if there is an active session, if not
 * redirect to login page
 *
 */
export let loader: LoaderFunction = async ({ request }) => {
  return await authenticator.isAuthenticated(request, {
    failureRedirect: "/login",
  });
};
```
There is only one action supported in this index route and that is to call the authenticator to log the user out of the application, see the code below.
```javascript
// app/routes/index.ts
/**
 *  handle the logout request
 *
 */
export const action: ActionFunction = async ({ request }) => {
  await authenticator.logout(request, { redirectTo: "/login" });
};
```
The last bit of the index route in the actual code to render the component. We use the `useLoaderData` hook to get the session information we are returning if there is an authenticated session. We then render the user name and token in the page along with a button to logout of the application
```react
// app/routes/index.ts
export default function DashboardPage() {
  const data = useLoaderData();
  return (
    <div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.4" }}>
      <h1>Welcome to Remix Protected Dashboard</h1>
      <p>{data?.name}   {data?.token}</p>
      <Form method="post">
        <button>Log Out</button>
      </Form>
    </div>
  );
}
```
The second route in this application, the login route is not protected but we don't want render the route if there is already a session; so we use the same `authenticator.isAuthenticated` method but redirect on success. If not successful, meaning the user is not authenticated, then we are going to render the page. Before rendering the page we check the session, using the `LoaderFunction`, to see if there are any errors available from the authenticator using the `sessionErrorKey`, all of this happens in the page's `LoaderFunction`
```javascript
// app/routes/login.ts
/**
 * get the cookie and see if there are any errors that were
 * generated when attempting to login
 *
 */
export const loader: LoaderFunction = async ({ request }) => {

  await authenticator.isAuthenticated(request, {
    successRedirect : "/"
  });

  const session = await sessionStorage.getSession(
    request.headers.get("Cookie")
  );

  const error = session.get("sessionErrorKey");
  return json<any>({ error });
};
```
The `ActionFunction` in the login route is for logging in or authenticating the user.
if successful, we route to the index route, if not we redirect back to login page where the `LoaderFunction` will determine if there are any associated errors to render in the page.
```javascript
/**
 * called when the user hits button to login
 *
 */
export const action: ActionFunction = async ({ request, context }) => {
  // call my authenticator
  const resp = await authenticator.authenticate("form", request, {
    successRedirect: "/",
    failureRedirect: "/login",
    throwOnError: true,
    context,
  });
  console.log(resp);
  return resp;
};
```
Finally we need to render the actual component page. On the page we have the input form fields for the login, the submit button and a seperate section to render the errors. The information for the errors are returned in the `useLoaderData` hook and rendered at the bottom of the page.
```react
export default function LoginPage() {
  // if i got an error it will come back with the loader data
  const loaderData = useLoaderData();
  console.log(loaderData);
  return (
    <div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.4" }}>
      <h1>Welcome to Remix-Auth Example</h1>
      <p>
        Based on the Form Strategy From{" "}
        <a href="https://github.com/sergiodxa/remix-auth" target={"_window"}>
          Remix-Auth Project
        </a>
      </p>
      <Form method="post">
        <input type="email" name="email" placeholder="email" required />
        <input
          type="password"
          name="password"
          placeholder="password"
          autoComplete="current-password"
        />
        <button>Sign In</button>
      </Form>
      <div>
        {loaderData?.error ? <p>ERROR: {loaderData?.error?.message}</p> : null}
      </div>
    </div>
  );
}
```
###Source Code
{% github aaronksaunders/remix-auth-form-strategy %}

###Links
- Remix Documentation - https://remix.run/docs/en/v1
- Remix Auth - https://github.com/sergiodxa/remix-auth
- Remix Auth Form Strategy - https://github.com/sergiodxa/remix-auth-form
- Source Code - https://github.com/aaronksaunders/remix-auth-form-strategy
- Remix Playlist  - https://buff.ly/3GuKVPS

###Follow Me 
- twitter - https://twitter.com/aaronksaunders
- github - https://github.com/aaronksaunders
- udemy - https://www.udemy.com/user/aaronsaunders
- gumroad - https://app.gumroad.com/fiwic