Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
3189555
Initial custom support
clarked-msft Dec 17, 2025
3c53d89
Derive openid issuer url from az environment
clarked-msft Dec 17, 2025
ed97273
Update openai params
clarked-msft Dec 17, 2025
eb9a278
Custom envireonment support
clarked-msft Dec 17, 2025
b6cda00
Add default video indexer endpoint in custom env
clarked-msft Dec 19, 2025
eaa483b
Correct graph users path for custom environment user search
clarked-msft Dec 19, 2025
837ec8f
Merge remote-tracking branch 'upstream/Development' into bicep-custom…
clarked-msft Jan 5, 2026
625d733
Merged upstream
clarked-msft Jan 5, 2026
fac697c
Revert cosmos container name
clarked-msft Jan 5, 2026
88970c9
Revert cosmos bicep change
clarked-msft Jan 5, 2026
278d62d
Merge branch 'Development' into bicep-custom-support
clarked-msft Jan 22, 2026
78623bc
Merge remote-tracking branch 'upstream/Development' into bicep-custom…
clarked-msft Jan 26, 2026
5373ed3
rmv whitespace
Bionic711 Jan 26, 2026
96f4819
rmv whitespace
Bionic711 Jan 26, 2026
48d4838
add whitespace
Bionic711 Jan 26, 2026
b193796
Merge Development
clarked-msft Feb 16, 2026
cc393a0
Remove extra / from custom URLs
clarked-msft Feb 16, 2026
60e54f5
Remove extra / from metadata url
clarked-msft Feb 16, 2026
5fbdd5f
Teams app testing
clarked-msft Feb 16, 2026
2314faf
Add teams manifest
clarked-msft Feb 18, 2026
91b4634
Add teams how-to
clarked-msft Feb 25, 2026
9ddc46f
Add login template
clarked-msft Feb 25, 2026
d7de4f6
Teams app docs
clarked-msft Feb 25, 2026
e41f43d
Fix login loop
clarked-msft Feb 25, 2026
22fe01f
Add plain text view
clarked-msft Feb 27, 2026
6514923
Merge Development
clarked-msft Mar 12, 2026
917e233
Configurable teams frame ancestors and origins
clarked-msft Mar 12, 2026
2103d21
Strip trailing path from login url
clarked-msft Mar 12, 2026
f3f2cc4
Merge Development
clarked-msft Mar 17, 2026
edde8f7
Update outline image
clarked-msft Mar 17, 2026
1b4627f
Disable appservice easyauth if teams sso is enabled
clarked-msft Mar 17, 2026
09d1a9f
Add teams manifest template
clarked-msft Mar 17, 2026
bd157fb
Update teams doc
clarked-msft Mar 17, 2026
d67c90a
Update teams doc
clarked-msft Mar 17, 2026
975a0b0
Fix teams_app link
clarked-msft Mar 17, 2026
c6118ed
Remove ENABLE_TEAMS_SSO in favor of adding to content url
clarked-msft Mar 17, 2026
7058779
Revert global agent change
clarked-msft Mar 17, 2026
0573332
Add release notes
clarked-msft Mar 17, 2026
cb4ca24
Update teams frame env var config
clarked-msft Mar 17, 2026
d883b9b
Remove reference to ENABLE_TEAMS_SSO from release notes
clarked-msft Mar 17, 2026
21c1228
Remove build_consent_url, not used
clarked-msft Mar 17, 2026
368ed3e
Add missing semicolon
clarked-msft Mar 17, 2026
620bcb4
Use bootstrap d-none to hide
clarked-msft Mar 17, 2026
6701282
Use debug_printf
clarked-msft Mar 17, 2026
d3ea472
Fix typo
clarked-msft Mar 17, 2026
0f5eae7
Remove X-Frame-Options
clarked-msft Mar 17, 2026
aee7377
Improve custom teams origins parsing
clarked-msft Mar 17, 2026
738132a
Potential fix for pull request finding
clarked-msft Mar 17, 2026
3997c58
Add enableTeamsSso flag to trigger trying teams sso before normal login
clarked-msft Mar 18, 2026
6574083
Set COOKIE_SAMESITE to None only if teams sso enable
clarked-msft Mar 18, 2026
03e3cad
Update teams token exchange error handling
clarked-msft Mar 18, 2026
6919265
Deny frames if not teams sso
clarked-msft Mar 18, 2026
bf3c8e7
Leave route_frontend_authentication logging unchanged
clarked-msft Mar 18, 2026
70b2564
Potential fix for pull request finding
clarked-msft Mar 18, 2026
4a3f9c8
Remnove teams_app_id config
clarked-msft Mar 18, 2026
bf420bf
Correct typo
clarked-msft Mar 18, 2026
1f4ace4
Fix indentation
clarked-msft Mar 18, 2026
828cf44
Update manifest template and teams doc
clarked-msft Mar 18, 2026
f4b07c1
Use log_event for loging logs...
clarked-msft Mar 18, 2026
022ca6a
Fix typo
clarked-msft Mar 18, 2026
efe6d4f
Fix comment indentation
clarked-msft Mar 18, 2026
1bdd8be
CSRF mitigation
clarked-msft Mar 19, 2026
e7afee2
Potential fix for pull request finding
clarked-msft Mar 19, 2026
5138e9d
Add login to content url in manifest template
clarked-msft Mar 19, 2026
ccd18aa
Fix scheme comparison in csrf mitigation
clarked-msft Mar 19, 2026
568e961
Removing basic CSRF mitigation.
clarked-msft Mar 19, 2026
5304cc0
Only update session cookie settings if teams enabled
clarked-msft Mar 19, 2026
f279b74
Fix comment locations
clarked-msft Mar 19, 2026
f0bdca3
Add additional guidance on disabling app service authentication
clarked-msft Mar 19, 2026
e03c011
Apply suggestions from code review
clarked-msft Mar 19, 2026
4e6b7a3
Merge branch 'Development' into teams-app
clarked-msft Apr 1, 2026
3b2d92a
Merged Development
clarked-msft Apr 3, 2026
2590647
Clean up teams release notes
clarked-msft Apr 3, 2026
3ec2f74
Merge commit 'e03c011c2bd6e34fb66d2c67230d1830839e0783' into teams-test
clarked-msft Apr 6, 2026
181c2ed
Merge branch 'upstream/Development' into teams-test
clarked-msft Apr 10, 2026
0da79f3
Merge branch 'Development' into teams-test
clarked-msft Apr 10, 2026
9ae35fc
Merge Development
clarked-msft Apr 10, 2026
ab6fdc3
Merge branch 'teams-test' into teams-app
clarked-msft Apr 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ flask_session
tmp**cwd
/tmp_images
nul

