@auth/core
@auth/core is the framework-neutral engine under Auth.js integrations. Give its Auth function a standard Web Request plus providers and security configuration, and it returns a standard Response for sign-in, OAuth callbacks, sessions, sign-out, CSRF handling, and built-in pages. It supports OAuth and OIDC providers, encrypted stateless JWT sessions, database sessions through adapters, email links, credentials, and experimental WebAuthn. This is authentication plumbing and protocol handling, not user authorization, account administration, password storage, or a drop-in UI product.
Use @auth/core directly for framework integration work or when its database-free encrypted sessions are the point. For ordinary new applications, follow the project's own recommendation and evaluate Better Auth first; official Auth.js framework packages are also safer than wiring the experimental core by hand.
Use it if
- You are writing an Auth.js integration for a framework or runtime built around standard Request and Response APIs
- You specifically need encrypted stateless sessions without a database, the feature the project README names as Auth.js's notable reason for new adoption
- You want many preconfigured OAuth and OIDC providers while keeping users and sessions in your own infrastructure
- You need one core that can switch between JWT sessions and a database adapter without outsourcing identity data to a hosted vendor
- You are starting a typical new application: the repository README says Auth.js is now part of Better Auth and recommends Better Auth for new projects unless a specific feature gap, especially stateless sessions, blocks you
- Your framework already has an official Auth.js integration: @auth/core labels itself experimental and says it is primarily used to implement framework-specific packages, so direct routing creates work those integrations already handle
- You want a batteries-included password system: the Credentials provider intentionally does no input validation, password hashing, reset flow, rate limiting, or abuse detection, and credentials-only sign-in requires JWT sessions
- You want email links, database sessions, or passkeys without database work: email and WebAuthn require an adapter with specific methods, Nodemailer and SimpleWebAuthn are optional peers, and WebAuthn still requires an experimental flag
- You need a small client-side auth helper: Bundlephobia measures 46.6 KB gzipped, the package includes Preact page rendering and protocol machinery, and Auth must remain on the trusted server side
Setup reality
Direct use is more involved than importing Auth. Route both GET and POST requests under the default /auth base path, or set basePath explicitly, and register the resulting callback URL with every OAuth provider. Every call needs a secret and a trusted host. The core Auth function does not read process.env for you; either pass provider credentials, secret, basePath, and trustHost directly or call the exported setEnvDefaults(process.env, config) before handling requests. That helper mutates the config and recognizes AUTH_SECRET, AUTH_URL, AUTH_TRUST_HOST, and AUTH_<PROVIDER>_ID or _SECRET. Only trust forwarded host headers when your proxy sanitizes them. With no adapter, sessions default to encrypted JWT cookies; adding an adapter changes the default to database sessions, which also means installing an adapter package, applying its schema and migrations, and supplying every method required by the selected features. Credentials input is unknown data and must be validated before password checks; the provider only works with JWT sessions. Email links require an adapter plus the optional nodemailer peer and SMTP configuration. WebAuthn requires both SimpleWebAuthn peers, adapter methods, and experimental.enableWebAuthn. HTTPS controls secure-cookie defaults, custom cookie settings can weaken those defaults, and callbacks decide what token fields reach the browser. Secret rotation accepts an array with the newest secret first. Test complete redirect, CSRF, proxy, cookie, account-linking, and failure flows before production, not only the happy OAuth callback.
Patterns
Bridge a Web Request to Auth.jshandle-auth-request
import { Auth, type AuthConfig } from '@auth/core';
import GitHub from '@auth/core/providers/github';
const config = {
providers: [
GitHub({
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
}),
],
secret: process.env.AUTH_SECRET!,
trustHost: true,
} satisfies AuthConfig;
export function handleAuth(request: Request) {
return Auth(request, config);
}Forward both GET and POST requests under /auth/*; trustHost is safe only when your server or proxy validates the Host headers.
Apply Auth.js environment conventionsload-auth-environment
import { Auth, setEnvDefaults, type AuthConfig } from '@auth/core';
import GitHub from '@auth/core/providers/github';
const config: AuthConfig = { providers: [GitHub] };
setEnvDefaults(process.env, config);
export const handleAuth = (request: Request) => Auth(request, config);Set AUTH_SECRET and AUTH_GITHUB_ID or AUTH_GITHUB_SECRET first; setEnvDefaults mutates config and direct Auth calls do not invoke it automatically.
Offer more than one OAuth providerconfigure-multiple-providers
import GitHub from '@auth/core/providers/github';
import Google from '@auth/core/providers/google';
const config = {
providers: [
GitHub({ clientId: githubId, clientSecret: githubSecret }),
Google({ clientId: googleId, clientSecret: googleSecret }),
],
secret,
trustHost: true,
};Each provider needs its own registered callback, such as /auth/callback/github and /auth/callback/google with the core default base path.
Point GitHub login at an Enterprise serveruse-github-enterprise
GitHub({
clientId: process.env.GHE_CLIENT_ID!,
clientSecret: process.env.GHE_CLIENT_SECRET!,
enterprise: {
baseUrl: 'https://github.company.example',
},
})The provider derives both OAuth and /api/v3 endpoints from baseUrl; register the callback on that Enterprise instance.
Allow only a verified company domainrestrict-sign-in
const config = {
// providers, secret, trustHost...
callbacks: {
async signIn({ profile }) {
const email = profile?.email;
return typeof email === 'string' && email.endsWith('@company.example');
},
},
};A domain suffix is authorization policy, not proof by itself; also require the provider's verified-email signal when its profile exposes one.
Copy a server token claim into the sessionexpose-jwt-claim
const config = {
session: { strategy: 'jwt' as const },
callbacks: {
async jwt({ token, user }) {
if (user) token.userId = user.id;
return token;
},
async session({ session, token }) {
session.user.id = token.userId as string;
return session;
},
},
};The session callback controls browser-visible data; do not copy provider access tokens or secrets unless the client truly needs them.
Persist users and sessions with a Prisma adapteruse-database-sessions
import { PrismaAdapter } from '@auth/prisma-adapter';
import { prisma } from './db.js';
const config = {
providers,
adapter: PrismaAdapter(prisma),
session: { strategy: 'database' as const },
secret,
trustHost: true,
};Install @auth/prisma-adapter separately and apply the Auth.js models and migrations; adding an adapter defaults sessions to database storage.
Validate a credentials sign-inauthorize-credentials
import Credentials from '@auth/core/providers/credentials';
Credentials({
credentials: {
email: { label: 'Email', type: 'email' },
password: { label: 'Password', type: 'password' },
},
async authorize(credentials) {
if (typeof credentials.email !== 'string') return null;
if (typeof credentials.password !== 'string') return null;
return verifyPassword(credentials.email, credentials.password);
},
})The provider performs no validation or password security for you and only supports JWT sessions; add rate limits and generic failure messages.
Configure passwordless email linkssend-email-link
import Nodemailer from '@auth/core/providers/nodemailer';
const config = {
adapter,
providers: [
Nodemailer({
server: process.env.EMAIL_SERVER!,
from: 'Sign in <login@example.com>',
}),
],
secret,
trustHost: true,
};Install a compatible nodemailer peer and configure an adapter; verification tokens require database methods even if other sessions use JWTs.
Rotate secrets without dropping sessionsrotate-auth-secret
const config = {
providers,
secret: [
process.env.AUTH_SECRET_NEW!,
process.env.AUTH_SECRET_OLD!,
],
trustHost: true,
};Put the newest secret first for new encryption; old entries remain only long enough to decrypt sessions created before rotation.
Replace the built-in sign-in and error pagesset-custom-pages
const config = {
providers,
secret,
trustHost: true,
basePath: '/auth',
pages: {
signIn: '/login',
error: '/auth-error',
verifyRequest: '/check-your-email',
},
};Custom pages are your routes and must not redirect back into protected auth checks, especially the error page.
Send auth errors to application loggingcapture-auth-logs
const config = {
providers,
secret,
trustHost: true,
logger: {
error(error: Error) { appLogger.error({ error }, 'auth error'); },
warn(code) { appLogger.warn({ code }, 'auth warning'); },
debug(message, metadata) { appLogger.debug({ metadata }, message); },
},
};Providing a custom logger makes the debug flag irrelevant; redact tokens, cookies, credentials, and provider responses before shipping metadata.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| better-auth | npm | Use it for a new application unless Auth.js's encrypted stateless session model fills a feature gap. |
| passport | npm | Use it in established Express or Connect middleware stacks that want strategy-based authentication and will own sessions and routes. |
| supertokens-node | npm | Use it when you want managed recipes for passwords, social login, sessions, account recovery, and optional hosted infrastructure. |