CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-slack--oauth

Official library for interacting with Slack's OAuth endpoints

Pending
Overview
Eval results
Files

state-management.mddocs/

State Management

CSRF protection through encrypted state parameters that securely transfer installation options between OAuth endpoints while preventing replay attacks.

Capabilities

StateStore Interface

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>;
}

State Data Structure

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;
}

Built-in State Stores

Ready-to-use implementations of the StateStore interface.

ClearStateStore

/**
 * 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>;
}

FileStateStore

/**
 * 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;
}

InstallPathOptions

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

docs

error-handling.md

index.md

installation-storage.md

oauth-flow.md

state-management.md

tile.json