sorry for the delayed response i was dealing with some issues, anyway ive made some changes but they are stuff i piced together from the internet so im not to sure about them and regarding the meta dat i just want to read the user role, and auth0 id, but i belive i should provide more context into what im actually trying to do,so im working on my final year project it supposed to be a secure qr code attendance tracking application that utilizes qr code technology , and the authentication ,authorization and role based access control are one of the most crusial parts of the application, in my auth0 universal login i added some custom fields to validate and assign roles to users using a machine to machine app , and in my client side what i want is to get the user auth0 id and his role and use that information to create offline credentials(the application is a blazor wasm app with pwa on .net7) and we need the offline credentials to handle offline mode and u can onnly get those once youve logged in atleast once online,so let me provide the current files i use for the auth0 authentication flow, the current issue im having is the auth-info page it does get loaded and i get and log the information like the user id and role but i never see the auth-info page as it gets unloaded instantly and i get sent back to out index page (/) and as i said im not sure of this method as this is my first time using auth0. the below are the main scripts i use concerning auth0. → (Program.cs)
// Configure Auth0 authentication
builder.Services.AddOidcAuthentication(options =>
{
// Bind standard Auth0 configuration values.
builder.Configuration.Bind("Auth0", options.ProviderOptions);
// Code Flow with PKCE for improved security.
options.ProviderOptions.ResponseType = "code";
// Specify the intended API in the additional parameters.
options.ProviderOptions.AdditionalProviderParameters.Add("audience", builder.Configuration["Auth0:Audience"]);
//Our role claim namespace inserted during the whole post login flow
options.UserOptions.RoleClaim = "https://air-code/roles";
// Optional: Add default scopes if needed.
options.ProviderOptions.DefaultScopes.Add("openid");
options.ProviderOptions.DefaultScopes.Add("profile");
options.ProviderOptions.DefaultScopes.Add("email");
});
// HTTP clients setup
// Base client without auth
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
// HTTP client with Auth0 token
builder.Services.AddHttpClient("AirCodeAPI",
client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress))
.AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();
builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>()
.CreateClient("AirCodeAPI"));
// Clear default JWT claim mappings to preserve original claim names from Auth0
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
// Add authorization services
builder.Services.AddAuthorizationCore();
//Auth(auth0 ish) service
builder.Services.AddScoped<IAuthService, AuthService>();
. (App.razor) @using AirCode.Layout.Main
@using Microsoft.AspNetCore.Components.Authorization
<CascadingAuthenticationState>
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
<Authorizing>
<p>Determining session state, please wait...</p>
</Authorizing>
<NotAuthorized>
<h1>Sorry</h1>
<p>You're not authorized to reach this page. You need to log in.</p>
</NotAuthorized>
</AuthorizeRouteView>
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
</CascadingAuthenticationState>
(IndexPage.razor)
await JSRuntime.InvokeVoidAsync("alert", "You are currently offline. Please check your internet connection and try again.");
isOnlineLoading = false;
return;
}
// Use the built-in authentication mechanism
NavigationManager.NavigateTo("authentication/login");
isOnlineLoading = false;
}
private async Task ContinueOffline()
{
isOfflineLoading = true;
// Check if offline credentials exist
bool hasCredentials = await JSRuntime.InvokeAsync<bool>("offlineManager.hasStoredCredentials");
if (!hasCredentials)
{
await JSRuntime.InvokeVoidAsync("offlineManager.showNoCredentialsMessage");
isOfflineLoading = false;
return;
}
// Validate offline credentials
bool isValid = true;
if (isValid)
{
// Redirect based on stored role
var userRole = UserRole.SuperiorAdmin;
switch (userRole.ToString()?.ToLower())
{
case "superioradmin":
NavigationManager.NavigateTo("/Admin/Dashboard");
break;
case "lecturer":
NavigationManager.NavigateTo("/Admin/Dashboard");
break;
case "student":
NavigationManager.NavigateTo("/Client/Dashboard");
break;
default:
await JSRuntime.InvokeVoidAsync("alert", "Unable to determine user role. Please login online.");
break;
}
}
else
{
await JSRuntime.InvokeVoidAsync("alert", "Your offline credentials are invalid or expired. Please login online.");
}
isOfflineLoading = false;
}
public void Dispose()
{
// Clean up JS interop
JSRuntime.InvokeVoidAsync("connectivityChecker.dispose");
objRef?.Dispose();
}
} (Authentication.razor)@page "/authentication/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@using Microsoft.Extensions.Configuration
@using Microsoft.JSInterop
@using AirCode.Services.Auth
@inject NavigationManager Navigation
@inject IConfiguration Configuration
@inject IJSRuntime JSRuntime
@inject IAuthService AuthService
@inject IAccessTokenProvider TokenProvider
<RemoteAuthenticatorView Action="@Action" OnLogInSucceeded="HandleLoginSuccess">
<LoggingIn>
<p>Redirecting to Auth0 login...</p>
@{
AuthService.LogAuthenticationMessage("Starting Auth0 login redirection");
}
</LoggingIn>
<CompletingLoggingIn>
<p>Completing login process...</p>
@{
AuthService.LogAuthenticationMessage("Processing login callback");
}
</CompletingLoggingIn>
<LogOut>
@{
var authority = $"https://{Configuration["Auth0:Domain"]}";
var clientId = Configuration["Auth0:ClientId"];
AuthService.LogAuthenticationMessage("Logging out user");
Navigation.NavigateTo($"{authority}/v2/logout?client_id={clientId}");
}
</LogOut>
</RemoteAuthenticatorView>
@code{
[Parameter] public string Action { get; set; }
private async Task HandleLoginSuccess(RemoteAuthenticationState state)
{
// Log successful authentication
await AuthService.LogAuthenticationMessageAsync("Authentication successful!");
// Request the token and log its status
var result = await TokenProvider.RequestAccessToken();
await AuthService.LogAuthenticationMessageAsync($"Access token status: {result.Status}");
if (result.TryGetToken(out var token))
{
await AuthService.LogAuthenticationMessageAsync($"JWT retrieved successfully. Length: {token.Value.Length} chars");
// Store token in session for debugging purposes
await JSRuntime.InvokeVoidAsync("authHelper.storeJwtLocally", token.Value);
// Log the first few characters
if (token.Value.Length > 10)
{
await AuthService.LogAuthenticationMessageAsync($"JWT preview: {token.Value.Substring(0, 10)}...");
}
// Testing a LONGER delay before navigation to ensure state updates are complete
await Task.Delay(1000); // Increased from 100ms to 1000ms (1 second)
// Use NavigateTo with forceLoad: false to preserve state
Navigation.NavigateTo("/auth-info", forceLoad: false);
}
else
{
await AuthService.LogAuthenticationMessageAsync("Failed to retrieve JWT token");
Navigation.NavigateTo("/"); // Fall back to homepage if token retrieval fails
}
}
} (AuthInfo.razor)@page "/auth-info"
@attribute [Authorize]
@using System.Security.Claims
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@using System.Text.Json
@using Microsoft.AspNetCore.Authorization
@using AirCode.Services.Auth
@using Microsoft.JSInterop
@using AirCode.Components.Auth
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject IAuthService AuthService
@inject IJSRuntime JSRuntime
@inject IAccessTokenProvider TokenProvider
@inject NavigationManager Navigation
<h1>Authentication Information</h1>
<TokenDebugger />
<AuthorizeView>
<Authorized>
<div class="auth-info">
<h2>User Information</h2>
<p><strong>Name:</strong> @context.User.Identity.Name</p>
<!-- Add Role Display Section -->
<h3>User Role</h3>
@{
var roleClaim = context.User.Claims.FirstOrDefault(c => c.Type == "https://air-code/roles");
var role = roleClaim?.Value ?? "No role assigned";
}
<div class="role-container mb-3 p-2 border border-success rounded">
<h4>Role</h4>
<p class="font-weight-bold">@role</p>
</div>
<h3>Claims</h3>
<div class="claims-container">
@foreach (var claim in context.User.Claims)
{
<div class="claim-item">
<strong>@claim.Type:</strong> @claim.Value
</div>
}
</div>
<h3>JWT Token</h3>
@if (_isLoading)
{
<p>Loading token information...</p>
}
else
{
@if (!string.IsNullOrEmpty(_userId))
{
<div class="user-id-container mb-3 p-2 border border-primary rounded">
<h4>Auth0 User ID</h4>
<p class="font-weight-bold">@_userId</p>
</div>
}
<div class="jwt-container">
<pre>@_jwt</pre>
</div>
<div class="raw-token mt-3">
<h4>Raw JWT Token</h4>
<pre class="bg-dark text-light p-2" style="max-height: 100px; overflow: auto;">@_rawJwt</pre>
</div>
<button class="btn btn-secondary mt-3" @onclick="CopyJwtToClipboard">Copy JWT to Clipboard</button>
}
<div class="mt-4">
<button class="btn btn-primary" @onclick="GoHome">Return to Home</button>
</div>
</div>
</Authorized>
<NotAuthorized>
<p>You need to log in to view authentication information.</p>
<button class="btn btn-primary" @onclick="GoHome">Return to Home</button>
</NotAuthorized>
</AuthorizeView>
@code {
private string _jwt = "Loading...";
private string _rawJwt = "";
private bool _isLoading = true;
private string _userId = string.Empty;
private string _userRole = string.Empty;
protected override async Task OnInitializedAsync()
{
await AuthService.LogAuthenticationMessageAsync("Loading AuthInfo page");
try
{
// First try to get token from session storage (if it was stored during login)
var storedToken = await JSRuntime.InvokeAsync<string>("authHelper.retrieveStoredJwt");
if (!string.IsNullOrEmpty(storedToken))
{
await AuthService.LogAuthenticationMessageAsync("Found JWT in session storage");
_rawJwt = storedToken;
await ProcessJwtToken(storedToken);
}
else
{
// Try to get token from TokenProvider
var tokenResult = await TokenProvider.RequestAccessToken();
if (tokenResult.TryGetToken(out var accessToken))
{
await AuthService.LogAuthenticationMessageAsync("Retrieved JWT from TokenProvider");
_rawJwt = accessToken.Value;
await ProcessJwtToken(accessToken.Value);
}
else
{
// Fall back to AuthService
await AuthService.LogAuthenticationMessageAsync("Trying to get JWT from AuthService");
var token = await AuthService.GetJwtTokenAsync();
if (token != "Token not found" && !token.StartsWith("Error:"))
{
_rawJwt = token;
await ProcessJwtToken(token);
}
else
{
_jwt = "JWT not found. Available claims are listed above.";
await AuthService.LogAuthenticationMessageAsync("JWT not found through any method");
}
}
}
}
catch (Exception ex)
{
_jwt = $"Error retrieving JWT: {ex.Message}";
await AuthService.LogAuthenticationMessageAsync($"Error in AuthInfo: {ex.Message}");
}
finally
{
_isLoading = false;
}
}
private async Task ProcessJwtToken(string token)
{
try
{
await AuthService.LogAuthenticationMessageAsync($"Processing JWT token, length: {token.Length}");
// Pretty print if it's JSON wait we could add this as function to our json helper later
var tokenParts = token.Split('.');
if (tokenParts.Length > 1)
{
var payload = tokenParts[1];
var paddedPayload = payload.PadRight(payload.Length + (4 - payload.Length % 4) % 4, '=');
var decodedBytes = Convert.FromBase64String(paddedPayload);
var jsonString = System.Text.Encoding.UTF8.GetString(decodedBytes);
try
{
var jsonElement = System.Text.Json.JsonSerializer.Deserialize<JsonElement>(jsonString);
var formattedJson = System.Text.Json.JsonSerializer.Serialize(jsonElement, new JsonSerializerOptions { WriteIndented = true });
_jwt = formattedJson;
// Extract and log the Auth0 user ID specifically
if (jsonElement.TryGetProperty("sub", out var subProperty))
{
_userId = subProperty.GetString();
await AuthService.LogAuthenticationMessageAsync($"Auth0 User ID: {_userId}");
}
// Extract and log the user role (works but info page dsapears instantly and we get sent
//back to / page get help)
if (jsonElement.TryGetProperty("https://air-code/roles", out var roleProperty))
{
_userRole = roleProperty.ValueKind == JsonValueKind.Array
? string.Join(", ", roleProperty.EnumerateArray().Select(r => r.GetString()))
: roleProperty.GetString();
await AuthService.LogAuthenticationMessageAsync($"User Role: {_userRole}");
}
await AuthService.LogAuthenticationMessageAsync("Successfully decoded and formatted JWT payload");
}
catch (Exception ex)
{
_jwt = jsonString;
await AuthService.LogAuthenticationMessageAsync($"Decoded JWT payload but couldn't format JSON: {ex.Message}");
}
}
else
{
_jwt = "Invalid JWT format (doesn't contain expected segments)";
await AuthService.LogAuthenticationMessageAsync("Invalid JWT format");
}
}
catch (Exception ex)
{
_jwt = $"Error processing JWT: {ex.Message}";
await AuthService.LogAuthenticationMessageAsync($"Error processing JWT: {ex.Message}");
}
}
// Added a longer delay to fix the navigation issue
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await AuthService.LogAuthenticationMessageAsync("AuthInfo page rendered successfully");
await JSRuntime.InvokeVoidAsync("console.log", "AuthInfo page rendered successfully");
// Log if we have role information
if (!string.IsNullOrEmpty(_userRole))
{
await JSRuntime.InvokeVoidAsync("console.log", $"User role extracted: {_userRole}");
}
}
}
private async Task CopyJwtToClipboard()
{
try
{
if (!string.IsNullOrEmpty(_rawJwt))
{
await JSRuntime.InvokeVoidAsync("navigator.clipboard.writeText", _rawJwt);
await AuthService.LogAuthenticationMessageAsync("JWT copied to clipboard");
}
}
catch (Exception ex)
{
await AuthService.LogAuthenticationMessageAsync($"Error copying JWT: {ex.Message}");
}
}
private void GoHome()
{
Navigation.NavigateTo("/");
}
}. (AuthService.cs)using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.JSInterop;
using System;
using System.Linq;
using System.Threading.Tasks;
namespace AirCode.Services.Auth
{
public interface IAuthService
{
Task<string> GetJwtTokenAsync();
Task LogAuthenticationMessageAsync(string message);
void LogAuthenticationMessage(string message);
Task<AccessTokenResult> GetAccessTokenResultAsync();
}
public class AuthService : IAuthService
{
private readonly AuthenticationStateProvider _authStateProvider;
private readonly IJSRuntime _jsRuntime;
private readonly IAccessTokenProvider _tokenProvider;
public AuthService(
AuthenticationStateProvider authStateProvider,
IJSRuntime jsRuntime,
IAccessTokenProvider tokenProvider)
{
_authStateProvider = authStateProvider;
_jsRuntime = jsRuntime;
_tokenProvider = tokenProvider;
}
public async Task<AccessTokenResult> GetAccessTokenResultAsync()
{
try
{
var tokenResult = await _tokenProvider.RequestAccessToken();
await LogAuthenticationMessageAsync($"Access token request status: {tokenResult.Status}");
return tokenResult;
}
catch (Exception ex)
{
await LogAuthenticationMessageAsync($"Error getting access token: {ex.Message}");
return null;
}
}
public async Task<string> GetJwtTokenAsync()
{
try
{
// Try to get token from AccessTokenProvider first
var tokenResult = await _tokenProvider.RequestAccessToken();
if (tokenResult.TryGetToken(out var accessToken))
{
await LogAuthenticationMessageAsync($"Token successfully retrieved from provider");
return accessToken.Value;
}
// Fall back to looking in claims
var authState = await _authStateProvider.GetAuthenticationStateAsync();
var user = authState.User;
if (user.Identity.IsAuthenticated)
{
// Try various possible claim types for the token
foreach (var possibleClaimType in new[] { "access_token", "id_token", "token", "jwt" })
{
var claim = user.FindFirst(possibleClaimType);
if (claim != null)
{
await LogAuthenticationMessageAsync($"Token found in claim: {possibleClaimType}");
return claim.Value;
}
}
// If we get here, log all claim types to help debugging
var claimTypes = string.Join(", ", user.Claims.Select(c => c.Type).Distinct());
await LogAuthenticationMessageAsync($"No token found. Available claim types: {claimTypes}");
}
return "Token not found";
}
catch (Exception ex)
{
await LogAuthenticationMessageAsync($"Error retrieving token: {ex.Message}");
return $"Error: {ex.Message}";
}
}
public void LogAuthenticationMessage(string message)
{
Console.WriteLine($"[Authentication] -> {message}");
}
//i think we can use our unity debug method with the [class name] - - - [function name] -> log. method here to
public async Task LogAuthenticationMessageAsync(string message)
{
LogAuthenticationMessage(message);
await _jsRuntime.InvokeVoidAsync("console.log", $"[Authentication] -> {message}");
}
}
} (Auth Helper.js) window.authHelper = {
logAuthMessage: function (message) {
console.log(`[Authentication] -> ${message}`);
},
storeJwtLocally: function (token) {
// Store JWT in sessionStorage for retrieval elsewhere
// Note: This is just for development/debugging purposes
if (token) {
sessionStorage.setItem('debug_jwt_token', token);
console.log('[Authentication] -> JWT stored in sessionStorage for debugging');
return true;
}
return false;
},
retrieveStoredJwt: function () {
return sessionStorage.getItem('debug_jwt_token') || null;
},
clearStoredJwt: function () {
sessionStorage.removeItem('debug_jwt_token');
console.log('[Authentication] -> JWT cleared from sessionStorage');
},
// Function to extract tokens from browser storage directly
extractAuthTokensFromStorage: function() {
try {
// Try to find authentication tokens in local storage
const keys = Object.keys(localStorage);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (key.includes('auth') || key.includes('token') || key.includes('oidc')) {
console.log(`[Authentication] -> Found potential token in localStorage: ${key}`);
try {
const value = localStorage.getItem(key);
// If it looks like a JWT (contains periods and is base64-ish)
if (value && value.includes('.') && value.length > 50) {
console.log(`[Authentication] -> This appears to be a JWT token`);
return value;
}
} catch (e) {
console.log(`[Authentication] -> Error reading localStorage key ${key}: ${e}`);
}
}
}
// Try session storage as well
const sessionKeys = Object.keys(sessionStorage);
for (let i = 0; i < sessionKeys.length; i++) {
const key = sessionKeys[i];
if (key.includes('auth') || key.includes('token') || key.includes('oidc')) {
console.log(`[Authentication] -> Found potential token in sessionStorage: ${key}`);
try {
const value = sessionStorage.getItem(key);
// If it looks like a JWT (contains periods and is base64-ish)
if (value && value.includes('.') && value.length > 50) {
console.log(`[Authentication] -> This appears to be a JWT token`);
return value;
}
} catch (e) {
console.log(`[Authentication] -> Error reading sessionStorage key ${key}: ${e}`);
}
}
}
return null;
} catch (e) {
console.log(`[Authentication] -> Error extracting tokens: ${e}`);
return null;
}
}
};
there is also the route gard i got from one of the discussions but i havent got around to using it yet..