Official library for interacting with Slack's OAuth endpoints
—
CSRF protection through encrypted state parameters that securely transfer installation options between OAuth endpoints while preventing replay attacks.
Interface for managing OAuth state parameters with encryption and verification.
/**
* Interface for managing OAuth state parameters
*/
interface StateStore {
/**
* Generate encrypted state parameter for OAuth URL
* @param installOptions - OAuth options to encode in state
* @param now - Current timestamp for expiration tracking
* @returns Encrypted state string for OAuth URL
*/
generateStateParam(
installOptions: InstallURLOptions,
now: Date
): Promise<string>;
/**
* Verify and decode state parameter from OAuth callback
* @param now - Current timestamp for expiration validation
* @param state - Encrypted state string from OAuth callback
* @returns Decoded installation options from state
* @throws InvalidStateError if state is invalid or expired
*/
verifyStateParam(now: Date, state: string): Promise<InstallURLOptions>;
}Data structure represented by the state parameter.
/**
* Data structure encoded in OAuth state parameter
*/
interface StateObj {
/** Timestamp when state was generated */
now: Date;
/** Installation options passed through OAuth flow */
installOptions: InstallURLOptions;
/** Optional random value for additional entropy */
random?: string | number;
}Ready-to-use implementations of the StateStore interface.
/**
* JWT-based state store with encryption using a secret key
*/
class ClearStateStore implements StateStore {
/**
* @param stateSecret - Secret key for JWT signing and encryption
* @param expirationSeconds - State expiration time in seconds (default: 600)
*/
constructor(stateSecret: string, expirationSeconds?: number);
async generateStateParam(
installOptions: InstallURLOptions,
now: Date
): Promise<string>;
async verifyStateParam(now: Date, state: string): Promise<InstallURLOptions>;
}/**
* File-based state store for development and simple deployments
*/
class FileStateStore implements StateStore {
/**
* @param args - Configuration options for file storage
*/
constructor(args: FileStateStoreArgs);
async generateStateParam(
installOptions: InstallURLOptions,
now: Date
): Promise<string>;
async verifyStateParam(now: Date, state: string): Promise<InstallURLOptions>;
}
/**
* Configuration options for FileStateStore
*/
interface FileStateStoreArgs {
/** Directory to store state files (default: homedir/.bolt-js-oauth-states) */
baseDir?: string;
/** State expiration time in seconds (default: 600) */
stateExpirationSeconds?: number;
/** Logger instance for debugging */
logger?: Logger;
}Options for configuring install path behavior with custom request handling.
interface InstallPathOptions {
/**
* Custom handler called before redirecting to Slack OAuth URL
* Return false to skip OAuth redirect and handle response manually
* @param request - Incoming HTTP request
* @param response - HTTP response for custom handling
* @param options - OAuth options for this installation
* @returns true to continue with OAuth, false to skip
*/
beforeRedirection?: (
request: IncomingMessage,
response: ServerResponse,
options?: InstallURLOptions
) => Promise<boolean>;
}Usage Examples:
import {
InstallProvider,
ClearStateStore,
FileStateStore,
StateStore,
StateObj
} from "@slack/oauth";
// Using default ClearStateStore (recommended)
const installer = new InstallProvider({
clientId: process.env.SLACK_CLIENT_ID!,
clientSecret: process.env.SLACK_CLIENT_SECRET!,
stateSecret: "my-secret-key", // Required for ClearStateStore
});
// Using FileStateStore
const fileStateStore = new FileStateStore({
baseDir: "./oauth-states",
expirationSeconds: 300, // 5 minutes
});
const installer2 = new InstallProvider({
clientId: process.env.SLACK_CLIENT_ID!,
clientSecret: process.env.SLACK_CLIENT_SECRET!,
stateStore: fileStateStore,
});
// Custom state store implementation
class RedisStateStore implements StateStore {
private redis: RedisClient;
private expirationSeconds: number;
constructor(redis: RedisClient, expirationSeconds = 600) {
this.redis = redis;
this.expirationSeconds = expirationSeconds;
}
async generateStateParam(installOptions: InstallURLOptions, now: Date): Promise<string> {
const stateId = `state_${Date.now()}_${Math.random()}`;
const stateObj: StateObj = {
now,
installOptions,
random: Math.random(),
};
// Store in Redis with expiration
await this.redis.setex(
stateId,
this.expirationSeconds,
JSON.stringify(stateObj)
);
return stateId;
}
async verifyStateParam(now: Date, state: string): Promise<InstallURLOptions> {
const stateData = await this.redis.get(state);
if (!stateData) {
throw new InvalidStateError("State not found or expired");
}
const stateObj: StateObj = JSON.parse(stateData);
// Check expiration
const ageMs = now.getTime() - new Date(stateObj.now).getTime();
if (ageMs > this.expirationSeconds * 1000) {
throw new InvalidStateError("State has expired");
}
// Clean up used state
await this.redis.del(state);
return stateObj.installOptions;
}
}
// Manual state operations
const stateStore = new ClearStateStore("secret-key");
const installOptions = {
scopes: ["chat:write"],
metadata: "custom-data",
};
// Generate state for OAuth URL
const state = await stateStore.generateStateParam(installOptions, new Date());
console.log("Generated state:", state);
// Later, verify state from callback
try {
const decodedOptions = await stateStore.verifyStateParam(new Date(), state);
console.log("Decoded options:", decodedOptions);
} catch (error) {
console.error("State verification failed:", error.message);
}
// Disable state verification (not recommended except for enterprise admin installs)
const noStateInstaller = new InstallProvider({
clientId: process.env.SLACK_CLIENT_ID!,
clientSecret: process.env.SLACK_CLIENT_SECRET!,
stateVerification: false, // Disables CSRF protection
});Install with Tessl CLI
npx tessl i tessl/npm-slack--oauth