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:

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., /overview or 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):

  1. User interacts with the app (e.g., viewing the overview page)
  2. Blazor app makes HTTP request to ApiService
  3. ApiAuthenticationHandler intercepts the request
  4. Handler asks MSAL.js for access token
  5. MSAL.js returns token from sessionStorage
  6. Handler adds token to request header: Authorization: Bearer eyJ0eXAi...
  7. 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):

  1. ApiService receives request with Bearer token
  2. JWT middleware extracts token from header
  3. Checks token signature (validates it's from Microsoft)
  4. Checks token issuer (must be CIAM tenant)
  5. Checks token audience (must be for this API)
  6. Checks token expiration (must not be expired)
  7. Checks user's groups claim (for authorization)
  8. If all valid → Allow request
  9. If any invalid → Return 401 Unauthorized

Who's involved:

  • ApiService
  • Microsoft Entra ID (signing keys downloaded periodically)

What's validated:

Result:

  • API processes request and returns data
  • Or returns 401/403 if unauthorized



Step 10: Token Renewal (Automatic)

What happens (when access token expires):

  1. Access token expires after 1 hour
  2. Next API call triggers token renewal
  3. MSAL.js uses refresh token to get new access token
  4. New access token stored in sessionStorage
  5. API call proceeds with new token
  6. 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:

TokenAudienceUsed For
API Tokenapi://4dad5d62-dc8c-4378-8bd0-ae736a4d73feCalling ApiService endpoints
Graph Tokenhttps://graph.microsoft.comUser management via Graph API

How It Works

  1. During login (Step 3), user consents to Graph API permission:

  2. When Graph API is needed, app requests Graph token:

    var result = await _tokenProvider.RequestAccessToken(
        new AccessTokenRequestOptions
        {
            Scopes = new[] { "https://graph.microsoft.com/User.Read" }
        });
    
  3. MSAL returns separate token from sessionStorage (or gets new one)

  4. GraphServiceClient uses this token to call Microsoft Graph API

Example: Get Current User

What happens:

  1. Admin opens their profile page
  2. Page calls: await GraphUserService.GetCurrentUserAsync()
  3. GraphServiceClient requests Graph token from MSAL
  4. MSAL returns token with audience https://graph.microsoft.com
  5. Request sent to Microsoft Graph API with Bearer token
  6. 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.Read scope 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

  • No labels