This guide continues from Use Prisma and adds the session feature.
Access tokens remain short-lived and stateless. The session feature adds server-side state for refresh tokens, so a refresh token can be rejected when its session is revoked.
This guide uses Redis for session storage because it supports TTL-based keys and sorted sets for per-user session indexes.
By the end of this guide, your application will be able to list active sessions, revoke a specific session, revoke other sessions, and reject refresh requests from revoked sessions.
Prerequisites
Section titled “Prerequisites”- A project completed from Use Prisma
- A running Redis instance
Install Redis client
Section titled “Install Redis client”Install the Redis client used in this guide:
pnpm add redisnpm install redisyarn add redisConfigure the Redis connection
Section titled “Configure the Redis connection”Add Redis connection settings to your existing .env file:
REDIS_HOST=localhostREDIS_PORT=6379Create RedisModule
Section titled “Create RedisModule”Create a small Redis module that provides a connected Redis client:
Directorysrc/
Directoryredis/
- redis.module.ts
import { Module } from '@nestjs/common';import { ConfigModule, ConfigService } from '@nestjs/config';import { createClient } from 'redis';
@Module({ imports: [ConfigModule], providers: [ { provide: 'REDIS_CLIENT', inject: [ConfigService], async useFactory(config: ConfigService) { const client = createClient({ socket: { host: config.getOrThrow('REDIS_HOST'), port: Number(config.getOrThrow('REDIS_PORT')), }, }); await client.connect(); return client; }, }, ], exports: ['REDIS_CLIENT'],})export class RedisModule {}Add the session feature
Section titled “Add the session feature”Add the feature
Run the CLI command:
brkpt auth add sessionThe CLI adds a new features/session/ folder.
Implement SessionAdapter
The session adapter has more methods than the previous adapters, but most of them are small Redis operations.
They fall into two groups:
- Session storage: create, read, update, and delete a session.
- User index: keep a per-user session list for listing and batch revocation.
The implementation below is enough to get started with Redis.
Create session.adapter.ts:
Directorysrc/
Directorybrkpt-auth/
Directoryadapters/
- session.adapter.ts
import { Inject, Injectable } from '@nestjs/common';import { type RedisClientType } from 'redis';
import { SessionData } from '../common/interfaces';import { SessionPort } from '../features/session/session.port';import { AuthJwtPayload } from './types';
@Injectable()export class SessionAdapter implements SessionPort { constructor( @Inject('REDIS_CLIENT') private readonly redis: RedisClientType, ) {}
private key(sessionId: string) { return `session:${sessionId}`; }
private userIndexKey(userId: unknown) { return `session:user:${String(userId)}`; }
async create( sessionId: string, data: SessionData, ttlMs: number, ): Promise<void> { await this.redis.set(this.key(sessionId), JSON.stringify(data), { expiration: { type: 'PX', value: ttlMs }, }); }
async exists(sessionId: string): Promise<boolean> { return !!(await this.redis.exists(this.key(sessionId))); }
async findById(sessionId: string): Promise<SessionData | null> { const data = await this.redis.get(this.key(sessionId)); return data ? (JSON.parse(data) as SessionData) : null; }
async update(sessionId: string, data: SessionData): Promise<void> { const key = this.key(sessionId); const ttlMs = await this.redis.pTTL(key); if (ttlMs > 0) { await this.redis.set(key, JSON.stringify(data), { expiration: { type: 'PX', value: ttlMs }, }); } }
async delete(sessionId: string): Promise<void> { await this.redis.del(this.key(sessionId)); }
async addToUserIndex( userId: unknown, sessionId: string, expiresAt: number, ): Promise<void> { await this.redis.zAdd(this.userIndexKey(userId), { score: expiresAt, value: sessionId, }); }
async removeFromUserIndex(userId: unknown, sessionId: string): Promise<void> { await this.redis.zRem(this.userIndexKey(userId), sessionId); }
async pruneUserIndex(userId: unknown, before: number): Promise<void> { await this.redis.zRemRangeByScore( this.userIndexKey(userId), '-inf', before, ); }
getUserIndexSessionIds(userId: unknown): Promise<string[]> { return this.redis.zRange(this.userIndexKey(userId), 0, -1); }
extractUserIdFromJwtPayload(payload: AuthJwtPayload): number { return payload.sub; }}Register the feature
Update features.ts and pass SessionAdapter to sessionFeature:
import { CoreAdapter } from './adapters/core.adapter';import { CredentialsAdapter } from './adapters/credentials.adapter';import { SessionAdapter } from './adapters/session.adapter';import { FeatureConfig } from './common/interfaces';import { coreFeature } from './features/core/core.feature';import { credentialsFeature } from './features/credentials/credentials.feature';import { sessionFeature } from './features/session/session.feature';
export const features: FeatureConfig[] = [ coreFeature(CoreAdapter), credentialsFeature(CredentialsAdapter), sessionFeature(SessionAdapter),];Import RedisModule
SessionAdapter depends on REDIS_CLIENT, which is exported by RedisModule.
Add RedisModule to brkpt-auth.module.ts:
import { PrismaModule } from '../prisma/prisma.module';import { RedisModule } from '../redis/redis.module';
@Module({ imports: [PrismaModule], imports: [PrismaModule, RedisModule], controllers: [...features.flatMap((f) => f.controllers)], providers: [...features.flatMap((f) => f.providers)],})export class BrkptAuthModule { // ...}Run and verify
Section titled “Run and verify”Start the application:
pnpm start:devnpm run start:devyarn start:devThe session feature adds the following endpoints:
| Method | Path | Description |
|---|---|---|
GET |
/auth/session |
List the current user’s active sessions |
DELETE |
/auth/session/:sessionId |
Revoke a specific session |
DELETE |
/auth/session/others |
Revoke all sessions except the current one |
Use an existing account from Get started, or create a new one with /auth/sign-up.
List sessions
Sign in multiple times with the same account. Each successful sign-in creates a new session.
Then list the active sessions:
GET /auth/sessionAuthorization: Bearer <access-token>The response should include the active sessions for the current user.
Revoke a session
Revoke one session by id:
DELETE /auth/session/<session-id>Authorization: Bearer <access-token>After a session is revoked, the refresh token associated with that session can no longer be used to call /auth/refresh.
Revoke other sessions
Revoke all sessions except the current one:
DELETE /auth/session/othersAuthorization: Bearer <access-token>This is useful for “sign out from other devices” flows.