Step-by-Step Process & Entities
Entities Involved
1. End User (Browser)
- The person accessing the Partner Portal
- Can be a contractor employee or grid company employee
- Uses their email and password to log in
2. Blazor WebAssembly Application (Frontend)
- The Partner Portal web interface running in the user's browser
- Built with .NET Blazor WebAssembly
- Location:
Source/EGU.PartnerPortal.Web - Runs entirely client-side (in the browser)
3. MSAL.js Library
- Microsoft Authentication Library for JavaScript
- Embedded in the Blazor app
- Handles all OAuth2/OpenID Connect communication
- Manages tokens and session state
4. Microsoft Entra ID (CIAM Tenant)
- Microsoft's cloud identity service
- Stores user credentials and profiles
- Tenant ID:
6073ce8b-73f3-4df4-9b80-5e40cdc6965f - Issues JWT tokens after successful authentication
- Hosted by Microsoft (external service)
5. ApiService (Backend API)
- The Partner Portal backend REST API
- Built with ASP.NET Core
- Location:
Source/EGU.PartnerPortal.ApiService - Validates JWT tokens on every API request
- Enforces authorization policies
High-Level Authentication Flows
Flow 1: User Login & Authentication (Steps 1-7)
┌─────────────────────────────────────────────────────────────────┐
│ 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 │
└────────────────────────────────┘
Flow 2: API Calls with Token Validation (Steps 8-9)
┌─────────────────────────────────────────────────────────────────┐
│ 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) │
└───────────────────────────────────────────────────────────────┘
Step-by-Step Authentication Process
Step 1: User Visits Protected Page
What happens:
- User navigates to
https://partners-dev.test-egzynergy.com/(or/overview) - Page has
[Authorize]attribute, requires authentication
Who's involved:
- End User (Browser)
- Blazor WebAssembly App
Result:
- Blazor app checks if user has a valid token in sessionStorage
- No token found → Proceed to Step 2
Step 2: Redirect to Login
What happens:
- MSAL.js detects no authentication
- Saves the original URL (e.g.,
/overview) to return later - Redirects browser to Microsoft login page
Who's involved:
- MSAL.js Library
- Microsoft Entra ID
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:
- User sees Microsoft login page
Step 3: User Enters Credentials
What happens:
- User enters email (e.g.,
john.doe@contractor.com) - User enters password
- Completes MFA challenge via Microsoft Authenticator app
- (First time only) User consents to app permissions
Who's involved:
- End User
- Microsoft Entra ID
Result:
- Microsoft Entra ID validates credentials
- If valid → Proceed to Step 4
- If invalid → Show error, retry
Step 4: Authorization Code Issued
What happens:
- Microsoft Entra ID generates a one-time authorization code
- Redirects browser back to the app with the code
Who's involved:
- Microsoft Entra ID
- Blazor WebAssembly App
Redirect back:
https://partners.egzynergy.com/authentication/login-callback
?code=0.AXAA-very-long-code-here
&state=random-state-value
Result:
- App receives authorization code (valid for 10 minutes)
Step 5: Exchange Code for Tokens
What happens:
- MSAL.js automatically exchanges the code for tokens
- Sends code + PKCE verifier to Microsoft token endpoint
- Microsoft validates the code and issues tokens
Who's involved:
- MSAL.js Library
- Microsoft Entra ID
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:
- App now has three tokens (access, refresh, ID)
Step 6: Store Tokens
What happens:
- MSAL.js stores all tokens in browser's sessionStorage
- Tokens are encrypted/encoded but not visible to user
- sessionStorage means tokens deleted when browser tab closes
Who's involved:
- MSAL.js Library
- Browser sessionStorage
What's stored:
- Access Token: Used to call ApiService endpoints (1 hour lifetime)
- Refresh Token: Used to get new access tokens (90 days lifetime)
- ID Token: Contains user info (name, email, groups)
Result:
- User is now authenticated
- Tokens ready for API calls
Step 7: Redirect to Original Page
What happens:
- MSAL.js redirects user back to original page they requested
- User sees the page they originally wanted (e.g.,
/overviewor root/)
Who's involved:
- MSAL.js Library
- Blazor WebAssembly App
Result:
- User successfully logged in and viewing protected content
Step 8: Making API Calls
What happens (every time app calls API):
- User interacts with the app (e.g., viewing the overview page)
- Blazor app makes HTTP request to ApiService
ApiAuthenticationHandlerintercepts the request- Handler asks MSAL.js for access token
- MSAL.js returns token from sessionStorage
- Handler adds token to request header:
Authorization: Bearer eyJ0eXAi... - Request sent to ApiService
Who's involved:
- Blazor WebAssembly App
- ApiAuthenticationHandler
- MSAL.js Library
- ApiService
Result:
- API request includes authentication proof
Step 9: API Validates Token
What happens (on ApiService):
- ApiService receives request with Bearer token
- JWT middleware extracts token from header
- Checks token signature (validates it's from Microsoft)
- Checks token issuer (must be CIAM tenant)
- Checks token audience (must be for this API)
- Checks token expiration (must not be expired)
- Checks user's groups claim (for authorization)
- If all valid → Allow request
- If any invalid → Return 401 Unauthorized
Who's involved:
- ApiService
- Microsoft Entra ID (signing keys downloaded periodically)
What's validated:
- Signature: Proves token issued by Microsoft, not forged
- Issuer:
https://6073ce8b-73f3-4df4-9b80-5e40cdc6965f.ciamlogin.com/.../v2.0 - Audience:
api://4dad5d62-dc8c-4378-8bd0-ae736a4d73fe - Expiration: Token must not be expired
- Groups: User must be in authorized groups
Result:
- API processes request and returns data
- Or returns 401/403 if unauthorized
Step 10: Token Renewal (Automatic)
What happens (when access token expires):
- Access token expires after 1 hour
- Next API call triggers token renewal
- MSAL.js uses refresh token to get new access token
- New access token stored in sessionStorage
- API call proceeds with new token
- User doesn't notice anything (seamless)
Who's involved:
- MSAL.js Library
- Microsoft Entra ID
When user must log in again:
- Refresh token expires (after 90 days)
- User closes browser tab (sessionStorage cleared)
- User clicks "Logout"
- Admin revokes user's access in Azure AD
Token Details
Access Token (JWT)
Purpose: Proves user is authenticated and authorized for API calls Lifetime: 1 hour Contains:
- User ID
- User email
- Azure AD groups (for authorization)
- Expiration time
- Issuer (Microsoft Entra ID)
- Audience (ApiService)
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
}
Refresh Token
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)
ID Token (JWT)
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 (Separate from Main Flow)
What Is It?
Microsoft Graph API is used for administrative and user management features - not for the main authentication flow described above.
When Is It Used?
1. Display User Information
- Showing user's name in navigation bar
- Displaying profile details
- File:
Components/Shared/NavBar.razor
2. Admin Features (Contractor/Grid Admins)
- List all users in organization
- View which groups a user belongs to
- Invite new users to the portal
- Add/remove users from groups
- Files:
ContractorAdmin.razor,GridAdmin.razor,EGAdmin.razor
Separate Token Required
Graph 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 |
How It Works
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
Example: Get Current User
What happens:
- Admin opens their profile page
- Page calls:
await GraphUserService.GetCurrentUserAsync() - GraphServiceClient requests Graph token from MSAL
- MSAL returns token with audience
https://graph.microsoft.com - Request sent to Microsoft Graph API with Bearer token
- Graph API returns user details (name, email)
Key Points
- ✅ Independent of main authentication - Graph token is separate
- ✅ Not required for basic app use - Only for admin features
- ✅ Managed automatically by MSAL - App requests by scope
- ✅ Same user session - Part of same login, different API
- ⚠️ Guest users - Limited to
User.Readscope only
Files Involved
Service: Components/Authentication/AuthenticationService/GraphUserService.cs Configuration: Program.cs:104-142 Usage: Admin pages, NavBar, Profile page
Security Mechanisms
PKCE (Proof Key for Code Exchange)
What: Random secret generated by MSAL.js before login Why: Prevents authorization code theft How: Code can only be exchanged by app that started the flow
Token Signature Validation
What: Cryptographic signature on every JWT token Why: Proves token issued by Microsoft, not forged How: ApiService downloads Microsoft's public keys, validates signature
Token Expiration
What: Every token has expiration timestamp Why: Limits damage if token stolen How: ApiService rejects expired tokens automatically
sessionStorage (Not localStorage)
What: Browser storage that clears when tab closes Why: Reduces risk if user leaves computer unlocked How: MSAL.js configured to use sessionStorage