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;
ownerIdsupplied 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
userorid→ 401; - empty or non-string ID → 401;
- expired/revoked session → 401;
- Alice cannot address Bob’s vault, devices, pairing, or rotation;
- browser
ownerIdequals 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.