I am new to blazor and implementing the session timeout (20 minutes of inactivity) functionality. It is a Blazor Web app (8.0) and using AzureADB2C to authenticate and SQL server for authorizing the roles.
There are so many .net versions out there, It is very difficult to identify the correct one for Web app 8.0. I searched everywhere in stackoverflow forums (most of the results are WebAssembly or server project), and I added following lines to program.cs, but it is not working.
builder.Services.ConfigureApplicationCookie(ops =>
{
ops.ExpireTimeSpan = TimeSpan.FromMinutes(20);
ops.SlidingExpiration = true;
});
//Here is appsettings.json
{
"AzureAdB2c": {
"callbackPath": "/signin-oidc",
"signedOutCallbackPath": "/signout-callback-oidc",
"clientId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx",
"domain": "xxxxxxxxb2cdev.onmicrosoft.com",
"instance": "https://xxxxxxxxb2cdev.b2clogin.com/",
"tenantId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx",
"SignUpSignInPolicyId": "B2C_1A_CC24PORTAL",
"RedirectUri": "https://dev-xxxxxxxx-ui-eus-as.azurewebsites.net"
},
"BuildConfiguration": "DEV",
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
//Here is logout link in NavMenu.razor
<div class="nav-item px-3">
<NavLink class="nav-link" **href="MicrosoftIdentity/Account/SignOut"**>
<span class="bi bi-box-arrow-right-nav-menu" aria-hidden="true"></span> Logout
</NavLink>
</div>
//Here is program.cs
using Blazored.LocalStorage;
using CC24_UI.Components;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.Identity.Web;
using Microsoft.Identity.Web.UI;
using System.Runtime;
using Serilog;
using Telerik.Blazor.Services;
var builder = WebApplication.CreateBuilder(args);
Log.Logger = new LoggerConfiguration()
.Enrich.FromLogContext()
.WriteTo.File(@"C:\home\logfiles\CC24.txt")
.CreateLogger();
Log.Information("builder environment: {0}", builder.Environment.EnvironmentName);
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(options =>
{
builder.Configuration.Bind("AzureAdB2c", options);
options.Events.OnSignedOutCallbackRedirect = async context =>
{
context.Response.Redirect("http://www.microsoft.com");
context.HandleResponse();
await Task.CompletedTask;
};
});
builder.Services.AddControllersWithViews()
.AddMicrosoftIdentityUI();
builder.Services.AddAuthorization(options =>
{
// By default, all incoming requests will be authorized according to the default policy
options.FallbackPolicy = options.DefaultPolicy;
});
builder.Services.AddRazorPages().AddMicrosoftIdentityUI();
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents().AddMicrosoftIdentityConsentHandler();
builder.Services.AddServerSideBlazor();
builder.Services.ConfigureApplicationCookie(ops =>
{
ops.ExpireTimeSpan = TimeSpan.FromMinutes(20);
ops.SlidingExpiration = true;
});
builder.Services.AddTelerikBlazor();
builder.Services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
builder.Services.AddBlazoredLocalStorage();
builder.Services.AddHttpContextAccessor();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error", createScopeForErrors: true);
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.MapRazorComponents<App>().AddInteractiveServerRenderMode();
app.UseAntiforgery();
app.Run();
Thanks in advance. I really appreciate your help and direction to control the user session timeout.
ConfigureApplicationCookie must be called after calling AddIdentity or AddDefaultIdentity.
Maybe this is the root cause for your issue, and the session not works. You can try to use AddIdentity
before AddAuthentication
. If this doesn't work, you can investigate in this direction. And I also provide a workaround for you, you can check the detailed steps below.
builder.Services.AddIdentity<IdentityUser, IdentityRole>(options =>
{
options.SignIn.RequireConfirmedAccount = true;
})
.AddDefaultTokenProviders();
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(options =>
{
builder.Configuration.Bind("AzureAdB2c", options);
options.Events.OnSignedOutCallbackRedirect = async context =>
{
context.Response.Redirect("http://www.microsoft.com");
context.HandleResponse();
await Task.CompletedTask;
};
});
builder.Services.AddControllersWithViews()
.AddMicrosoftIdentityUI();
builder.Services.AddAuthorization(options =>
{
// By default, all incoming requests will be authorized according to the default policy
options.FallbackPolicy = options.DefaultPolicy;
});
builder.Services.AddRazorPages().AddMicrosoftIdentityUI();
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents().AddMicrosoftIdentityConsentHandler();
builder.Services.AddServerSideBlazor();
builder.Services.ConfigureApplicationCookie(ops =>
{
ops.ExpireTimeSpan = TimeSpan.FromMinutes(20);
ops.SlidingExpiration = true;
});
WorkAround
1. SessionTimeoutService.cs
using Microsoft.JSInterop;
namespace BlazorApp
{
public class SessionTimeoutService
{
private readonly IJSRuntime _jsRuntime;
private bool _isTimerStarted = false;
public SessionTimeoutService(IJSRuntime jsRuntime)
{
_jsRuntime = jsRuntime;
}
public async Task StartUserInactivityCheck()
{
if (!_isTimerStarted)
{
_isTimerStarted = true;
Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + "open the Page, session start.");
await _jsRuntime.InvokeVoidAsync("startInactivityTimer", DotNetObjectReference.Create(this));
}
}
[JSInvokable("OnUserInactive")]
public void OnUserInactive()
{
Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")+ "Find User Inactive");
// Todo : Force user logout
}
}
}
2. Register it
using BlazorApp;
using BlazorApp.Components;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
builder.Services.AddScoped<SessionTimeoutService>();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error", createScopeForErrors: true);
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAntiforgery();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
app.Run();
3. MainLayout.razor
@inherits LayoutComponentBase
@inject SessionTimeoutService SessionTimeoutService
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<main>
<div class="top-row px-4">
<a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
</div>
<article class="content px-4">
@Body
</article>
</main>
</div>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
@code {
private bool _isInitialized = false;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
Console.WriteLine($"OnAfterRenderAsync called. First Render: {firstRender}");
if (firstRender && !_isInitialized)
{
Console.WriteLine("Initializing inactivity check...");
_isInitialized = true;
await SessionTimeoutService.StartUserInactivityCheck();
}
}
}
4. App.razor
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
<link rel="stylesheet" href="app.css" />
<link rel="stylesheet" href="BlazorApp.styles.css" />
<link rel="icon" type="image/png" href="favicon.png" />
<HeadOutlet />
</head>
<body>
@* make sure rendermode is InteractiveServer *@
<Routes @rendermode="InteractiveServer" />
<script src="_framework/blazor.web.js"></script>
<script src="js/inactivityTimer.js"></script>
</body>
</html>
5. wwwroot/js/inactivityTimer.js
window.startInactivityTimer = (dotNetHelper) => {
let timeout;
function resetTimer() {
clearTimeout(timeout);
timeout = setTimeout(() => {
dotNetHelper.invokeMethodAsync("OnUserInactive");
}, 10 * 1000); // For test 10s, you can change it to 20min 20*60*1000
}
window.onload = resetTimer;
document.onmousemove = resetTimer;
document.onkeydown = resetTimer;
document.onclick = resetTimer;
document.onscroll = resetTimer;
};
6. Test Result
There may be some slight deviations due to the test environment, about a few seconds, which should be acceptable for 20 minutes.