Auth.js

Auth.js is a well-known library for authentication used by various JS frameworks. With Auth.js, we benefit from reduced complexity. We also have access to a range of authentication providers, such as GitHub, Google, Facebook, among others. Moreover, it can be integrated into multiple frameworks including Qwik.

Auth.js offers several features that enhance simplicity, productivity, flexibility, and provider diversity. Here are the main features of Auth.js:

  • Providers: Auth.js supports multiple providers, simplifying the authentication process in our applications (e.g., Github, Google, Facebook, Twitter). It also offers Single Sign-On (SSO) services as well as traditional authentication.
  • Management: Auth.js greatly assists us in concentrating on our business logic. It manages tokens, stores them, and refreshes them automatically.
  • Configuration: Configuring Auth.js is straightforward. It offers easy installation, error handling, custom forms for login and registration, and effortless integration with providers.
  • Integrations: Auth.js seamlessly integrates with JS frameworks, aided by its comprehensive documentation that provides a clear guide to follow.
  • Security: While Auth.js is developer-friendly, it's essential to recognize the underlying complexity that ensures a high level of security for our data.

Be aware that the Auth.js library is still in a pre-1.0 stage and could have bugs.

Installation

You can add Auth.js easily by using the following Qwik starter script:

npm run qwik add auth

This command will add new packages:

  • @auth/core
  • @builder.io/qwik-auth

and create a new file named plugin@auth.ts with an example configuration.

Manual Action Required

After installing the auth package using npm run qwik add auth the @auth/core package will need to be added to the dependency optimization settings of the vite.config.js file:

vite.config.js
export default defineConfig(() => {
  return {
    plugins: [qwikCity(), qwikVite(), tsconfigPaths()],
    dev: {
      headers: {
        'Cache-Control': 'public, max-age=0',
      },
    },
    preview: {
      headers: {
        'Cache-Control': 'public, max-age=600',
      },
    },
    optimizeDeps: {
      include: [ "@auth/core" ]
    }
  };
});

Note on Manual Deployments with Node

When deploying applications manually using Node.js, particularly with frameworks like Express, the server or Node process does not inherently know if it's being served under HTTP or HTTPS. Unlike hosting services like Vercel, Netlify, or Cloudflare, where the ๏ปฟORIGIN configuration is automatically managed, manual setups require explicit specification. To ensure that your Node.js application recognizes the correct protocol and host it is being served under, set the following environment variables:

  • ๏ปฟORIGIN: Set this to the URL of your application. For example: ORIGIN=https://your-app-name.example.com
  • PROTOCOL_HEADER: This is used to indicate the original protocol requested by the client. Commonly, this is specified in proxy configurations. Set this variable to: PROTOCOL_HEADER=X-Forwarded-Proto
  • HOST_HEADER: This header helps identify the original host requested by the client. It is particularly necessary when your Node environment is behind a proxy or load balancer. Set this variable to: HOST_HEADER=X-Forwarded-Host

Qwik API

useAuthSession

A routeLoader$ that returns a session object or an empty object if there is no session. The contents of the session object that is returned are configurable with the session callback. Session data can also be retrieved using the session REST API.

import { component$ } from '@builder.io/qwik';
import { useAuthSession } from '~/routes/plugin@auth';
 
export default component$(() => {
  const session = useAuthSession();
  return <p>{session.value?.user?.email}</p>;
});
 

useAuthSignin

A routeAction$ used to initiate a signin flow or send the user to the signin page listing all possible providers. The CSRF token is handled internally when signing in using useAuthSignin.

Parameters

  • providerId: An optional string parameter with the name of the provider. When supplied will initiate the Authorization Request to your Identity Provider. When omitted will redirect to the built-in/unbranded sign-in page.
  • options: An optional object of options.
    • callbackUrl: An optional string specifying to which URL the user will be redirected after signing in. Defaults to the page URL the sign-in is initiated from.
  • authorizationParams: An optional object of additional parameters sent to the /authorize endpoint. See the Authorization Request OIDC spec for some ideas.

