This is the companion blog post for a video I have on using AWS Amplify for Authentication in a Remix Application.
We show how to implement complete authentication flows to your application with minimal boilerplate. We then make a database query using the AWS Appsync API with GraphQL to retrieve data.
This video does not walkthrough setting up an AWS Amplify environment, there are plenty of videos cover that already, this just shows how to using Remix within your preconfigured environment
Setup
In Root.tsx we configure Amplify and set up the provider so we can use the hooks later to get information about the authenticated user/
// root.jsx
// AMPLIFY
import { Amplify } from "aws-amplify";
import config from "../src/aws-exports";
import styles from "@aws-amplify/ui-react/styles.css";
import { Authenticator } from "@aws-amplify/ui-react";
Amplify.configure({ ...config });
// root.jsx
export default function App() {
return (
<html lang="en">
<head>
<Meta />
<Links />
</head>
<body>
<Authenticator.Provider>
<Outlet />
<ScrollRestoration />
<Scripts />
<LiveReload />
</Authenticator.Provider>
</body>
</html>
);
}
Check For Authenticated User/Session
In routes/index.jsx
we check in the LoaderFunction to see if we have a valid session. If we do not have a valid session then we redirect to the login route, otherwise we redirect to the task page.
The requireUserId
takes a redirectTo
parameter which in this case is /login
and throws and exception with a redirect which causes the route to change
// routes/index.jsx
import { redirect } from "@remix-run/node";
import { requireUserId } from "~/session.server";
/**
* check for authenticated user, if not redirect to
* login
*
* @param {*} param0
* @returns
*/
export async function loader({ request }) {
const id = await requireUserId(request, "/login");
return redirect("/tasks");
}
Authentication Flow
On the routes/login.jsx
page is where a lot of the activity happens, In this page we are just rendering the AWS Amplify Authenticator UI Component and it manages the login and create account functionality for us.
// routes/login.jsx
<Authenticator>
{({ signOut, user }) => (<h1>LOADING...</h1>)}
</Authenticator>
Once the login is complete, we use the user returned from the useAuthenticator
hook to get the current user which is then passed to the server through the function setUserSessionInfo
which sets some form data with the parameters and uses fetcher
to call the routes ActionFunction.
// routes/login.jsx
export function Login() {
// for calling action
const fetcher = useFetcher();
const { user } = useAuthenticator((context) => [context.user]);
useEffect(() => {
console.log(user);
setUserSessionInfo(user);
}, [user]);
/**
*
*/
const setUserSessionInfo = useCallback((user) => {
// if i have a user then submit the tokens to the
// action function to set up the cookies for server
// authentication
if (user && fetcher.type === "init") {
fetcher.submit(
{
accessToken: user?.signInUserSession?.accessToken?.jwtToken,
idToken: user?.signInUserSession?.idToken?.jwtToken,
},
{ method: "post" }
);
}
},[user]);
Finally the setUserSessionInfo
function calls the action; you can see in the action below we call our function to set the cookie session using the information provided to us from Amplify.
// login.jsx
/**
*
*/
export const action: ActionFunction = async ({ request }) => {
// get data from the form
let formData = await request.formData();
let accessToken = formData.get("accessToken");
let idToken = formData.get("idToken");
// create the user session
return await createUserSession({
request,
userInfo: {
accessToken,
idToken,
},
redirectTo: "/tasks",
});
};
This is the function createUserSession
in session.server.ts
where we use the cookie package provided to us by Remix to save the session information so we can retrieve it later to confirm we have an authenticated user.
export async function createUserSession({
request,
userInfo,
redirectTo,
}: {
request: Request;
userInfo: any;
redirectTo: string;
}) {
const session = await getSession(request);
session.set(USER_SESSION_KEY, userInfo);
return redirect(redirectTo || "/tasks", {
headers: {
"Set-Cookie": await sessionStorage.commitSession(session, {
maxAge: 60 * 60 * 24 * 7 // 7 days
}),
},
});
}
Get Some Data From Database
First we need to check that we have a user session by checking the cookie.
// tasks.tsx
export async function loader({ request }) {
const response = await requireUserId(request, "/login");
const { accessToken, idToken } = response || {}
Next we create an AWSAppSyncClient
that we can use to query from the databased using graphql. The important thing to note is that we are using the accessToken
from the session cookie to make the authenticated API call.
all of the config
props come the the aws-exports.js
file
// tasks.tsx
const client = new AWSAppSyncClient({
url: config.aws_appsync_graphqlEndpoint,
region: config.aws_appsync_region,
auth: {
type: config.aws_appsync_authenticationType,
jwtToken: () => accessToken,
},
disableOffline: true,
offlineConfig: {
keyPrefix: "myPrefix",
},
});
Now that we have an authenticated client, we can make our query and return the data from the loader.
const { data } = await client.query({
query: gql(`query MyQuery {
listTasks {
nextToken
startedAt
items {
id
description
createdAt
}
}
}
`),
authMode: config.aws_appsync_authenticationType,
});
console.log(data.listTasks.items);
return json({
accessToken,
idToken,
tasks: data.listTasks.items
});
The only action in this component is to logout of the application. Here we call another function from session.server
to log us out and clean up the cookies.
// tasks.jsx
export const action = async ({ request }) => {
console.log("in logout action");
return await logout(request);
};
The session.server
function logout
clears the session cooke and redirects back to login page
export async function logout(request: Request) {
try {
console.log("server logout");
const session = await getSession(request);
return redirect("/", {
headers: {
"Set-Cookie": await sessionStorage.destroySession(session),
},
});
} catch (e) {
console.log("server logout error", e)
}
}
Finally the client side of the component. We render the data we got back from the loader function.
// tasks.jsx
export default function Tasks() {
const fetcher = useFetcher();
const { accessToken, idToken, tasks } = useLoaderData();
const [user, setUser] = useState();
useEffect(() => {
Auth.currentUserInfo().then((userInfo) => setUser(userInfo));
}, [accessToken, idToken]);
return (
<div style={{ padding: 16 }}>
<Heading level={3} textAlign="center">
Private Page
</Heading>
<h3>
{user && `Logged in with authenticated user ${user?.attributes?.email}`}
</h3>
<button
className="ui button"
type="button"
onClick={async () => {
// amplify sign out
await Auth.signOut({ global: true });
// clear out our session cookie...
fetcher.submit({}, { method: "post" });
}}
>
Log Out
</button>
<div className="ui segment">
<h4>Data Loaded From Amplify</h4>
{/* <pre>{JSON.stringify(tasks, null, 2)}</pre> */}
<div className="ui list divided large relaxed">
{tasks?.map((t) => {
return <div className="ui item " key={t.id}>
<div className="ui content">
<div>{t.description}</div>
<div>{t.createdAt}</div>
</div>
</div>;
})}
</div>
</div>
</div>
);
}
Documentation Links
- Remix Cookie Package - https://remix.run/docs/en/v1/api/remix#cookies
- AWS Amplify Web Client SDK - https://docs.amplify.aws/cli/start/install/
- AWS Amplify Node SDK - https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/getting-started-nodejs.html
- AWS Amplify UI Components - https://ui.docs.amplify.aws/react/components/authenticator
Source Code
Working with Remix: AWS Amplify Authentication Using Authenticator UI and AppSync Integration
code sample of integrating AWS Amplify with a Remix Application. We show how to implement complete authentication flows to your application with minimal boilerplate. We then make a database query using the AWS Appsync API to retrieve data.
-
Video - https://youtu.be/1XN-vZYd2_I
Development
From your terminal:
npm run dev
This starts your app in development mode, rebuilding assets on file changes.
Deployment
First, build your app for production:
npm run build
Then run the app in production mode:
npm start
Now you'll need to pick a host to deploy it to.
DIY
If you're familiar with deploying node applications, the built-in Remix app server is production-ready.
Make sure to deploy the output of remix build
build/
public/build/
Using a Template
When…
Top comments (0)