auth
Configuration
// config/auth.ts
defineConfig({
adapter: AuthAdapter<TUser, TCredentials>, // required
signInPath?: string, // where guards send anonymous users
eager?: boolean, // load the session before first render; default true
})
The provider binds auth in the container and, unless eager: false, restores the session during boot.
Session
interface Session<TUser> {
user: TUser | null
expiresAt?: number // epoch ms, when known
can(ability: string, subject?: unknown): boolean
}
The router's requireAuth() and can() guards read context.session, which the provider supplies.
Hooks
| Hook | Returns |
|---|---|
useAuth() |
The Auth object: session, user, load(), signIn(credentials), signOut(), refresh(), getToken(), subscribe(listener). |
useSession() |
The live Session. Re-renders on sign-in and sign-out. |
useUser() |
session.user. |
useCan() |
session.can. |
Adapters
tokenAdapter
tokenAdapter<TUser, TCredentials = { email: string; password: string }>({
fetchUser: (token) => Promise<TUser | null>, // required
signIn?: (credentials) => Promise<{ token; refreshToken?; user? }>,
refresh?: (refreshToken) => Promise<{ token; refreshToken? } | null>,
signOut?: (token) => Promise<void>,
storage?: TokenStorage, // default memoryStorage()
refreshStorage?: TokenStorage, // default memoryStorage()
abilities?: Abilities<TUser>,
})
memoryStorage() and localStorageTokens(key = 'kitr:token') are the built-in TokenStorages. Write your own with { get(), set(token) }.
betterAuthAdapter
betterAuthAdapter<TUser>(client, { abilities?, signIn?: (client, credentials) => Promise<unknown> })
Uses client.signIn.email unless you pass signIn.
customAdapter
customAdapter<TUser, TCredentials>({
getSession: () => Promise<Session<TUser> | null>, // required
signIn?, signOut?, refresh?, getToken?, onChange?,
})
Build sessions with createSession(user, abilities?, { expiresAt? }).
defineAbilities
const abilities = defineAbilities<User>({ 'name': (user, subject?) => boolean })
abilities.can(user, 'name', subject) // false when user is null
abilities.for(user)('name', subject)
Checks return false for an anonymous user, an unknown ability and a rule that throws. Annotate user in each rule instead of passing <User> to keep the ability names typed.
@kitr/auth/server
No React. Works with any @kitr/api router.
authMiddleware<TUser>({
verify: (token, ctx) => TUser | null | Promise<TUser | null>, // required
from?: 'bearer' | { cookie: string }, // default 'bearer'
optional?: boolean, // let anonymous requests through
abilities?: Abilities<TUser>,
})
requireUser<TUser>(ctx): TUser // throws 401 when anonymous
currentUser<TUser>(ctx): TUser | null
The middleware sets ctx.locals.user and ctx.locals.can(ability, subject), and throws 401 UNAUTHORIZED when there is no valid user and optional is off.
Cookie sessions
createCookieSession({
secret: string | string[], // at least 32 characters; the first signs, all verify
store: SessionStore, // required
cookie?: { name?: string; domain?: string; path?: string; sameSite?: 'lax' | 'strict'; secure?: boolean },
maxAge?: number, // idle seconds before expiry; default 7 days (sliding)
absoluteMaxAge?: number, // hard limit in seconds; default 30 days
}): CookieSessions
sessions.resolve(request, response?) // the live SessionRecord or null; slides the expiry
sessions.create(userId, response) // new session and Set-Cookie, replacing the request's session
sessions.rotate(request, response) // new id for the same session
sessions.destroy(request, response) // end the request's session and expire the cookie
sessions.revoke(id) // end one session
sessions.revokeAll(userId) // end every session of a user; resolves how many were live
sessions.list(userId) // the user's live sessions
interface SessionStore {
get(id: string): Promise<SessionRecord | null>
set(record: SessionRecord, ttlMs: number): Promise<void>
delete(id: string): Promise<void>
deleteForUser(userId: string): Promise<void>
listForUser(userId: string): Promise<SessionRecord[]>
}
createMemorySessionStore()
createStorageSessionStore(driver, { prefix? })
createRedisSessionStore(redis, { prefix? })
Passwords and users
hashPassword(password): Promise<string> // scrypt, or PBKDF2-SHA256 (600,000 rounds) without scrypt
hashPasswordWebCrypto(password): Promise<string> // always PBKDF2
verifyPassword(password, hash): Promise<boolean> // constant time; false for a malformed hash
needsRehash(hash): boolean
interface UserProvider<TUser> {
findByEmail(email: string): Promise<StoredUser<TUser> | null>
findById(id: string): Promise<StoredUser<TUser> | null>
create(input: NewUser): Promise<StoredUser<TUser>>
setPasswordHash?(id: string, hash: string): Promise<void>
markEmailVerified?(id: string): Promise<void>
}
createMemoryUserProvider<TUser>({ build, seed? })
Requests and the controller
runWithSession(auth, request, fn) // render fn inside the request's session scope (app/server.ts)
sessionMiddleware(auth) // API middleware: resolves the cookie, 401 without a session
getRequestSession() / getRequestUser()
createSessionController({ sessions, users, mail?, appUrl? })
auth is { sessions, users }. The controller methods are session (GET, Cache-Control: no-store), signIn, signUp, signOut, signOutAll, requestVerification, verifyEmail, requestPasswordReset and resetPassword; each state-changing one verifies the CSRF token.
@kitr/auth additions
sessionAdapter<TUser>({ abilities?, fetchSession, signIn, signOut? }) // cookie-backed adapter; caches through the query layer
ensureSession(ctx) // beforeLoad step: loads the session in the request's app (server) or the cache (browser)