...
- 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