Signature
function createAuth<TUser>(
config: AuthConfig<TUser>,
): (options?: AuthFactoryOptions) => AuthInstance<TUser>createAuth is the main entry point for session-based authentication. It accepts an AuthConfig object and returns a factory function. Each call to the factory produces an AuthInstance scoped to the current request’s cookies.
import { createAuth, createHash } from 'ideal-auth';
const hash = createHash();
const auth = createAuth({
secret: process.env.AUTH_SECRET!,
cookie: cookieBridge,
resolveUser: async (id) => db.user.findUnique({ where: { id } }),
resolveUserByCredentials: async (creds) =>
db.user.findUnique({ where: { email: creds.email } }),
hash,
});
// In a request handler:
const session = auth();
const user = await session.user();AuthConfig
| Field | Type | Required | Default |
|---|---|---|---|
secret |
string |
Yes | — |
cookie |
CookieBridge |
Yes | — |
session.cookieName |
string |
No | 'ideal_session' |
session.maxAge |
number (seconds) |
No | 604800 (7 days) |
session.rememberMaxAge |
number (seconds) |
No | 2592000 (30 days) |
session.cookie |
Partial<ConfigurableCookieOptions> |
No | secure in production, sameSite: 'lax', path: '/' |
resolveUser |
(id: string) => Promise<TUser | null | undefined> |
Yes (unless sessionFields is provided) |
— |
sessionFields |
(keyof TUser & string)[] |
Yes (unless resolveUser is provided) |
— |
resolveUserByCredentials |
(credentials: Record<string, unknown>) => Promise<AnyUser | null | undefined> |
No | — |
hash |
HashInstance |
No | — |
credentialKey |
string |
No | 'password' |
passwordField |
string |
No | 'password' |
attemptUser |
(credentials: Record<string, unknown>) => Promise<TUser | null | undefined> |
No | — |
secret
A string of at least 32 characters used to encrypt session cookies. Generate one with the CLI:
bunx ideal-auth secretcookie
A CookieBridge object that tells ideal-auth how to interact with cookies in your framework.
session
Optional session configuration. The defaults work well for most applications.
cookieName— Name of the session cookie stored in the browser.maxAge— Lifetime of the session cookie in seconds. Used whenrememberis not specified in login options.rememberMaxAge— Lifetime of the session cookie in seconds whenremember: trueis passed to login.cookie— Partial cookie options merged with the defaults. You can overridesameSite,path,domain, andsecure. ThehttpOnlyflag is always forced totrueand cannot be overridden.
resolveUser
Called when the session is read to hydrate the full user object from a user ID. Must return the user object or null if the user no longer exists.
resolveUser: async (id) => {
return db.user.findUnique({ where: { id } });
}resolveUserByCredentials
Used by attempt() in Laravel-style mode to look up a user by the non-password fields of the credentials object. The credentialKey field (default 'password') is stripped before the object is passed to this function.
This callback can return any user shape — it does not need to match TUser. It only needs to include the id field and the password hash field (default 'password'). The returned object is used internally for hash verification and then the password field is stripped from the cache.
resolveUserByCredentials: async (credentials) => {
// credentials = { email: "user@example.com" }
// (password was already stripped)
// Can return the full DB row — password is only used for verification
return db.user.findUnique({ where: { email: credentials.email } });
}hash
A HashInstance created by createHash(). Required for the Laravel-style attempt() mode.
credentialKey
The key in the credentials object that contains the password. Defaults to 'password'. This key is stripped from credentials before passing them to resolveUserByCredentials.
passwordField
The property name on the user object that stores the hashed password. Defaults to 'password'. Used by attempt() to read the stored hash for verification.
attemptUser
A function for manual mode that receives the full credentials object and returns a user if authentication succeeds. Takes precedence over the Laravel-style flow if both are configured.
AuthInstance
The object returned by calling the factory function. All methods are async.
login
login(user: TUser, options?: LoginOptions): Promise<void>Sets the session cookie with the given user. The user object must have an id property (string).
const session = auth();
const user = await db.user.findUnique({ where: { email } });
if (user) {
await session.login(user);
}loginById
loginById(id: string, options?: LoginOptions): Promise<void>Resolves a user by ID using resolveUser, then sets the session cookie. Throws an error with the message 'Login failed' if the user is not found.
const session = auth();
try {
await session.loginById('user_abc123');
} catch (error) {
// error.message === 'Login failed'
}attempt
attempt(
credentials: Record<string, unknown>,
options?: LoginOptions,
): Promise<boolean>Finds a user by credentials, verifies the password, and logs the user in if valid. Returns true on success, false on failure. See Attempt Modes for details.
const session = auth();
const success = await session.attempt({
email: 'user@example.com',
password: 'correct-password',
});
if (!success) {
return { error: 'Invalid credentials.' };
}attemptWithReason
attemptWithReason(
credentials: Record<string, unknown>,
options?: LoginOptions,
): Promise<AttemptResult>Identical work to attempt() — same lookup, same verification, same login on success — but returns why it failed instead of discarding it. No extra query: attempt() already resolved the user and already knew which branch failed.
const result = await auth().attemptWithReason({
email: 'user@example.com',
password: 'their-password',
});
if (!result.ok) {
return { error: messageFor(result.reason) };
}reason |
Meaning |
|---|---|
no_user |
Nothing matched the lookup. |
no_password |
The account exists but has no password set — a social or passkey signup. |
bad_password |
The password did not match. |
rejected |
attemptUser() declined. Why is that callback’s business, not the library’s. |
Success returns { ok: true } and no user object. The resolved row still carries the password hash at that point, and handing it back is how it ends up somewhere it shouldn’t — call user() for the session-safe copy.
Timing is unaffected. Both methods run hash.verify() unconditionally before any branch, against a dummy hash when no user was found, so a caller that only logs the reason keeps the constant-time property it never asked to give up.
logout
logout(): Promise<void>Deletes the session cookie, ending the user’s session.
const session = auth();
await session.logout();check
check(): Promise<boolean>Returns true if the current request has a valid session with a resolvable user. Does not throw.
const session = auth();
if (await session.check()) {
// user is authenticated
}user
user(): Promise<TUser | null>Returns the authenticated user object or null if no valid session exists. The result is cached for the lifetime of the AuthInstance — subsequent calls return the same object without re-querying.
const session = auth();
const currentUser = await session.user();
if (!currentUser) {
redirect('/login');
}id
id(): Promise<string | null>Returns the user ID from the session cookie or null if no valid session exists. This reads only the session cookie and does not call resolveUser.
const session = auth();
const userId = await session.id();LoginOptions
interface LoginOptions {
remember?: boolean;
}Controls the lifetime of the session cookie set during login.
| Value | Behavior | Cookie maxAge |
|---|---|---|
true |
Long-lived session | rememberMaxAge (default 30 days) |
false |
Session cookie (browser close) | undefined (no maxAge) |
| omitted | Default session lifetime | maxAge (default 7 days) |
// "Remember me" checked — 30-day session
await session.login(user, { remember: true });
// "Remember me" unchecked — session ends when browser closes
await session.login(user, { remember: false });
// Default — 7-day session
await session.login(user);CookieBridge
interface CookieBridge {
get(name: string): Promise<string | undefined> | string | undefined;
set(name: string, value: string, options: CookieOptions): Promise<void> | void;
delete(name: string): Promise<void> | void;
}The cookie bridge is how ideal-auth interacts with your framework’s cookie API. Each method can be synchronous or asynchronous.
Next.js (App Router) example:
import { cookies } from 'next/headers';
const cookieBridge = {
async get(name: string) {
const jar = await cookies();
return jar.get(name)?.value;
},
async set(name: string, value: string, options: any) {
const jar = await cookies();
jar.set(name, value, options);
},
async delete(name: string) {
const jar = await cookies();
jar.delete(name);
},
};Express example:
function createCookieBridge(req, res) {
return {
get(name: string) {
return req.cookies[name];
},
set(name: string, value: string, options: any) {
res.cookie(name, value, options);
},
delete(name: string) {
res.clearCookie(name);
},
};
}Attempt Modes
The attempt() method supports two modes for resolving and verifying users.
Laravel-style mode
Provide hash and resolveUserByCredentials in the config. When attempt() is called:
- The
credentialKey(default'password') is stripped from the credentials object. - The remaining fields are passed to
resolveUserByCredentialsto look up the user. - The password is verified against
user[passwordField]using thehashinstance. - If verification succeeds, the user is logged in.
import { createAuth, createHash } from 'ideal-auth';
const hash = createHash();
const auth = createAuth({
secret: process.env.AUTH_SECRET!,
cookie: cookieBridge,
resolveUser: async (id) => db.user.findUnique({ where: { id } }),
resolveUserByCredentials: async (credentials) => {
// credentials = { email: "user@example.com" }
return db.user.findUnique({ where: { email: credentials.email } });
},
hash,
// credentialKey: 'password', // default
// passwordField: 'password', // default
});
// Usage
const success = await auth().attempt({
email: 'user@example.com',
password: 'their-password',
});Manual mode
Provide attemptUser for full control over credential resolution and password verification. This takes precedence over the Laravel-style flow if both are configured.
import { createAuth, createHash } from 'ideal-auth';
const hash = createHash();
const auth = createAuth({
secret: process.env.AUTH_SECRET!,
cookie: cookieBridge,
resolveUser: async (id) => db.user.findUnique({ where: { id } }),
attemptUser: async (credentials) => {
const user = await db.user.findUnique({
where: { email: credentials.email as string },
});
if (!user) return null;
const valid = await hash.verify(
credentials.password as string,
user.password,
);
return valid ? user : null;
},
});
// Usage is identical
const success = await auth().attempt({
email: 'user@example.com',
password: 'their-password',
});Types
All types are available as type-only exports:
import type {
AttemptFailure,
AttemptResult,
AuthConfig,
AuthFactoryOptions,
AuthInstance,
CookieBridge,
CookieOptions,
ConfigurableCookieOptions,
LoginOptions,
} from 'ideal-auth';