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
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;
};
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
});
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
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";
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",
});
};
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" });
};
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>
);
}
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 });
};
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;
};
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>
);
}
Source Code
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
- Remix Docmentation - 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
- Remix Playlist - https://buff.ly/3GuKVPS
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
Top comments (1)
I think this example is misleading. Particularly, you have
Authenticator<User | Error | null>
but your form strategy doesn't return anError
ornull
. You never use the resolved value ofisAuthenticated
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 possiblynull
or anError
. I started a discussion in the library github because this tripped me up, see: github.com/sergiodxa/remix-auth/di...