Skip to content
Ownfold
Esc
navigateopen⌘Jpreview
On this page

Auth.js

Integrate Auth.js with stable session IDs, callbacks, module augmentation, request-aware loading, ownership, account lifecycle, and tests.

@ownfold/auth-js converts a request-aware Auth.js session loader into Ownfold’s authentication resolver. It accepts only session.user.id; it never falls back to email or display name.

Install packages

pnpm add @ownfold/auth-js@beta @ownfold/server@beta next-auth

Expose a stable user ID

Auth.js session shapes depend on the configured session strategy, adapter, and callbacks. Ensure the server session includes your immutable application user ID.

import NextAuth from "next-auth"

export const { auth, handlers, signIn, signOut } = NextAuth({
  providers: [],
  callbacks: {
    session({ session, user, token }) {
      const id = user?.id ?? token.sub
      if (typeof id !== "string" || id.length === 0) return session

      return {
        ...session,
        user: {
          ...session.user,
          id,
        },
      }
    },
  },
})

Use the value that is stable for your chosen strategy. Database sessions commonly expose the adapter user; JWT sessions commonly retain the subject in token.sub. Verify actual Auth.js behavior in tests rather than assuming both are populated.

Add TypeScript module augmentation

import type { DefaultSession } from "next-auth"

declare module "next-auth" {
  interface Session {
    user: {
      id: string
    } & DefaultSession["user"]
  }
}

Make sure the declaration file is included by tsconfig.json. Preserve DefaultSession["user"] so the augmentation does not erase default name, email, and image types.

Mount Auth.js

import { handlers } from "@/auth"

export const { GET, POST } = handlers

Auth.js and Ownfold use separate catch-all routes. Auth.js owns /api/auth/*; Ownfold commonly owns /api/ownfold/*.

Configure Ownfold

import { authJsUserResolver } from "@ownfold/auth-js"
import { createVaultServer } from "@ownfold/server"
import { auth } from "@/auth"

export const vaultServer = createVaultServer({
  adapter,
  getUserId: authJsUserResolver(async () => auth()),
})
export const vaultServer = createVaultServer({
  adapter,
  getUserId: authJsUserResolver(async (request) => {
    return loadAuthJsSession(request)
  }),
})

The loader receives the exact Ownfold Web Request. In integrations where Auth.js reads cookies from global framework helpers, it may not need the argument. In other runtimes, forward the request headers/cookies through the host’s supported Auth.js API.

Ownership invariant

The ID returned by Auth.js must match:

  • the ID stored on Ownfold vault metadata;
  • application record ownership rows;
  • ownerId supplied to browser encryption and decryption;
  • every server instance and deployment region.

Email, provider account ID, username, and organization membership are not safe substitutes unless your application deliberately defines one as its immutable primary identity.

Server Components and route handlers

A Server Component may call auth() to decide whether to render or redirect. That does not unlock the vault. Browser state and plaintext remain available only below the client boundary.

Ownfold route handlers independently call the configured resolver. Do not treat a layout-level auth check as authorization for an API mutation; requests can reach routes without rendering the layout.

JWT versus database sessions

Ensure the token subject remains the immutable application ID across callbacks and provider linking. Session revocation semantics may differ from database-backed sessions; test forced sign-out and compromised-session response.

Ensure the adapter user ID is exposed through the session callback and preserved during user migrations. Back up auth and Ownfold ownership data consistently enough to avoid identity rollback mismatches.

Password reset and lost keys

Auth.js password or provider recovery restores a valid login session only. It does not recover an Ownfold root key. The user still needs an authorized device or Recovery Kit plus its separate secret.

Do not derive the Recovery Kit secret automatically from an Auth.js credential, save it in a user row, attach it to a JWT, or place it in a password-reset flow.

Email and password credentials

Auth.js Credentials providers deliberately leave account creation and password persistence to the host application. Parse registration and sign-in bodies strictly, store a slow salted password hash, use generic registration and sign-in responses that resist account enumeration, and rate-limit both endpoints. Return the application’s immutable user ID from authorize; never use the email address as the Ownfold owner ID.

The Next.js example uses Node.js scrypt with a random 16-byte salt, a versioned stored-hash format, and JWT sessions. GitHub OAuth is enabled only when both provider environment variables exist. The account password is never passed to an Ownfold API and cannot replace the separate Recovery Kit secret.

Account deletion and ID migration

Delete or export application ciphertext and Ownfold coordination records while the Auth.js user can still authenticate. Deleting the auth row first can orphan records under an ID no safe endpoint can claim.

Changing a user ID requires a coordinated ownership migration. If encrypted record AAD includes the old ownerId, plan an explicit client-side record migration; changing only database ownership can make decryption context fail.

Testing

Cover at least:

  • anonymous request → 401;
  • valid session with stable ID → correct vault;
  • session missing user or id → 401;
  • empty or non-string ID → 401;
  • expired/revoked session → 401;
  • Alice cannot address Bob’s vault, devices, pairing, or rotation;
  • browser ownerId equals server Auth.js ID;
  • JWT and database strategies, if both are supported;
  • cookies survive the production proxy and HTTPS configuration.

Troubleshooting

TypeScript says Session.user.id does not exist

Add module augmentation, include the declaration file in tsconfig.json, and populate the field in the session callback. A type declaration alone does not change runtime data.

Ownfold receives null from auth()

Confirm the session cookie reaches the Ownfold route and the route runs in a supported runtime. Inspect server-side Auth.js configuration without logging tokens or cookies.

The session has email but no ID

Fix the Auth.js callback/adapter mapping. The resolver deliberately refuses email fallback because mutable identity would destabilize vault ownership.

The user logged in but cannot decrypt old records

Authentication and key access are separate. Check enrolled device state or restore with the Recovery Kit; do not add a server decryption path.

Complete example

examples/next-auth-postgres demonstrates email/password registration and sign-in, optional GitHub OAuth, the App Router, Auth.js session resolution, PostgreSQL coordination, custom lifecycle UI, and ciphertext-only notes end to end.

Last updated on August 4, 2026

Was this page helpful?