Learn Foo Authentication

Currently, only Next.js with React is implemented. The documentation will be updated whenever more integrations are made available.

Getting started

Install the following dependencies:

  • @foo-auth/core
  • @foo-auth/next
  • @foo-auth/provider-credentials
  • @foo-auth/react
  • @foo-auth/session-cookie

Configuration

First, let’s define some environment variables.

./env
# App origin (defaults to empty, or same origin)
NEXT_PUBLIC_ORIGIN=http://localhost:3000
# App base path (defaults to emtpy)
#NEXT_PUBLIC_BASE_PATH=/foo
# The path to the API routes under NEXT_PUBLIC_BASE_PATH (defaults to /api)
#NEXT_PUBLIC_API_PATH=/api

Then, let’s define configurations that will be available on both client and server. The following is a detailed congiruation, however not all options are mandatory.

./foo-auth.config.ts
import type { FooAuthEndpoints } from "@foo-auth/core";
import type { FooAuthPages } from "@foo-auth/next";

/**
 * All of the following endpoints are relative to the API path.
 * For example: "/session" becomes "/api/session" (if base path is empty)
 *
 * NOTE : this constant is optional. If not defined, or if any property is not
 *        defined, then the missing properties will be the following values.
 */
export const endpointPaths: FooAuthEndpoints = {
  callback: "/callback", // the return URL for external providers
  csrfToken: "/csrf-token", // the CSRF
  session: "/session",
  signIn: "/sign-in",
  signOut: "/sign-out",
};

/**
 * All of the following pages are relative to the base path.
 * For example: "/" remains the same (if base path is empty)
 *
 * NOTE : this constant is optional. If not defined, or if any property is not
 *        defined, then the missing properties will be the following values.
 */
export const pages: FooAuthPages = {
  home: "/",
  signin: "/sign-in",
  signout: "/sign-out",
};

/**
 * The following value is mandatory and is used to secure CSRF tokens, or
 * other session variables. Only the first 32 characters will be used.
 */
export const secret: string = "... a string of at least 32 characters ...";

This configuration may be a JSON file as it must only contain data which could be imported on the client side. For server side configuration, please read below.

The minimal configuration is the following:

./foo-auth.config.json
{
  "secret": "... a string of at least 32 characters ..."
}

Lastly, we must define a few interfaces:

./src/types/foo-auth.ts
import type { User } from "./path/to/models/Users";

/**
 * The session object returned from the '/session' endpoint
 */
export type UserSession = {
  user: Omit<User, "password"> | null | undefined;
};

/**
 * The snapshot of the data saved inside the encrypted session
 */
export type SessionSnapshot = {
  userId: string;
};

/**
 * The credential information (if using the credentials provider)
 */
export type UserCredentials = {
  username: string;
  password: string;
};

Next.js Configuration

With the .env, types, and configuration defined,, we can add the settings required for the interfaces and endpoints.

./src/foo-auth.next.ts
import { createSecretKey } from "@foo-auth/core";
import { sessionCookie } from "@foo-auth/session-cookie";

import { credentials } from "@foo-auth/provider-credentials";

import { endpointPaths, pages, secret } from "../foo-auth.config";

import Users from "./path/to/models/Users";

import type { NextFooAuthOptions } from "@foo-auth/next";

import type {
  User,
  SessionSnapshot,
  UserSession,
  UserCredentials,
} from "./types/foo-auth";

export const fooAuthOptions: NextFooAuthOptions<UserSession> = {
  endpointPaths,
  pages,

  session: sessionCookie<UserSession, SessionSnapshot>({
    saveSession(sessionValue) {
      return { sub: sessionValue.user?.id ?? "" };
    },
    async restoreSession(snapshot) {
      const user = snapshot?.sub
        ? await Users.getById(snapshot.sub, {
            password: false, // do not retrieve password
          })
        : null;

      // returning null will invalidate the session
      return suer ? { user } : null;
    },
  }),

  providers: [
    credentials<UserCredentials, UserSession>({
      async authenticate(credentials: UserCredentials) {
        const user = await Users.findOne(
          {
            username: credentials.username,
            password: credentials.password,
          },
          {
            password: false, // do not retrieve password
          }
        );

        // returning null will cancel the authentication
        return user ? { user } : null;
      },
    }),
  ],

  secretKey: createSecretKey(secret),
};

