-
Notifications
You must be signed in to change notification settings - Fork 107
Teams app #801
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: Development
Are you sure you want to change the base?
Teams app #801
Changes from all commits
3189555
3c53d89
ed97273
eb9a278
b6cda00
eaa483b
837ec8f
625d733
fac697c
88970c9
278d62d
78623bc
5373ed3
96f4819
48d4838
b193796
cc393a0
60e54f5
5fbdd5f
2314faf
91b4634
9ddc46f
d7de4f6
e41f43d
22fe01f
6514923
917e233
2103d21
f3f2cc4
edde8f7
1b4627f
09d1a9f
bd157fb
d67c90a
975a0b0
c6118ed
7058779
0573332
cb4ca24
d883b9b
21c1228
368ed3e
620bcb4
6701282
d3ea472
0f5eae7
aee7377
738132a
3997c58
6574083
03e3cad
6919265
bf3c8e7
70b2564
4a3f9c8
bf420bf
1f4ace4
828cf44
f4b07c1
022ca6a
efe6d4f
1bdd8be
e7afee2
5138e9d
ccd18aa
568e961
5304cc0
f279b74
f0bdca3
e03c011
4e6b7a3
3b2d92a
2590647
3ec2f74
181c2ed
0da79f3
9ae35fc
ab6fdc3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -98,29 +98,6 @@ | |
|
|
||
| SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key-change-in-production') | ||
|
|
||
| # Security Headers Configuration | ||
| SECURITY_HEADERS = { | ||
| 'X-Content-Type-Options': 'nosniff', | ||
| 'X-Frame-Options': 'DENY', | ||
| 'X-XSS-Protection': '1; mode=block', | ||
| 'Referrer-Policy': 'strict-origin-when-cross-origin', | ||
| 'Content-Security-Policy': ( | ||
| "default-src 'self'; " | ||
| "script-src 'self' 'unsafe-inline' 'unsafe-eval'; " | ||
| #"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net https://code.jquery.com https://stackpath.bootstrapcdn.com; " | ||
| "style-src 'self' 'unsafe-inline'; " | ||
| #"style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://stackpath.bootstrapcdn.com; " | ||
| "img-src 'self' data: https: blob:; " | ||
| "font-src 'self'; " | ||
| #"font-src 'self' https://cdn.jsdelivr.net https://stackpath.bootstrapcdn.com; " | ||
| "connect-src 'self' https: wss: ws:; " | ||
| "media-src 'self' blob:; " | ||
| "object-src 'none'; " | ||
| "frame-ancestors 'self'; " | ||
| "base-uri 'self';" | ||
| ) | ||
| } | ||
|
|
||
| # Security Configuration | ||
| ENABLE_STRICT_TRANSPORT_SECURITY = os.getenv('ENABLE_HSTS', 'false').lower() == 'true' | ||
| HSTS_MAX_AGE = int(os.getenv('HSTS_MAX_AGE', '31536000')) # 1 year default | ||
|
|
@@ -210,6 +187,7 @@ def get_allowed_extensions(enable_video=False, enable_audio=False): | |
| MICROSOFT_PROVIDER_AUTHENTICATION_SECRET = os.getenv("MICROSOFT_PROVIDER_AUTHENTICATION_SECRET") | ||
| LOGIN_REDIRECT_URL = os.getenv("LOGIN_REDIRECT_URL") | ||
| HOME_REDIRECT_URL = os.getenv("HOME_REDIRECT_URL") # Front Door URL for home page | ||
|
|
||
| AZURE_ENVIRONMENT = os.getenv("AZURE_ENVIRONMENT", "public") # public, usgovernment, custom | ||
|
|
||
| WORD_CHUNK_SIZE = 400 | ||
|
|
@@ -232,6 +210,12 @@ def get_allowed_extensions(enable_video=False, enable_audio=False): | |
| AUTHORITY = f"https://login.microsoftonline.com/{TENANT_ID}" | ||
| authority = AzureAuthorityHosts.AZURE_PUBLIC_CLOUD | ||
|
|
||
| # Teams SSO Configuration | ||
| ENABLE_TEAMS_SSO = os.getenv("ENABLE_TEAMS_SSO", "false").lower() == "true" | ||
| TEAMS_FRAME_ANCESTORS = os.getenv("TEAMS_FRAME_ANCESTORS", "") # e.g. "https://teams.microsoft.com https://*.teams.microsoft.com" - should be set to Teams domains if in airgap, otherwise can be left blank to allow from any domain since we validate the origin in the frontend against allowed Teams domains | ||
| CUSTOM_TEAMS_ORIGINS_RAW = os.getenv("CUSTOM_TEAMS_ORIGINS", "") # JSON array of valid domains for Teams SSO if in airgap, otherwise this is pulled from Teams, e.g. ["https://teams.microsoft.com", "https://*.teams.microsoft.com"] | ||
| CUSTOM_TEAMS_ORIGINS = json.loads(CUSTOM_TEAMS_ORIGINS_RAW) if CUSTOM_TEAMS_ORIGINS_RAW else [] | ||
clarked-msft marked this conversation as resolved.
Show resolved
Hide resolved
clarked-msft marked this conversation as resolved.
Show resolved
Hide resolved
clarked-msft marked this conversation as resolved.
Show resolved
Hide resolved
clarked-msft marked this conversation as resolved.
Show resolved
Hide resolved
clarked-msft marked this conversation as resolved.
Show resolved
Hide resolved
clarked-msft marked this conversation as resolved.
Show resolved
Hide resolved
clarked-msft marked this conversation as resolved.
Show resolved
Hide resolved
clarked-msft marked this conversation as resolved.
Show resolved
Hide resolved
clarked-msft marked this conversation as resolved.
Show resolved
Hide resolved
clarked-msft marked this conversation as resolved.
Show resolved
Hide resolved
Comment on lines
+214
to
+217
|
||
|
|
||
| if AZURE_ENVIRONMENT == "custom": | ||
| OIDC_METADATA_URL = CUSTOM_OIDC_METADATA_URL_VALUE or f"https://login.microsoftonline.com/{TENANT_ID}/v2.0/.well-known/openid-configuration" | ||
| resource_manager = CUSTOM_RESOURCE_MANAGER_URL_VALUE | ||
|
|
@@ -248,13 +232,44 @@ def get_allowed_extensions(enable_video=False, enable_audio=False): | |
| video_indexer_endpoint = "https://api.videoindexer.ai.azure.us" | ||
| search_resource_manager = "https://search.azure.us" | ||
| KEY_VAULT_DOMAIN = ".vault.usgovcloudapi.net" | ||
| if ENABLE_TEAMS_SSO and not TEAMS_FRAME_ANCESTORS: | ||
| # In US Government, we need to restrict the frame ancestors to the specific Teams domains to allow the SSO flow to work, since we can't rely on the frontend to validate the origin against the public Teams domains. | ||
| TEAMS_FRAME_ANCESTORS = "https://teams.microsoft.us https://*.teams.microsoft.us" | ||
clarked-msft marked this conversation as resolved.
Show resolved
Hide resolved
clarked-msft marked this conversation as resolved.
Show resolved
Hide resolved
clarked-msft marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| else: | ||
| OIDC_METADATA_URL = f"https://login.microsoftonline.com/{TENANT_ID}/v2.0/.well-known/openid-configuration" | ||
| resource_manager = "https://management.azure.com" | ||
| credential_scopes=[resource_manager + "/.default"] | ||
| cognitive_services_scope = "https://cognitiveservices.azure.com/.default" | ||
| video_indexer_endpoint = "https://api.videoindexer.ai" | ||
| KEY_VAULT_DOMAIN = ".vault.azure.net" | ||
| if ENABLE_TEAMS_SSO and not TEAMS_FRAME_ANCESTORS: | ||
| # In public cloud, we can allow from any domain since we validate the origin in the frontend against allowed Teams domains. | ||
| TEAMS_FRAME_ANCESTORS = "https://teams.microsoft.com https://*.teams.microsoft.com" | ||
clarked-msft marked this conversation as resolved.
Show resolved
Hide resolved
clarked-msft marked this conversation as resolved.
Show resolved
Hide resolved
clarked-msft marked this conversation as resolved.
Show resolved
Hide resolved
clarked-msft marked this conversation as resolved.
Show resolved
Hide resolved
clarked-msft marked this conversation as resolved.
Show resolved
Hide resolved
clarked-msft marked this conversation as resolved.
Show resolved
Hide resolved
clarked-msft marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| # Security Headers Configuration | ||
| SECURITY_HEADERS = { | ||
| 'X-Content-Type-Options': 'nosniff', | ||
| 'X-XSS-Protection': '1; mode=block', | ||
clarked-msft marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 'Referrer-Policy': 'strict-origin-when-cross-origin', | ||
| 'Content-Security-Policy': ( | ||
| "default-src 'self'; " | ||
| "script-src 'self' 'unsafe-inline' 'unsafe-eval'; " | ||
| #"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net https://code.jquery.com https://stackpath.bootstrapcdn.com; " | ||
| "style-src 'self' 'unsafe-inline'; " | ||
| #"style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://stackpath.bootstrapcdn.com; " | ||
| "img-src 'self' data: https: blob:; " | ||
| "font-src 'self'; " | ||
| #"font-src 'self' https://cdn.jsdelivr.net https://stackpath.bootstrapcdn.com; " | ||
| "connect-src 'self' https: wss: ws:; " | ||
| "media-src 'self' blob:; " | ||
| "object-src 'none'; " | ||
| f"frame-ancestors 'self' {TEAMS_FRAME_ANCESTORS}; " | ||
clarked-msft marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| "base-uri 'self';" | ||
clarked-msft marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ) | ||
| } | ||
|
|
||
| if not ENABLE_TEAMS_SSO: | ||
| SECURITY_HEADERS['X-Frame-Options'] = 'DENY' # Prevent framing if Teams SSO is not enabled | ||
|
Comment on lines
+249
to
+272
|
||
|
|
||
| def get_redis_cache_infrastructure_endpoint(redis_hostname: str) -> str: | ||
| """ | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,7 +1,7 @@ | ||||||||||||||||||||||||
| # route_frontend_authentication.py | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| from unittest import result | ||||||||||||||||||||||||
| from config import * | ||||||||||||||||||||||||
| from functions_appinsights import log_event | ||||||||||||||||||||||||
| from functions_authentication import _build_msal_app, _load_cache, _save_cache, clear_requested_oauth_scopes, get_requested_oauth_scopes | ||||||||||||||||||||||||
| from functions_debug import debug_print | ||||||||||||||||||||||||
| from swagger_wrapper import swagger_route, get_auth_security | ||||||||||||||||||||||||
|
|
@@ -32,6 +32,23 @@ def register_route_frontend_authentication(app): | |||||||||||||||||||||||
| @app.route('/login') | ||||||||||||||||||||||||
| @swagger_route(security=get_auth_security()) | ||||||||||||||||||||||||
| def login(): | ||||||||||||||||||||||||
| # Check if this is a Teams context (via query parameter) | ||||||||||||||||||||||||
| # teams=true: Attempt Teams SSO detection | ||||||||||||||||||||||||
| # teams=false: Skip Teams SSO, use standard Azure AD flow | ||||||||||||||||||||||||
| # No parameter: Default to Teams SSO detection (for backward compatibility) | ||||||||||||||||||||||||
| teams_param = request.args.get('teams', 'true') | ||||||||||||||||||||||||
| is_teams = teams_param == 'true' | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if is_teams and ENABLE_TEAMS_SSO: | ||||||||||||||||||||||||
| # Render a page that will detect Teams and handle SSO | ||||||||||||||||||||||||
| from functions_settings import get_settings, sanitize_settings_for_user | ||||||||||||||||||||||||
| settings = get_settings() | ||||||||||||||||||||||||
| return render_template('login.html', | ||||||||||||||||||||||||
| client_id=CLIENT_ID, | ||||||||||||||||||||||||
| enable_teams_sso=is_teams, | ||||||||||||||||||||||||
| custom_teams_origins=CUSTOM_TEAMS_ORIGINS, | ||||||||||||||||||||||||
clarked-msft marked this conversation as resolved.
Show resolved
Hide resolved
clarked-msft marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||
| app_settings=sanitize_settings_for_user(settings)) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
clarked-msft marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||
| # Clear potentially stale cache/user info before starting new login | ||||||||||||||||||||||||
| session.pop("user", None) | ||||||||||||||||||||||||
| session.pop("token_cache", None) | ||||||||||||||||||||||||
|
|
@@ -212,6 +229,77 @@ def authorized_api(): | |||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| return jsonify(result, 200) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| @app.route('/auth/teams/token-exchange', methods=['POST']) | ||||||||||||||||||||||||
| @swagger_route(security=get_auth_security()) | ||||||||||||||||||||||||
| def teams_token_exchange(): | ||||||||||||||||||||||||
clarked-msft marked this conversation as resolved.
Show resolved
Hide resolved
clarked-msft marked this conversation as resolved.
Show resolved
Hide resolved
clarked-msft marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||
| Exchange a Teams SSO token for an access token using On-Behalf-Of (OBO) flow. | ||||||||||||||||||||||||
| This endpoint receives the Teams SSO token from the frontend and exchanges it | ||||||||||||||||||||||||
| for tokens that can access Microsoft Graph and other APIs. | ||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||
clarked-msft marked this conversation as resolved.
Show resolved
Hide resolved
clarked-msft marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||
| # Feature gate: only allow token exchange when Teams SSO is enabled | ||||||||||||||||||||||||
| if not ENABLE_TEAMS_SSO: | ||||||||||||||||||||||||
| return jsonify({"error": "teams_sso_disabled"}), 404 | ||||||||||||||||||||||||
clarked-msft marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| data = request.get_json() | ||||||||||||||||||||||||
clarked-msft marked this conversation as resolved.
Show resolved
Hide resolved
clarked-msft marked this conversation as resolved.
Show resolved
Hide resolved
clarked-msft marked this conversation as resolved.
Show resolved
Hide resolved
clarked-msft marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||
| teams_token = data.get('token') or {} | ||||||||||||||||||||||||
clarked-msft marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
clarked-msft marked this conversation as resolved.
Show resolved
Hide resolved
clarked-msft marked this conversation as resolved.
Show resolved
Hide resolved
Comment on lines
+245
to
+247
|
||||||||||||||||||||||||
| data = request.get_json() | |
| teams_token = data.get('token') or {} | |
| data = request.get_json(silent=True) | |
| if not isinstance(data, dict): | |
| return jsonify({ | |
| "error": "invalid_request", | |
| "error_description": "Request body must be a valid JSON object." | |
| }), 400 | |
| teams_token = data.get("token") |
clarked-msft marked this conversation as resolved.
Show resolved
Hide resolved
clarked-msft marked this conversation as resolved.
Show resolved
Hide resolved
clarked-msft marked this conversation as resolved.
Show resolved
Hide resolved
clarked-msft marked this conversation as resolved.
Show resolved
Hide resolved
clarked-msft marked this conversation as resolved.
Show resolved
Hide resolved
clarked-msft marked this conversation as resolved.
Show resolved
Hide resolved
clarked-msft marked this conversation as resolved.
Show resolved
Hide resolved
clarked-msft marked this conversation as resolved.
Show resolved
Hide resolved
Copilot
AI
Apr 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
log_event(..., exceptionTraceback=True) is used here on a non-exception error path and with the default INFO level, which can emit misleading NoneType: None tracebacks. Log this as an error-level event (and include structured properties like MSAL error code/description) without forcing an exception traceback unless you’re inside an actual exception handler.
clarked-msft marked this conversation as resolved.
Show resolved
Hide resolved
clarked-msft marked this conversation as resolved.
Show resolved
Hide resolved
clarked-msft marked this conversation as resolved.
Show resolved
Hide resolved
clarked-msft marked this conversation as resolved.
Show resolved
Hide resolved
Copilot
AI
Apr 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This endpoint assumes result.get("id_token_claims") exists and is a dict, but config.SCOPE is Graph-only (no openid/profile/email), so the OBO result may not include ID token claims. If session["user"] becomes None, session['user'].get(...) will throw and the login will fail. Populate session user data from a reliable source (e.g., decode the incoming Teams JWT assertion claims, or call Graph /me with the acquired access token) and guard against missing claims.
clarked-msft marked this conversation as resolved.
Show resolved
Hide resolved
clarked-msft marked this conversation as resolved.
Show resolved
Hide resolved
clarked-msft marked this conversation as resolved.
Show resolved
Hide resolved
clarked-msft marked this conversation as resolved.
Show resolved
Hide resolved
|
Large diffs are not rendered by default.
Uh oh!
There was an error while loading. Please reload this page.