Authentication Context - Cash Forecast Analyzer
**Document Purpose**: This document captures the complete authentication implementation from the Cash Forecast Analyzer project. It serves as the reference for extracting authentication logic into a reusable Azure Function-based authentication service.
Authentication Context - Cash Forecast Analyzer
Document Purpose: This document captures the complete authentication implementation from the Cash Forecast Analyzer project. It serves as the reference for extracting authentication logic into a reusable Azure Function-based authentication service.
Date Created: December 19, 2025
Source Project: cash-forecast-analyzer
Target Project: peakmade-auth-service (to be created)
Table of Contents
- Overview
- Architecture
- Azure AD App Registration
- Environment Configuration
- Authentication Flow
- Code Implementation
- Session Management
- SharePoint Integration
- Security Considerations
- Migration Path
Overview
What We Built
A complete OAuth 2.0 authentication system using Microsoft Entra ID (Azure AD) with:
- User authentication via MSAL (Microsoft Authentication Library)
- Delegated permissions for Microsoft Graph API
- Delegated permissions for SharePoint Online
- Server-side session management with Flask-Session
- Token caching and refresh logic
- Login/logout flows with proper SSO cleanup
Why We Built It This Way
- Delegated Permissions: Users authenticate as themselves, not as a service principal
- SharePoint Access: Required separate consent flow due to scope limitations
- Session Storage: File-based sessions prevent cookie overflow (tokens are large)
- Token Refresh: MSAL handles automatic token refresh using refresh tokens
Technologies Used
- MSAL:
msal==1.26.0- Microsoft Authentication Library for Python - Flask-Session:
Flask-Session==0.8.0- Server-side session storage - Flask:
Flask==3.0.0- Web framework - Office365-REST-Python-Client:
Office365-REST-Python-Client==2.5.3- SharePoint API client
Architecture
Current Architecture (Monolithic)
User Browser
↓
Flask App (app.py)
↓
services/auth.py (AzureADAuth class)
↓
MSAL Library
↓
Microsoft Entra ID
↓ (returns tokens)
Flask Session Storage
↓
Protected Routes (require @login_required decorator)
Target Architecture (Microservice)
Client Apps (Cash Forecast, Others)
↓ HTTP Request
Azure Function App (Auth Service)
├── /api/auth/login
├── /api/auth/callback
├── /api/auth/validate
├── /api/auth/refresh
├── /api/auth/userinfo
└── /api/auth/logout
↓
MSAL Library
↓
Microsoft Entra ID
↓ (returns tokens)
Azure Key Vault (token storage)
↓
Returns JWT to client app
Azure AD App Registration
App Registration Details
Name: Cash Forecast Analyzer (or similar)
Platform: Web application
Tenant: PeakMade organization tenant
API Permissions Configured
-
Microsoft Graph
User.Read(Delegated) - Read user profile- Status: Granted, consented
-
SharePoint
https://peakcampus.sharepoint.com/.default(Delegated)- Status: Granted, requires separate consent flow
- Reason: Cannot be combined with Graph scopes in single auth request
Redirect URIs
- Local Development:
http://localhost:5000/auth/callback - Production:
https://<your-app>.azurewebsites.net/auth/callback - Future Auth Service:
https://<auth-service>.azurewebsites.net/api/auth/callback
Authentication Configuration
- Client ID: From Azure AD app registration (public)
- Client Secret: Generated secret (confidential, stored in Key Vault)
- Tenant ID: PeakMade organization tenant ID
- Authority:
https://login.microsoftonline.com/<tenant-id>
Token Configuration
- Access Token Format: JWT
- Token Version: v2.0
- ID Token: Included (contains user claims)
- Refresh Token: Enabled
Environment Configuration
Required Environment Variables
# Azure AD Authentication
AZURE_AD_CLIENT_ID=<your-client-id>
AZURE_AD_CLIENT_SECRET=<your-client-secret>
AZURE_AD_TENANT_ID=<your-tenant-id>
AZURE_AD_REDIRECT_URI=http://localhost:5000/auth/callback # or production URL
# SharePoint Configuration
SHAREPOINT_SITE_URL=https://peakcampus.sharepoint.com/sites/BaseCampApps
# Flask Session
SECRET_KEY=<strong-random-key> # Used for signing session cookies
SESSION_TYPE=filesystem # Store sessions on disk, not in cookies
Local Development (.env file)
Create a .env file in project root (never commit this):
AZURE_AD_CLIENT_ID=your-client-id
AZURE_AD_CLIENT_SECRET=your-secret
AZURE_AD_TENANT_ID=your-tenant-id
AZURE_AD_REDIRECT_URI=http://localhost:5000/auth/callback
SHAREPOINT_SITE_URL=https://peakcampus.sharepoint.com/sites/BaseCampApps
SECRET_KEY=dev-secret-key-change-in-production
Production Configuration (Azure App Service)
Set as Application Settings in Azure Portal:
- Navigate to Azure App Service → Configuration → Application Settings
- Add each variable as a new application setting
- Use Azure Key Vault references for secrets:
@Microsoft.KeyVault(SecretUri=...)
Authentication Flow
Initial Login Flow
sequenceDiagram
participant User
participant App
participant MSAL
participant EntraID
User->>App: Click "Login"
App->>MSAL: get_authorization_request_url(scopes=["User.Read"])
MSAL->>EntraID: Redirect with auth request
EntraID->>User: Show Microsoft login page
User->>EntraID: Enter credentials
EntraID->>App: Redirect to /auth/callback?code=xyz
App->>MSAL: acquire_token_by_authorization_code(code)
MSAL->>EntraID: Exchange code for token
EntraID->>MSAL: Return access_token, refresh_token, id_token
MSAL->>App: Return token response
App->>App: Store user info + tokens in session
App->>User: Redirect to SharePoint consent
SharePoint Consent Flow
sequenceDiagram
participant User
participant App
participant MSAL
participant EntraID
User->>App: Automatic redirect after login
App->>App: Set session['sharepoint_consent_flow'] = True
App->>MSAL: get_authorization_request_url(scopes=["sharepoint/.default"])
MSAL->>EntraID: Redirect with SharePoint consent
EntraID->>User: Show consent screen for SharePoint
User->>EntraID: Grant consent
EntraID->>App: Redirect to /auth/callback?code=abc
App->>App: Check if sharepoint_consent_flow == True
App->>MSAL: acquire_token_by_authorization_code(code, sharepoint_scopes)
MSAL->>EntraID: Exchange code for SharePoint token
EntraID->>MSAL: Return SharePoint access_token
MSAL->>App: Return token response
App->>App: Store sharepoint_access_token in session
App->>User: Redirect to main app (index)
Token Refresh Flow
sequenceDiagram
participant App
participant MSAL
participant EntraID
App->>MSAL: acquire_token_silent(account, scopes)
MSAL->>MSAL: Check cache for valid token
alt Token valid in cache
MSAL->>App: Return cached access_token
else Token expired
MSAL->>EntraID: Use refresh_token to get new token
EntraID->>MSAL: Return new access_token + refresh_token
MSAL->>App: Return new tokens
App->>App: Update session with new refresh_token
else Refresh failed
MSAL->>App: Return None
App->>App: Redirect to /login
end
Logout Flow
1. User clicks "Logout"
2. App clears Flask session (all tokens removed)
3. App redirects to Azure AD logout endpoint
4. Azure AD clears SSO session
5. Azure AD redirects back to app homepage (unauthenticated)
Code Implementation
Core Authentication Class (services/auth.py)
class AzureADAuth:
"""Handle Azure AD OAuth authentication flows"""
def __init__(self, app=None):
self.client_id = os.environ.get('AZURE_AD_CLIENT_ID')
self.client_secret = os.environ.get('AZURE_AD_CLIENT_SECRET')
self.tenant_id = os.environ.get('AZURE_AD_TENANT_ID')
self.authority = f"https://login.microsoftonline.com/{self.tenant_id}"
self.scopes = ["User.Read"]
self.sharepoint_url = os.environ.get('SHAREPOINT_SITE_URL')
def get_msal_app(self, cache=None):
"""Create MSAL confidential client application"""
return msal.ConfidentialClientApplication(
self.client_id,
authority=self.authority,
client_credential=self.client_secret,
token_cache=cache
)
def get_auth_url(self):
"""Generate initial login URL"""
msal_app = self.get_msal_app()
return msal_app.get_authorization_request_url(
scopes=self.scopes,
redirect_uri=self.redirect_uri
)
def get_sharepoint_consent_url(self):
"""Generate SharePoint consent URL"""
msal_app = self.get_msal_app()
sharepoint_scopes = [f"{self.sharepoint_url}/.default"]
return msal_app.get_authorization_request_url(
scopes=sharepoint_scopes,
redirect_uri=self.redirect_uri
)
def acquire_token_by_auth_code(self, code):
"""Exchange authorization code for tokens"""
msal_app = self.get_msal_app()
result = msal_app.acquire_token_by_authorization_code(
code,
scopes=self.scopes,
redirect_uri=self.redirect_uri
)
return result if "access_token" in result else None
def acquire_sharepoint_token_by_code(self, code):
"""Exchange code for SharePoint-specific token"""
msal_app = self.get_msal_app(cache=None)
sharepoint_scopes = [f"{self.sharepoint_url}/.default"]
result = msal_app.acquire_token_by_authorization_code(
code,
scopes=sharepoint_scopes,
redirect_uri=self.redirect_uri
)
return result if "access_token" in result else None
def get_token_from_cache(self):
"""Get token from cache or refresh if needed"""
account = session.get('account')
if not account:
return None
msal_app = self.get_msal_app()
result = msal_app.acquire_token_silent(
scopes=self.scopes,
account=account
)
if result and "access_token" in result:
if "refresh_token" in result:
session['refresh_token'] = result['refresh_token']
return result["access_token"]
return None
def get_sharepoint_token(self):
"""Get SharePoint access token from session"""
return session.get('sharepoint_access_token')
Route Handlers (app.py)
@app.route('/login')
def login():
"""Initiate Azure AD login flow"""
auth_url = azure_auth.get_auth_url()
return redirect(auth_url)
@app.route('/auth/callback')
def auth_callback():
"""Handle Azure AD callback after login"""
code = request.args.get('code')
if not code:
return jsonify({'error': 'No authorization code'}), 400
# Check if this is SharePoint consent flow
is_sharepoint_consent = session.get('sharepoint_consent_flow', False)
if is_sharepoint_consent:
# Handle SharePoint token
result = azure_auth.acquire_sharepoint_token_by_code(code)
session.pop('sharepoint_consent_flow', None)
if result and 'access_token' in result:
session['sharepoint_access_token'] = result['access_token']
session['sharepoint_consented'] = True
return redirect(url_for('index'))
return jsonify({'error': 'SharePoint consent failed'}), 401
else:
# Handle initial login
result = azure_auth.acquire_token_by_auth_code(code)
if not result:
return jsonify({'error': 'Failed to acquire token'}), 401
# Store user info in session
id_token_claims = result.get('id_token_claims', {})
session['user'] = {
'name': id_token_claims.get('name'),
'email': id_token_claims.get('preferred_username'),
'id': id_token_claims.get('oid')
}
session['account'] = {
'home_account_id': id_token_claims.get('oid'),
'environment': 'login.microsoftonline.com',
'realm': id_token_claims.get('tid')
}
session['refresh_token'] = result.get('refresh_token')
session['authenticated'] = True
# Redirect to SharePoint consent
return redirect(url_for('sharepoint_consent'))
@app.route('/auth/sharepoint-consent')
@login_required
def sharepoint_consent():
"""Initiate SharePoint consent flow"""
session['sharepoint_consent_flow'] = True
session['sharepoint_consented'] = False
auth_url = azure_auth.get_sharepoint_consent_url()
return redirect(auth_url)
@app.route('/logout')
def logout():
"""Log out user and clear session"""
session.clear()
tenant_id = os.environ.get('AZURE_AD_TENANT_ID')
logout_url = f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/logout"
post_logout_redirect = request.host_url
logout_url += f"?post_logout_redirect_uri={post_logout_redirect}"
return redirect(logout_url)
Route Protection Decorator
def login_required(f):
"""Decorator to protect routes that require authentication"""
@wraps(f)
def decorated_function(*args, **kwargs):
if not session.get('authenticated'):
session['next_url'] = request.url
return redirect(url_for('login'))
return f(*args, **kwargs)
return decorated_function
# Usage:
@app.route('/')
@login_required
def index():
return render_template('index.html')
Session Management
Flask-Session Configuration
# In app.py
app.config['SESSION_TYPE'] = 'filesystem' # Store on disk, not in cookies
app.config['SESSION_FILE_DIR'] = './flask_session' # Session storage directory
app.config['SESSION_PERMANENT'] = False # Session expires when browser closes
app.config['SESSION_USE_SIGNER'] = True # Sign session cookies for security
Session(app)
Session Data Structure
session = {
# User Information
'user': {
'name': 'John Doe',
'email': 'john@peakmade.com',
'id': 'oid-from-azure-ad'
},
# Account Info for Token Refresh
'account': {
'home_account_id': 'oid-from-azure-ad',
'environment': 'login.microsoftonline.com',
'realm': 'tenant-id'
},
# Authentication State
'authenticated': True,
'refresh_token': 'encrypted-refresh-token',
# SharePoint Access
'sharepoint_access_token': 'sharepoint-bearer-token',
'sharepoint_consented': True,
# Flow Control
'sharepoint_consent_flow': False, # True during consent redirect
'next_url': '/original-requested-url' # Redirect after login
}
Why File-Based Sessions?
- Cookie Size Limit: Tokens are large (2-4KB each), cookies max at 4KB
- Security: Tokens never sent to browser, stored server-side only
- Scalability Consideration: For production, migrate to Redis or SQL for multi-instance apps
SharePoint Integration
Why Separate Consent Flow?
Azure AD has a limitation: you cannot request both Graph API scopes (User.Read) and SharePoint delegated permissions (https://sharepoint/.default) in the same authorization request. This is because:
- SharePoint uses a different resource identifier
- Combining them causes scope validation errors
- Microsoft's guidance is to request them separately
SharePoint Token Usage
from office365.sharepoint.client_context import ClientContext
from office365.runtime.auth.token_response import TokenResponse
# Get token from session
sharepoint_token = azure_auth.get_sharepoint_token()
# Create SharePoint client
ctx = ClientContext(sharepoint_site_url)
# Create TokenResponse object for authentication
token_obj = TokenResponse()
token_obj.token_type = "Bearer"
token_obj.access_token = sharepoint_token
# Authenticate context with token
ctx.with_access_token(token_obj)
# Now you can access SharePoint resources
properties = ctx.web.lists.get_by_title("Properties_0").items.get().execute_query()
SharePoint Permissions Granted
- Read: Access to read list items, files, and site information
- Write: Permission to create/update items (if needed)
- Scope: Limited to specific site (
/sites/BaseCampApps)
Security Considerations
What We Did Right
- Confidential Client: Using client secret (not public client)
- HTTPS Only: Redirect URIs enforce HTTPS in production
- Server-Side Sessions: Tokens never exposed to browser
- Signed Cookies: Session cookies are cryptographically signed
- Token Refresh: Automatic refresh prevents re-authentication
- SSO Logout: Clears both app session and Azure AD SSO session
- Minimal Scopes: Only request permissions actually needed
Security Best Practices for Auth Service
- Key Vault: Store client secrets in Azure Key Vault
- Managed Identity: Use managed identity for Key Vault access
- JWT Signing: Issue signed JWTs to client apps (don't share raw tokens)
- Token Expiration: Short-lived JWTs (15-60 minutes)
- CORS: Restrict API to known client app origins
- Rate Limiting: Prevent brute force attacks
- Audit Logging: Log all authentication events
Production Hardening Checklist
- Move secrets to Azure Key Vault
- Enable Azure AD Conditional Access policies
- Implement multi-factor authentication (MFA)
- Set up Azure Monitor alerts for failed logins
- Configure App Service authentication (Easy Auth) as backup
- Use Redis for session storage in multi-instance deployments
- Enable Azure Web Application Firewall (WAF)
- Implement IP allowlisting if appropriate
Migration Path
Step 1: Extract to Separate Module
Current state: Auth code embedded in app.py and services/auth.py
Target state: Standalone auth service callable via HTTP
What to Extract:
AzureADAuthclass → Azure Function handler- Route handlers (
/login,/auth/callback,/logout) → Function endpoints - Session management → JWT token issuance
- Environment variables → Key Vault references
Step 2: Create Azure Function App Structure
peakmade-auth-service/
├── function_app.py # Main function app entry
├── auth/
│ ├── __init__.py
│ ├── login.py # GET /api/auth/login
│ ├── callback.py # GET /api/auth/callback
│ ├── validate.py # POST /api/auth/validate
│ ├── refresh.py # POST /api/auth/refresh
│ ├── userinfo.py # GET /api/auth/userinfo
│ └── logout.py # POST /api/auth/logout
├── shared/
│ ├── __init__.py
│ ├── msal_helper.py # MSAL wrapper (from services/auth.py)
│ ├── jwt_helper.py # JWT generation/validation
│ └── keyvault_helper.py # Secret management
├── requirements.txt
├── host.json
├── local.settings.json
└── README.md
Step 3: Define API Contract
# POST /api/auth/login
# Request: { "client_app_id": "cash-forecast", "redirect_uri": "https://..." }
# Response: { "auth_url": "https://login.microsoftonline.com/...", "state": "xyz" }
# GET /api/auth/callback?code=abc&state=xyz
# Response: { "jwt": "eyJ...", "user": {...}, "expires_at": 1234567890 }
# POST /api/auth/validate
# Request: { "jwt": "eyJ..." }
# Response: { "valid": true, "user": {...}, "expires_at": 1234567890 }
# POST /api/auth/refresh
# Request: { "jwt": "eyJ..." }
# Response: { "jwt": "eyJ...", "expires_at": 1234567890 }
# GET /api/auth/userinfo
# Headers: Authorization: Bearer <jwt>
# Response: { "name": "...", "email": "...", "id": "..." }
# POST /api/auth/logout
# Request: { "jwt": "eyJ..." }
# Response: { "logout_url": "https://login.microsoftonline.com/.../logout" }
Step 4: Update Cash Forecast App to Use Auth Service
# New auth client in services/auth_client.py
class AuthServiceClient:
def __init__(self, auth_service_url):
self.auth_service_url = auth_service_url
def get_login_url(self, redirect_uri):
response = requests.post(
f"{self.auth_service_url}/api/auth/login",
json={"client_app_id": "cash-forecast", "redirect_uri": redirect_uri}
)
return response.json()["auth_url"]
def exchange_code_for_jwt(self, code, state):
response = requests.get(
f"{self.auth_service_url}/api/auth/callback",
params={"code": code, "state": state}
)
return response.json()
def validate_jwt(self, jwt_token):
response = requests.post(
f"{self.auth_service_url}/api/auth/validate",
json={"jwt": jwt_token}
)
return response.json()
# Updated route handlers
@app.route('/login')
def login():
auth_client = AuthServiceClient(os.environ.get('AUTH_SERVICE_URL'))
auth_url = auth_client.get_login_url(url_for('auth_callback', _external=True))
return redirect(auth_url)
@app.route('/auth/callback')
def auth_callback():
code = request.args.get('code')
state = request.args.get('state')
auth_client = AuthServiceClient(os.environ.get('AUTH_SERVICE_URL'))
result = auth_client.exchange_code_for_jwt(code, state)
session['jwt'] = result['jwt']
session['user'] = result['user']
return redirect(url_for('index'))
Step 5: Benefits After Migration
- Reusability: Any app can call auth service (Python, Node.js, C#, etc.)
- Centralized Updates: Change auth logic once, all apps benefit
- Consistent Security: One codebase to audit and harden
- Simpler Client Apps: No MSAL dependency in each app
- Easier Testing: Mock auth service for integration tests
- Compliance: Centralized audit trail for authentication
Cost Estimation
Current Cost (Per App)
- Azure App Service: ~$55/month (Basic B1 tier)
- Session Storage: Included (local disk)
- Bandwidth: Minimal (auth redirects only)
- Total per app: ~$55/month
If you have 5 apps doing auth: 5 × $55 = $275/month
Option 1: Azure Functions (Consumption Plan)
- Execution Cost: $0.20 per million executions
- Memory Cost: $0.000016/GB-second
- Estimate: 10,000 logins/month = 40,000 function calls (login, callback, validate, refresh)
- Monthly Cost: ~$0.01 (basically free under free tier)
- Storage: Azure Storage for session state ~$0.05/month
- Total: ~$1/month (5 apps using same service)
Option 2: Azure Functions (Premium Plan)
- EP1 Plan: ~$175/month (always-on, VNet integration, faster cold starts)
- Better For: High traffic (>100k requests/month), require <100ms response times
- Total: ~$175/month (5 apps using same service)
Option 3: Dedicated App Service for Auth
- Basic B1: ~$55/month (same as current per-app cost)
- Benefit: Simplicity, traditional hosting
- Total: ~$55/month (5 apps using same service)
Recommendation
- Start with Consumption Plan (~$1/month) - cheapest, scales automatically
- Upgrade to Premium Plan if you need:
- Sub-100ms auth response times
- VNet integration for private endpoints
-
100k authentications per month
Savings: $275/month → $1-175/month = $100-274/month saved
Next Steps
For Auth Service Creation
- Create new GitHub repo:
peakmade-auth-service - Copy this document to new repo as
CONTEXT.md - Build Azure Function structure from extracted code
- Set up Azure Function App in Azure Portal
- Deploy and test with cash forecast app as first client
- Document API for other teams to integrate
For Cash Forecast App
- Keep this document as reference:
AUTHENTICATION_CONTEXT.md - Add link to auth service repo in
README.md - After auth service deployed, update app to use auth client
- Remove
services/auth.pyand MSAL dependencies - Update
requirements.txt(remove msal, Flask-Session)
Questions to Answer Before Building
- What JWT library to use? (PyJWT recommended)
- Where to store refresh tokens? (Azure Key Vault or Cosmos DB)
- How long should JWTs live? (Recommend 15-60 minutes)
- What claims to include in JWT? (user_id, email, name, tenant_id, scopes)
- Should we support SharePoint tokens in v1? (Recommend yes, make it optional)
References
Documentation
- MSAL Python Documentation
- Azure AD OAuth 2.0 Documentation
- Azure Functions Python Developer Guide
- Flask-Session Documentation
Related Files in This Repo
app.py- Route handlers and Flask configservices/auth.py- AzureADAuth class implementationrequirements.txt- Python dependencies.env.example- Environment variable template (if it exists)
Contact
For questions about this implementation:
- Original developer: [Your contact info]
- Project repo: https://github.com/PeakMade/cash-forecasting-analyzer
- Date implemented: December 2025
Document Version: 1.0
Last Updated: December 19, 2025
Status: Ready for extraction to auth service
Related Documents
Design Document: BharatSeva AI
BharatSeva AI is a multi-agent orchestration system built on AWS using Amazon Bedrock Agents with Claude 3.5 Sonnet as the foundation model. The system deploys 10 AI agents (1 Master Orchestrator + 9 Specialist Agents) to assist India's informal sector workers in navigating government schemes across three domains: PM Vishwakarma (artisan credit), PMFBY (crop insurance), and BOCW (construction worker welfare).
OpenClaw Enterprise Transformation Plan
Transform OpenClaw from a single-user personal AI assistant into a **dual-mode platform** that is simultaneously:
Qwen Image and Edit: Open-sourcing and Local GGUF Generations with Lightning
Daniel Sandner, for article on https://sandner.art/
Qwen3-TTS — Model Reference
Models: `Qwen/Qwen3-TTS-12Hz-0.6B-CustomVoice` and `Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice`