Password Authentication with Auth.js in Astro and Customizing Session Information (auth-astro)

koyopro - Oct 1 - - Dev Community

Background

  • I wanted to implement a feature in Astro that allows authentication with an email and password and stores session information in cookies.
  • It seemed possible with auth-astro, which is officially introduced in Astro.
    • auth-astro is a wrapper library that makes Auth.js (formerly NextAuth.js) easier to use in Astro.
  • I couldn't find consolidated information for the following two points, so I organized them:
    • How to set up password authentication.
    • How to customize the authentication information saved in cookies.

Important Note

After trying this, I found Auth.js to be somewhat difficult to use, so it's worth considering carefully before deciding to use it. Refer to the Impressions section for details.

Environment

  • Node.js: 20.11
  • astro: 4.15.2
  • auth-astro: 4.1.2
  • @auth/core: 0.35.3

※ In auth-astro 4.1.2, "@auth/core": "^0.32.0" is specified in the dependencies, but version 0.32.x had an issue where a 500 error occurred during error handling for CredentialsSignin, so I specified the latest version.

Final Code

Assuming that auth-astro has already been integrated into a project created with Astro, you can set up password authentication and customize session information by configuring auth.config.mjs as follows. The example below saves the logged-in user's ID as a session token in cookies.

// auth.config.mjs
import { CredentialsSignin } from "@auth/core/errors";
import Credentials from "@auth/core/providers/credentials";
import { defineConfig } from "auth-astro";

export default defineConfig({
  providers: [
    Credentials({
      name: "Email",
      credentials: {
        email: { type: "email", required: true, label: "Email" },
        password: { type: "password", required: true, label: "Password" },
      },
      authorize: async (credentials) => {
        const { email, password } = credentials;
        const user = await authorize(email, password); // your logic here
        if (!user) throw new CredentialsSignin();
        return { id: user.id };
      },
    }),
  ],
  callbacks: {
    async jwt({ user, token }) {
      if (user) {
        return { id: user.id };
      }
      return token;
    },
    session({ session, token }) {
      return {
        ...session,
        user: { id: token.id },
      };
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

UI

Using the login screen provided by Auth.js, the following behavior can be seen:

Login Screen (/api/auth/signin)

Image

When Authentication Fails (/api/auth/signin?error=CredentialsSignin&code=credentials)

Image

When Authentication Succeeds

After successful authentication, you are redirected to another page, and you can retrieve session information using getSession.

// pages/example.astro
---
import { getSession } from 'auth-astro/server';

const session = await getSession(Astro.request)
console.log(session)
// {
//   user: { id: 1 },
//   expireds: '2024-10-01T00:00:00.000Z'
// }
---
{session?.user ? (
  <p>Logged in as User {session.user.id}</p>
) : (
  <p>Not logged in</p>
)}
Enter fullscreen mode Exit fullscreen mode

When authentication is successful, a cookie named authjs.session-token is saved, with its contents encrypted in JWT format by Auth.js.

Example of a Session Token

eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIiwia2lkIjoiTjEzYW5aS3hzMmpXT0pDUmNYaTRCYjZCMkdUNmxZdmxaa3lUVEdfbHRYZG14UEJVYkVkNkJxaVlRRnhNdTFuZjRBa3BGRmxhTlI1dW11ZENRc3JaQ0EifQ..IAkEwYkG1M9Y-J5rTju63g.haLgQeSxUEqctr2Gjn23sMaNEAaGb9y1ott-dZnws-oo7Sdcz1fnPRRIvpLpBe6LkJN2JMELAHbElbAl-41BrgAzUDTeWnGlNTeFVa3Em6iWObNtfwX_H7nOtnddB3OZ.aOZQ_jzdVkNvdepksoAFc9I8Qz7fuFlcAtJcN-Tp-ng
Enter fullscreen mode Exit fullscreen mode

Customizing the Session Token

Default Behavior

When using Credentials, the default behavior is to save email as the unique key in the cookie. If you don't need to change the format, customization of the callbacks is unnecessary. The following settings would suffice.

// auth.config.mjs
import { CredentialsSignin } from "@auth/core/errors";
import Credentials from "@auth/core/providers/credentials";
import { defineConfig } from "auth-astro";

export default defineConfig({
  providers: [
    Credentials({
      name: "Email",
      credentials: {
        email: { type: "email", required: true, label: "Email" },
        password: { type: "password", required: true, label: "Password" },
      },
      authorize: async (credentials) => {
        const { email, password } = credentials;
        const user = await authorize(email, password); // your logic here
        if (!user) throw new CredentialsSignin();
        return { email: user.email };
      },
    }),
  ],
});
Enter fullscreen mode Exit fullscreen mode

Contents of the session

const session = await getSession(Astro.request)
console.log(session)
// {
//   user: { email: 'test@example.com', name: undefined, image: undefined },
//   expireds: '2024-10-01T00:00:00.000Z'
// }
Enter fullscreen mode Exit fullscreen mode

Customization Details

If you want to save information other than the email in the session token, you will need to specify the callbacks. The example below replaces the email with the user ID. This is reflected in the code shown in the "Final Code" section of this article.

authorize: async (credentials) => {
  // ...
  return { id: user.id }; // Change the return format
},
Enter fullscreen mode Exit fullscreen mode
callbacks: {
  // Change the token format saved in the cookie
  async jwt({ user, token }) {
    if (user) {
      return { id: user.id };
    }
    return token;
  },
  // Change the format of the information read from the token
  session({ session, token }) {
    return {
      ...session,
      user: { id: token.id },
    };
  },
},
Enter fullscreen mode Exit fullscreen mode

In this case, you can retrieve the session in the following format:

const session = await getSession(Astro.request)
console.log(session)
// {
//   user: { id: 1 },
//   expireds: '2024-10-01T00:00:00.000Z'
// }
Enter fullscreen mode Exit fullscreen mode

Impressions

The feature I wanted to implement, "password authentication with email and saving session information in cookies with Astro," was achieved with the above configuration.

However, after working with Auth.js, I felt that customizing its behavior might be challenging.

For example, in the following cases, there is no official support, so you would likely need to prepare a custom page and implement it yourself:

  • If you want to change error messages (for multilingual support, etc.)
  • If you want to modify the appearance of the login page

Also, keeping the email address entered in the field after an authentication failure seems quite difficult with the current Auth.js system. Since there is a redirect during the authentication process, the information entered in the form is lost.

In my research, I also came across the following opinions:

  • You're not a bad engineer, NextAuth is a bad library. - Reddit

    Conversely, it feels like NextAuth was written with the express goal of avoiding the complexity of auth, and it does it badly. It lets you get something working quickly, but later causes immense pain trying to customise it.

  • Why is next-auth (or Auth.js) so popular? - Reddit

    I have tried to build a new website with auth, and my experience with Auth.js (v5) was nothing short of a disaster. The docs was horrible, it offers little customizability, and the configuration just doesn't work. If I were the project lead, I wouldn't promote this piece of shit until it gets stable.

If you're using the default behavior of Auth.js, there might not be any issues, but if you're considering any customizations, it might be worth reviewing beforehand.

References

. . . . . . . .
Terabox Video Player