Skip to content

hambergerpls/auth-remix

Repository files navigation

auth-remix (Remix Auth) (Experimental)

auth-remix is currently experimental. The API will change in the future.

Remix Auth is the community Remix integration for Auth.js. It provides a simple way to add authentication to your Remix app in a few lines of code.

TODO

  • auth-remix/node
  • auth-remix/cloudflare
  • auth-remix/deno
  • examples/node
  • examples/cloudflare
  • examples/deno
  • Tests for auth-remix/node
  • Tests for auth-remix/cloudflare
  • Tests for auth-remix/deno
  • Credentials example
  • OAuth example
  • Magic Link example
  • WebAuthn example

Installation

npm install auth-remix

Usage

// src/lib/auth.server.ts

/**
 *  Ensure the current file name has `.server` in order to explicitly
 *  mark it that it should be server-side only.
 */

import Credentials from "auth-remix/providers/credentials";
import Google from "auth-remix/providers/google";
import { RemixAuth } from "auth-remix/node"; // or cloudflare/deno

/**
 *  We import { RemixAuth } and call the function with a provider and destructure the object returned.
 *  In this example, we use { Credentials } provider for a simple password-based authentication.
 *
 *  { loader } and { action } are handlers to be exported at `auth.$` route.
 *  { getSession } function is used to retrieve the current user's session.
 *  { getCsrfToken } function is used to retrieve csrfToken and set it in cookie.
 *  { signIn } is a function to sign in a user.
 *  { signOut } is a function to sign out a user.
 */
export const { loader, action, getSession, getCsrfToken, signIn, signOut } = RemixAuth({ 
  // adapter: (env) => D1Adapter(env.db) for cloudflare
  // adapter: DrizzleAdapter(db, schema)
  providers: [
    // clientId and secret will be impicitly set from env!
    // e.g. AUTH_GOOGLE_ID & AUTH_GOOGLE_SECRET from env
    // reference: https://authjs.dev/guides/environment-variables#environment-variable-inference
    Google({}) 
    Credentials({
      id: "credentials",
      name: "Password",
      credentials: {
        password: { label: "Password", type: "password" },
      },
      async authorize(credentials) {
        if (credentials.password === "password") {
          return {
            email: "bob@alice.com",
            name: "Bob Alice",
            image: "https://avatars.githubusercontent.com/u/67470890?s=200&v=4",
          }
        }
        if (credentials.password === "123") {
          return {
            email: "alice@bob.com",
            name: "Alice Bob",
            image: "https://avatars.githubusercontent.com/u/67470890?s=200&v=4",
          }
        }
        return null;
      }
    })
  ]
});
// src/routes/auth.$.ts
/** 
 * We export the loader and action function from RemixAuth at auth.$ route.
 * This acts as the /auth/<action>/<provider> endpoint for Auth.js
 * */
export { loader, action } from "~/lib/auth.server";

Don't forget to set the AUTH_SECRET environment variable in your .env file. This should be a minimum of 32 characters, random string. On UNIX systems you can use openssl rand -hex 32 or check out https://generate-secret.vercel.app/32.

Signing in and signing out

// src/routes/signin.tsx
import { BuiltInProviderType } from "@auth/core/providers";
import { json, ActionFunction, LoaderFunctionArgs } from "@remix-run/node"; //or cloudflare/deno
import { Form, useActionData, useLoaderData } from "@remix-run/react";
import { getCsrfToken, signIn } from "~/lib/auth.server";

/**
 *  Auth.js expects a csrf token for signing in. Therefore, we need to
 *  get the token using { getCsrfToken } . The response returned
 *  will contain `Set-Cookie` headers that also contain the csrf token to
 *  be set on the browser cookie.
 */
export const loader = async ({ request, context }: LoaderFunctionArgs) => {
  const csrfTokenResponse = await getCsrfToken({ request, context });
  if (!csrfTokenResponse.ok) {
    throw new Error("Error fetching csrf");
  }
  const { csrfToken } = await csrfTokenResponse.json()
  return json( { csrfToken }, { headers: csrfTokenResponse.headers } );
}

/**
 *  We retrieve the { provider } value from the form and pass 
 *  the `POST` { request } and { provider } to the signIn function.
 *  Having provider value in the `Form` is useful if we want to
 *  implement multiple sign-in methods. We can optionally pass a
 *  { redirectTo } option if we want to redirect the user to a
 *  specific page after authentication.
 */
export const action: ActionFunction = async ({ request, context }) => {
  const provider = ( await request.clone().formData() ).get("provider")
  const loginResponse = await signIn(
    { request, context },
    provider as BuiltInProviderType ?? "credentials",
    { redirectTo: new URL( request.url ).searchParams.get("redirectTo") ?? "" }
  );
  if (!loginResponse.ok) {
    json({ error: ( await loginResponse.json() ).message })
  }
  return loginResponse;
}

/**
 *  We use { useLoaderData } to retrieve the csrfToken
 *  and put the value in a hidden input field.
 */
export default function SignInPage() {
  const { csrfToken } = useLoaderData<typeof loader>();
  const error = useActionData<typeof action>();
  return (
    <div>
      { error && <p>{error}</p> }
      <Form method="POST">
        <label htmlFor="password">Password</label>
        <input name="password" type="password" />
        <input name="provider" type="hidden" value="credentials" />
        <input name="csrfToken" type="hidden" value={csrfToken} />
      </Form>
    </div>
  )
}

You can sign in or out by calling signIn or signOut function exported from your src/lib/auth.server.ts. Make sure to include the csrfToken in the request body for all sign-in and sign-out requests.

Authorization

You can protect routes by checking for the presence of a session and then redirect to a login page if the session is not present. This can be done via layout nesting as follows:

// src/routes/_protected.tsx
import { json, LoaderFunctionArgs, redirect } from "@remix-run/node"; //or cloudflare/deno
import { Outlet } from "@remix-run/react";
import { getSession } from "~/lib/auth.server";

/**
 *  The `_protected` layout acts as a protected shell that fetches
 *  the current user from the current session. We can redirect the
 *  user to a sign-in page if the user is not logged in, else we
 *  return the user data which is accessible by the children.
 */
export const loader = async ({ request, context }: LoaderFunctionArgs) => {
  const user = await getSession({ request, context });
  if (!user || !user.user) {
    return redirect(`/signin?redirectTo=${request.url}`);
  }
  return json({ user: user.user })
}

export default function ProtectedPage() {
  return <Outlet />
}

Per Route

import { getSession } from "~/lib/auth.server";

export const loader = async ({ request, context }: LoaderFunctionArgs) => {
  const user = await getSession({ request, context });
  if (!user) {
    return redirect(`/`);
  }
  return json( { user } );
}

Managing sessions

You can access the session data from the parent layout as follows

// src/routes/_protected.profile.tsx
import { Form, useRouteLoaderData } from "@remix-run/react"
import { loader } from "./_protected";

/**
 *  We don't need to use { getSession } function to fetch the current user.
 *  We can use Remix { useRouteLoaderData } to get the user data from the parent
 *  layout by searching with route id.
 */
export default function ProfilePage() {
  const { user } = useRouteLoaderData<typeof loader>("routes/_protected");

  return (
    <div>
      <p>Name: {user.name}</p>
      <p>Email: {user.email}</p>
      <img src={user.image ?? ""} />
      <p>Raw: {JSON.stringify(user)}</p>
      <Form method="GET" action="/signout">
        <button type="submit">Sign Out</button>
      </Form>
    </div>
  )
}

Contributing

Feel free to open a PR.

License

ISC