# Teams manifest
application/teams_app/manifest.json
/.github/plans
*.xlsx
/artifacts/tests
Expand Down
9 changes: 8 additions & 1 deletion application/single_app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
sys.stderr = codecs.getwriter('utf-8')(sys.stderr.buffer, 'strict')

import app_settings_cache
from functions_appinsights import *
from config import *
from semantic_kernel import Kernel
from semantic_kernel_loader import initialize_semantic_kernel
Expand All @@ -31,7 +32,6 @@
from functions_documents import *
from functions_search import *
from functions_settings import *
from functions_appinsights import *
from functions_activity_logging import *

import threading
Expand Down Expand Up @@ -102,6 +102,13 @@
app.config['VERSION'] = VERSION
app.config['SECRET_KEY'] = SECRET_KEY

if ENABLE_TEAMS_SSO:
app.config.update(
SESSION_COOKIE_SECURE=True, # required if you use SameSite=None
SESSION_COOKIE_SAMESITE="None",
SESSION_COOKIE_HTTPONLY=True,
)

# Ensure filesystem session directory (when used) points to a writable path inside container.
if SESSION_TYPE == 'filesystem':
app.config['SESSION_FILE_DIR'] = globals().get('SESSION_FILE_DIR', os.environ.get('SESSION_FILE_DIR', '/app/flask_session'))
Expand Down
61 changes: 38 additions & 23 deletions application/single_app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 []
Comment on lines +214 to +217
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment on TEAMS_FRAME_ANCESTORS says it can be left blank to allow framing from any domain, but the CSP is always built as frame-ancestors 'self' {TEAMS_FRAME_ANCESTORS}; so a blank value will only allow 'self' and will block Teams iframe embedding. Either require/populate TEAMS_FRAME_ANCESTORS when ENABLE_TEAMS_SSO is true (especially for AZURE_ENVIRONMENT=="custom"), or update the comment/logic so the behavior matches the guidance.

