{"_id":"@agentine/aegis","name":"@agentine/aegis","dist-tags":{"latest":"1.0.0"},"versions":{"1.0.0":{"name":"@agentine/aegis","version":"1.0.0","description":"Modern, TypeScript-first authentication middleware for Node.js. Drop-in passport.js replacement.","type":"module","main":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"import":"./dist/index.js","require":"./dist/index.js","types":"./dist/index.d.ts"}},"repository":{"type":"git","url":"git+https://github.com/agentine/aegis.git"},"scripts":{"build":"tsc","test":"node --import tsx --test tests/integration.test.ts tests/oauth2.test.ts tests/phase3.test.ts tests/phase4.test.ts","typecheck":"tsc --noEmit","bench":"npx tsx benchmarks/bench.ts"},"keywords":["auth","authentication","passport","middleware","express","oauth","login"],"license":"MIT","engines":{"node":">=18.0.0"},"devDependencies":{"@types/express":"^5.0.0","@types/express-session":"^1.18.0","@types/node":"^22.0.0","express":"^4.21.0","express-session":"^1.18.0","tsx":"^4.21.0","typescript":"^5.7.0"},"_id":"@agentine/aegis@1.0.0","gitHead":"7a27fb0f5e2aa79bef16905460dbbb7e894c5547","bugs":{"url":"https://github.com/agentine/aegis/issues"},"homepage":"https://github.com/agentine/aegis#readme","_nodeVersion":"22.22.0","_npmVersion":"10.9.4","dist":{"integrity":"sha512-zGyRgA4DmvWaulcLcisZDm+WPAORZj9A3cg9QkzxNGkELNLDjuZmjNWQYx8BAGmIeN5l3w9x4e0Sm+3rBwiZbw==","shasum":"6b040e323e8361d276beb94f4f8758b25240dda1","tarball":"https://registry.npmjs.org/@agentine/aegis/-/aegis-1.0.0.tgz","fileCount":36,"unpackedSize":204088,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@agentine%2faegis@1.0.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCKxMyuAOruekYtVGeUZgjpfpFamYfVZUrwED5Pl8+rZgIhALCRlk9OVnkpyzN0sWA/l6BVH9qBpr01+I/Mm+lkMP4E"}]},"_npmUser":{"name":"mtingers","email":"matthingersoll@gmail.com"},"directories":{},"maintainers":[{"name":"mtingers","email":"matthingersoll@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/aegis_1.0.0_1773471538292_0.8917128788940645"},"_hasShrinkwrap":false}},"time":{"created":"2026-03-14T06:58:58.220Z","1.0.0":"2026-03-14T06:58:58.469Z","modified":"2026-03-14T06:58:58.822Z"},"maintainers":[{"name":"mtingers","email":"matthingersoll@gmail.com"}],"description":"Modern, TypeScript-first authentication middleware for Node.js. Drop-in passport.js replacement.","homepage":"https://github.com/agentine/aegis#readme","keywords":["auth","authentication","passport","middleware","express","oauth","login"],"repository":{"type":"git","url":"git+https://github.com/agentine/aegis.git"},"bugs":{"url":"https://github.com/agentine/aegis/issues"},"license":"MIT","readme":"# @agentine/aegis\n\n**Modern, TypeScript-first authentication middleware for Node.js.** Drop-in replacement for passport.js — same API, zero dependencies, async/await native, PKCE by default.\n\n[![npm](https://img.shields.io/npm/v/@agentine/aegis)](https://www.npmjs.com/package/@agentine/aegis)\n[![Node.js](https://img.shields.io/badge/node-%3E%3D18-brightgreen)](https://nodejs.org/)\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)\n\n---\n\n## Why aegis?\n\nPassport.js has 6.3M weekly downloads and a single maintainer who stopped active development in 2023. With 393 open issues and no modern OAuth features, using passport in a security-critical application means accepting known gaps:\n\n- No PKCE support (authorization code interception attacks)\n- No TypeScript (relies on DefinitelyTyped, often out of sync)\n- Callback-based internals incompatible with async/await\n- Session fixation and state validation bugs unpatched\n- 500+ separate strategy packages, each with its own abandoned maintainer\n\naegis fixes all of this while keeping the same API. Migrating is an import path change.\n\n| Feature | passport.js | aegis |\n|---|---|---|\n| TypeScript | DefinitelyTyped (external) | Native, full generics |\n| API style | Callbacks only | Async/await + callbacks |\n| Strategies | 500+ separate packages | Built-in (one install) |\n| Dependencies | 3+ per strategy | Zero |\n| OAuth PKCE | Not supported | Default for all flows |\n| Session security | Manual regeneration | Auto-regenerate on login |\n| State parameter | Optional, easy to skip | Enforced by default |\n| Framework support | Express only | Express, Fastify, Koa |\n| Node.js minimum | Not documented | Node 18+ (LTS) |\n\n---\n\n## Installation\n\n```bash\nnpm install @agentine/aegis\n```\n\nRequires Node.js 18 or later. No additional strategy packages needed.\n\n---\n\n## Quick Start\n\n```typescript\nimport express from 'express';\nimport session from 'express-session';\nimport aegis, { LocalStrategy } from '@agentine/aegis';\n\nconst app = express();\n\napp.use(express.json());\napp.use(session({ secret: 'keyboard cat', resave: false, saveUninitialized: false }));\napp.use(aegis.initialize());\napp.use(aegis.session());\n\n// Register a strategy\naegis.use(new LocalStrategy(async (username, password) => {\n  const user = await db.users.findOne({ username });\n  if (!user || !await user.verifyPassword(password)) return false;\n  return user;\n}));\n\n// Session serialization\naegis.serializeUser(async (user) => user.id);\naegis.deserializeUser(async (id) => db.users.findById(id));\n\n// Login route\napp.post('/login', aegis.authenticate('local', {\n  successRedirect: '/dashboard',\n  failureRedirect: '/login',\n}));\n\n// Protected route\napp.get('/dashboard', (req, res) => {\n  if (!req.isAuthenticated()) return res.redirect('/login');\n  res.json({ user: req.user });\n});\n\n// Logout\napp.post('/logout', (req, res) => {\n  req.logout(() => res.redirect('/login'));\n});\n```\n\n---\n\n## Migration from passport.js\n\nMigrating from passport is an import path change:\n\n```typescript\n// Before\nimport passport from 'passport';\nimport { Strategy as LocalStrategy } from 'passport-local';\nimport { Strategy as GoogleStrategy } from 'passport-google-oauth20';\nimport { Strategy as GitHubStrategy } from 'passport-github2';\n\n// After — option 1 (alias, zero code changes)\nimport passport from '@agentine/aegis';\nimport { LocalStrategy, GoogleStrategy, GitHubStrategy } from '@agentine/aegis';\n\n// After — option 2 (rename)\nimport aegis, { LocalStrategy, GoogleStrategy, GitHubStrategy } from '@agentine/aegis';\n```\n\nAll passport API methods work identically:\n- `passport.use()` / `passport.unuse()`\n- `passport.initialize()` / `passport.session()`\n- `passport.authenticate()`\n- `passport.serializeUser()` / `passport.deserializeUser()`\n- `req.login()` / `req.logIn()`\n- `req.logout()` / `req.logOut()`\n- `req.isAuthenticated()` / `req.isUnauthenticated()`\n- `failureFlash`, `successFlash`, `passReqToCallback` options\n\nNew in aegis (not in passport):\n- Async verify functions return a value instead of calling `done(null, user)`\n- PKCE enabled by default on all OAuth flows\n- `state` parameter enforced by default\n- Session regeneration on login (prevents session fixation)\n\n---\n\n## Strategies\n\n### Local Strategy\n\nUsername/password authentication from `req.body`.\n\n```typescript\nimport { LocalStrategy } from '@agentine/aegis';\n\n// Async verify (recommended)\naegis.use(new LocalStrategy(async (username, password) => {\n  const user = await User.findOne({ username });\n  if (!user || !await bcrypt.compare(password, user.passwordHash)) return false;\n  return user;\n}));\n\n// Callback verify (passport-compatible)\naegis.use(new LocalStrategy((username, password, done) => {\n  User.findOne({ username }, (err, user) => {\n    if (err) return done(err);\n    if (!user || !user.verifyPassword(password)) return done(null, false, { message: 'Invalid credentials' });\n    return done(null, user);\n  });\n}));\n\n// Custom field names\naegis.use(new LocalStrategy(\n  { usernameField: 'email', passwordField: 'pass' },\n  async (email, password) => { /* ... */ },\n));\n```\n\n**Options:**\n\n| Option | Type | Default | Description |\n|---|---|---|---|\n| `usernameField` | `string` | `'username'` | `req.body` field for the username |\n| `passwordField` | `string` | `'password'` | `req.body` field for the password |\n| `passReqToCallback` | `boolean` | `false` | Pass `req` as first argument to the verify function |\n\n---\n\n### OAuth2 Strategy (generic)\n\nBase class for custom OAuth 2.0 providers. All provider strategies extend this.\n\n```typescript\nimport { OAuth2Strategy } from '@agentine/aegis';\n\naegis.use(new OAuth2Strategy(\n  {\n    authorizationURL: 'https://provider.example.com/oauth/authorize',\n    tokenURL: 'https://provider.example.com/oauth/token',\n    clientID: process.env.CLIENT_ID,\n    clientSecret: process.env.CLIENT_SECRET,\n    callbackURL: 'https://myapp.com/auth/callback',\n    scope: ['read:user', 'read:email'],\n  },\n  async (accessToken, refreshToken, profile) => {\n    return User.findOrCreate({ providerId: profile.id });\n  },\n));\n```\n\n**Options:**\n\n| Option | Type | Default | Description |\n|---|---|---|---|\n| `authorizationURL` | `string` | — | Provider authorization endpoint |\n| `tokenURL` | `string` | — | Provider token endpoint |\n| `clientID` | `string` | — | OAuth client ID |\n| `clientSecret` | `string` | — | OAuth client secret |\n| `callbackURL` | `string` | — | Your application's redirect URI |\n| `scope` | `string \\| string[]` | — | Requested scopes |\n| `pkce` | `boolean` | `true` | Enable PKCE (S256) — strongly recommended |\n| `state` | `boolean` | `true` | Enable CSRF state parameter — strongly recommended |\n| `passReqToCallback` | `boolean` | `false` | Pass `req` as first argument to verify |\n\n---\n\n### Google Strategy\n\n```typescript\nimport { GoogleStrategy } from '@agentine/aegis';\n\naegis.use(new GoogleStrategy(\n  {\n    clientID: process.env.GOOGLE_CLIENT_ID,\n    clientSecret: process.env.GOOGLE_CLIENT_SECRET,\n    callbackURL: '/auth/google/callback',\n    // scope defaults to ['openid', 'profile', 'email']\n  },\n  async (accessToken, refreshToken, profile) => {\n    return User.findOrCreate({\n      googleId: profile.id,\n      email: profile.emails?.[0]?.value,\n      displayName: profile.displayName,\n    });\n  },\n));\n\napp.get('/auth/google', aegis.authenticate('google'));\napp.get('/auth/google/callback',\n  aegis.authenticate('google', { failureRedirect: '/login' }),\n  (req, res) => res.redirect('/'),\n);\n```\n\n**Profile fields:** `id`, `displayName`, `name.givenName`, `name.familyName`, `emails[].value`, `photos[].value`\n\n---\n\n### GitHub Strategy\n\n```typescript\nimport { GitHubStrategy } from '@agentine/aegis';\n\naegis.use(new GitHubStrategy(\n  {\n    clientID: process.env.GITHUB_CLIENT_ID,\n    clientSecret: process.env.GITHUB_CLIENT_SECRET,\n    callbackURL: '/auth/github/callback',\n    // scope defaults to ['read:user', 'user:email']\n  },\n  async (accessToken, refreshToken, profile) => {\n    return User.findOrCreate({ githubId: profile.id });\n  },\n));\n\napp.get('/auth/github', aegis.authenticate('github'));\napp.get('/auth/github/callback',\n  aegis.authenticate('github', { failureRedirect: '/login' }),\n  (req, res) => res.redirect('/'),\n);\n```\n\n**Note:** GitHub does not always return the email in the main profile. aegis automatically fetches `/user/emails` and includes verified addresses when available.\n\n---\n\n### Facebook Strategy\n\n```typescript\nimport { FacebookStrategy } from '@agentine/aegis';\n\naegis.use(new FacebookStrategy(\n  {\n    clientID: process.env.FACEBOOK_APP_ID,\n    clientSecret: process.env.FACEBOOK_APP_SECRET,\n    callbackURL: '/auth/facebook/callback',\n    scope: ['email', 'public_profile'],\n    profileFields: ['id', 'name', 'email', 'picture'], // optional\n  },\n  async (accessToken, refreshToken, profile) => {\n    return User.findOrCreate({ facebookId: profile.id });\n  },\n));\n```\n\n---\n\n### Twitter/X Strategy\n\nUses Twitter API v2 with OAuth 2.0 (not legacy OAuth 1.0a). PKCE is required and always enabled.\n\n```typescript\nimport { TwitterStrategy } from '@agentine/aegis';\n\naegis.use(new TwitterStrategy(\n  {\n    clientID: process.env.TWITTER_CLIENT_ID,\n    clientSecret: process.env.TWITTER_CLIENT_SECRET,\n    callbackURL: '/auth/twitter/callback',\n    // scope defaults to ['tweet.read', 'users.read', 'offline.access']\n  },\n  async (accessToken, refreshToken, profile) => {\n    return User.findOrCreate({ twitterId: profile.id });\n  },\n));\n```\n\n**Note:** Twitter API v2 does not return email addresses.\n\n---\n\n### Apple Strategy (Sign in with Apple)\n\nApple uses JWT-based ID tokens instead of a userinfo endpoint. aegis verifies the JWT signature using Apple's published JWKS (cached for 1 hour), validates issuer/audience/expiry, and enforces nonce-based replay protection.\n\nApple only sends user name data on the **first** login. Store it at that point.\n\n```typescript\nimport { AppleStrategy } from '@agentine/aegis';\n\naegis.use(new AppleStrategy(\n  {\n    clientID: 'com.example.myapp',   // Your App ID / Services ID\n    clientSecret: process.env.APPLE_CLIENT_SECRET, // Signed JWT (see Apple docs)\n    callbackURL: '/auth/apple/callback',\n    scope: ['name', 'email'],\n  },\n  async (accessToken, refreshToken, profile) => {\n    return User.findOrCreate({\n      appleId: profile.id,\n      email: profile.emails?.[0]?.value,\n      // Store name on first login — Apple won't send it again\n      displayName: profile.displayName || undefined,\n    });\n  },\n));\n\n// Apple POSTs the callback — use POST route\napp.post('/auth/apple/callback',\n  aegis.authenticate('apple', { failureRedirect: '/login' }),\n  (req, res) => res.redirect('/'),\n);\n```\n\n**Note:** The Apple `clientSecret` is a signed JWT, not a simple string. See [Apple's documentation](https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens) for how to generate it.\n\n---\n\n### Microsoft Strategy (Azure AD / Microsoft Entra)\n\nSupports personal Microsoft accounts (`common` tenant) and Azure AD tenants.\n\n```typescript\nimport { MicrosoftStrategy } from '@agentine/aegis';\n\naegis.use(new MicrosoftStrategy(\n  {\n    clientID: process.env.MICROSOFT_CLIENT_ID,\n    clientSecret: process.env.MICROSOFT_CLIENT_SECRET,\n    callbackURL: '/auth/microsoft/callback',\n    tenant: 'common',  // or your tenant ID for organization-only login\n    // scope defaults to ['openid', 'profile', 'email', 'User.Read']\n  },\n  async (accessToken, refreshToken, profile) => {\n    return User.findOrCreate({ microsoftId: profile.id });\n  },\n));\n```\n\n---\n\n### OIDC Strategy (OpenID Connect)\n\nGeneric OpenID Connect with automatic endpoint discovery via `.well-known/openid-configuration`. Works with any compliant provider (Okta, Auth0, Keycloak, etc.).\n\n```typescript\nimport { OIDCStrategy } from '@agentine/aegis';\n\naegis.use(new OIDCStrategy(\n  {\n    issuer: 'https://accounts.example.com',\n    clientID: process.env.OIDC_CLIENT_ID,\n    clientSecret: process.env.OIDC_CLIENT_SECRET,\n    callbackURL: '/auth/oidc/callback',\n    // scope defaults to ['openid', 'profile', 'email']\n  },\n  async (accessToken, refreshToken, profile) => {\n    return User.findOrCreate({ sub: profile.id });\n  },\n));\n```\n\nEndpoint discovery is cached after the first request. The id_token is validated per OIDC Core: signature (RSA256/384/512 via JWKS), issuer, audience, expiry, and nonce.\n\n---\n\n### SAML Strategy\n\nSP-initiated SSO with XML signature verification. Requires the IdP's signing certificate.\n\n```typescript\nimport { SAMLStrategy } from '@agentine/aegis';\n\naegis.use(new SAMLStrategy(\n  {\n    entryPoint: 'https://idp.example.com/saml2/sso',\n    issuer: 'https://myapp.com',        // Your entity ID\n    callbackURL: 'https://myapp.com/auth/saml/callback',\n    cert: process.env.SAML_IDP_CERT,   // IdP signing certificate (PEM or raw base64)\n  },\n  async (profile) => {\n    return User.findOrCreate({\n      samlId: profile.nameID,\n      email: profile.attributes['email'],\n    });\n  },\n));\n\n// Initiate SAML flow\napp.get('/auth/saml', aegis.authenticate('saml'));\n\n// Handle IdP POST-back\napp.post('/auth/saml/callback',\n  express.urlencoded({ extended: false }),\n  aegis.authenticate('saml', { failureRedirect: '/login' }),\n  (req, res) => res.redirect('/'),\n);\n```\n\n**SAML profile fields:**\n\n| Field | Description |\n|---|---|\n| `issuer` | IdP entity ID from the assertion |\n| `nameID` | The user's NameID value |\n| `nameIDFormat` | NameID format URI |\n| `sessionIndex` | SAML session index (for SLO) |\n| `attributes` | All `AttributeStatement` values as `Record<string, string>` |\n\n---\n\n## Framework Adapters\n\n### Express (default)\n\nExpress is the default. No adapter import needed — just use aegis middleware directly:\n\n```typescript\nimport aegis from '@agentine/aegis';\nimport express from 'express';\nimport session from 'express-session';\n\nconst app = express();\napp.use(express.json());\napp.use(session({ secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false }));\napp.use(aegis.initialize());\napp.use(aegis.session());\n```\n\n### Fastify\n\n```typescript\nimport Fastify from 'fastify';\nimport fastifySession from '@fastify/session';\nimport aegis, { LocalStrategy } from '@agentine/aegis';\nimport { toFastifyHook } from '@agentine/aegis';\n\nconst fastify = Fastify();\n\nawait fastify.register(fastifySession, { secret: process.env.SESSION_SECRET });\n\naegis.use(new LocalStrategy(async (username, password) => { /* ... */ }));\naegis.serializeUser(async (user) => user.id);\naegis.deserializeUser(async (id) => User.findById(id));\n\nfastify.addHook('preHandler', toFastifyHook(aegis.initialize()));\nfastify.addHook('preHandler', toFastifyHook(aegis.session()));\n\nfastify.post('/login', {\n  preHandler: toFastifyHook(\n    aegis.authenticate('local', { session: false }),\n  ),\n}, async (request, reply) => {\n  return { user: request.raw.user };\n});\n```\n\n### Koa\n\n```typescript\nimport Koa from 'koa';\nimport session from 'koa-session';\nimport bodyParser from 'koa-bodyparser';\nimport aegis, { LocalStrategy } from '@agentine/aegis';\nimport { toKoaMiddleware } from '@agentine/aegis';\n\nconst app = new Koa();\napp.keys = [process.env.SESSION_SECRET];\n\napp.use(session({}, app));\napp.use(bodyParser());\napp.use(toKoaMiddleware(aegis.initialize()));\napp.use(toKoaMiddleware(aegis.session()));\n\n// The authenticated user is available on ctx.state.user\napp.use(async (ctx) => {\n  if (ctx.path === '/login' && ctx.method === 'POST') {\n    await new Promise<void>((resolve, reject) => {\n      toKoaMiddleware(aegis.authenticate('local', { session: true }))(ctx as any, async () => {\n        resolve();\n      });\n    });\n    ctx.redirect('/');\n  }\n});\n```\n\n---\n\n## TypeScript Usage\n\naegis is TypeScript-first. Use the `User` generic to get full type safety across your application:\n\n```typescript\nimport { Authenticator, LocalStrategy } from '@agentine/aegis';\n\ninterface AppUser {\n  id: string;\n  email: string;\n  role: 'admin' | 'user';\n}\n\n// Typed authenticator\nconst auth = new Authenticator<AppUser>();\n\nauth.serializeUser(async (user: AppUser) => user.id);\nauth.deserializeUser(async (id): Promise<AppUser | null> => db.users.findById(id));\n\nauth.use(new LocalStrategy<AppUser>(async (username, password) => {\n  const user = await db.users.findOne({ email: username });\n  if (!user || !await bcrypt.compare(password, user.passwordHash)) return false;\n  return user; // typed as AppUser\n}));\n\n// req.user is typed as AppUser | undefined\napp.get('/profile', (req, res) => {\n  if (!req.isAuthenticated()) return res.status(401).json({ error: 'Not authenticated' });\n  res.json({ email: req.user.email, role: req.user.role }); // fully typed\n});\n```\n\n### Multiple independent authenticators\n\n```typescript\nconst userAuth = new Authenticator<User>();\nconst adminAuth = new Authenticator<Admin>();\n\n// Each has its own strategy registry and session serialization\nuserAuth.use(new LocalStrategy<User>(verifyUser));\nadminAuth.use(new LocalStrategy<Admin>(verifyAdmin));\n```\n\n---\n\n## API Reference\n\n### `new Authenticator<User>()`\n\nThe main class. The default export is a pre-created instance (`new Authenticator()`).\n\n#### `.use(strategy)` / `.use(name, strategy)`\n\nRegister a strategy. Strategies self-name via their `name` property; pass an explicit name to override.\n\n#### `.unuse(name)`\n\nRemove a registered strategy.\n\n#### `.initialize(options?)`\n\nReturns middleware that augments `req` with `login()`, `logout()`, `isAuthenticated()`, and `isUnauthenticated()`.\n\n| Option | Default | Description |\n|---|---|---|\n| `userProperty` | `'user'` | Property name on `req` where the user is stored |\n\n#### `.session(options?)`\n\nReturns middleware that restores authentication from the session on each request.\n\n| Option | Default | Description |\n|---|---|---|\n| `optional` | `false` | Don't fail if no session is present |\n\n#### `.authenticate(strategy, options?, callback?)`\n\nReturns authentication middleware.\n\n`strategy` can be a single name or an array of names (tried in order until one succeeds).\n\n| Option | Type | Description |\n|---|---|---|\n| `session` | `boolean` | Save user to session on success (default: `true`) |\n| `optional` | `boolean` | Pass through without error if authentication fails |\n| `successRedirect` | `string` | Redirect on success |\n| `failureRedirect` | `string` | Redirect on failure |\n| `failureFlash` | `string \\| boolean` | Flash failure message (requires `connect-flash`) |\n| `successFlash` | `string \\| boolean` | Flash success message (requires `connect-flash`) |\n| `failureMessage` | `string \\| boolean` | Store failure message in `req.session.messages` |\n| `successMessage` | `string \\| boolean` | Store success message in `req.session.messages` |\n| `assignProperty` | `string` | Store user on `req[property]` instead of establishing a session |\n\n**Custom callback:**\n\n```typescript\napp.post('/login', (req, res, next) => {\n  aegis.authenticate('local', (err, user, info) => {\n    if (err) return next(err);\n    if (!user) return res.status(401).json({ error: info?.message });\n    req.login(user, (err) => {\n      if (err) return next(err);\n      res.json({ user });\n    });\n  })(req, res, next);\n});\n```\n\n#### `.authorize(strategy, options?)`\n\nLinks an additional account to an existing session without replacing `req.user`. The linked account is stored on `req.account` (or `req[options.assignProperty]`).\n\n#### `.serializeUser(fn)` / `.deserializeUser(fn)`\n\nRegister session serialization/deserialization. Both async and callback styles are supported:\n\n```typescript\n// Async\naegis.serializeUser(async (user) => user.id);\naegis.deserializeUser(async (id) => User.findById(id));\n\n// Callback (passport-compatible)\naegis.serializeUser((user, done) => done(null, user.id));\naegis.deserializeUser((id, done) => User.findById(id, done));\n```\n\n### `req` methods (added by `initialize()`)\n\n| Method | Description |\n|---|---|\n| `req.isAuthenticated()` | Returns `true` if a user is authenticated |\n| `req.isUnauthenticated()` | Returns `true` if no user is authenticated |\n| `req.login(user, done)` | Log in a user (establishes session) |\n| `req.logout(done)` | Log out the current user (clears session) |\n| `req.user` | The authenticated user object |\n\n### `AuthenticationError`\n\nThrown when authentication fails and no redirect is configured:\n\n```typescript\nimport { AuthenticationError } from '@agentine/aegis';\n\n// In Express error handler:\napp.use((err, req, res, next) => {\n  if (err instanceof AuthenticationError) {\n    return res.status(err.status).json({ error: err.message });\n  }\n  next(err);\n});\n```\n\n---\n\n## Security Best Practices\n\n### PKCE (Proof Key for Code Exchange)\n\nPKCE is **enabled by default** for all OAuth 2.0 flows. It prevents authorization code interception attacks even when TLS is terminated early. Do not disable it unless your provider explicitly does not support it.\n\n```typescript\n// PKCE is on by default — no configuration needed\nnew GoogleStrategy({ clientID, clientSecret, callbackURL }, verify);\n\n// Explicit disable (not recommended)\nnew OAuth2Strategy({ /* ... */, pkce: false }, verify);\n```\n\n### State Parameter (CSRF Protection)\n\nThe state parameter is **enforced by default** on all OAuth 2.0 flows. aegis generates a cryptographically random 48-hex-char state, stores it in the session, and validates it on callback. Mismatches return HTTP 403.\n\n### Session Regeneration\n\naegis regenerates the session ID on every successful login to prevent session fixation attacks. This requires `express-session` (or compatible) to expose `req.session.regenerate()`.\n\n### Redirect URL Validation\n\nThe `authenticate()` middleware validates all redirect URLs before issuing redirects:\n- Relative paths (`/path`) are allowed\n- `https://` URLs are allowed (for OAuth provider redirects)\n- `http://localhost` URLs are allowed (for development)\n- Protocol-relative URLs (`//evil.com`) are rejected with HTTP 400\n- Non-https schemes (`javascript:`, `data:`, etc.) are rejected\n\n### Cookie Security\n\nConfigure `express-session` with appropriate cookie settings for production:\n\n```typescript\napp.use(session({\n  secret: process.env.SESSION_SECRET,\n  resave: false,\n  saveUninitialized: false,\n  cookie: {\n    httpOnly: true,      // Prevent XSS access to the cookie\n    secure: true,        // Require HTTPS (set this in production)\n    sameSite: 'lax',     // CSRF protection\n    maxAge: 24 * 60 * 60 * 1000, // 24 hours\n  },\n}));\n```\n\n### SAML Security Notes\n\n- The `cert` option is required. aegis will throw at construction time if it is missing.\n- Signature verification uses the provided certificate for both the response-level and assertion-level signatures.\n- `InResponseTo` attribute is validated to prevent unsolicited response injection.\n- `Destination` attribute is validated to prevent response re-use across service providers.\n- Assertion conditions (`NotBefore`, `NotOnOrAfter`, `AudienceRestriction`) are validated with 5-minute clock skew tolerance.\n\n---\n\n## Benchmarks\n\nRun against Node.js 22 on Apple M-series:\n\n```\ninitialize() middleware:\n  aegis initialize(): 1,086,818 ops/sec (0.92 µs/op)\n\nauthenticate(\"local\") — success:\n  aegis local auth (success): 685,556 ops/sec (1.46 µs/op)\n\nauthenticate(\"local\") — failure:\n  aegis local auth (failure): 487,755 ops/sec (2.05 µs/op)\n```\n\nRun the benchmarks yourself:\n\n```bash\nnpm run bench\n```\n\n---\n\n## License\n\nMIT\n","readmeFilename":"README.md","_rev":"1-73991e122dc051eaca0d384240d12067"}