DEV Community

Aaron K Saunders
Aaron K Saunders

Posted on

Remix Authentication Using Remix-Auth Package

This is additional information related to a video walkthrough of a sample Remix application using the remix-auth package which is a passport-like framework for simplifying authentication of your remix application using specific packaged strategies.

In this example, I am using the Form Strategy to show a simple login flow.

After creating your Remix application, install the required npm packages

npm install remix-auth remix-auth-form
Enter fullscreen mode Exit fullscreen mode

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

// 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;
};
Enter fullscreen mode Exit fullscreen mode

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.

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
});
Enter fullscreen mode Exit fullscreen mode

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

// 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
Enter fullscreen mode Exit fullscreen mode

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

// app/routes/index.ts
import { ActionFunction, Form, LoaderFunction, useLoaderData } from "remix";
import authenticator from "~/services/auth.server";
Enter fullscreen mode Exit fullscreen mode

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.

// 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",
  });
};
Enter fullscreen mode Exit fullscreen mode

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.

// app/routes/index.ts
/**
 *  handle the logout request
 *
 */
export const action: ActionFunction = async ({ request }) => {
  await authenticator.logout(request, { redirectTo: "/login" });
};
Enter fullscreen mode Exit fullscreen mode

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

// 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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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

// 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 });
};
Enter fullscreen mode Exit fullscreen mode

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.

/**
 * 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;
};
Enter fullscreen mode Exit fullscreen mode

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.

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

Source Code

GitHub logo aaronksaunders / remix-auth-form-strategy

Remix Authentication Using Remix-Auth Package and the Form Strategy

Remix Authentication Using Remix-Auth Package

#remix #remixrun #reactjs

This is a walkthrough of a sample application using the remix-auth package which is a passport-like framework for simplifying authentication of your remix application using specific packaged strategies.

In this example, I am using the Form Strategy to show a simple login flow

Remix Playlist - https://buff.ly/3GuKVPS

Links

Links

Follow Me

Oldest comments (1)

Collapse
 
justinhackin profile image
Justin Barca • Edited

I think this example is misleading. Particularly, you have Authenticator<User | Error | null> but your form strategy doesn't return an Error or null. You never use the resolved value of isAuthenticated and so the problem is not exposed. A more complete example would use the resolved value (user id) to fetch some data for the protected route and you would discover the issues with the resolved value being possibly null or an Error. I started a discussion in the library github because this tripped me up, see: github.com/sergiodxa/remix-auth/di...