Copilot uses AI. Check for mistakes.

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
Expand All @@ -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"
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"

# Security Headers Configuration
SECURITY_HEADERS = {
'X-Content-Type-Options': 'nosniff',
'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'; "
f"frame-ancestors 'self' {TEAMS_FRAME_ANCESTORS}; "
"base-uri 'self';"
)
}

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
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR includes runtime and deployer changes (not just docs), but VERSION in config.py is still 0.241.006. Repo instructions require bumping only the 3rd segment after code changes; please increment the version (e.g., to 0.241.007) and keep release notes aligned.

Copilot uses AI. Check for mistakes.

def get_redis_cache_infrastructure_endpoint(redis_hostname: str) -> str:
"""
Expand Down
90 changes: 89 additions & 1 deletion application/single_app/route_frontend_authentication.py
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
Expand Down Expand Up @@ -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,
app_settings=sanitize_settings_for_user(settings))

# Clear potentially stale cache/user info before starting new login
session.pop("user", None)
session.pop("token_cache", None)
Expand Down Expand Up @@ -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():
"""
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.
"""
try:
# Feature gate: only allow token exchange when Teams SSO is enabled
if not ENABLE_TEAMS_SSO:
return jsonify({"error": "teams_sso_disabled"}), 404

data = request.get_json()
teams_token = data.get('token') or {}

Comment on lines +245 to +247
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

request.get_json() can return None (missing/invalid JSON body or wrong Content-Type). The next line calls data.get(...), which will raise and return a 500. Handle a non-dict JSON body explicitly and return a 400 with a clear error instead of falling into the generic exception handler.

Suggested change
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")

Copilot uses AI. Check for mistakes.
if not teams_token:
return jsonify({"error": "No token provided"}), 400

# Build MSAL app
msal_app = _build_msal_app(cache=_load_cache())

# Use OBO flow to exchange the Teams token
result = msal_app.acquire_token_on_behalf_of(
user_assertion=teams_token,
scopes=SCOPE
)

if "error" in result:
error_description = result.get("error_description", result.get("error"))
log_event(f"Teams token exchange failure: {error_description}", exceptionTraceback=True)
return jsonify({
"error": result.get("error"),
"error_description": error_description
}), 400
Comment on lines +260 to +266
Copy link

Copilot AI Apr 10, 2026

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.

Copilot uses AI. Check for mistakes.

# Store user identity info from ID token claims
session["user"] = result.get("id_token_claims")

# Save the token cache to session
_save_cache(msal_app.token_cache)

user_name = session['user'].get('name', 'Unknown')
log_event(f"Teams SSO: User {user_name} authenticated successfully.")
Comment on lines +268 to +275
Copy link

Copilot AI Apr 10, 2026

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.

Copilot uses AI. Check for mistakes.

# Log the login activity
try:
from functions_activity_logging import log_user_login
user_id = session['user'].get('oid') or session['user'].get('sub')
if user_id:
log_user_login(user_id, 'teams_sso')
except Exception as e:
debug_print(f"Could not log Teams login activity: {e}")

# Return success with user info
return jsonify({
"success": True,
"user": {
"name": user_name,
"email": session['user'].get('preferred_username'),
"id": session['user'].get('oid')
}
}), 200

except Exception as e:
debug_print(f"Teams token exchange error: {str(e)}")
return jsonify({
"error": "token_exchange_failed",
"error_description": "An unexpected error occurred during token exchange."
}), 500

@app.route('/logout/local')
@swagger_route(security=get_auth_security())
def local_logout():
Expand Down
2 changes: 2 additions & 0 deletions application/single_app/static/js/MicrosoftTeams.min.js

Large diffs are not rendered by default.

Loading
Loading