NOTE: You can also set the authorizationParams through the provider.authorizationParams configuration

Example using useAuthSignin with the <Form> component and optional providerId and options.callbackUrl:

import { component$ } from '@builder.io/qwik';
import { Form } from '@builder.io/qwik-city';
import { useAuthSignin } from '~/routes/plugin@auth';
 
export default component$(() => {
  const signIn = useAuthSignin();
  return (
    <Form action={signIn}>
      <input type="hidden" name="providerId" value="github" />
      <input type="hidden" name="options.callbackUrl" value="http://qwik-auth-example.com/dashboard" />
      <button>Sign In</button>
    </Form>
  );
});

Example using useAuthSignin programmatically with optional providerId and options.callbackUrl:

import { component$ } from '@builder.io/qwik';
import { useAuthSignin } from '~/routes/plugin@auth';
 
export default component$(() => {
  const signIn = useAuthSignin();
  return (
    <button onClick$={() => signIn.submit({ providerId: 'github', options: { callbackUrl: 'http://qwik-auth-example.com/dashboard' } })}>Sign In</button>
  );
});

useAuthSignout

A routeAction$ used to initiate a signout flow. The user session will be invalidated/removed from the cookie/database, depending on the flow you chose to store sessions.

Parameters

  • callbackUrl: An optional string specifying to which URL the user will be redirected after signing out. Defaults to the page URL the sign-in is initiated from.

The 'callbackUrl' must be considered valid by the redirect callback handler. By default, it requires the URL to be an absolute URL at the same host name, or you can also supply a relative URL starting with a slash. If it does not match it will redirect to the homepage. You can define your own redirect callback to allow other URLs.

Example using useAuthSignout with the <Form> component and optional callbackUrl:

import { component$ } from '@builder.io/qwik';
import { Form } from '@builder.io/qwik-city';
import { useAuthSignout } from '~/routes/plugin@auth';
 
export default component$(() => {
  const signOut = useAuthSignout();
  return (
    <Form action={signOut}>
      <input type="hidden" name="callbackUrl" value="/signedout" />
      <button>Sign Out</button>
    </Form>
  );
});

Example using useAuthSignout programmatically with optional callbackUrl:

import { component$ } from '@builder.io/qwik';
import { useAuthSignout } from '~/routes/plugin@auth';
 
export default component$(() => {
  const signOut = useAuthSignout();
  return <button onClick$={() => signOut.submit({ callbackUrl: '/signedout' })}>Sign Out</button>;
});

REST API

All the same REST APIs as provided by Auth.js are available.

signin

GET /api/auth/signin

Displays the built-in/unbranded sign-in page.

POST /api/auth/signin/:provider

Starts a provider-specific sign-in flow. In the case of an OAuth provider, calling this endpoint will initiate the Authorization Request to your Identity Provider. This endpoint is also used by the useAuthSignin action internally.

callback

GET/POST /api/auth/callback/:provider

signout

GET /api/auth/signout

Displays the built-in/unbranded sign out page.

POST /api/auth/signout

Handles signing the user out - this is a POST submission to prevent malicious links from triggering signing a user out without their consent. The user session will be invalidated/removed from the cookie/database, depending on the flow you chose to store sessions. This endpoint is also used by the useAuthSignout method internally.

session

GET /api/auth/session

Returns client-safe session object - or an empty object if there is no session. The contents of the session object that is returned are configurable with the session callback. Session data can also be retrieved using the useAuthSession routerLoader.

csrf

GET /api/auth/csrf

Returns object containing CSRF token. In NextAuth.js, CSRF protection is present on all authentication routes. It uses the "double submit cookie method", which uses a signed HttpOnly, host-only cookie. The CSRF token returned by this endpoint must be passed as form variable named csrfToken in all POST submissions to any API endpoint.

providers

GET /api/auth/providers

Returns a list of configured OAuth services and details (e.g. sign in and callback URLs) for each service. It is useful to dynamically generate custom sign up pages and to check what callback URLs are configured for each OAuth provider that is configured.

