You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.NET 8 Blazor多应用同时登出及postlogout_redirect_uri安全配置问题

基于.NET 8的Blazor WASM与Server应用Azure AD B2C跨应用登出及安全防护方案

问题背景

现有两个基于.NET 8的应用:Blazor WASM(客户端)和Blazor Server(服务端),需实现用户在两个应用中同时登出,并保障postlogout_redirect_uri的安全性。

当前实现逻辑

1. 触发Azure AD B2C登出(Usermenu.razor.cs)

private async Task NavigateToLogout()
{
    string idToken = "";

    var authenticationResult = await TokenProvider.RequestAccessToken();

    if (authenticationResult.TryGetToken(out var accessToken))
    {
        idToken = accessToken.Value;
    }

    string postLogoutRedirectUri = Configuration.GetValue<string>("AzureAdB2c:logoutEndpoint");
    string authority = Configuration.GetValue<string>("AzureAdB2c:Authority");

    var logoutUrl = $"{authority}/oauth2/v2.0/logout?" +
                    $"post_logout_redirect_uri={Uri.EscapeDataString(postLogoutRedirectUri)}&" +
                    $"id_token_hint={Uri.EscapeDataString(idToken)}";

    AppState.NavigationManager.NavigateTo(logoutUrl);
}

其中postLogoutRedirectUri配置为https://localhost:5007/logoutendpoint,已在服务端应用注册中添加该地址。

2. 服务端登出端点(LogoutEndpoint.razor.cs)

public partial class LogoutEndpoint
{
    [Inject] AppState AppState { get; set; }

    private bool _isLoading { get; set; } = false;

    protected override void OnInitialized()
    {
        _isLoading = true;

        AppState.NavigationManager.NavigateToLogout("authentication/logout");

        _isLoading = false;
    }
}

遇到的问题

  • 开启B2C策略配置<SingleSignOn Scope="Policy" EnforceIdTokenHintOnLogout="true" />时,第二次登出请求因缺少合法的id_token_hint被拦截;
  • 关闭该配置后,用户登出后再次登录会循环跳转到/logoutendpoint,最终回到登录页。

解决方案

一、实现跨应用同时登出

1. 修正id_token_hint获取逻辑(核心错误修复)

当前代码误用Access Token作为id_token_hint,需改为获取ID Token(Azure AD B2C要求id_token_hint必须是用户的ID Token):

private async Task NavigateToLogout()
{
    string idToken = "";

    // 获取当前用户的ID Token(以MSAL为例)
    var accounts = await _msalClient.GetAccountsAsync();
    if (accounts.Any())
    {
        var silentResult = await _msalClient.AcquireTokenSilent(new[] { "openid" }, accounts.First())
                                            .ExecuteAsync();
        idToken = silentResult.IdToken;
    }

    string postLogoutRedirectUri = Configuration.GetValue<string>("AzureAdB2c:logoutEndpoint");
    string authority = Configuration.GetValue<string>("AzureAdB2c:Authority");

    var logoutUrl = $"{authority}/oauth2/v2.0/logout?" +
                    $"post_logout_redirect_uri={Uri.EscapeDataString(postLogoutRedirectUri)}&" +
                    $"id_token_hint={Uri.EscapeDataString(idToken)}";

    AppState.NavigationManager.NavigateTo(logoutUrl);
}

2. 跨应用本地登出触发

Azure AD B2C的全局登出只会清除自身会话,需手动触发两个应用的本地会话清除:

  • 在服务端LogoutEndpoint页面完成自身登出后,通过隐藏iframe加载客户端登出端点,触发Blazor WASM的本地登出:
    @if (_isLoading)
    {
        <p>正在登出...</p>
    }
    else
    {
        <!-- 隐藏iframe触发客户端登出 -->
        <iframe src="https://localhost:xxxx/authentication/logout" style="display:none"></iframe>
        <p>登出完成,即将跳转首页...</p>
    }
    
  • 服务端登出逻辑添加身份状态校验,避免重复执行登出:
    [Inject] AuthenticationStateProvider AuthStateProvider { get; set; }
    
    protected override async Task OnInitializedAsync()
    {
        _isLoading = true;
    
        var authState = await AuthStateProvider.GetAuthenticationStateAsync();
        if (!authState.User.Identity.IsAuthenticated)
        {
            // 用户已登出,直接跳转首页
            AppState.NavigationManager.NavigateTo("/");
            _isLoading = false;
            return;
        }
    
        // 执行服务端本地登出
        AppState.NavigationManager.NavigateToLogout("authentication/logout");
    
        _isLoading = false;
    }
    

二、保障postlogout_redirect_uri安全性

1. 依赖Azure AD B2C内置校验

在Azure AD B2C应用注册的身份验证页面,仅添加可信的post_logout_redirect_uri地址。Azure AD B2C会自动校验请求中的地址是否在允许列表内,非法地址会被直接拒绝。

2. 添加state参数防篡改

构造登出URL时生成随机state值并存储,在登出回调页面校验该值,防止CSRF攻击和地址篡改:

  • 触发登出时生成并存储state:
    private async Task NavigateToLogout()
    {
        // ... 省略ID Token获取逻辑 ...
    
        string state = Guid.NewGuid().ToString();
        // 存储state(服务端用Session,客户端用SessionStorage)
        AppState.NavigationManager.SessionStorage.SetItemAsString("logoutState", state);
    
        var logoutUrl = $"{authority}/oauth2/v2.0/logout?" +
                        $"post_logout_redirect_uri={Uri.EscapeDataString(postLogoutRedirectUri)}&" +
                        $"id_token_hint={Uri.EscapeDataString(idToken)}&" +
                        $"state={Uri.EscapeDataString(state)}";
    
        AppState.NavigationManager.NavigateTo(logoutUrl);
    }
    
  • 在LogoutEndpoint页面校验state:
    protected override void OnInitialized()
    {
        _isLoading = true;
    
        // 解析返回的state参数
        var queryParams = new Uri(AppState.NavigationManager.Uri).ParseQueryString();
        string returnedState = queryParams["state"];
        string storedState = AppState.NavigationManager.SessionStorage.GetItemAsString("logoutState");
    
        // 校验state合法性
        if (string.IsNullOrWhiteSpace(returnedState) || !returnedState.Equals(storedState))
        {
            AppState.NavigationManager.NavigateTo("/error");
            _isLoading = false;
            return;
        }
    
        // 清除存储的state
        AppState.NavigationManager.SessionStorage.RemoveItem("logoutState");
    
        // ... 省略登出逻辑 ...
    }
    

3. 保留EnforceIdTokenHintOnLogout=true配置

该配置强制要求登出请求携带合法的id_token_hint,能有效防止未经授权的恶意登出请求,修正id_token_hint获取逻辑后即可正常使用。


内容的提问来源于stack exchange,提问作者Leroy Gabriëls

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 03:05:18