Managing User Sessions and OIDC Session Extensions with Logto: A Complete Guide
Logto extends standard OIDC sessions using a dedicated oidc_session_extensions table to persist interaction data like lastSubmission across authentication flows, enabling rich token customization and comprehensive session management through the Management API.
The logto-io/logto repository implements a sophisticated two-layer session architecture that separates core OpenID Connect state from Logto-specific extensions. While standard OIDC session data lives in oidc_model_instances, Logto stores supplementary context—such as consent interaction details and client identifiers—in a parallel oidc_session_extensions table. This design allows the platform to maintain strict OIDC compliance while supporting advanced features like custom JWT claims based on sign-in context.
Understanding the OIDC Session Extensions Architecture
The Database Schema
Logto creates the oidc_session_extensions table through a database alteration script introduced in version 1.29.0. This table links extension data to core sessions via a composite primary key while storing JSON-based interaction context.
// packages/schemas/alterations/1.29.0-1749026308-add-oidc-session-extension-table.ts
await pool.query(sql`
create table oidc_session_extensions (
tenant_id varchar(21) not null references tenants (id) on update cascade on delete cascade,
session_uid varchar(128) not null,
account_id varchar(12) not null references users (id) on update cascade on delete cascade,
last_submission jsonb not null default '{}'::jsonb,
created_at timestamptz not null default(now()),
updated_at timestamptz not null default(now()),
primary key (tenant_id, session_uid)
);
`);
The schema stores critical fields including session_uid (linking to the OIDC session), account_id (referencing the user), and last_submission (containing the interaction context as JSONB). This structure enables Logto to persist complex sign-in metadata—such as IP addresses and user agents—beyond the standard OIDC session lifetime.
Row-Level Security and Automated Updates
The alteration applies applyTableRls for row-level security enforcement and establishes a trigger that automatically updates the updated_at timestamp on every modification. This ensures tenant isolation and accurate audit tracking across multi-tenant deployments.
Querying Session Extensions with OidcSessionExtensionsQueries
All database operations for session extensions are encapsulated in the OidcSessionExtensionsQueries class located at packages/core/src/queries/oidc-session-extensions.ts. This abstraction layer provides type-safe access to the extensions table with built-in conflict resolution.
Insert with Conflict Resolution
The insert method uses buildInsertIntoWithPool to handle upsert operations, automatically updating fields when a session extension already exists:
// packages/core/src/queries/oidc-session-extensions.ts
public readonly insert = buildInsertIntoWithPool(this.pool)(OidcSessionExtensions, {
onConflict: {
fields: [fields.tenantId, fields.sessionUid],
setExcludedFields: [
fields.lastSubmission,
fields.updatedAt,
fields.accountId,
fields.clientId,
],
},
returning: true,
});
This approach ensures that lastSubmission, accountId, and clientId remain synchronized with the latest interaction data while preserving the original creation timestamp.
Retrieval and Deletion Methods
The query class exposes several specialized methods for session management:
findBySessionUid(sessionUid: string)– Retrieves extension data for a specific OIDC sessionfindUserActiveSessionsWithExtensions(accountId: string)– Lists all active sessions with their extensions for a given userfindUserActiveSessionWithExtension(accountId: string, sessionUid: string)– Fetches a specific session with its extension datadeleteBySessionUid(sessionUid: string)– Removes extension data when a session terminatesdeleteByAccountId(accountId: string)– Cleans up all extensions for a user (useful for account deletion)
Enriching Sessions in the Session Library
The high-level session API transforms raw database records into structured objects suitable for the Management API and token customization workflows.
Formatting Session Data
Located in packages/core/src/libraries/session/index.ts, the formatSessionWithExtension function validates and structures the combined OIDC payload and extension data:
// packages/core/src/libraries/session/index.ts
const formatSessionWithExtension = (session: SessionInstanceWithExtension) => {
const { lastSubmission, clientId, accountId, payload, ...rest } = session;
const interactionContextResult =
jwtCustomizerUserInteractionContextGuard.safeParse(lastSubmission);
const payloadResult = oidcSessionInstancePayloadGuard.safeParse(payload);
if (!payloadResult.success) {
throw new RequestError({ code: 'oidc.invalid_session_payload', status: 500 });
}
return {
...rest,
payload: payloadResult.data,
lastSubmission: interactionContextResult.success ? interactionContextResult.data : null,
clientId,
accountId,
};
};
This function applies runtime validation using Zod guards (jwtCustomizerUserInteractionContextGuard and oidcSessionInstancePayloadGuard) to ensure type safety before exposing data to downstream consumers.
Public API Types
The Management API returns standardized session objects defined in packages/schemas/src/types/user-sessions.ts:
// packages/schemas/src/types/user-sessions.ts
export const userExtendedSessionGuard = z.object({
payload: oidcSessionInstancePayloadGuard,
lastSubmission: jwtCustomizerUserInteractionContextGuard.nullable(),
clientId: z.string().nullable(),
accountId: z.string().nullable(),
expiresAt: z.number(),
});
These types export as GetUserSessionsResponse and GetUserSessionResponse, powering the GET /users/:userId/sessions and GET /users/:userId/sessions/:sessionId endpoints.
Persisting Interaction Data During Consent
When users complete authentication or consent flows, Logto captures the interaction context and persists it to the extensions table before destroying the transient OIDC interaction object.
The Consent Flow Implementation
The saveInteractionLastSubmissionToSession function in packages/core/src/libraries/session/consent.ts handles this persistence:
// packages/core/src/libraries/session/consent.ts
const saveInteractionLastSubmissionToSession = async (
queries: Queries,
interactionDetails: Awaited<ReturnType<Provider['interactionDetails']>>
) => {
const { session, lastSubmission, params: { client_id: clientId } } = interactionDetails;
if (!session || !lastSubmission) {
return;
}
const { oidcSessionExtensions } = queries;
const result = jsonObjectGuard.safeParse(lastSubmission);
if (result.success) {
await oidcSessionExtensions.insert({
sessionUid: session.uid,
accountId: session.accountId,
lastSubmission: result.data,
...conditional(typeof clientId === 'string' && { clientId }),
});
}
};
This function executes during the consent phase, ensuring that sign-in context (IP address, user agent, authentication methods) survives the transition from the temporary interaction to the persistent session, enabling later retrieval during token generation.
Accessing Session Data for Token Customization
Logto exposes stored interaction data to JWT customization scripts, allowing developers to inject sign-in context into access tokens and ID tokens.
Reading Extensions for JWT Claims
The getInteractionLastSubmission function in packages/core/src/oidc/extra-token-claims.ts retrieves the stored context:
// packages/core/src/oidc/extra-token-claims.ts
const getInteractionLastSubmission = async (
queries: Queries,
{ accountId, sessionUid }: AccessToken
) => {
const { oidcSessionExtensions } = queries;
const sessionExtension = await oidcSessionExtensions.findBySessionUid(sessionUid);
if (!sessionExtension || sessionExtension.accountId !== accountId) {
return;
}
const { lastSubmission } = sessionExtension;
const interactionData = jwtCustomizerUserInteractionContextGuard.safeParse(lastSubmission);
if (!interactionData.success) {
return;
}
return interactionData.data;
};
This verification step ensures that the session belongs to the requesting user before exposing potentially sensitive interaction metadata.
Implementation in Custom Scripts
When building extra token claims, Logto injects the interaction data into the customization context. Developers access this data in scripts stored via the Logto Admin UI:
// Example JWT customiser script
exports = async (payload, context) => {
// context.interaction contains the parsed lastSubmission
const { ip, userAgent } = context.interaction ?? {};
return {
...payload,
client_ip: ip,
auth_context: userAgent
};
};
This capability enables security policies that embed device fingerprinting or network context directly into tokens issued by the Logto OIDC provider.
Practical Implementation Examples
Retrieving Active Sessions via Node SDK
To fetch a user's sessions with their extensions using the session library:
import { createSessionLibrary } from '@logto/core/src/libraries/session/index.js';
import Queries from '@logto/core/src/tenants/Queries.js';
// Initialize with tenant-specific queries
const sessionLib = createSessionLibrary(queries);
// Retrieve all active sessions with extension data
const sessions = await sessionLib.findUserActiveSessionsWithExtensions(userId);
// sessions contains: payload, lastSubmission, clientId, accountId, expiresAt
console.log(sessions[0]?.lastSubmission?.ip);
Revoking Session Grants Programmatically
To revoke all grants associated with a session (e.g., during logout):
import { SessionGrantRevokeTarget } from '@logto/schemas';
await sessionLib.revokeSessionAssociatedGrants({
provider,
authorizations: session.authorizations,
target: SessionGrantRevokeTarget.All,
});
This triggers the underlying oidc-provider to invalidate refresh tokens and access tokens linked to the session authorizations.
Accessing Interaction Context in Management APIs
When calling GET /users/:userId/sessions, the response includes the structured extension data:
{
"id": "session-uid-here",
"payload": { /* OIDC session payload */ },
"lastSubmission": {
"ip": "203.0.113.42",
"userAgent": "Mozilla/5.0...",
"authMethods": ["password", "totp"]
},
"clientId": "my-app",
"accountId": "user123",
"expiresAt": 1699123456
}
Summary
- Logto uses a dual-table architecture separating core OIDC sessions (
oidc_model_instances) from Logto-specific extensions (oidc_session_extensions) to maintain compliance while enabling rich features. - The
oidc_session_extensionstable storeslastSubmissiondata,clientId, andaccountIdwith row-level security and automatic timestamp updates. OidcSessionExtensionsQueriesprovides type-safe CRUD operations with PostgreSQL conflict resolution to handle session updates idempotently.- The session library validates and formats raw database records into
userExtendedSessionGuardstructures for the Management API. - Consent flow persistence captures interaction context before destruction, making sign-in metadata available for the session lifetime.
- Token customization retrieves stored interaction data via
getInteractionLastSubmission, injecting security context into JWT claims through thecontext.interactionobject.
Frequently Asked Questions
How does Logto separate standard OIDC data from custom session extensions?
Logto stores standard OIDC session parameters in oidc_model_instances (managed by the underlying oidc-provider library) while maintaining a parallel oidc_session_extensions table for Logto-specific data. This separation allows the platform to remain compliant with OIDC specifications while supporting extensions like interaction context and client metadata. The two tables link via the session_uid field, with the extensions table enforcing foreign key constraints to both the tenants and users tables.
What data is stored in the lastSubmission field of a session extension?
The lastSubmission field contains a JSONB object captured during the authentication or consent interaction, typically including the user's IP address, user agent string, authentication methods used (e.g., password, TOTP), and other sign-in context. According to the schema in packages/schemas/src/types/user-sessions.ts, this data validates against jwtCustomizerUserInteractionContextGuard and becomes accessible in JWT customization scripts under context.interaction.
Can I query session extensions directly for a specific user?
Yes, the OidcSessionExtensionsQueries class exposes findUserActiveSessionsWithExtensions(accountId), which returns all active sessions for a user joined with their extension data. This method powers the Management API endpoints GET /users/:userId/sessions and returns validated objects conforming to userExtendedSessionGuard, including the parsed lastSubmission, clientId, and expiration timestamps.
How do I access session interaction data when customizing JWT claims?
Within a custom JWT script (configured in the Logto Admin Console), interaction data is available via the context.interaction object. This data is retrieved internally by the getInteractionLastSubmission function in packages/core/src/oidc/extra-token-claims.ts, which validates the session ownership and parses the stored JSONB before injection. You can access fields like context.interaction.ip or context.interaction.userAgent to add location-based or device-based claims to issued tokens.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →