MSAL Node provides a comprehensive token caching system that stores authentication artifacts in memory with support for serialization, distributed caching, and custom cache plugins. The cache system improves performance by avoiding unnecessary network requests and enables silent token acquisition.
Main token cache implementation providing in-memory storage with serialization support.
/**
* Token cache interface for basic cache operations
*/
interface ITokenCache {
/** Get all cached accounts */
getAllAccounts(): Promise<AccountInfo[]>;
/** Get account by home account ID */
getAccountByHomeId(homeAccountId: string): Promise<AccountInfo | null>;
/** Get account by local account ID */
getAccountByLocalId(localAccountId: string): Promise<AccountInfo | null>;
/** Remove account and associated tokens from cache */
removeAccount(account: AccountInfo): Promise<void>;
}
/**
* Main token cache class with serialization support
*/
class TokenCache implements ITokenCache, ISerializableTokenCache {
/** Get all cached accounts */
getAllAccounts(): Promise<AccountInfo[]>;
/** Get account by home account ID */
getAccountByHomeId(homeAccountId: string): Promise<AccountInfo | null>;
/** Get account by local account ID */
getAccountByLocalId(localAccountId: string): Promise<AccountInfo | null>;
/** Remove account and associated tokens from cache */
removeAccount(account: AccountInfo): Promise<void>;
/** Serialize cache to JSON string */
serialize(): Promise<string>;
/** Deserialize cache from JSON string */
deserialize(cache: string): Promise<void>;
/** Get all cached access tokens */
getAllAccessTokens(): Promise<AccessTokenEntity[]>;
/** Get all cached refresh tokens */
getAllRefreshTokens(): Promise<RefreshTokenEntity[]>;
/** Get all cached ID tokens */
getAllIdTokens(): Promise<IdTokenEntity[]>;
/** Clear all cache entries */
clear(): Promise<void>;
}Usage Example:
import { PublicClientApplication } from "@azure/msal-node";
const pca = new PublicClientApplication({
auth: {
clientId: "your-client-id"
}
});
// Get token cache instance
const cache = pca.getTokenCache();
// Get all accounts
const accounts = await cache.getAllAccounts();
console.log("Cached accounts:", accounts.length);
// Get specific account
if (accounts.length > 0) {
const account = await cache.getAccountByHomeId(accounts[0].homeAccountId);
console.log("Account:", account?.username);
}
// Remove account from cache
if (accounts.length > 0) {
await cache.removeAccount(accounts[0]);
console.log("Account removed from cache");
}Serialize and deserialize cache for persistent storage.
/**
* Interface for serializable token cache
*/
interface ISerializableTokenCache {
/** Serialize cache to JSON string */
serialize(): Promise<string>;
/** Deserialize cache from JSON string */
deserialize(cache: string): Promise<void>;
}
/**
* Cache serialization types
*/
type JsonCache = {
/** Account entities indexed by key */
Account: Record<string, SerializedAccountEntity>;
/** ID token entities indexed by key */
IdToken: Record<string, SerializedIdTokenEntity>;
/** Access token entities indexed by key */
AccessToken: Record<string, SerializedAccessTokenEntity>;
/** Refresh token entities indexed by key */
RefreshToken: Record<string, SerializedRefreshTokenEntity>;
/** App metadata entities indexed by key */
AppMetadata: Record<string, SerializedAppMetadataEntity>;
};
/**
* In-memory cache representation
*/
type InMemoryCache = {
/** Account entities */
accounts: Record<string, AccountEntity>;
/** ID token entities */
idTokens: Record<string, IdTokenEntity>;
/** Access token entities */
accessTokens: Record<string, AccessTokenEntity>;
/** Refresh token entities */
refreshTokens: Record<string, RefreshTokenEntity>;
/** App metadata entities */
appMetadata: Record<string, AppMetadataEntity>;
};
/**
* Key-value store for cache operations
*/
type CacheKVStore = Record<string, ValidCacheType>;
/**
* Valid cache value types
*/
type ValidCacheType =
| AccountEntity
| IdTokenEntity
| AccessTokenEntity
| RefreshTokenEntity
| AppMetadataEntity;Usage Example:
import fs from "fs/promises";
const cache = pca.getTokenCache();
// Serialize cache to file
const serializedCache = await cache.serialize();
await fs.writeFile("./token-cache.json", serializedCache);
console.log("Cache saved to file");
// Load cache from file
try {
const cacheData = await fs.readFile("./token-cache.json", "utf8");
await cache.deserialize(cacheData);
console.log("Cache loaded from file");
} catch (error) {
console.log("No existing cache file found");
}
// Clear cache
await cache.clear();
console.log("Cache cleared");Detailed structure of cached entities for serialization.
/**
* Serialized account entity
*/
type SerializedAccountEntity = {
/** Home account ID (user identifier across tenants) */
home_account_id: string;
/** Environment (authority host) */
environment: string;
/** Realm (tenant ID) */
realm: string;
/** Local account ID (user identifier within tenant) */
local_account_id: string;
/** Username (UPN or email) */
username: string;
/** Account type */
authority_type: string;
/** Display name */
name?: string;
/** Last modification timestamp */
last_modification_time?: string;
/** Additional client info */
client_info?: string;
};
/**
* Serialized ID token entity
*/
type SerializedIdTokenEntity = {
/** Home account ID */
home_account_id: string;
/** Environment */
environment: string;
/** Credential type */
credential_type: string;
/** Client ID */
client_id: string;
/** ID token JWT */
secret: string;
/** Realm */
realm: string;
};
/**
* Serialized access token entity
*/
type SerializedAccessTokenEntity = {
/** Home account ID */
home_account_id: string;
/** Environment */
environment: string;
/** Credential type */
credential_type: string;
/** Client ID */
client_id: string;
/** Access token */
secret: string;
/** Realm */
realm: string;
/** Target scopes */
target: string;
/** Expiration timestamp */
expires_on: string;
/** Extended expiration timestamp */
extended_expires_on?: string;
/** Cached at timestamp */
cached_at: string;
/** Token type (Bearer) */
token_type?: string;
/** Key ID for key-based tokens */
key_id?: string;
};
/**
* Serialized refresh token entity
*/
type SerializedRefreshTokenEntity = {
/** Home account ID */
home_account_id: string;
/** Environment */
environment: string;
/** Credential type */
credential_type: string;
/** Client ID */
client_id: string;
/** Refresh token */
secret: string;
/** Family ID for family of client IDs */
family_id?: string;
/** Target scopes */
target?: string;
/** Realm */
realm?: string;
};
/**
* Serialized app metadata entity
*/
type SerializedAppMetadataEntity = {
/** Client ID */
client_id: string;
/** Environment */
environment: string;
/** Family ID */
family_id?: string;
};Pluggable cache system for custom storage backends.
/**
* Cache plugin interface for custom cache implementations
*/
interface ICachePlugin {
/** Called before cache access */
beforeCacheAccess(tokenCacheContext: TokenCacheContext): Promise<void>;
/** Called after cache access */
afterCacheAccess(tokenCacheContext: TokenCacheContext): Promise<void>;
}
/**
* Token cache context provided to cache plugins
*/
type TokenCacheContext = {
/** Token cache instance */
tokenCache: ISerializableTokenCache;
/** Whether cache has changed */
hasChanged: boolean;
/** Client ID of the application */
clientId: string;
/** Account information if available */
account?: AccountInfo;
/** Requested scopes */
scopes?: string[];
};
/**
* Cache options for configuring cache behavior
*/
type CacheOptions = {
/** Cache plugin for custom storage */
cachePlugin?: ICachePlugin;
/**
* @deprecated claims-based-caching functionality will be removed in the next version
*/
claimsBasedCachingEnabled?: boolean;
};Usage Example:
import fs from "fs/promises";
// Custom file-based cache plugin
class FileCachePlugin implements ICachePlugin {
private cacheFilePath = "./msal-cache.json";
async beforeCacheAccess(cacheContext: TokenCacheContext): Promise<void> {
try {
const cacheData = await fs.readFile(this.cacheFilePath, "utf8");
await cacheContext.tokenCache.deserialize(cacheData);
} catch (error) {
// Cache file doesn't exist yet
console.log("No existing cache file found");
}
}
async afterCacheAccess(cacheContext: TokenCacheContext): Promise<void> {
if (cacheContext.hasChanged) {
const serializedCache = await cacheContext.tokenCache.serialize();
await fs.writeFile(this.cacheFilePath, serializedCache);
console.log("Cache updated and saved to file");
}
}
}
// Use custom cache plugin
const pca = new PublicClientApplication({
auth: {
clientId: "your-client-id"
},
cache: {
cachePlugin: new FileCachePlugin()
}
});Distributed cache plugin for Redis and other external cache systems.
/**
* Interface for external cache clients (Redis, etc.)
*/
interface ICacheClient {
/** Retrieve value from cache using key */
get(key: string): Promise<string>;
/** Save value to cache using key */
set(key: string, value: string): Promise<string>;
}
/**
* Interface for cache partitioning strategies
*/
interface IPartitionManager {
/** Get partition key for current context */
getKey(): Promise<string>;
/** Extract partition key from account entity */
extractKey(accountEntity: AccountEntity): Promise<string>;
}
/**
* Distributed cache plugin for external cache systems like Redis, DynamoDB, etc.
*/
class DistributedCachePlugin implements ICachePlugin {
constructor(client: ICacheClient, partitionManager: IPartitionManager);
/** Called before cache access to deserialize from external cache */
beforeCacheAccess(cacheContext: TokenCacheContext): Promise<void>;
/** Called after cache access to serialize to external cache */
afterCacheAccess(cacheContext: TokenCacheContext): Promise<void>;
}Usage Example:
import Redis from "ioredis";
// Redis cache client implementation
class RedisCacheClient implements ICacheClient {
private redis: Redis;
constructor(redisUrl: string) {
this.redis = new Redis(redisUrl);
}
async get(key: string): Promise<string | null> {
return await this.redis.get(key);
}
async set(key: string, value: string): Promise<void> {
await this.redis.set(key, value, "EX", 3600); // 1 hour expiration
}
}
// Simple partition manager
class SimplePartitionManager implements IPartitionManager {
private clientId: string;
constructor(clientId: string) {
this.clientId = clientId;
}
getKey(): string {
return `msal-cache:${this.clientId}`;
}
extractKey(accountEntity: AccountEntity): string {
return `msal-cache:${this.clientId}:${accountEntity.homeAccountId}`;
}
}
// Use distributed cache
const redisClient = new RedisCacheClient("redis://localhost:6379");
const partitionManager = new SimplePartitionManager("your-client-id");
const distributedCachePlugin = new DistributedCachePlugin(redisClient, partitionManager);
const pca = new PublicClientApplication({
auth: {
clientId: "your-client-id"
},
cache: {
cachePlugin: distributedCachePlugin
}
});Advanced account management operations.
/**
* Account information structure
*/
type AccountInfo = {
/** Home account ID (unique across tenants) */
homeAccountId: string;
/** Environment (authority host) */
environment: string;
/** Tenant ID */
tenantId: string;
/** Username (UPN or email) */
username: string;
/** Local account ID (unique within tenant) */
localAccountId: string;
/** Display name */
name?: string;
/** ID token claims */
idTokenClaims?: IdTokenClaims;
};
/**
* ID token claims structure
*/
type IdTokenClaims = {
/** Issuer */
iss?: string;
/** Subject (user ID) */
sub?: string;
/** Audience */
aud?: string;
/** Expiration time */
exp?: number;
/** Issued at time */
iat?: number;
/** Authentication time */
auth_time?: number;
/** Nonce */
nonce?: string;
/** Preferred username */
preferred_username?: string;
/** Name */
name?: string;
/** Email */
email?: string;
/** Object ID */
oid?: string;
/** Tenant ID */
tid?: string;
/** Version */
ver?: string;
};Usage Example:
// Account management operations
const accounts = await cache.getAllAccounts();
// Filter accounts by tenant
const tenantAccounts = accounts.filter(account =>
account.tenantId === "specific-tenant-id"
);
// Find account by username
const userAccount = accounts.find(account =>
account.username === "user@domain.com"
);
// Get detailed account information
if (userAccount) {
console.log("Account details:");
console.log("- Home Account ID:", userAccount.homeAccountId);
console.log("- Tenant ID:", userAccount.tenantId);
console.log("- Username:", userAccount.username);
console.log("- Display Name:", userAccount.name);
if (userAccount.idTokenClaims) {
console.log("- Email:", userAccount.idTokenClaims.email);
console.log("- Object ID:", userAccount.idTokenClaims.oid);
}
}
// Remove specific account
if (userAccount) {
await cache.removeAccount(userAccount);
console.log("Account removed from cache");
}Best practices for cache performance and management.
/**
* Cache performance considerations
*/
type CachePerformanceOptions = {
/** Enable cache compression */
enableCompression?: boolean;
/** Cache size limits */
maxCacheSize?: number;
/** Token expiration buffer */
expirationBuffer?: number;
/** Cleanup interval */
cleanupInterval?: number;
};Usage Example:
// High-performance cache plugin with compression and cleanup
class OptimizedCachePlugin implements ICachePlugin {
private cacheFile = "./optimized-cache.json";
private lastCleanup = Date.now();
private cleanupInterval = 60000; // 1 minute
async beforeCacheAccess(cacheContext: TokenCacheContext): Promise<void> {
// Periodic cleanup of expired tokens
if (Date.now() - this.lastCleanup > this.cleanupInterval) {
await this.cleanupExpiredTokens(cacheContext);
this.lastCleanup = Date.now();
}
// Load cache from file
try {
const cacheData = await fs.readFile(this.cacheFile, "utf8");
await cacheContext.tokenCache.deserialize(cacheData);
} catch (error) {
// No cache file exists
}
}
async afterCacheAccess(cacheContext: TokenCacheContext): Promise<void> {
if (cacheContext.hasChanged) {
const serializedCache = await cacheContext.tokenCache.serialize();
// Compress cache data (optional)
const compressedCache = this.compressCache(serializedCache);
await fs.writeFile(this.cacheFile, compressedCache);
}
}
private async cleanupExpiredTokens(cacheContext: TokenCacheContext): Promise<void> {
// Implementation to remove expired access tokens
const accessTokens = await cacheContext.tokenCache.getAllAccessTokens();
const now = Date.now() / 1000;
for (const token of accessTokens) {
if (token.expiresOn && parseInt(token.expiresOn) < now) {
// Remove expired token
console.log("Removing expired token");
}
}
}
private compressCache(cache: string): string {
// Optional: implement compression
return cache;
}
}// Default behavior - cache only exists in memory
const pca = new PublicClientApplication({
auth: { clientId: "your-client-id" }
// No cache plugin = in-memory only
});// File-based cache for desktop applications
const pca = new PublicClientApplication({
auth: { clientId: "your-client-id" },
cache: { cachePlugin: new FileCachePlugin() }
});// Redis-based cache for scalable web applications
const pca = new PublicClientApplication({
auth: { clientId: "your-client-id" },
cache: { cachePlugin: new DistributedCachePlugin(redisClient, partitionManager) }
});// Encrypted file cache for sensitive environments
const pca = new PublicClientApplication({
auth: { clientId: "your-client-id" },
cache: { cachePlugin: new EncryptedFileCachePlugin(encryptionKey) }
});