@tgai96/outlook-mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +396 -0
- package/auth/index.js +64 -0
- package/auth/oauth-server.js +178 -0
- package/auth/token-manager.js +139 -0
- package/auth/token-storage.js +317 -0
- package/auth/tools.js +171 -0
- package/calendar/accept.js +64 -0
- package/calendar/cancel.js +64 -0
- package/calendar/create.js +69 -0
- package/calendar/decline.js +64 -0
- package/calendar/delete.js +59 -0
- package/calendar/index.js +123 -0
- package/calendar/list.js +77 -0
- package/cli.js +246 -0
- package/config.js +108 -0
- package/email/folder-utils.js +175 -0
- package/email/index.js +157 -0
- package/email/list.js +78 -0
- package/email/mark-as-read.js +101 -0
- package/email/read.js +128 -0
- package/email/search.js +285 -0
- package/email/send.js +120 -0
- package/folder/create.js +124 -0
- package/folder/index.js +78 -0
- package/folder/list.js +264 -0
- package/folder/move.js +163 -0
- package/index.js +148 -0
- package/package.json +54 -0
- package/rules/create.js +248 -0
- package/rules/index.js +175 -0
- package/rules/list.js +202 -0
- package/utils/graph-api.js +192 -0
- package/utils/mock-data.js +145 -0
- package/utils/odata-helpers.js +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
# Modular Outlook MCP Server
|
|
2
|
+
|
|
3
|
+
This is a modular implementation of the Outlook MCP (Model Context Protocol) server that connects Claude with Microsoft Outlook through the Microsoft Graph API.
|
|
4
|
+
|
|
5
|
+
## Credits
|
|
6
|
+
|
|
7
|
+
This repository is built on the efforts and work of [ryaker's outlook-mcp](https://github.com/ryaker/outlook-mcp) project.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Authentication**: OAuth 2.0 authentication with Microsoft Graph API using PKCE flow
|
|
12
|
+
- **Automatic Token Refresh**: Tokens automatically refresh in the background - no manual re-authentication needed
|
|
13
|
+
- **Email Management**: List, search, read, send, and mark emails as read/unread
|
|
14
|
+
- **Calendar Management**: List, create, accept, decline, and delete calendar events
|
|
15
|
+
- **Folder Management**: List and manage email folders
|
|
16
|
+
- **Rules Management**: Create and manage email rules
|
|
17
|
+
- **Modular Structure**: Clean separation of concerns for better maintainability
|
|
18
|
+
- **OData Filter Handling**: Proper escaping and formatting of OData queries
|
|
19
|
+
- **Test Mode**: Simulated responses for testing without real API calls
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
### Quick Run (No Install)
|
|
24
|
+
|
|
25
|
+
**Step 1: Configure credentials (first time only)**
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npx outlook-mcp config
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Enter your Azure credentials when prompted. They'll be saved to:
|
|
32
|
+
- **macOS/Linux**: `~/.outlook-mcp/config.json`
|
|
33
|
+
- **Windows**: `%USERPROFILE%\.outlook-mcp\config.json` (e.g., `C:\Users\YourName\.outlook-mcp\config.json`)
|
|
34
|
+
|
|
35
|
+
**Step 2: Authenticate**
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npx outlook-mcp auth
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Browser will automatically open for Microsoft authentication. After successful authentication, tokens are saved to:
|
|
42
|
+
- **macOS/Linux**: `~/.outlook-mcp/tokens.json`
|
|
43
|
+
- **Windows**: `%USERPROFILE%\.outlook-mcp\tokens.json` (e.g., `C:\Users\YourName\.outlook-mcp\tokens.json`)
|
|
44
|
+
|
|
45
|
+
**Step 3: Test all tools (optional)**
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npx outlook-mcp test-all-tools
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
This runs a comprehensive test suite to verify all tools are working correctly.
|
|
52
|
+
|
|
53
|
+
## Available Commands
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# Start the MCP server (for use with Claude Desktop)
|
|
57
|
+
npx outlook-mcp
|
|
58
|
+
|
|
59
|
+
# Start the authentication server
|
|
60
|
+
npx outlook-mcp auth
|
|
61
|
+
|
|
62
|
+
# Configure Azure credentials interactively
|
|
63
|
+
npx outlook-mcp config
|
|
64
|
+
|
|
65
|
+
# Test all MCP tools with your stored tokens
|
|
66
|
+
npx outlook-mcp test-all-tools
|
|
67
|
+
|
|
68
|
+
# Show help message
|
|
69
|
+
npx outlook-mcp --help
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Authentication & Token Management
|
|
73
|
+
|
|
74
|
+
### Initial Authentication
|
|
75
|
+
|
|
76
|
+
1. Run `npx outlook-mcp auth` to start the authentication server
|
|
77
|
+
2. Your browser will automatically open to Microsoft's login page
|
|
78
|
+
3. Sign in and grant permissions
|
|
79
|
+
4. Tokens are automatically saved to:
|
|
80
|
+
- **macOS/Linux**: `~/.outlook-mcp/tokens.json`
|
|
81
|
+
- **Windows**: `%USERPROFILE%\.outlook-mcp\tokens.json` (e.g., `C:\Users\YourName\.outlook-mcp\tokens.json`)
|
|
82
|
+
|
|
83
|
+
### Automatic Token Refresh
|
|
84
|
+
|
|
85
|
+
**You only need to authenticate once!** The server automatically handles token refresh:
|
|
86
|
+
|
|
87
|
+
- When an access token expires (typically after 1 hour), the server automatically uses the refresh token to get a new one
|
|
88
|
+
- The new token is saved automatically - no user intervention needed
|
|
89
|
+
- This happens transparently in the background whenever any tool is called
|
|
90
|
+
|
|
91
|
+
**You'll only need to manually authenticate again if:**
|
|
92
|
+
- The refresh token expires (typically after 90 days of inactivity)
|
|
93
|
+
- The refresh token is revoked (password change, security event, etc.)
|
|
94
|
+
- You want to change permissions/scopes
|
|
95
|
+
- The token file is deleted
|
|
96
|
+
|
|
97
|
+
### Token Storage
|
|
98
|
+
|
|
99
|
+
Files are saved in the `.outlook-mcp` directory in your home folder:
|
|
100
|
+
|
|
101
|
+
**macOS/Linux**: `~/.outlook-mcp/`
|
|
102
|
+
- Config: `~/.outlook-mcp/config.json`
|
|
103
|
+
- Tokens: `~/.outlook-mcp/tokens.json`
|
|
104
|
+
|
|
105
|
+
**Windows**: `%USERPROFILE%\.outlook-mcp\`
|
|
106
|
+
- Config: `%USERPROFILE%\.outlook-mcp\config.json` (e.g., `C:\Users\YourName\.outlook-mcp\config.json`)
|
|
107
|
+
- Tokens: `%USERPROFILE%\.outlook-mcp\tokens.json` (e.g., `C:\Users\YourName\.outlook-mcp\tokens.json`)
|
|
108
|
+
|
|
109
|
+
**Contents**:
|
|
110
|
+
- `config.json`: Azure credentials (client ID, client secret, test mode setting)
|
|
111
|
+
- `tokens.json`: Access token, refresh token, expiration times, and granted scopes
|
|
112
|
+
|
|
113
|
+
**Security**: Never commit these files to version control (they're in `.gitignore`)
|
|
114
|
+
|
|
115
|
+
## Installation
|
|
116
|
+
|
|
117
|
+
### Prerequisites
|
|
118
|
+
- Node.js 14.0.0 or higher
|
|
119
|
+
- npm or yarn package manager
|
|
120
|
+
- Azure account for app registration
|
|
121
|
+
|
|
122
|
+
### Install Dependencies
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
npm install
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Configuration
|
|
129
|
+
|
|
130
|
+
### Adding to MCP Client
|
|
131
|
+
|
|
132
|
+
To use this MCP server with an MCP client, you can either use the published npm package or a local installation:
|
|
133
|
+
|
|
134
|
+
#### Option 1: Using Published npm Package (Recommended)
|
|
135
|
+
|
|
136
|
+
If the package is published to npm, you can use it directly:
|
|
137
|
+
|
|
138
|
+
```json
|
|
139
|
+
{
|
|
140
|
+
"mcpServers": {
|
|
141
|
+
"outlook": {
|
|
142
|
+
"command": "npx",
|
|
143
|
+
"args": [
|
|
144
|
+
"-y",
|
|
145
|
+
"@yourusername/outlook-mcp"
|
|
146
|
+
]
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**Note**: The package name is `@tgai96/outlook-mcp` once published.
|
|
153
|
+
|
|
154
|
+
#### Option 2: Using Local Installation
|
|
155
|
+
|
|
156
|
+
For local development or if the package isn't published yet:
|
|
157
|
+
|
|
158
|
+
**macOS**:
|
|
159
|
+
```json
|
|
160
|
+
{
|
|
161
|
+
"mcpServers": {
|
|
162
|
+
"outlook": {
|
|
163
|
+
"command": "npx",
|
|
164
|
+
"args": [
|
|
165
|
+
"file:///Users/john/outlook-mcp"
|
|
166
|
+
]
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
**Windows**:
|
|
173
|
+
```json
|
|
174
|
+
{
|
|
175
|
+
"mcpServers": {
|
|
176
|
+
"outlook": {
|
|
177
|
+
"command": "npx",
|
|
178
|
+
"args": [
|
|
179
|
+
"file:///C:/Users/john/outlook-mcp"
|
|
180
|
+
]
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**Linux**:
|
|
187
|
+
```json
|
|
188
|
+
{
|
|
189
|
+
"mcpServers": {
|
|
190
|
+
"outlook": {
|
|
191
|
+
"command": "npx",
|
|
192
|
+
"args": [
|
|
193
|
+
"file:///home/john/outlook-mcp"
|
|
194
|
+
]
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**Note**: Replace the path with the actual path to your `outlook-mcp` directory.
|
|
201
|
+
|
|
202
|
+
### Server Configuration File
|
|
203
|
+
|
|
204
|
+
Credentials can be stored in:
|
|
205
|
+
- **macOS/Linux**: `~/.outlook-mcp/config.json`
|
|
206
|
+
- **Windows**: `%USERPROFILE%\.outlook-mcp\config.json` (e.g., `C:\Users\YourName\.outlook-mcp\config.json`)
|
|
207
|
+
|
|
208
|
+
```json
|
|
209
|
+
{
|
|
210
|
+
"MS_CLIENT_ID": "your-client-id-here",
|
|
211
|
+
"MS_CLIENT_SECRET": "your-client-secret-here",
|
|
212
|
+
"USE_TEST_MODE": false
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Environment Variables
|
|
217
|
+
|
|
218
|
+
Alternatively, you can use environment variables:
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
export MS_CLIENT_ID="your-client-id"
|
|
222
|
+
export MS_CLIENT_SECRET="your-client-secret"
|
|
223
|
+
export USE_TEST_MODE="false"
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
**Priority**: Environment variables > Config file > Defaults
|
|
227
|
+
|
|
228
|
+
## Testing Tools
|
|
229
|
+
|
|
230
|
+
### Method 1: Test All Tools (Recommended)
|
|
231
|
+
|
|
232
|
+
Run the comprehensive test suite:
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
npx outlook-mcp test-all-tools
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
This tests all available tools and provides a detailed report.
|
|
239
|
+
|
|
240
|
+
### Method 2: Using MCP Inspector
|
|
241
|
+
|
|
242
|
+
The MCP Inspector provides an interactive way to test tools:
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
npm run inspect
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
This will:
|
|
249
|
+
- Start the MCP server
|
|
250
|
+
- Open an interactive inspector interface
|
|
251
|
+
- Allow you to:
|
|
252
|
+
- List all available tools
|
|
253
|
+
- Call tools with parameters
|
|
254
|
+
- See responses in real-time
|
|
255
|
+
|
|
256
|
+
**Example in Inspector:**
|
|
257
|
+
```
|
|
258
|
+
> tools/list
|
|
259
|
+
> tools/call {"name": "list-emails", "arguments": {"count": 5}}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## Available Tools
|
|
263
|
+
|
|
264
|
+
### Authentication Tools
|
|
265
|
+
- `about` - Get server information
|
|
266
|
+
- `authenticate` - Authenticate with Microsoft (auto-refreshes if tokens exist)
|
|
267
|
+
- `check-auth-status` - Check authentication status with human-readable expiration times
|
|
268
|
+
|
|
269
|
+
### Email Tools
|
|
270
|
+
- `list-emails` - List emails from a folder
|
|
271
|
+
- `search-emails` - Search emails with various criteria
|
|
272
|
+
- `read-email` - Read full email content
|
|
273
|
+
- `send-email` - Send a new email
|
|
274
|
+
- `mark-as-read` - Mark email as read or unread
|
|
275
|
+
|
|
276
|
+
### Calendar Tools
|
|
277
|
+
- `list-events` - List calendar events
|
|
278
|
+
- `create-event` - Create a new calendar event
|
|
279
|
+
- `accept-event` - Accept a calendar event
|
|
280
|
+
- `decline-event` - Decline a calendar event
|
|
281
|
+
- `cancel-event` - Cancel a calendar event
|
|
282
|
+
|
|
283
|
+
### Folder Tools
|
|
284
|
+
- `list-folders` - List email folders
|
|
285
|
+
|
|
286
|
+
### Rules Tools
|
|
287
|
+
- `list-rules` - List email rules
|
|
288
|
+
- `create-rule` - Create a new email rule
|
|
289
|
+
|
|
290
|
+
## Directory Structure
|
|
291
|
+
|
|
292
|
+
```
|
|
293
|
+
outlook-mcp/
|
|
294
|
+
├── index.js # Main entry point
|
|
295
|
+
├── config.js # Configuration settings
|
|
296
|
+
├── cli.js # CLI command handler
|
|
297
|
+
├── auth/ # Authentication modules
|
|
298
|
+
│ ├── index.js # Authentication exports
|
|
299
|
+
│ ├── token-storage.js # Token storage and automatic refresh
|
|
300
|
+
│ ├── token-manager.js # Legacy token manager
|
|
301
|
+
│ └── tools.js # Auth-related tools
|
|
302
|
+
├── calendar/ # Calendar functionality
|
|
303
|
+
│ ├── index.js # Calendar exports
|
|
304
|
+
│ ├── list.js # List events
|
|
305
|
+
│ ├── create.js # Create event
|
|
306
|
+
│ ├── delete.js # Delete event
|
|
307
|
+
│ ├── cancel.js # Cancel event
|
|
308
|
+
│ ├── accept.js # Accept event
|
|
309
|
+
│ └── decline.js # Decline event
|
|
310
|
+
├── email/ # Email functionality
|
|
311
|
+
│ ├── index.js # Email exports
|
|
312
|
+
│ ├── list.js # List emails
|
|
313
|
+
│ ├── search.js # Search emails
|
|
314
|
+
│ ├── read.js # Read email
|
|
315
|
+
│ ├── send.js # Send email
|
|
316
|
+
│ ├── mark-as-read.js # Mark email as read/unread
|
|
317
|
+
│ └── folder-utils.js # Folder path resolution
|
|
318
|
+
├── folder/ # Folder management
|
|
319
|
+
│ ├── index.js # Folder exports
|
|
320
|
+
│ └── list.js # List folders
|
|
321
|
+
├── rules/ # Rules management
|
|
322
|
+
│ ├── index.js # Rules exports
|
|
323
|
+
│ ├── list.js # List rules
|
|
324
|
+
│ └── create.js # Create rule
|
|
325
|
+
└── utils/ # Utility functions
|
|
326
|
+
├── graph-api.js # Microsoft Graph API helper
|
|
327
|
+
├── odata-helpers.js # OData query building
|
|
328
|
+
└── mock-data.js # Test mode data
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
## Troubleshooting
|
|
332
|
+
|
|
333
|
+
### Token Refresh Issues
|
|
334
|
+
|
|
335
|
+
If you see "Access is denied" errors:
|
|
336
|
+
1. Check that your token includes `Mail.ReadWrite` scope (required for marking emails as read)
|
|
337
|
+
2. Re-authenticate:
|
|
338
|
+
- **macOS/Linux**: `rm ~/.outlook-mcp/tokens.json && npx outlook-mcp auth`
|
|
339
|
+
- **Windows**: `del %USERPROFILE%\.outlook-mcp\tokens.json && npx outlook-mcp auth`
|
|
340
|
+
3. Verify scopes in your tokens file include all needed permissions:
|
|
341
|
+
- **macOS/Linux**: `~/.outlook-mcp/tokens.json`
|
|
342
|
+
- **Windows**: `%USERPROFILE%\.outlook-mcp\tokens.json`
|
|
343
|
+
|
|
344
|
+
### Authentication Errors
|
|
345
|
+
|
|
346
|
+
- **"MS_CLIENT_ID is not configured"**: Run `npx outlook-mcp config` or set environment variables
|
|
347
|
+
- **"Token file not found"**: Run `npx outlook-mcp auth` to authenticate
|
|
348
|
+
- **"Access is denied"**: Check Azure app permissions and re-authenticate
|
|
349
|
+
|
|
350
|
+
### Search Errors
|
|
351
|
+
|
|
352
|
+
- **"$orderBy is not supported with $search"**: This is fixed - the server now handles this correctly
|
|
353
|
+
- Search results are returned in relevance order (Microsoft Graph default) when using `$search`
|
|
354
|
+
|
|
355
|
+
## Publishing to npm
|
|
356
|
+
|
|
357
|
+
To publish this package to npm:
|
|
358
|
+
|
|
359
|
+
1. **Update package.json metadata** (optional but recommended):
|
|
360
|
+
- `name` field is already set to `@tgai96/outlook-mcp`
|
|
361
|
+
- `author` field is set to `tgai96`
|
|
362
|
+
- Optionally add `repository`, `bugs`, and `homepage` fields if you have a GitHub repo
|
|
363
|
+
|
|
364
|
+
2. **Login to npm**:
|
|
365
|
+
```bash
|
|
366
|
+
npm login
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
3. **Publish to npm**:
|
|
370
|
+
```bash
|
|
371
|
+
npm publish
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
For scoped packages like `@tgai96/outlook-mcp`, use:
|
|
375
|
+
```bash
|
|
376
|
+
npm publish --access public
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
4. **After publishing**, users can use it in their MCP client configuration:
|
|
380
|
+
```json
|
|
381
|
+
{
|
|
382
|
+
"mcpServers": {
|
|
383
|
+
"outlook": {
|
|
384
|
+
"command": "npx",
|
|
385
|
+
"args": [
|
|
386
|
+
"-y",
|
|
387
|
+
"@tgai96/outlook-mcp"
|
|
388
|
+
]
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
## License
|
|
395
|
+
|
|
396
|
+
MIT
|
package/auth/index.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication module for Outlook MCP server
|
|
3
|
+
*/
|
|
4
|
+
const TokenStorage = require('./token-storage');
|
|
5
|
+
const tokenManager = require('./token-manager');
|
|
6
|
+
const { authTools } = require('./tools');
|
|
7
|
+
const config = require('../config');
|
|
8
|
+
|
|
9
|
+
// Create a singleton TokenStorage instance for automatic token refresh
|
|
10
|
+
let tokenStorageInstance = null;
|
|
11
|
+
|
|
12
|
+
function getTokenStorage() {
|
|
13
|
+
if (!tokenStorageInstance) {
|
|
14
|
+
// Pass config from config.js to TokenStorage so it can read MS_CLIENT_ID from config file
|
|
15
|
+
// Also pass scopes to ensure token refresh includes all necessary permissions
|
|
16
|
+
tokenStorageInstance = new TokenStorage({
|
|
17
|
+
clientId: config.AUTH_CONFIG.clientId || process.env.MS_CLIENT_ID,
|
|
18
|
+
clientSecret: config.AUTH_CONFIG.clientSecret || process.env.MS_CLIENT_SECRET,
|
|
19
|
+
scopes: config.AUTH_CONFIG.scopes || (process.env.MS_SCOPES ? process.env.MS_SCOPES.split(' ') : ['offline_access', 'User.Read', 'Mail.Read', 'Mail.ReadWrite', 'Mail.Send'])
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
return tokenStorageInstance;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Ensures the user is authenticated and returns an access token
|
|
27
|
+
* Automatically refreshes the token if it's expired or near expiration
|
|
28
|
+
* @param {boolean} forceNew - Whether to force a new authentication
|
|
29
|
+
* @returns {Promise<string>} - Access token
|
|
30
|
+
* @throws {Error} - If authentication fails
|
|
31
|
+
*/
|
|
32
|
+
async function ensureAuthenticated(forceNew = false) {
|
|
33
|
+
if (forceNew) {
|
|
34
|
+
// Force re-authentication
|
|
35
|
+
throw new Error('Authentication required: interactive user authentication needed.');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Use TokenStorage for automatic token refresh
|
|
39
|
+
const tokenStorage = getTokenStorage();
|
|
40
|
+
|
|
41
|
+
// Check if client ID is configured
|
|
42
|
+
if (!tokenStorage.config.clientId) {
|
|
43
|
+
console.error('[ensureAuthenticated] MS_CLIENT_ID is not configured');
|
|
44
|
+
throw new Error('Authentication required: MS_CLIENT_ID not configured. Please set MS_CLIENT_ID in config.json or environment variables.');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
console.error('[ensureAuthenticated] Attempting to get valid access token...');
|
|
48
|
+
const accessToken = await tokenStorage.getValidAccessToken();
|
|
49
|
+
|
|
50
|
+
if (!accessToken) {
|
|
51
|
+
console.error('[ensureAuthenticated] Failed to get valid access token');
|
|
52
|
+
throw new Error('Authentication required: interactive user authentication needed.');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
console.error('[ensureAuthenticated] Successfully obtained access token');
|
|
56
|
+
return accessToken;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = {
|
|
60
|
+
tokenManager,
|
|
61
|
+
tokenStorage: getTokenStorage,
|
|
62
|
+
authTools,
|
|
63
|
+
ensureAuthenticated
|
|
64
|
+
};
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const querystring = require('querystring');
|
|
3
|
+
const https = require('https');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const crypto = require('crypto'); // Added for generating random string
|
|
6
|
+
const TokenStorage = require('./token-storage'); // Assuming TokenStorage is in the same directory
|
|
7
|
+
|
|
8
|
+
// HTML templates
|
|
9
|
+
function escapeHtml(unsafe) {
|
|
10
|
+
return unsafe
|
|
11
|
+
.replace(/&/g, "&")
|
|
12
|
+
.replace(/</g, "<")
|
|
13
|
+
.replace(/>/g, ">")
|
|
14
|
+
.replace(/"/g, """)
|
|
15
|
+
.replace(/'/g, "'");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const templates = {
|
|
19
|
+
authError: (error, errorDescription) => `
|
|
20
|
+
<html>
|
|
21
|
+
<body style="font-family: Arial, sans-serif; text-align: center; margin-top: 50px;">
|
|
22
|
+
<h1 style="color: #e74c3c;">❌ Authorization Failed</h1>
|
|
23
|
+
<p><strong>Error:</strong> ${escapeHtml(error)}</p>
|
|
24
|
+
${errorDescription ? `<p><strong>Description:</strong> ${escapeHtml(errorDescription)}</p>` : ''}
|
|
25
|
+
<p>You can close this window and try again.</p>
|
|
26
|
+
</body>
|
|
27
|
+
</html>`,
|
|
28
|
+
authSuccess: `
|
|
29
|
+
<html>
|
|
30
|
+
<body style="font-family: Arial, sans-serif; text-align: center; margin-top: 50px;">
|
|
31
|
+
<h1 style="color: #2ecc71;">✅ Authentication Successful</h1>
|
|
32
|
+
<p>You have successfully authenticated with Microsoft Graph API.</p>
|
|
33
|
+
<p>You can close this window.</p>
|
|
34
|
+
</body>
|
|
35
|
+
</html>`,
|
|
36
|
+
tokenExchangeError: (error) => `
|
|
37
|
+
<html>
|
|
38
|
+
<body style="font-family: Arial, sans-serif; text-align: center; margin-top: 50px;">
|
|
39
|
+
<h1 style="color: #e74c3c;">❌ Token Exchange Failed</h1>
|
|
40
|
+
<p>Failed to exchange authorization code for access token.</p>
|
|
41
|
+
<p><strong>Error:</strong> ${escapeHtml(error instanceof Error ? error.message : String(error))}</p>
|
|
42
|
+
<p>You can close this window and try again.</p>
|
|
43
|
+
</body>
|
|
44
|
+
</html>`,
|
|
45
|
+
tokenStatus: (status) => `
|
|
46
|
+
<html>
|
|
47
|
+
<body style="font-family: Arial, sans-serif; text-align: center; margin-top: 50px;">
|
|
48
|
+
<h1>🔐 Token Status</h1>
|
|
49
|
+
<p>${escapeHtml(status)}</p>
|
|
50
|
+
</body>
|
|
51
|
+
</html>`
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
function createAuthConfig(envPrefix = 'MS_') {
|
|
55
|
+
return {
|
|
56
|
+
clientId: process.env[`${envPrefix}CLIENT_ID`] || '',
|
|
57
|
+
clientSecret: process.env[`${envPrefix}CLIENT_SECRET`] || '',
|
|
58
|
+
redirectUri: process.env[`${envPrefix}REDIRECT_URI`] || 'http://localhost:3333/auth/callback',
|
|
59
|
+
scopes: (process.env[`${envPrefix}SCOPES`] || 'offline_access User.Read Mail.Read').split(' '),
|
|
60
|
+
tokenEndpoint: process.env[`${envPrefix}TOKEN_ENDPOINT`] || 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
|
|
61
|
+
authEndpoint: process.env[`${envPrefix}AUTH_ENDPOINT`] || 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize'
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function setupOAuthRoutes(app, tokenStorage, authConfig, envPrefix = 'MS_') {
|
|
66
|
+
if (!authConfig) {
|
|
67
|
+
authConfig = createAuthConfig(envPrefix);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!(tokenStorage instanceof TokenStorage)) {
|
|
71
|
+
console.error("Error: tokenStorage is not an instance of TokenStorage. OAuth routes will not function correctly.");
|
|
72
|
+
// Optionally, you could throw an error here or disable the routes
|
|
73
|
+
// throw new Error("Invalid tokenStorage provided to setupOAuthRoutes");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
app.get('/auth', (req, res) => {
|
|
78
|
+
if (!authConfig.clientId) {
|
|
79
|
+
return res.status(500).send(templates.authError('Configuration Error', 'Client ID is not configured.'));
|
|
80
|
+
}
|
|
81
|
+
const state = crypto.randomBytes(16).toString('hex'); // Generate a random 16-byte string
|
|
82
|
+
// Store state in session or similar mechanism if available.
|
|
83
|
+
// For a server without sessions, this state would need to be passed through and verified differently,
|
|
84
|
+
// or a temporary server-side storage (like a short-lived cache) would be needed.
|
|
85
|
+
// For this example, we'll assume session middleware is configured elsewhere if this were a full app.
|
|
86
|
+
// If using express-session: req.session.oauthState = state;
|
|
87
|
+
// Since this is a module, actual session handling is outside its direct scope,
|
|
88
|
+
// but it's crucial for the consuming application to handle state verification.
|
|
89
|
+
|
|
90
|
+
const authorizationUrl = `${authConfig.authEndpoint}?` +
|
|
91
|
+
querystring.stringify({
|
|
92
|
+
client_id: authConfig.clientId,
|
|
93
|
+
response_type: 'code',
|
|
94
|
+
redirect_uri: authConfig.redirectUri,
|
|
95
|
+
scope: authConfig.scopes.join(' '),
|
|
96
|
+
response_mode: 'query',
|
|
97
|
+
state: state
|
|
98
|
+
});
|
|
99
|
+
res.redirect(authorizationUrl);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
app.get('/auth/callback', async (req, res) => {
|
|
103
|
+
const { code, error, error_description, state } = req.query;
|
|
104
|
+
|
|
105
|
+
// IMPORTANT: State validation is crucial for CSRF protection.
|
|
106
|
+
// The application using this module MUST implement a way to store the 'state' generated in /auth
|
|
107
|
+
// (e.g., in a user session if using express-session, or a short-lived cache)
|
|
108
|
+
// and then verify it here against the 'state' received from the OAuth provider.
|
|
109
|
+
// For example, if using express-session:
|
|
110
|
+
// const savedState = req.session.oauthState;
|
|
111
|
+
// if (!state || state !== savedState) {
|
|
112
|
+
// console.error("OAuth callback state mismatch. Potential CSRF attack.");
|
|
113
|
+
// return res.status(400).send(templates.authError('Invalid State', 'CSRF token mismatch. Please try authenticating again.'));
|
|
114
|
+
// }
|
|
115
|
+
// delete req.session.oauthState; // Clean up session state
|
|
116
|
+
|
|
117
|
+
// Since this module itself doesn't manage sessions, we'll log a warning if state is missing,
|
|
118
|
+
// but actual enforcement must be done by the consuming application.
|
|
119
|
+
// The Gemini review recommended uncommenting the rejection.
|
|
120
|
+
// However, the consuming app (CLI or server) is responsible for session/state storage.
|
|
121
|
+
// This module *cannot* validate state if it wasn't involved in storing it.
|
|
122
|
+
// The PR author (ranxian) needs to implement state storage & validation in the calling server (sse-server.js or outlook-auth-server.js).
|
|
123
|
+
// For now, enforcing a missing state here would break flows where state *is* passed but not validated by *this specific module*.
|
|
124
|
+
// The best this module can do is check for presence and rely on the consumer to validate the actual value.
|
|
125
|
+
// The original PR #10's outlook-auth-server.js used Date.now() and didn't store/validate it beyond this.
|
|
126
|
+
// The new sse-server.js also doesn't show session management for state.
|
|
127
|
+
// So, we will make the check for presence mandatory as per Gemini's suggestion.
|
|
128
|
+
if (!state) {
|
|
129
|
+
console.error("OAuth callback received without a 'state' parameter. Rejecting request to prevent potential CSRF attack.");
|
|
130
|
+
return res.status(400).send(templates.authError('Missing State Parameter', 'The state parameter was missing from the OAuth callback. This is a security risk. Please try authenticating again.'));
|
|
131
|
+
}
|
|
132
|
+
// Further validation of the state's VALUE (e.g., req.session.oauthState === state) is the responsibility
|
|
133
|
+
// of the application integrating this module, as session management is outside this module's scope.
|
|
134
|
+
// if (req.session && req.session.oauthState !== state) {
|
|
135
|
+
// return res.status(400).send(templates.authError('Invalid State Parameter', 'CSRF detected. State mismatch.'));
|
|
136
|
+
// }
|
|
137
|
+
// if (req.session) delete req.session.oauthState;
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
if (error) {
|
|
141
|
+
return res.status(400).send(templates.authError(error, error_description));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!code) {
|
|
145
|
+
return res.status(400).send(templates.authError('Missing Authorization Code', 'No authorization code was provided in the callback.'));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
await tokenStorage.exchangeCodeForTokens(code);
|
|
150
|
+
res.send(templates.authSuccess);
|
|
151
|
+
} catch (exchangeError) {
|
|
152
|
+
console.error('Token exchange error:', exchangeError);
|
|
153
|
+
res.status(500).send(templates.tokenExchangeError(exchangeError));
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
app.get('/token-status', async (req, res) => {
|
|
158
|
+
try {
|
|
159
|
+
const token = await tokenStorage.getValidAccessToken();
|
|
160
|
+
if (token) {
|
|
161
|
+
const expiryDate = new Date(tokenStorage.getExpiryTime());
|
|
162
|
+
res.send(templates.tokenStatus(`Access token is valid. Expires at: ${expiryDate.toLocaleString()}`));
|
|
163
|
+
} else {
|
|
164
|
+
res.send(templates.tokenStatus('No valid access token found. Please authenticate.'));
|
|
165
|
+
}
|
|
166
|
+
} catch (err) {
|
|
167
|
+
res.status(500).send(templates.tokenStatus(`Error checking token status: ${err.message}`));
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module.exports = {
|
|
173
|
+
setupOAuthRoutes,
|
|
174
|
+
createAuthConfig,
|
|
175
|
+
// Exporting templates for potential direct use or testing, though not typical
|
|
176
|
+
// templates
|
|
177
|
+
};
|
|
178
|
+
// Adding a newline at the end of the file as requested by Gemini Code Assist
|