lex-microsoft_teams 0.5.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.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +16 -0
- data/.gitignore +15 -0
- data/.rspec +3 -0
- data/.rubocop.yml +56 -0
- data/CHANGELOG.md +59 -0
- data/CLAUDE.md +206 -0
- data/Dockerfile +6 -0
- data/Gemfile +12 -0
- data/Gemfile.lock +103 -0
- data/LICENSE +21 -0
- data/README.md +183 -0
- data/docs/plans/2026-03-15-meetings-transcripts-design.md +506 -0
- data/docs/plans/2026-03-16-delegated-oauth-browser-flow-design.md +198 -0
- data/docs/plans/2026-03-16-delegated-oauth-browser-flow-plan.md +1176 -0
- data/lex-microsoft_teams.gemspec +32 -0
- data/lib/legion/extensions/microsoft_teams/actors/cache_bulk_ingest.rb +41 -0
- data/lib/legion/extensions/microsoft_teams/actors/cache_sync.rb +54 -0
- data/lib/legion/extensions/microsoft_teams/actors/direct_chat_poller.rb +105 -0
- data/lib/legion/extensions/microsoft_teams/actors/message_processor.rb +23 -0
- data/lib/legion/extensions/microsoft_teams/actors/observed_chat_poller.rb +111 -0
- data/lib/legion/extensions/microsoft_teams/client.rb +68 -0
- data/lib/legion/extensions/microsoft_teams/helpers/browser_auth.rb +139 -0
- data/lib/legion/extensions/microsoft_teams/helpers/callback_server.rb +82 -0
- data/lib/legion/extensions/microsoft_teams/helpers/client.rb +38 -0
- data/lib/legion/extensions/microsoft_teams/helpers/high_water_mark.rb +59 -0
- data/lib/legion/extensions/microsoft_teams/helpers/prompt_resolver.rb +64 -0
- data/lib/legion/extensions/microsoft_teams/helpers/session_manager.rb +150 -0
- data/lib/legion/extensions/microsoft_teams/helpers/subscription_registry.rb +140 -0
- data/lib/legion/extensions/microsoft_teams/helpers/token_cache.rb +209 -0
- data/lib/legion/extensions/microsoft_teams/local_cache/extractor.rb +258 -0
- data/lib/legion/extensions/microsoft_teams/local_cache/record_parser.rb +199 -0
- data/lib/legion/extensions/microsoft_teams/local_cache/sstable_reader.rb +121 -0
- data/lib/legion/extensions/microsoft_teams/runners/adaptive_cards.rb +59 -0
- data/lib/legion/extensions/microsoft_teams/runners/auth.rb +116 -0
- data/lib/legion/extensions/microsoft_teams/runners/bot.rb +409 -0
- data/lib/legion/extensions/microsoft_teams/runners/cache_ingest.rb +122 -0
- data/lib/legion/extensions/microsoft_teams/runners/channel_messages.rb +52 -0
- data/lib/legion/extensions/microsoft_teams/runners/channels.rb +53 -0
- data/lib/legion/extensions/microsoft_teams/runners/chats.rb +51 -0
- data/lib/legion/extensions/microsoft_teams/runners/local_cache.rb +62 -0
- data/lib/legion/extensions/microsoft_teams/runners/meetings.rb +68 -0
- data/lib/legion/extensions/microsoft_teams/runners/messages.rb +48 -0
- data/lib/legion/extensions/microsoft_teams/runners/presence.rb +31 -0
- data/lib/legion/extensions/microsoft_teams/runners/subscriptions.rb +76 -0
- data/lib/legion/extensions/microsoft_teams/runners/teams.rb +33 -0
- data/lib/legion/extensions/microsoft_teams/runners/transcripts.rb +45 -0
- data/lib/legion/extensions/microsoft_teams/transport/exchanges/messages.rb +15 -0
- data/lib/legion/extensions/microsoft_teams/transport/messages/teams_message.rb +16 -0
- data/lib/legion/extensions/microsoft_teams/transport/queues/messages_process.rb +16 -0
- data/lib/legion/extensions/microsoft_teams/version.rb +9 -0
- data/lib/legion/extensions/microsoft_teams.rb +44 -0
- metadata +139 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# Delegated OAuth Browser Flow Design
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Add browser-based delegated OAuth authentication to lex-microsoft_teams so users can authenticate with their own Microsoft account for Graph API access. Authorization Code + PKCE as the primary flow, Device Code as the automatic fallback for headless environments.
|
|
6
|
+
|
|
7
|
+
## Architecture
|
|
8
|
+
|
|
9
|
+
Opt-in delegated auth: user explicitly triggers via `legion auth teams` CLI command, a bot command (`auth`/`login`), or by enabling `microsoft_teams.auth.delegated.enabled` in settings. Once authenticated, tokens refresh silently. Browser re-opens only when the refresh_token is expired/revoked.
|
|
10
|
+
|
|
11
|
+
Two callback server paths: ephemeral TCPServer for CLI (works without daemon), Sinatra route on the existing API (port 4567) for daemon-initiated re-auth.
|
|
12
|
+
|
|
13
|
+
Tokens stored in HashiCorp Vault via legion-crypt. In-memory cache in TokenCache for fast access.
|
|
14
|
+
|
|
15
|
+
## Components
|
|
16
|
+
|
|
17
|
+
### New Files
|
|
18
|
+
|
|
19
|
+
| Component | File | Purpose |
|
|
20
|
+
|-----------|------|---------|
|
|
21
|
+
| `Helpers::BrowserAuth` | `helpers/browser_auth.rb` | Orchestrator: PKCE generation, headless detection, browser opening, flow selection (Auth Code vs Device Code) |
|
|
22
|
+
| `Helpers::CallbackServer` | `helpers/callback_server.rb` | Ephemeral TCPServer on random port, localhost only, receives `?code=&state=`, shuts down after |
|
|
23
|
+
|
|
24
|
+
### Modified Files
|
|
25
|
+
|
|
26
|
+
| Component | File | Changes |
|
|
27
|
+
|-----------|------|---------|
|
|
28
|
+
| `Runners::Auth` | `runners/auth.rb` | Add `authorize_url`, `exchange_code`, `refresh_delegated_token` methods |
|
|
29
|
+
| `Helpers::TokenCache` | `helpers/token_cache.rb` | Add delegated token slot, refresh_token support, Vault read/write |
|
|
30
|
+
|
|
31
|
+
### External Files (LegionIO main repo)
|
|
32
|
+
|
|
33
|
+
| Component | File | Purpose |
|
|
34
|
+
|-----------|------|---------|
|
|
35
|
+
| `API::Routes::OAuthCallback` | `api/routes/oauth_callback.rb` | `GET /api/oauth/microsoft_teams/callback` receives code, signals waiting thread |
|
|
36
|
+
| `CLI::Auth` | `cli/auth.rb` | `legion auth teams` command triggers BrowserAuth with ephemeral server |
|
|
37
|
+
|
|
38
|
+
## Auth Flow
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
User triggers auth:
|
|
42
|
+
CLI: `legion auth teams`
|
|
43
|
+
Settings: microsoft_teams.auth.delegated.enabled = true
|
|
44
|
+
Bot command: `auth` or `login`
|
|
45
|
+
|
|
46
|
+
┌──────────────────────────┐
|
|
47
|
+
│ Can we open a browser? │
|
|
48
|
+
└────────────┬─────────────┘
|
|
49
|
+
yes │ no (headless/SSH/TTY check)
|
|
50
|
+
┌───────────┴───────────┐
|
|
51
|
+
▼ ▼
|
|
52
|
+
Auth Code + PKCE Device Code
|
|
53
|
+
│ │
|
|
54
|
+
1. Generate PKCE pair 1. Request device_code
|
|
55
|
+
2. Start callback server 2. Display URL + code
|
|
56
|
+
3. Open browser 3. Auto-open browser to
|
|
57
|
+
4. User signs in devicelogin (if possible)
|
|
58
|
+
5. Callback receives 4. Poll for token
|
|
59
|
+
?code=...&state=...
|
|
60
|
+
6. Exchange code for
|
|
61
|
+
tokens
|
|
62
|
+
│ │
|
|
63
|
+
└───────────┬───────────┘
|
|
64
|
+
▼
|
|
65
|
+
Store tokens in Vault
|
|
66
|
+
(access_token, refresh_token,
|
|
67
|
+
expires_at, scopes)
|
|
68
|
+
│
|
|
69
|
+
▼
|
|
70
|
+
TokenCache holds in-memory copy
|
|
71
|
+
Silent refresh before expiry
|
|
72
|
+
Re-pop browser only if
|
|
73
|
+
refresh_token is revoked/expired
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Headless Detection
|
|
77
|
+
|
|
78
|
+
- macOS: always assume GUI available
|
|
79
|
+
- Linux: check `ENV['DISPLAY']` or `ENV['WAYLAND_DISPLAY']`
|
|
80
|
+
- Windows: always assume GUI available
|
|
81
|
+
- Fallback: if `system("open", url)` (or platform equivalent) fails, switch to device code
|
|
82
|
+
|
|
83
|
+
### Browser Opening
|
|
84
|
+
|
|
85
|
+
Platform detection via `RbConfig::CONFIG['host_os']`:
|
|
86
|
+
- macOS: `system("open", url)`
|
|
87
|
+
- Linux: `system("xdg-open", url)`
|
|
88
|
+
- Windows: `system("start", url)`
|
|
89
|
+
|
|
90
|
+
(Pattern from `references/ruby_llm-mcp/lib/ruby_llm/mcp/auth/browser/opener.rb`)
|
|
91
|
+
|
|
92
|
+
### PKCE
|
|
93
|
+
|
|
94
|
+
- `code_verifier`: 43-128 character random string (`SecureRandom.urlsafe_base64(32)`)
|
|
95
|
+
- `code_challenge`: `Base64.urlsafe_encode64(Digest::SHA256.digest(code_verifier), padding: false)`
|
|
96
|
+
- `code_challenge_method`: `S256`
|
|
97
|
+
|
|
98
|
+
### State Parameter
|
|
99
|
+
|
|
100
|
+
- `SecureRandom.hex(32)` — verified on callback to prevent CSRF
|
|
101
|
+
|
|
102
|
+
### Callback Server (Ephemeral)
|
|
103
|
+
|
|
104
|
+
- `TCPServer.new('127.0.0.1', 0)` — OS assigns random available port
|
|
105
|
+
- Reads HTTP request, parses query string for `code` and `state`
|
|
106
|
+
- Returns a simple HTML "You can close this window" response
|
|
107
|
+
- Shuts down immediately after receiving the callback
|
|
108
|
+
- Timeout: 120 seconds (configurable)
|
|
109
|
+
|
|
110
|
+
### Callback Server (Sinatra/Daemon)
|
|
111
|
+
|
|
112
|
+
- `GET /api/oauth/microsoft_teams/callback`
|
|
113
|
+
- Receives `code` and `state`, signals a waiting `ConditionVariable`
|
|
114
|
+
- Returns HTML redirect or "Authentication complete" page
|
|
115
|
+
|
|
116
|
+
## Token Lifecycle
|
|
117
|
+
|
|
118
|
+
### Initial Auth (User-Triggered)
|
|
119
|
+
|
|
120
|
+
1. User triggers auth
|
|
121
|
+
2. BrowserAuth generates PKCE pair
|
|
122
|
+
3. Checks browser availability → Auth Code or Device Code
|
|
123
|
+
4. Obtains access_token + refresh_token
|
|
124
|
+
5. Writes to Vault at `vault_path`
|
|
125
|
+
6. TokenCache loads into memory
|
|
126
|
+
|
|
127
|
+
### Silent Refresh (Automatic)
|
|
128
|
+
|
|
129
|
+
1. TokenCache checks `expires_at - refresh_buffer` on every `cached_delegated_token` call
|
|
130
|
+
2. If within buffer: `POST oauth2/v2.0/token` with `grant_type=refresh_token`
|
|
131
|
+
3. Microsoft returns new access_token + rotated refresh_token
|
|
132
|
+
4. Write both back to Vault, update in-memory cache
|
|
133
|
+
5. No user interaction
|
|
134
|
+
|
|
135
|
+
### Re-Auth (Refresh Token Expired/Revoked)
|
|
136
|
+
|
|
137
|
+
1. Refresh returns `invalid_grant`
|
|
138
|
+
2. TokenCache clears delegated slot
|
|
139
|
+
3. Daemon running: trigger BrowserAuth via Sinatra callback route
|
|
140
|
+
4. CLI: prompt user to run `legion auth teams`
|
|
141
|
+
5. `Legion::Events.emit('microsoft_teams.auth.expired')` for extensions to react
|
|
142
|
+
|
|
143
|
+
### Daemon Startup
|
|
144
|
+
|
|
145
|
+
1. If `delegated.enabled: true`, read token from Vault
|
|
146
|
+
2. Valid (not expired) → load into TokenCache
|
|
147
|
+
3. Expired but refresh_token present → attempt silent refresh
|
|
148
|
+
4. No token or refresh fails → emit event, log message
|
|
149
|
+
5. Never auto-pop browser on startup without prior explicit auth
|
|
150
|
+
|
|
151
|
+
## Settings
|
|
152
|
+
|
|
153
|
+
```yaml
|
|
154
|
+
microsoft_teams:
|
|
155
|
+
auth:
|
|
156
|
+
tenant_id: "..."
|
|
157
|
+
client_id: "..."
|
|
158
|
+
client_secret: "..." # client_credentials flow (existing)
|
|
159
|
+
delegated:
|
|
160
|
+
enabled: false # opt-in gate
|
|
161
|
+
scopes: "OnlineMeetings.Read OnlineMeetingTranscript.Read.All offline_access"
|
|
162
|
+
refresh_buffer: 300 # seconds before expiry to refresh
|
|
163
|
+
vault_path: "secret/legionio/microsoft_teams/delegated_token"
|
|
164
|
+
callback_timeout: 120 # seconds to wait for browser callback
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Vault Storage
|
|
168
|
+
|
|
169
|
+
Path: `secret/legionio/microsoft_teams/delegated_token`
|
|
170
|
+
|
|
171
|
+
```json
|
|
172
|
+
{
|
|
173
|
+
"access_token": "eyJ...",
|
|
174
|
+
"refresh_token": "0.AR...",
|
|
175
|
+
"expires_at": "2026-03-16T22:30:00Z",
|
|
176
|
+
"scopes": "OnlineMeetings.Read OnlineMeetingTranscript.Read.All offline_access"
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Entra App Registration Requirements
|
|
181
|
+
|
|
182
|
+
The existing LegionIO Entra app needs:
|
|
183
|
+
- `fallback_public_client_enabled = true` (already set)
|
|
184
|
+
- `public_client.redirect_uris` must include `http://localhost` (wildcard port)
|
|
185
|
+
- Or register specific redirect URIs: `http://localhost:4567/api/oauth/microsoft_teams/callback` for daemon, and `http://localhost` for ephemeral
|
|
186
|
+
- Delegated permissions for desired scopes (requires admin consent in managed tenants)
|
|
187
|
+
|
|
188
|
+
## Scope
|
|
189
|
+
|
|
190
|
+
Teams-specific only. Build in lex-microsoft_teams, extract to a shared gem later if other extensions need it.
|
|
191
|
+
|
|
192
|
+
## Dependencies
|
|
193
|
+
|
|
194
|
+
No new gem dependencies. Uses:
|
|
195
|
+
- `SecureRandom`, `Digest::SHA256`, `Base64` (stdlib)
|
|
196
|
+
- `socket` (stdlib, for TCPServer)
|
|
197
|
+
- `legion-crypt` (existing, for Vault access)
|
|
198
|
+
- `faraday` (existing)
|