or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

error-handling.mdindex.mdinstallation-storage.mdoauth-flow.mdstate-management.md

state-management.mddocs/

0

# State Management

1

2

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

3

4

## Capabilities

5

6

### StateStore Interface

7

8

Interface for managing OAuth state parameters with encryption and verification.

9

10

```typescript { .api }

11

/**

12

* Interface for managing OAuth state parameters

13

*/

14

interface StateStore {

15

/**

16

* Generate encrypted state parameter for OAuth URL

17

* @param installOptions - OAuth options to encode in state

18

* @param now - Current timestamp for expiration tracking

19

* @returns Encrypted state string for OAuth URL

20

*/

21

generateStateParam(

22

installOptions: InstallURLOptions,

23

now: Date

24

): Promise<string>;

25

26

/**

27

* Verify and decode state parameter from OAuth callback

28

* @param now - Current timestamp for expiration validation

29

* @param state - Encrypted state string from OAuth callback

30

* @returns Decoded installation options from state

31

* @throws InvalidStateError if state is invalid or expired

32

*/

33

verifyStateParam(now: Date, state: string): Promise<InstallURLOptions>;

34

}

35

```

36

37

### State Data Structure

38

39

Data structure represented by the state parameter.

40

41

```typescript { .api }

42

/**

43

* Data structure encoded in OAuth state parameter

44

*/

45

interface StateObj {

46

/** Timestamp when state was generated */

47

now: Date;

48

/** Installation options passed through OAuth flow */

49

installOptions: InstallURLOptions;

50

/** Optional random value for additional entropy */

51

random?: string | number;

52

}

53

```

54

55

### Built-in State Stores

56

57

Ready-to-use implementations of the StateStore interface.

58

59

#### ClearStateStore

60

61

```typescript { .api }

62

/**

63

* JWT-based state store with encryption using a secret key

64

*/

65

class ClearStateStore implements StateStore {

66

/**

67

* @param stateSecret - Secret key for JWT signing and encryption

68

* @param expirationSeconds - State expiration time in seconds (default: 600)

69

*/

70

constructor(stateSecret: string, expirationSeconds?: number);

71

72

async generateStateParam(

73

installOptions: InstallURLOptions,

74

now: Date

75

): Promise<string>;

76

77

async verifyStateParam(now: Date, state: string): Promise<InstallURLOptions>;

78

}

79

```

80

81

#### FileStateStore

82

83

```typescript { .api }

84

/**

85

* File-based state store for development and simple deployments

86

*/

87

class FileStateStore implements StateStore {

88

/**

89

* @param args - Configuration options for file storage

90

*/

91

constructor(args: FileStateStoreArgs);

92

93

async generateStateParam(

94

installOptions: InstallURLOptions,

95

now: Date

96

): Promise<string>;

97

98

async verifyStateParam(now: Date, state: string): Promise<InstallURLOptions>;

99

}

100

101

/**

102

* Configuration options for FileStateStore

103

*/

104

interface FileStateStoreArgs {

105

/** Directory to store state files (default: homedir/.bolt-js-oauth-states) */

106

baseDir?: string;

107

/** State expiration time in seconds (default: 600) */

108

stateExpirationSeconds?: number;

109

/** Logger instance for debugging */

110

logger?: Logger;

111

}

112

```

113

114

### InstallPathOptions

115

116

Options for configuring install path behavior with custom request handling.

117

118

```typescript { .api }

119

interface InstallPathOptions {

120

/**

121

* Custom handler called before redirecting to Slack OAuth URL

122

* Return false to skip OAuth redirect and handle response manually

123

* @param request - Incoming HTTP request

124

* @param response - HTTP response for custom handling

125

* @param options - OAuth options for this installation

126

* @returns true to continue with OAuth, false to skip

127

*/

128

beforeRedirection?: (

129

request: IncomingMessage,

130

response: ServerResponse,

131

options?: InstallURLOptions

132

) => Promise<boolean>;

133

}

134

```

135

136

**Usage Examples:**

137

138

```typescript

139

import {

140

InstallProvider,

141

ClearStateStore,

142

FileStateStore,

143

StateStore,

144

StateObj

145

} from "@slack/oauth";

146

147

// Using default ClearStateStore (recommended)

148

const installer = new InstallProvider({

149

clientId: process.env.SLACK_CLIENT_ID!,

150

clientSecret: process.env.SLACK_CLIENT_SECRET!,

151

stateSecret: "my-secret-key", // Required for ClearStateStore

152

});

153

154

// Using FileStateStore

155

const fileStateStore = new FileStateStore({

156

baseDir: "./oauth-states",

157

expirationSeconds: 300, // 5 minutes

158

});

159

160

const installer2 = new InstallProvider({

161

clientId: process.env.SLACK_CLIENT_ID!,

162

clientSecret: process.env.SLACK_CLIENT_SECRET!,

163

stateStore: fileStateStore,

164

});

165

166

// Custom state store implementation

167

class RedisStateStore implements StateStore {

168

private redis: RedisClient;

169

private expirationSeconds: number;

170

171

constructor(redis: RedisClient, expirationSeconds = 600) {

172

this.redis = redis;

173

this.expirationSeconds = expirationSeconds;

174

}

175

176

async generateStateParam(installOptions: InstallURLOptions, now: Date): Promise<string> {

177

const stateId = `state_${Date.now()}_${Math.random()}`;

178

const stateObj: StateObj = {

179

now,

180

installOptions,

181

random: Math.random(),

182

};

183

184

// Store in Redis with expiration

185

await this.redis.setex(

186

stateId,

187

this.expirationSeconds,

188

JSON.stringify(stateObj)

189

);

190

191

return stateId;

192

}

193

194

async verifyStateParam(now: Date, state: string): Promise<InstallURLOptions> {

195

const stateData = await this.redis.get(state);

196

if (!stateData) {

197

throw new InvalidStateError("State not found or expired");

198

}

199

200

const stateObj: StateObj = JSON.parse(stateData);

201

202

// Check expiration

203

const ageMs = now.getTime() - new Date(stateObj.now).getTime();

204

if (ageMs > this.expirationSeconds * 1000) {

205

throw new InvalidStateError("State has expired");

206

}

207

208

// Clean up used state

209

await this.redis.del(state);

210

211

return stateObj.installOptions;

212

}

213

}

214

215

// Manual state operations

216

const stateStore = new ClearStateStore("secret-key");

217

const installOptions = {

218

scopes: ["chat:write"],

219

metadata: "custom-data",

220

};

221

222

// Generate state for OAuth URL

223

const state = await stateStore.generateStateParam(installOptions, new Date());

224

console.log("Generated state:", state);

225

226

// Later, verify state from callback

227

try {

228

const decodedOptions = await stateStore.verifyStateParam(new Date(), state);

229

console.log("Decoded options:", decodedOptions);

230

} catch (error) {

231

console.error("State verification failed:", error.message);

232

}

233

234

// Disable state verification (not recommended except for enterprise admin installs)

235

const noStateInstaller = new InstallProvider({

236

clientId: process.env.SLACK_CLIENT_ID!,

237

clientSecret: process.env.SLACK_CLIENT_SECRET!,

238

stateVerification: false, // Disables CSRF protection

239

});

240

```