Examples

GitHub

  1. Follow the GitHub OAuth Guide to get your GitHub Client ID, GitHub Client Secrets and generate AUTH_SECRET using openssl rand -base64 32 or Secret Generator.
  2. Since the default plugin@auth.ts uses GitHub as an example, we don't need to change anything there. However a provider other than GitHub could be used or additional provides could be added. Auth.js also supports many additional options that can be set in this file.
src/routes/plugin@auth.ts
import { serverAuth$ } from '@builder.io/qwik-auth';
import GitHub from '@auth/core/providers/github';
import type { Provider } from '@auth/core/providers';
 
export const { onRequest, useAuthSession, useAuthSignin, useAuthSignout } = serverAuth$(
  ({ env }) => ({
    secret: env.get("AUTH_SECRET"),
    trustHost: true,
    providers: [
      GitHub({
        clientId: env.get("GITHUB_ID"),
        clientSecret: env.get("GITHUB_SECRET"),
      }),
    ] as Provider[],
  })
);

IMPORTANT: Make sure to keep the onRequest export as it is used to handle the oAuth flow redirects. Once the user finished oAuth flow, GitHub (or any other provider) will redirect the user back to the application to /api/auth/callback/github (or /api/auth/callback/[otherProvider]). The onRequest middleware function will handle requests to this endpoint and finish the oAuth flow.

  1. Create or edit the .env.local file at the root of your project to store secrets
.env.local
GITHUB_ID=
GITHUB_SECRET=
AUTH_SECRET=

IMPORTANT: Please read the Qwik documentation about Environment Variables to ensure you are using them safely. Many provider secrets should be kept secure and not exposed to the client/browser.

  1. The application is now ready to implement authentication using Auth.js.
  2. Enjoy!

Credentials

Warning: This functionality is discourage by Auth.js.

https://next-auth.js.org/providers/credentials

  • The functionality provided for credentials based authentication is intentionally limited to discourage use of passwords due to the inherent security risks associated with them and the additional complexity associated with supporting usernames and passwords.
  1. Since the default plugin@auth.ts uses GitHub as an example, we need to replace it with Credentials.
src/routes/plugin@auth.ts
import { serverAuth$ } from '@builder.io/qwik-auth';
import Credentials from "@auth/core/providers/credentials";
import type { Provider } from '@auth/core/providers';
 
export const { onRequest, useAuthSession, useAuthSignin, useAuthSignout } = serverAuth$(
  ({ env }) => ({
    secret: env.get("AUTH_SECRET"),
    trustHost: true,
    providers: [
      Credentials({
        async authorize(credentials, req) {
          // Add logic here to look up the user from the credentials supplied
          const user = {
            id: 1,
            name: "Mike",
            email: "mike@example.com",
          };
 
          return user;
        },
      }),
    ] as Provider[],
  })
);
  1. Create or edit the .env.local file at the root of your project to store secrets
.env.local
AUTH_SECRET=

IMPORTANT: Please read the Qwik documentation about Environment Variables to ensure you are using them safely. Many provider secrets should be kept secure and not exposed to the client/browser.

  1. The application is now ready to implement authentication using Auth.js.
  2. Enjoy!

Route Protection

Session data can be accessed via the route event.sharedMap. So a route can be protected and redirect using something like this located in a layout.tsx or page index.tsx:

export const onRequest: RequestHandler = (event) => {
  const session: Session | null = event.sharedMap.get('session');
  if (!session || new Date(session.expires) < new Date()) {
    throw event.redirect(302, `/api/auth/signin?callbackUrl=${event.url.pathname}`);
  }
};

Note: If placed in a layout.tsx ensure that the redirect destination does not share the same layout.tsx or a redirection loop could occur.

Contributors

Thanks to all the contributors who have helped make this documentation better!

  • the-r3aper7
  • ulic75
  • jakovljevic-mladen
  • ayoub9494
  • igorbabko
  • yaikohi
  • mrhoodz
  • VinuB-Dev
  • hugomonte
  • VarPDev