Azure B2C 网络核心开发工作流

Azure B2C Net Core Development Workflow

我已经克隆了以下存储库 active-directory-b2c-dotnetcore-webapp 并将其用作新应用程序的起点。一切都按预期工作,直到我在代码更改后重新编译我的代码。似乎每当我在正常开发过程中重新编译我的应用程序时,acquireTokenSilent 函数 returns 就会显示一条会话过期消息。这显然是有问题的,因为它迫使我在每次代码更改后重新向我的 azure 租户进行身份验证。也许这与配置的 .net 核心缓存策略有关,而不是 azure b2c。这是startup.cs中的身份验证中间件:

public void ConfigureServices(IServiceCollection services)
    {
        services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();

        services.AddAuthentication(sharedOptions =>
        {
            sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            sharedOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
        })
        .AddAzureAdB2C(options => Configuration.Bind("Authentication:AzureAdB2C", options))
        .AddCookie();

        // Add framework services.
        services.AddMvc();

        // Adds a default in-memory implementation of IDistributedCache.
        services.AddDistributedMemoryCache();
        services.AddSession(options =>
        {
            options.IdleTimeout = TimeSpan.FromHours(1);
            options.CookieHttpOnly = true;
        });


    }

以及处理令牌缓存的 class:

   public class MSALSessionCache
    {
        private static ReaderWriterLockSlim SessionLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
        string UserId = string.Empty;
        string CacheId = string.Empty;
        HttpContext httpContext = null;

        TokenCache cache = new TokenCache();



   public MSALSessionCache(string userId, HttpContext httpcontext)
    {
        // not object, we want the SUB
        UserId = userId;
        CacheId = UserId + "_TokenCache";
        httpContext = httpcontext;
        Load();
    }

    public TokenCache GetMsalCacheInstance()
    {
        cache.SetBeforeAccess(BeforeAccessNotification);
        cache.SetAfterAccess(AfterAccessNotification);
        Load();
        return cache;
    }

    public void SaveUserStateValue(string state)
    {
        SessionLock.EnterWriteLock();
        httpContext.Session.SetString(CacheId + "_state", state);
        SessionLock.ExitWriteLock();
    }
    public string ReadUserStateValue()
    {
        string state = string.Empty;
        SessionLock.EnterReadLock();
        state = (string)httpContext.Session.GetString(CacheId + "_state");
        SessionLock.ExitReadLock();
        return state;
    }
    public void Load()
    {
        SessionLock.EnterReadLock();
        cache.Deserialize(httpContext.Session.Get(CacheId));
        SessionLock.ExitReadLock();
    }

    public void Persist()
    {
        SessionLock.EnterWriteLock();

        // Optimistically set HasStateChanged to false. We need to do it early to avoid losing changes made by a concurrent thread.
        cache.HasStateChanged = false;

        // Reflect changes in the persistent store
        httpContext.Session.Set(CacheId, cache.Serialize());
        SessionLock.ExitWriteLock();
    }

    // Triggered right before MSAL needs to access the cache.
    // Reload the cache from the persistent store in case it changed since the last access.
    void BeforeAccessNotification(TokenCacheNotificationArgs args)
    {
        Load();
    }

    // Triggered right after MSAL accessed the cache.
    void AfterAccessNotification(TokenCacheNotificationArgs args)
    {
        // if the access operation resulted in a cache update
        if (cache.HasStateChanged)
        {
            Persist();
        }
    }
}

在以下代码块中调用 acquireTokenSilentAsync 后会引发会话过期。这只会在重新编译应用程序后发生:

[Authorize]
    public async Task<IActionResult> Api()
    {
        string responseString = "";
        try
        {
            // Retrieve the token with the specified scopes
            var scope = AzureAdB2COptions.ApiScopes.Split(' ');
            string signedInUserID = HttpContext.User.FindFirst(ClaimTypes.NameIdentifier).Value;
            TokenCache userTokenCache = new MSALSessionCache(signedInUserID, this.HttpContext).GetMsalCacheInstance();
            ConfidentialClientApplication cca = new ConfidentialClientApplication(AzureAdB2COptions.ClientId, AzureAdB2COptions.Authority, AzureAdB2COptions.RedirectUri, new ClientCredential(AzureAdB2COptions.ClientSecret), userTokenCache, null);

            AuthenticationResult result = await cca.AcquireTokenSilentAsync(scope, cca.Users.FirstOrDefault(), AzureAdB2COptions.Authority, false);

            HttpClient client = new HttpClient();
            HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, AzureAdB2COptions.ApiUrl);

            // Add token to the Authorization header and make the request
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
            HttpResponseMessage response = await client.SendAsync(request);

            // Handle the response
            switch (response.StatusCode)
            {
                case HttpStatusCode.OK:
                    responseString = await response.Content.ReadAsStringAsync();
                    break;
                case HttpStatusCode.Unauthorized:
                    responseString = $"Please sign in again. {response.ReasonPhrase}";
                    break;
                default:
                    responseString = $"Error calling API. StatusCode=${response.StatusCode}";
                    break;
            }
        }
        catch (MsalUiRequiredException ex)
        {
            responseString = $"Session has expired. Please sign in again. {ex.Message}";
        }
        catch (Exception ex)
        {
            responseString = $"Error calling API: {ex.Message}";
        }

        ViewData["Payload"] = $"{responseString}";            
        return View();
    }

有没有办法让这个会话在重新编译后仍然存在?

我发现许多与 B2C 身份验证相关的 azure 样本对于常见用例来说是不完整的。幸运的是,我偶然发现了这个 repository,它添加了一个 Reauthenticate 全局过滤器。我修改了过滤器以适合我的 OpenIdAuthenticationScheme,如下所示:

 internal class ReauthenticationRequiredException : Exception
{
}
internal class ReauthenticationRequiredFilter : IExceptionFilter
{
    private readonly AzureAdB2COptions _options;

    public ReauthenticationRequiredFilter(IOptions<AzureAdB2COptions> options)
    {
        this._options = options.Value;
    }

    public void OnException(ExceptionContext context)
    {
        if (!context.ExceptionHandled && IsReauthenticationRequired(context.Exception))
        {

            context.Result = new ChallengeResult(
                OpenIdConnectDefaults.AuthenticationScheme,
                new AuthenticationProperties {RedirectUri = context.HttpContext.Request.Path});


            context.ExceptionHandled = true;
        }
    }

    private static bool IsReauthenticationRequired(Exception exception)
    {
        if (exception is ReauthenticationRequiredException)
        {
            return true;
        }

        if (exception.InnerException != null)
        {
            return IsReauthenticationRequired(exception.InnerException);
        }

        return false;
    }
}

现在,在重新编译我的应用程序后,该应用程序只是重定向到似乎刷新了令牌的租户。我希望我能投票给原作者!感谢贡献。

这与令牌缓存有关。

The sample README 注释 MSALSessionCache 是令牌缓存的示例实现。此示例实现将缓存数据保存在内存中。它不会在应用程序重新启动时保留缓存数据,也不会在服务器场中共享它(除非您有粘性会话)。

有关将缓存数据持久保存到分布式缓存的选项,请参阅here