Azure AD SSO
OBO Flow Architecture
StackFlow implements Microsoft's On-Behalf-Of (OBO) authentication flow to enable seamless Azure AD single sign-on. When a user authenticates with their Microsoft credentials, the OBO exchange endpoint converts the Azure AD access token into a StackFlow Cognito token, preserving user identity and delegated permissions throughout the session.
- Azure AD App Registration: App must have
openid,profile,email,offline_access,User.Read,GroupMember.Read.Allscopes with admin consent - Reply URL: Must include
https://stackflow-identity-373544523367.auth.us-east-1.amazoncognito.com/oauth2/idpresponse - Cognito Identity Provider: AzureAD IdP created in pool
us-east-1_WKK1AVJ2m - Lambda:
stackflow-dev-obo-token-exchangedeployed and healthy behind API Gateway606pvqo245 - Secrets Manager:
stackflow/azure-sso/client-secretwithclient_idandclient_secretkeys, encrypted withmrk-bd842691514c4d74a02992b8dc11fe16 - Env Var:
AZURE_TENANT_ID=df4d171f-6cca-4c87-84cd-f299e4fca3a9set inStackFlowAPILambda
The OBO endpoint is hosted at https://606pvqo245.execute-api.us-east-1.amazonaws.com/obo/exchange and runs as a dedicated Lambda function behind API Gateway. The Azure tenant ID is df4d171f-6cca-4c87-84cd-f299e4fca3a9.
Azure AD (Tenant: df4d171f-6cca-4c87-84cd-f299e4fca3a9)
│
│ Azure AD Access Token
▼
OBO Exchange Endpoint (606pvqo245.execute-api.us-east-1.amazonaws.com/obo/exchange)
│
│ Validates Azure token, maps groups → roles
│
▼
Cognito Token Exchange (us-east-1_WKK1AVJ2m)
│
│ StackFlow JWT (with role claims)
▼
StackFlowAPI Lambda → Aurora PostgreSQL
Azure App Registration
To use Azure AD SSO, your Azure administrator must register StackFlow as an enterprise application in the Azure portal. The app registration requires specific API permissions and redirect URI configuration.
| Setting | Value |
|---|---|
| Tenant ID | df4d171f-6cca-4c87-84cd-f299e4fca3a9 |
| Redirect URI | https://<your-instance>.stackflow-tech.com/auth/azure/callback |
| API Permissions | User.Read, GroupMember.Read.All, offline_access |
| Token Type | Access tokens (for OBO), ID tokens (for sign-in) |
| Supported Account Types | Accounts in this organizational directory only |
StackFlowGenericSecretRotation Lambda function.
Configuring the OBO Endpoint
The OBO exchange endpoint is configured via System Properties in the StackFlow admin console. Navigate to Admin → Authentication → Azure AD SSO and provide the Application (client) ID, Directory (tenant) ID, and client secret (stored securely in Secrets Manager).
# Store the Azure client secret in Secrets Manager
aws secretsmanager create-secret --name "stackflow/azure-sso/client-secret" --description "Azure AD app client secret for OBO flow" --secret-string '{"client_id":"YOUR_CLIENT_ID","client_secret":"YOUR_SECRET"}' --kms-key-id mrk-bd842691514c4d74a02992b8dc11fe16 --region us-east-1
Group Mapping
Azure AD security groups are mapped to StackFlow roles via the group mapping configuration. When a user authenticates via OBO, the exchange endpoint queries Microsoft Graph for the user's group memberships and applies the configured mapping rules to assign StackFlow roles.
| Azure AD Group | StackFlow Role | Permissions |
|---|---|---|
SG-StackFlow-Admins | super_admin | Full platform access |
SG-StackFlow-ITSM-Managers | itsm_manager | ITSM module management |
SG-StackFlow-Agents | itsm_agent | Incident/change work |
SG-StackFlow-ReadOnly | viewer | Read-only across all modules |
Testing the Flow
# Test the OBO exchange endpoint directly
curl -X POST https://606pvqo245.execute-api.us-east-1.amazonaws.com/obo/exchange -H "Content-Type: application/json" -d '{"azure_token": "AZURE_ACCESS_TOKEN", "tenant_id": "df4d171f-6cca-4c87-84cd-f299e4fca3a9"}'
# Expected response:
# {
# "access_token": "eyJ...",
# "id_token": "eyJ...",
# "refresh_token": "eyJ...",
# "expires_in": 3600,
# "stackflow_user_id": "usr_...",
# "roles": ["itsm_agent"]
# }
GroupMember.Read.All permission is granted and consented before testing the OBO flow. Missing consent is the most common cause of OBO failures.
OBO Token Exchange -- Code
#!/usr/bin/env python3
"""Test the OBO token exchange endpoint."""
import requests
import json
OBO_ENDPOINT = 'https://606pvqo245.execute-api.us-east-1.amazonaws.com/obo/exchange'
def exchange_azure_token(azure_access_token: str) -> dict:
response = requests.post(OBO_ENDPOINT,
headers={'Content-Type': 'application/json'},
json={
'azure_token': azure_access_token,
'tenant_id': 'df4d171f-6cca-4c87-84cd-f299e4fca3a9'
},
timeout=15
)
response.raise_for_status()
return response.json()
# Usage:
# token_data = exchange_azure_token('eyJ...')
# print('StackFlow JWT:', token_data['access_token'][:50], '...')
# print('Roles:', token_data['roles'])
# Register Cognito IdP for Azure AD via AWS CLI
aws cognito-idp create-identity-provider \
--user-pool-id us-east-1_WKK1AVJ2m \
--provider-name AzureAD \
--provider-type OIDC \
--provider-details '{
"client_id": "YOUR_AZURE_APP_CLIENT_ID",
"client_secret": "YOUR_AZURE_APP_CLIENT_SECRET",
"attributes_request_method": "GET",
"oidc_issuer": "https://login.microsoftonline.com/df4d171f-6cca-4c87-84cd-f299e4fca3a9/v2.0",
"authorize_scopes": "openid profile email"
}' \
--attribute_mapping '{
"email": "email",
"given_name": "given_name",
"family_name": "family_name",
"username": "sub"
}' \
--region us-east-1
#!/usr/bin/env python3
"""Decode and verify a Cognito JWT -- useful for debugging SSO claim issues."""
import base64
import json
def decode_jwt(token: str) -> dict:
"""Decode JWT payload without verification (for debugging only)."""
parts = token.split('.')
if len(parts) != 3:
raise ValueError('Invalid JWT format')
# Add padding
payload = parts[1] + '=' * (4 - len(parts[1]) % 4)
decoded = base64.urlsafe_b64decode(payload)
claims = json.loads(decoded)
print(json.dumps(claims, indent=2, default=str))
return claims
# Usage:
# claims = decode_jwt('eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...')
# print('Tenant ID:', claims.get('custom:tenant_id'))
# print('Role:', claims.get('custom:role'))
# print('Expires:', claims.get('exp'))