Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 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
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
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,3 +45,6 @@ flask_session
tmp**cwd
/tmp_images
nul

# Teams manifest
application/teams_app/manifest.json
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 @@ -100,6 +100,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'] = SESSION_FILE_DIR if 'SESSION_FILE_DIR' in globals() else os.environ.get('SESSION_FILE_DIR', '/app/flask_session')
Expand Down
64 changes: 39 additions & 25 deletions application/single_app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,33 +94,10 @@
EXECUTOR_TYPE = 'thread'
EXECUTOR_MAX_WORKERS = 30
SESSION_TYPE = 'filesystem'
VERSION = "0.239.011"
VERSION = "0.239.012"

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 @@ -185,7 +162,6 @@ def get_allowed_extensions(enable_video=False, enable_audio=False):
CUSTOM_REDIS_CACHE_INFRASTRUCTURE_URL_VALUE = os.getenv("CUSTOM_REDIS_CACHE_INFRASTRUCTURE_URL_VALUE", "")
CUSTOM_OIDC_METADATA_URL_VALUE = os.getenv("CUSTOM_OIDC_METADATA_URL_VALUE", "")


# Azure AD Configuration
CLIENT_ID = os.getenv("CLIENT_ID")
APP_URI = f"api://{CLIENT_ID}"
Expand All @@ -195,6 +171,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 @@ -212,6 +189,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 []

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 @@ -228,13 +211,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

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
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 @@ -207,6 +224,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 {}

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

# 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.")

# 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')
@swagger_route(security=get_auth_security())
def 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