Working with Remix: AWS Amplify Authentication Using Authenticator UI and AppSync Integration
Tags: remix, aws, amplify, webdev
Published: 2022-06-06 23:20:46

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

{% youtube https://youtu.be/1XN-vZYd2_I %}

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

```javascript
// 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 });
```

```javascript
// 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


```javascript
// 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.

```javascript
// 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.

```javascript
// 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.

```javascript
// 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.

```javascript
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.

```javascript
// 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

```javascript
// 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.

```javascript
  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.

```javascript
// 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

```javascript
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.

```javascript
// 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
{% github aaronksaunders/amplify-remix-todos-1 %}