Source/EGU.PartnerPortal.Web6073ce8b-73f3-4df4-9b80-5e40cdc6965fSource/EGU.PartnerPortal.ApiService┌─────────────────────────────────────────────────────────────────┐
│ LOGIN & AUTHENTICATION FLOW (Steps 1-7) │
└─────────────────────────────────────────────────────────────────┘
┌──────────────┐
│ End User │
│ Browser │
└──────┬───────┘
│
│ Step 1: Navigate to app
│ (https://partners-dev.test-egzynergy.com/)
▼
┌──────────────────────┐
│ Blazor WebAssembly │
│ (Partner Portal) │
└──────┬───────────────┘
│
│ Step 2: Check sessionStorage
│ No valid token found
│ → Redirect to login
▼
┌──────────────────────────────┐
│ Microsoft Entra ID (CIAM) │
│ Login Page │
└──────┬───────────────────────┘
│
│ Step 3: User enters
│ email + password + MFA
▼
┌──────────────────────────────┐
│ Microsoft Entra ID │
│ Validates Credentials │
└──────┬───────────────────────┘
│
│ ✅ Valid credentials
│
│ Step 4: Returns
│ authorization code
▼
┌──────────────────────┐
│ MSAL.js Library │
│ (in browser) │
└──────┬───────────────┘
│
│ Step 5: Exchange code
│ for tokens (PKCE)
▼
┌──────────────────────────────┐
│ Microsoft Entra ID │
│ Token Endpoint │
└──────┬───────────────────────┘
│
│ Returns 3 tokens:
│ • Access Token (1h)
│ • Refresh Token (90d)
│ • ID Token (1h)
▼
┌──────────────────────┐
│ Browser │
│ sessionStorage │
└──────┬───────────────┘
│
│ Step 6: Tokens stored
│ (indexed by scope)
│
│ Step 7: Redirect back
│ to original page
▼
┌────────────────────────────────┐
│ ✅ USER AUTHENTICATED │
│ Ready to use the app │
└────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ API CALL & VALIDATION FLOW (Steps 8-9) │
└─────────────────────────────────────────────────────────────────┘
┌────────────────────────────────┐
│ User interacts with app │
│ (e.g., views overview page) │
└────────────┬───────────────────┘
│
│ Step 8: Blazor makes API request
▼
┌──────────────────────────────────┐
│ ApiAuthenticationHandler │
│ (HTTP Message Handler) │
└──────┬───────────────────────────┘
│
│ Requests token from MSAL.js
│ Scope: api://4dad5d62.../ApiService_UserAccess
▼
┌──────────────────────┐
│ MSAL.js Library │
│ sessionStorage │
└──────┬───────────────┘
│
│ Returns access token
│ (or refreshes if expired)
▼
┌──────────────────────────────┐
│ HTTP Request │
│ GET /api/v1/WorkOrder/... │
│ Authorization: Bearer eyJ... │
└──────┬───────────────────────┘
│
│ Step 9: Request received
▼
┌──────────────────────────────────┐
│ ApiService │
│ JWT Authentication Middleware │
└──────┬───────────────────────────┘
│
│ Token Validation:
│ ✓ Signature (Azure AD keys)
│ ✓ Issuer (CIAM tenant)
│ ✓ Audience (this API)
│ ✓ Expiration (not expired)
│ ✓ Groups (authorization)
│
├─── ✅ Valid ────┐ ❌ Invalid ───┐
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Controller │ │ 200 OK │ │ 401/403 │
│ Processes │ │ Returns │ │ Unauthorized│
│ Request │ │ Data │ │ Forbidden │
└──────────────┘ └──────────────┘ └──────────────┘
┌───────────────────────────────────────────────────────────────┐
│ Step 10: TOKEN RENEWAL (automatic, happens in background) │
│ │
│ When access token expires (after 1 hour): │
│ 1. MSAL.js detects expired token │
│ 2. Uses refresh token to get new access token │
│ 3. Stores new access token in sessionStorage │
│ 4. User continues working (no interruption) │
└───────────────────────────────────────────────────────────────┘
What happens:
https://partners-dev.test-egzynergy.com/ (or /overview)[Authorize] attribute, requires authenticationWho's involved:
Result:
What happens:
/overview) to return laterWho's involved:
Redirect URL:
https://6073ce8b-73f3-4df4-9b80-5e40cdc6965f.ciamlogin.com/.../authorize
?client_id=84c38b43-12e4-4c26-8292-8910d79aa532
&redirect_uri=https://partners.egzynergy.com/authentication/login-callback
&response_type=code
Result:
What happens:
john.doe@contractor.com)Who's involved:
Result:
What happens:
Who's involved:
Redirect back:
https://partners.egzynergy.com/authentication/login-callback
?code=0.AXAA-very-long-code-here
&state=random-state-value
Result:
What happens:
Who's involved:
Request to Microsoft:
POST https://.../oauth2/v2.0/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
code=0.AXAA-very-long-code-here
client_id=84c38b43-12e4-4c26-8292-8910d79aa532
redirect_uri=https://partners.egzynergy.com/authentication/login-callback
code_verifier=PKCE-verifier
Response from Microsoft:
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGc...",
"refresh_token": "0.AXAA...",
"id_token": "eyJ0eXAiOiJKV1QiLCJhbGc...",
"expires_in": 3600
}
Result:
What happens:
Who's involved:
What's stored:
Result:
What happens:
/overview or root /)Who's involved:
Result:
What happens (every time app calls API):
ApiAuthenticationHandler intercepts the requestAuthorization: Bearer eyJ0eXAi...Who's involved:
Result:
What happens (on ApiService):
Who's involved:
What's validated:
https://6073ce8b-73f3-4df4-9b80-5e40cdc6965f.ciamlogin.com/.../v2.0api://4dad5d62-dc8c-4378-8bd0-ae736a4d73feResult:
What happens (when access token expires):
Who's involved:
When user must log in again:
Purpose: Proves user is authenticated and authorized for API calls Lifetime: 1 hour Contains:
Example (decoded):
{
"iss": "https://6073ce8b-73f3-4df4-9b80-5e40cdc6965f.ciamlogin.com/.../v2.0",
"aud": "api://4dad5d62-dc8c-4378-8bd0-ae736a4d73fe",
"sub": "abc123...",
"name": "John Doe",
"email": "john.doe@contractor.com",
"groups": ["ac6ec653-2ae3-457a-9302-d42429d83bee"],
"exp": 1733754123
}
Purpose: Get new access tokens without re-login Lifetime: 90 days Contains: Encrypted data (not readable) Note: Single-use (new refresh token issued with each renewal)
Purpose: User identity information for the frontend Lifetime: 1 hour Contains: Similar to access token but for frontend use Note: Not used for API authorization
Microsoft Graph API is used for administrative and user management features - not for the main authentication flow described above.
1. Display User Information
Components/Shared/NavBar.razor2. Admin Features (Contractor/Grid Admins)
ContractorAdmin.razor, GridAdmin.razor, EGAdmin.razorGraph API requires its own access token, different from the API token:
| Token | Audience | Used For |
|---|---|---|
| API Token | api://4dad5d62-dc8c-4378-8bd0-ae736a4d73fe | Calling ApiService endpoints |
| Graph Token | https://graph.microsoft.com | User management via Graph API |
During login (Step 3), user consents to Graph API permission:
When Graph API is needed, app requests Graph token:
var result = await _tokenProvider.RequestAccessToken(
new AccessTokenRequestOptions
{
Scopes = new[] { "https://graph.microsoft.com/User.Read" }
});
MSAL returns separate token from sessionStorage (or gets new one)
GraphServiceClient uses this token to call Microsoft Graph API
What happens:
await GraphUserService.GetCurrentUserAsync()https://graph.microsoft.comUser.Read scope onlyService: Components/Authentication/AuthenticationService/GraphUserService.cs Configuration: Program.cs:104-142 Usage: Admin pages, NavBar, Profile page