0
# Authentication and OAuth
1
2
OAuth token management, authentication flows, and signin processes including token requests, responses, and exchange mechanisms for secure bot authentication in Bot Framework applications.
3
4
## Core Authentication Models
5
6
### Token Response
7
8
Response model containing OAuth tokens returned from authentication providers.
9
10
```python { .api }
11
class TokenResponse(Model):
12
def __init__(self, *, connection_name: str = None, token: str = None,
13
expiration: str = None, channel_id: str = None, **kwargs): ...
14
```
15
16
```python
17
from botbuilder.schema import TokenResponse
18
19
# Token response from OAuth provider
20
token_response = TokenResponse(
21
connection_name="MyOAuthConnection",
22
token="eyJhbGciOiJSUzI1NiIsImtpZCI6...",
23
expiration="2023-12-31T23:59:59Z",
24
channel_id="webchat"
25
)
26
```
27
28
### Token Request
29
30
Request model for initiating OAuth token requests.
31
32
```python { .api }
33
class TokenRequest(Model):
34
def __init__(self, *, provider: str = None, settings: dict = None, **kwargs): ...
35
```
36
37
```python
38
from botbuilder.schema import TokenRequest
39
40
token_request = TokenRequest(
41
provider="AzureAD",
42
settings={
43
"scopes": ["User.Read", "Mail.Read"],
44
"tenant": "common"
45
}
46
)
47
```
48
49
## Token Exchange Models
50
51
### Token Exchange Invoke Request
52
53
Model for token exchange operations in Bot Framework applications.
54
55
```python { .api }
56
class TokenExchangeInvokeRequest(Model):
57
def __init__(self, *, id: str = None, connection_name: str = None,
58
token: str = None, properties: dict = None, **kwargs): ...
59
```
60
61
```python
62
from botbuilder.schema import TokenExchangeInvokeRequest
63
64
exchange_request = TokenExchangeInvokeRequest(
65
id="unique-exchange-id",
66
connection_name="MyOAuthConnection",
67
token="original-token-to-exchange",
68
properties={"scope": "additional-permissions"}
69
)
70
```
71
72
### Token Exchange Invoke Response
73
74
Response model for token exchange operations.
75
76
```python { .api }
77
class TokenExchangeInvokeResponse(Model):
78
def __init__(self, *, id: str = None, connection_name: str = None,
79
failure_detail: str = None, **kwargs): ...
80
```
81
82
```python
83
from botbuilder.schema import TokenExchangeInvokeResponse
84
85
# Successful exchange response
86
success_response = TokenExchangeInvokeResponse(
87
id="unique-exchange-id",
88
connection_name="MyOAuthConnection"
89
)
90
91
# Failed exchange response
92
failure_response = TokenExchangeInvokeResponse(
93
id="unique-exchange-id",
94
connection_name="MyOAuthConnection",
95
failure_detail="Token expired or invalid"
96
)
97
```
98
99
### Token Exchange State
100
101
State information for token exchange flows.
102
103
```python { .api }
104
class TokenExchangeState(Model):
105
def __init__(self, *, connection_name: str = None, conversation = None,
106
relates_to = None, bot_url: str = None, **kwargs): ...
107
```
108
109
```python
110
from botbuilder.schema import TokenExchangeState, ConversationReference
111
112
exchange_state = TokenExchangeState(
113
connection_name="MyOAuthConnection",
114
conversation=conversation_reference,
115
bot_url="https://mybot.azurewebsites.net"
116
)
117
```
118
119
## Authentication Cards
120
121
### OAuth Card
122
123
Card specifically designed for OAuth authentication flows.
124
125
```python { .api }
126
class OAuthCard(Model):
127
def __init__(self, *, text: str = None, connection_name: str = None,
128
buttons: List[CardAction] = None, **kwargs): ...
129
```
130
131
```python
132
from botbuilder.schema import OAuthCard, CardAction
133
134
oauth_card = OAuthCard(
135
text="Please sign in to access your account",
136
connection_name="MyOAuthConnection",
137
buttons=[
138
CardAction(
139
type="signin",
140
title="Sign In",
141
value="https://oauth.provider.com/signin"
142
)
143
]
144
)
145
```
146
147
### Signin Card
148
149
Generic signin card for authentication prompts.
150
151
```python { .api }
152
class SigninCard(Model):
153
def __init__(self, *, text: str = None, buttons: List[CardAction] = None, **kwargs): ...
154
```
155
156
```python
157
from botbuilder.schema import SigninCard, CardAction
158
159
signin_card = SigninCard(
160
text="Authentication required to continue",
161
buttons=[
162
CardAction(type="signin", title="Sign In", value="signin_url"),
163
CardAction(type="imBack", title="Cancel", value="cancel")
164
]
165
)
166
```
167
168
## Authentication Constants
169
170
### Signin Constants
171
172
Constants for signin operations and event names.
173
174
```python { .api }
175
class SignInConstants:
176
verify_state_operation_name: str = "signin/verifyState"
177
token_exchange_operation_name: str = "signin/tokenExchange"
178
token_response_event_name: str = "tokens/response"
179
```
180
181
```python
182
from botbuilder.schema import SignInConstants
183
184
# Use constants for consistent event handling
185
if activity.name == SignInConstants.token_response_event_name:
186
# Handle token response
187
pass
188
elif activity.name == SignInConstants.verify_state_operation_name:
189
# Handle state verification
190
pass
191
```
192
193
### Caller ID Constants
194
195
Constants for identifying Bot Framework channels and authentication contexts.
196
197
```python { .api }
198
class CallerIdConstants:
199
public_azure_channel: str = "urn:botframework:azure"
200
us_gov_channel: str = "urn:botframework:azureusgov"
201
bot_to_bot_prefix: str = "urn:botframework:aadappid:"
202
```
203
204
```python
205
from botbuilder.schema import CallerIdConstants
206
207
def is_azure_channel(caller_id: str) -> bool:
208
return caller_id == CallerIdConstants.public_azure_channel
209
210
def is_bot_to_bot(caller_id: str) -> bool:
211
return caller_id and caller_id.startswith(CallerIdConstants.bot_to_bot_prefix)
212
```
213
214
## Authentication Flow Patterns
215
216
### Basic OAuth Flow
217
218
```python
219
from botbuilder.schema import (
220
Activity, ActivityTypes, Attachment, OAuthCard,
221
CardAction, TokenResponse
222
)
223
224
async def start_oauth_flow(connection_name: str) -> Activity:
225
"""Initiate OAuth authentication flow"""
226
oauth_card = OAuthCard(
227
text="Please sign in to continue using the bot",
228
connection_name=connection_name,
229
buttons=[
230
CardAction(type="signin", title="Sign In")
231
]
232
)
233
234
attachment = Attachment(
235
content_type="application/vnd.microsoft.card.oauth",
236
content=oauth_card
237
)
238
239
return Activity(
240
type=ActivityTypes.message,
241
text="Authentication required",
242
attachments=[attachment]
243
)
244
245
async def handle_token_response(activity: Activity) -> str:
246
"""Handle token response from OAuth provider"""
247
if activity.name == "tokens/response":
248
token_response = TokenResponse(**activity.value)
249
return token_response.token
250
return None
251
```
252
253
### Token Exchange Flow
254
255
```python
256
from botbuilder.schema import (
257
TokenExchangeInvokeRequest, TokenExchangeInvokeResponse,
258
InvokeResponse
259
)
260
261
async def handle_token_exchange(request: TokenExchangeInvokeRequest) -> InvokeResponse:
262
"""Handle token exchange invoke"""
263
try:
264
# Attempt to exchange token
265
exchanged_token = await exchange_token(
266
request.token,
267
request.connection_name
268
)
269
270
response = TokenExchangeInvokeResponse(
271
id=request.id,
272
connection_name=request.connection_name
273
)
274
275
return InvokeResponse(
276
status=200,
277
body=response
278
)
279
280
except Exception as e:
281
error_response = TokenExchangeInvokeResponse(
282
id=request.id,
283
connection_name=request.connection_name,
284
failure_detail=str(e)
285
)
286
287
return InvokeResponse(
288
status=412, # Precondition Failed
289
body=error_response
290
)
291
```
292
293
### State Verification
294
295
```python
296
from botbuilder.schema import SignInConstants
297
298
async def verify_signin_state(activity: Activity) -> bool:
299
"""Verify signin state for security"""
300
if activity.name == SignInConstants.verify_state_operation_name:
301
state = activity.value.get('state')
302
# Verify state matches expected value
303
return await validate_state(state)
304
return False
305
```
306
307
## Integration with Bot Framework
308
309
### Authentication Middleware
310
311
```python
312
from botbuilder.schema import Activity, ActivityTypes
313
314
class AuthenticationMiddleware:
315
def __init__(self, connection_name: str):
316
self.connection_name = connection_name
317
318
async def on_message_activity(self, turn_context, next_handler):
319
"""Check authentication before processing messages"""
320
token = await self.get_user_token(turn_context)
321
322
if not token:
323
# Send OAuth card
324
oauth_activity = await self.create_oauth_prompt()
325
await turn_context.send_activity(oauth_activity)
326
return
327
328
# User is authenticated, continue processing
329
await next_handler()
330
331
async def get_user_token(self, turn_context) -> str:
332
"""Get user token from token store"""
333
# Implementation depends on token store
334
pass
335
336
async def create_oauth_prompt(self) -> Activity:
337
"""Create OAuth authentication prompt"""
338
return await start_oauth_flow(self.connection_name)
339
```
340
341
### Secure API Calls
342
343
```python
344
import aiohttp
345
from botbuilder.schema import TokenResponse
346
347
async def make_authenticated_api_call(token: str, api_url: str):
348
"""Make API call with OAuth token"""
349
headers = {
350
'Authorization': f'Bearer {token}',
351
'Content-Type': 'application/json'
352
}
353
354
async with aiohttp.ClientSession() as session:
355
async with session.get(api_url, headers=headers) as response:
356
if response.status == 401:
357
raise AuthenticationError("Token expired or invalid")
358
return await response.json()
359
360
class AuthenticationError(Exception):
361
"""Custom exception for authentication errors"""
362
pass
363
```
364
365
## Security Best Practices
366
367
### Token Management
368
369
1. **Store tokens securely** - Never log or persist tokens in plain text
370
2. **Validate token expiration** - Check expiration before using tokens
371
3. **Handle token refresh** - Implement refresh token flows where supported
372
4. **Scope validation** - Verify tokens have required scopes
373
374
```python
375
from datetime import datetime
376
from botbuilder.schema import TokenResponse
377
378
def is_token_valid(token_response: TokenResponse) -> bool:
379
"""Validate token expiration"""
380
if not token_response.expiration:
381
return True # No expiration set
382
383
expiration = datetime.fromisoformat(token_response.expiration.replace('Z', '+00:00'))
384
return datetime.now(expiration.tzinfo) < expiration
385
386
def validate_token_scopes(token: str, required_scopes: list) -> bool:
387
"""Validate token has required scopes"""
388
# Implementation depends on token format (JWT, etc.)
389
pass
390
```
391
392
### Connection Security
393
394
1. **Use HTTPS** - All OAuth flows must use HTTPS
395
2. **Validate state parameters** - Prevent CSRF attacks
396
3. **Secure redirect URIs** - Use exact match for redirect URIs
397
4. **Rate limiting** - Implement rate limiting for auth requests
398
399
```python
400
import hashlib
401
import secrets
402
403
def generate_state() -> str:
404
"""Generate secure state parameter"""
405
return secrets.token_urlsafe(32)
406
407
def validate_state(received_state: str, expected_state: str) -> bool:
408
"""Validate state parameter to prevent CSRF"""
409
return secrets.compare_digest(received_state, expected_state)
410
```
411
412
## Error Handling
413
414
```python
415
from botbuilder.schema import Activity, ActivityTypes
416
417
def create_auth_error_response(error_message: str) -> Activity:
418
"""Create user-friendly authentication error response"""
419
return Activity(
420
type=ActivityTypes.message,
421
text=f"Authentication failed: {error_message}. Please try signing in again."
422
)
423
424
def handle_oauth_errors(error_type: str) -> Activity:
425
"""Handle different OAuth error scenarios"""
426
error_messages = {
427
'access_denied': "Access was denied. Please try again or contact support.",
428
'invalid_request': "Invalid authentication request. Please try again.",
429
'server_error': "Authentication server error. Please try again later.",
430
'temporarily_unavailable': "Authentication service temporarily unavailable."
431
}
432
433
message = error_messages.get(error_type, "Unknown authentication error occurred.")
434
return create_auth_error_response(message)
435
```