Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions docs/pages/guides/extending-the-session.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,87 @@ const providers: Provider[] = [
]
```

## Dynamic Session Duration

You can now dynamically control session duration based on user preferences or roles. The `maxAge` option accepts a static value, `"session"` for browser session cookies, or a function that returns either dynamically.

### Remember Me Feature

```ts filename="auth.ts"
export default {
session: {
maxAge: async ({ token }) => {
// Check if user selected "Remember Me" during login
if (token?.rememberMe) {
return 30 * 24 * 60 * 60 // 30 days
}
return "session" // Browser session cookie
}
},
callbacks: {
jwt({ token, user, trigger }) {
if (trigger === "signIn" && user) {
// Save rememberMe preference in JWT
token.rememberMe = user.rememberMe
}
return token
}
}
}
```

### Role-Based Session Duration

```ts filename="auth.ts"
export default {
session: {
maxAge: async ({ token }) => {
// Different session durations based on user role
switch (token?.role) {
case "admin":
return 4 * 60 * 60 // 4 hours for admins
case "user":
return 24 * 60 * 60 // 24 hours for regular users
case "guest":
return "session" // Session cookie for guests
default:
return 30 * 24 * 60 * 60 // Default 30 days
}
}
}
}
```

### Device-Based Sessions

```ts filename="auth.ts"
export default {
session: {
maxAge: async ({ token, trigger }) => {
// Shorter sessions on public/shared devices
if (token?.deviceType === "public") {
return 60 * 60 // 1 hour
}

// Session cookies for sensitive operations
if (trigger === "update" && token?.isSensitiveOperation) {
return "session"
}

return 7 * 24 * 60 * 60 // Default 7 days
}
}
}
```

<Callout type="info">
When using `"session"` as the maxAge value, the cookie will expire when the browser is closed. This is useful for public computers or when users don't want their session to persist.
</Callout>

<Callout type="warning">
For database sessions, `"session"` cookies still require a database expiry. The database record will use the default maxAge for cleanup purposes, but the cookie itself will be a session cookie.
</Callout>

## Resources

- [Concepts. Session strategies](/concepts/session-strategies)
Expand Down
22 changes: 20 additions & 2 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,11 +253,29 @@ export interface AuthConfig {
*/
strategy?: "jwt" | "database"
/**
* Relative time from now in seconds when to expire the session
* Relative time from now in seconds when to expire the session,
* or "session" to create a session cookie that expires when the browser closes.
* Can also be a function that returns the value dynamically.
*
* @default 2592000 // 30 days
* @example
* ```ts
* // Static session cookie (browser session)
* maxAge: "session"
*
* // Dynamic based on user preferences
* maxAge: async ({ token }) => {
* return token?.rememberMe ? 30 * 24 * 60 * 60 : "session"
* }
* ```
*/
maxAge?: number
maxAge?: number | "session" | ((params: {
user?: import("./types.js").User
token?: import("./jwt.js").JWT
trigger?: "signIn" | "signUp" | "update"
isNewUser?: boolean
session?: any
}) => number | "session" | PromiseLike<number | "session">)
/**
* How often the session should be updated in seconds.
* If set to `0`, session is updated every time.
Expand Down
107 changes: 84 additions & 23 deletions packages/core/src/lib/actions/callback/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,33 @@ import type {
User,
} from "../../../types.js"
import type { Cookie, SessionStore } from "../../utils/cookie.js"
import type { JWT } from "../../../jwt.js"
import {
assertInternalOptionsWebAuthn,
verifyAuthenticate,
verifyRegister,
} from "../../utils/webauthn-utils.js"

/**
* Resolve the maxAge value for session cookies.
* Handles static values, "session" for session cookies, and dynamic functions.
*/
async function resolveMaxAge(
maxAge: InternalOptions["session"]["maxAge"],
params: {
user?: User
token?: JWT
trigger?: "signIn" | "signUp" | "update"
isNewUser?: boolean
session?: any
}
): Promise<number | "session"> {
if (typeof maxAge === "function") {
return await maxAge(params)
}
return maxAge
}

/** Handle callbacks from login services */
export async function callback(
request: RequestInternal,
Expand Down Expand Up @@ -161,13 +182,23 @@ export async function callback(
// Encode token
const newToken = await jwt.encode({ ...jwt, token, salt })

// Set cookie expiry date
const cookieExpires = new Date()
cookieExpires.setTime(cookieExpires.getTime() + sessionMaxAge * 1000)

const sessionCookies = sessionStore.chunk(newToken, {
expires: cookieExpires,
// Resolve maxAge dynamically
const resolvedMaxAge = await resolveMaxAge(sessionMaxAge, {
user,
token,
trigger: isNewUser ? "signUp" : "signIn",
isNewUser,
})

// Set cookie expiry date
const cookieOptions: { expires?: Date } = {}
if (resolvedMaxAge !== "session") {
const cookieExpires = new Date()
cookieExpires.setTime(cookieExpires.getTime() + resolvedMaxAge * 1000)
cookieOptions.expires = cookieExpires
}

const sessionCookies = sessionStore.chunk(newToken, cookieOptions)
cookies.push(...sessionCookies)
}
} else {
Expand Down Expand Up @@ -285,13 +316,23 @@ export async function callback(
// Encode token
const newToken = await jwt.encode({ ...jwt, token, salt })

// Set cookie expiry date
const cookieExpires = new Date()
cookieExpires.setTime(cookieExpires.getTime() + sessionMaxAge * 1000)

const sessionCookies = sessionStore.chunk(newToken, {
expires: cookieExpires,
// Resolve maxAge dynamically
const resolvedMaxAge = await resolveMaxAge(sessionMaxAge, {
user,
token,
trigger: isNewUser ? "signUp" : "signIn",
isNewUser,
})

// Set cookie expiry date
const cookieOptions: { expires?: Date } = {}
if (resolvedMaxAge !== "session") {
const cookieExpires = new Date()
cookieExpires.setTime(cookieExpires.getTime() + resolvedMaxAge * 1000)
cookieOptions.expires = cookieExpires
}

const sessionCookies = sessionStore.chunk(newToken, cookieOptions)
cookies.push(...sessionCookies)
}
} else {
Expand Down Expand Up @@ -374,13 +415,23 @@ export async function callback(
// Encode token
const newToken = await jwt.encode({ ...jwt, token, salt })

// Resolve maxAge dynamically
const resolvedMaxAge = await resolveMaxAge(sessionMaxAge, {
user: loggedInUser,
token,
trigger: isNewUser ? "signUp" : "signIn",
isNewUser,
})

// Set cookie expiry date
const cookieExpires = new Date()
cookieExpires.setTime(cookieExpires.getTime() + sessionMaxAge * 1000)
const cookieOptions: { expires?: Date } = {}
if (resolvedMaxAge !== "session") {
const cookieExpires = new Date()
cookieExpires.setTime(cookieExpires.getTime() + resolvedMaxAge * 1000)
cookieOptions.expires = cookieExpires
}

const sessionCookies = sessionStore.chunk(newToken, {
expires: cookieExpires,
})
const sessionCookies = sessionStore.chunk(newToken, cookieOptions)

cookies.push(...sessionCookies)
}
Expand Down Expand Up @@ -482,13 +533,23 @@ export async function callback(
// Encode token
const newToken = await jwt.encode({ ...jwt, token, salt })

// Set cookie expiry date
const cookieExpires = new Date()
cookieExpires.setTime(cookieExpires.getTime() + sessionMaxAge * 1000)

const sessionCookies = sessionStore.chunk(newToken, {
expires: cookieExpires,
// Resolve maxAge dynamically
const resolvedMaxAge = await resolveMaxAge(sessionMaxAge, {
user,
token,
trigger: isNewUser ? "signUp" : "signIn",
isNewUser,
})

// Set cookie expiry date
const cookieOptions: { expires?: Date } = {}
if (resolvedMaxAge !== "session") {
const cookieExpires = new Date()
cookieExpires.setTime(cookieExpires.getTime() + resolvedMaxAge * 1000)
cookieOptions.expires = cookieExpires
}

const sessionCookies = sessionStore.chunk(newToken, cookieOptions)
cookies.push(...sessionCookies)
}
} else {
Expand Down
67 changes: 56 additions & 11 deletions packages/core/src/lib/actions/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,29 @@ import { JWTSessionError, SessionTokenError } from "../../errors.js"
import { fromDate } from "../utils/date.js"

import type { Adapter } from "../../adapters.js"
import type { InternalOptions, ResponseInternal, Session } from "../../types.js"
import type { InternalOptions, ResponseInternal, Session, User } from "../../types.js"
import type { Cookie, SessionStore } from "../utils/cookie.js"
import type { JWT } from "../../jwt.js"

/**
* Resolve the maxAge value for session cookies.
* Handles static values, "session" for session cookies, and dynamic functions.
*/
async function resolveMaxAge(
maxAge: InternalOptions["session"]["maxAge"],
params: {
user?: User
token?: JWT
trigger?: "signIn" | "signUp" | "update"
isNewUser?: boolean
session?: any
}
): Promise<number | "session"> {
if (typeof maxAge === "function") {
return await maxAge(params)
}
return maxAge
}

/** Return a session object filtered via `callbacks.session` */
export async function session(
Expand Down Expand Up @@ -53,7 +74,15 @@ export async function session(
session: newSession,
})

const newExpires = fromDate(sessionMaxAge)
const resolvedMaxAge = await resolveMaxAge(sessionMaxAge, {
token,
trigger: isUpdate ? "update" : undefined,
session: newSession,
})

const newExpires = resolvedMaxAge === "session"
? fromDate(30 * 24 * 60 * 60) // Use a default for internal calculations
: fromDate(resolvedMaxAge)

if (token !== null) {
// By default, only exposes a limited subset of information to the client
Expand All @@ -72,9 +101,12 @@ export async function session(
const newToken = await jwt.encode({ ...jwt, token, salt })

// Set cookie, to also update expiry date on cookie
const sessionCookies = sessionStore.chunk(newToken, {
expires: newExpires,
})
const cookieOptions: { expires?: Date } = {}
if (resolvedMaxAge !== "session") {
cookieOptions.expires = newExpires
}

const sessionCookies = sessionStore.chunk(newToken, cookieOptions)

response.cookies?.push(...sessionCookies)

Expand Down Expand Up @@ -110,15 +142,26 @@ export async function session(
const { user, session } = userAndSession

const sessionUpdateAge = options.session.updateAge

// Resolve maxAge for database sessions
const resolvedMaxAge = await resolveMaxAge(sessionMaxAge, {
user,
trigger: isUpdate ? "update" : undefined,
session: newSession,
})

// For database sessions, we need a numeric value
const numericMaxAge = resolvedMaxAge === "session" ? sessionMaxAge : resolvedMaxAge

// Calculate last updated date to throttle write updates to database
// Formula: ({expiry date} - sessionMaxAge) + sessionUpdateAge
// e.g. ({expiry date} - 30 days) + 1 hour
const sessionIsDueToBeUpdatedDate =
session.expires.valueOf() -
sessionMaxAge * 1000 +
numericMaxAge * 1000 +
sessionUpdateAge * 1000

const newExpires = fromDate(sessionMaxAge)
const newExpires = fromDate(numericMaxAge)
// Trigger update of session expiry date and write to database, only
// if the session was last updated more than {sessionUpdateAge} ago
if (sessionIsDueToBeUpdatedDate <= Date.now()) {
Expand All @@ -143,13 +186,15 @@ export async function session(
response.body = sessionPayload

// Set cookie again to update expiry
const cookieOptions = { ...options.cookies.sessionToken.options }
if (resolvedMaxAge !== "session") {
cookieOptions.expires = newExpires
}

response.cookies?.push({
name: options.cookies.sessionToken.name,
value: sessionToken,
options: {
...options.cookies.sessionToken.options,
expires: newExpires,
},
options: cookieOptions,
})

// @ts-expect-error
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/lib/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ export async function init({
// JWT options
jwt: {
secret: config.secret!, // Asserted in assert.ts
maxAge: config.session?.maxAge ?? maxAge, // default to same as `session.maxAge`
maxAge: config.session?.maxAge ?? maxAge, // default to same as `session.maxAge` or function
encode: jwt.encode,
decode: jwt.decode,
...config.jwt,
Expand Down
Loading