And now we add the API endpoints:

./src/api/auth/[...auth].ts
import { fooAuthNext } from "@foo-auth/next";

import { fooAuthOptions } from "../../foo-auth.next";

export default fooAuthNext(fooAuthOptions);

At this point, the endpoints should be available for querying. After starting the app, try the CSRF token endpoint.

fetch("/api/csrf-token")
  .then((response) => response.json())
  .then((result) => console.log(result));
// -> { "csrfToken": "... generated hash ..." }

Wrapping up with React

Let’s have a welcome page to see who is signed-in.

Home Page

./src/pages/index.tsx
import { withServerSideAuthProps } from "@foo-auth/next";
import { useSession } from "@foo-auth/react";

import { fooAuthOptions } from "../foo-auth.next";

import type { UserSession } from "../types/foo-auth";

export const getServerSideProps = withServerSideAuthProps<UserSession>(
  fooAuthOptions,
  async (context) => {
    return {
      props: {
        ...context.sessionProps,
        // add more props below....
      },
    };
  }
);

export default function IndexPage() {
  const session = useSession<UserSession>();
  const fullName = session?.user
    ? `${session.user.firstName} ${session.user.lastName}`
    : "Guest";

  return (
    <>
      <h2 className="text-2xl">Hello {fullName} !</h2>
      <p>(Destroy session cookie and refresh.)</p>
    </>
  );
}

In order to use the useSession hook, the session provider needs to be rendered.

./src/pages/_app.tsx
import { SessionProvider } from "@foo-auth/react";

import { QueryClient, QueryClientProvider } from "react-query";

import type { FooAuthPageProps } from "@foo-auth/next";
import type { UserSession } from "../types/foo-auth";
import type { AppType } from "next/app";

const queryClient = new QueryClient();

const FooAuthApp: AppType<FooAuthPageProps<UserSession>> = ({
  Component,
  pageProps: { session, endpointPaths, ...pageProps },
}) => {
  return (
    <QueryClientProvider client={queryClient}>
      <SessionProvider session={session} endpointPaths={endpointPaths}>
        <Component {...pageProps} />
      </SessionProvider>
    </QueryClientProvider>
  );
};

export default FooAuthApp;

Sign In Page

./src/pages/sign-in.tsx
import { useMutation, useQuery, useQueryClient } from "react-query";

import { withServerSideAuthProps } from "@foo-auth/next";
import { useSessionQueries } from "@foo-auth/react";

import { fooAuthOptions } from "../foo-auth.next";
import UserCredentialsForm from "../components/UserCredentialsForm";

import type { UserSession, UserCredentials } from "../types/foo-auth";

export const getServerSideProps = withServerSideAuthProps<UserSession>(
  fooAuthOptions,
  async (context) => {
    return {
      props: {
        ...context.sessionProps,
        // add more props below....
      },
    };
  }
);

export default function SignInPage() {
  const queryClient = useQueryClient();
  const { csrfTokenQuery, signInMutation } = useSessionQueries<UserSession>();

  const { isLoading: csrfLoading, data: csrfData } = useQuery(
    ["csrf"],
    csrfTokenQuery,
    {
      refetchOnWindowFocus: false,
    }
  );

  const signIn = useMutation(
    async (variables: GetSignInMutationOptions<UserCredentials>) =>
      signInMutation(variables),
    {
      onSuccess: () => {
        // Invalidate session
        queryClient.invalidateQueries("session");
      },
    }
  );

  const handleSubmit = (credentials: UserCredentials) => {
    signIn.mutate({
      providerName: "credentials",
      payload: {
        ...credentials,
        ...csrfData,
      },
    });
  };

  return <UserCredentialsForm onSubmit={handleSubmit} />;
}

That’s it! The above example leaves out application-specific implementations, such as the UserCredentialsForm component, as well as data persistence.