Working with Remix: AWS Amplify Authentication Using Authenticator UI and AppSync Integration

Aaron K Saunders - Jun 6 '22 - - Dev Community

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

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

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

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

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

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

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 || {}

Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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>
  );
}

Enter fullscreen mode Exit fullscreen mode

Documentation Links

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.

Development

From your terminal:

npm run dev
Enter fullscreen mode Exit fullscreen mode

This starts your app in development mode, rebuilding assets on file changes.

Deployment

First, build your app for production:

npm run build
Enter fullscreen mode Exit fullscreen mode

Then run the app in production mode:

npm start
Enter fullscreen mode Exit fullscreen mode

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…




. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .