v2026.1 Open Portal ↗
On this page

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.

⚙️ Minimum Requirements
  • Azure AD App Registration: App must have openid, profile, email, offline_access, User.Read, GroupMember.Read.All scopes 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-exchange deployed and healthy behind API Gateway 606pvqo245
  • Secrets Manager: stackflow/azure-sso/client-secret with client_id and client_secret keys, encrypted with mrk-bd842691514c4d74a02992b8dc11fe16
  • Env Var: AZURE_TENANT_ID=df4d171f-6cca-4c87-84cd-f299e4fca3a9 set in StackFlowAPI Lambda

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.

SettingValue
Tenant IDdf4d171f-6cca-4c87-84cd-f299e4fca3a9
Redirect URIhttps://<your-instance>.stackflow-tech.com/auth/azure/callback
API PermissionsUser.Read, GroupMember.Read.All, offline_access
Token TypeAccess tokens (for OBO), ID tokens (for sign-in)
Supported Account TypesAccounts in this organizational directory only
Client Secret vs Certificate: StackFlow recommends using a certificate-based credential for the Azure app registration in production. The certificate thumbprint is stored in AWS Secrets Manager and rotated automatically by the 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 GroupStackFlow RolePermissions
SG-StackFlow-Adminssuper_adminFull platform access
SG-StackFlow-ITSM-Managersitsm_managerITSM module management
SG-StackFlow-Agentsitsm_agentIncident/change work
SG-StackFlow-ReadOnlyviewerRead-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"]
# }
Tip: Use the Azure AD Token Details view in the Azure Portal to verify that the 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'))