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。
我已经克隆了以下存储库 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。