Я работаю с двумя поставщиками удостоверений, оба реализованы с использованием IdentityServer4 в ASP.NET MVC Core 2.2. Один из них используется в качестве внешнего поставщика другим. Давайте назовем их «первичными» и «внешними». На основного поставщика ссылается непосредственно веб-приложение. Внешний провайдер - это необязательный метод входа в систему, предоставляемый основным провайдером.
Веб-приложение использует библиотеку oidc-client-js для реализации аутентификации. Операция выхода из системы в веб-приложении вызывает UserManager.signoutRedirect
. Это прекрасно работает, когда используется основной поставщик удостоверений (подсказка о выходе из системы не отображается). Однако при использовании внешнего поставщика пользователю предлагается выйти из внешнего поставщика.
Последовательность запросов при выходе из системы:
- GET http://{primary}/connect/endsession?id_token_hint=...&post_logout_redirect_uri=http://{webapp}
- GET http://{primary}/Account/Logout?logoutId=...
- GET http://{external}/connect/endsession?state=...&post_logout_redirect_uri=http://{primary}/signout-callback-{idp}&x-client-SKU=ID_NETSTANDARD2_0&x-client-ver=5.3.0.0
- GET http://{external}/Account/Logout?logoutId=...
Последний запрос, приведенный выше, отображает экран подтверждения выхода из системы от внешнего поставщика.
Код дляСтраница / Account / Logout на основном провайдере практически идентична примеру кода в документации :
[HttpGet]
public async Task<IActionResult> Logout(string logoutId)
{
var vm = await BuildLogoutViewModelAsync(logoutId);
if (!vm.ShowLogoutPrompt)
{
// If the request is authenticated don't show the prompt,
// just log the user out by calling the POST handler directly.
return Logout(vm);
}
return View(vm);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Logout(LogoutInputModel model)
{
var vm = await BuildLoggedOutViewModelAsync(model.LogoutId);
if (User?.Identity.IsAuthenticated)
{
// delete local authentication cookie
await _signInManager.SignOutAsync();
// raise the logout event
await _events.RaiseAsync(new UserLogoutSuccessEvent(User.GetSubjectId(), User.GetDisplayName()));
}
// check if we need to trigger sign-out at an upstream identity provider
if (vm.TriggerExternalSignout)
{
// build a return URL so the upstream provider will redirect back
// to us after the user has logged out. this allows us to then
// complete our single sign-out processing.
var url = Url.Action("Logout", new { logoutId = vm.LogoutId });
// this triggers a redirect to the external provider for sign-out
var ap = new AuthenticationProperties { RedirectUri = url };
return SignOut(ap, vm.ExternalAuthenticationScheme);
}
return View("LoggedOut", vm);
}
Метод BuildLogoutViewModelAsync
вызывает GetLogoutContextAsync
, чтобы проверить, выполнен ли выход из системы. аутентифицируется, например, так:
public async Task<LogoutViewModel> BuildLogoutViewModelAsync(string logoutId)
{
var vm = new LogoutViewModel
{
LogoutId = logoutId,
ShowLogoutPrompt = true
};
var context = await _interaction.GetLogoutContextAsync(logoutId);
if (context?.ShowSignoutPrompt == false)
{
// It's safe to automatically sign-out
vm.ShowLogoutPrompt = false;
}
return vm;
}
Метод BuildLoggedOutViewModelAsync
в основном просто проверяет наличие внешнего провайдера идентификации и устанавливает свойство TriggerExternalSignout
, если оно было использовано.
Я ненавижу делать этостена кода, но я включу код ConfigureServices
, используемый для настройки основного сервера идентификации, потому что он, вероятно, имеет значение:
var authenticationBuilder = services.AddAuthentication();
authenticationBuilder.AddOpenIdConnect(openIdConfig.Scheme, "external", ConfigureOptions);
void ConfigureOptions(OpenIdConnectOptions opts)
{
opts.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
opts.SignOutScheme = IdentityServerConstants.SignoutScheme;
opts.Authority = openIdConfig.ProviderAuthority;
opts.ClientId = openIdConfig.ClientId;
opts.ClientSecret = openIdConfig.ClientSecret;
opts.ResponseType = "code id_token";
opts.RequireHttpsMetadata = false;
opts.CallbackPath = $"/signin-{openIdConfig.Scheme}";
opts.SignedOutCallbackPath = $"/signout-callback-{openIdConfig.Scheme}";
opts.RemoteSignOutPath = $"/signout-{openIdConfig.Scheme}";
opts.Scope.Clear();
opts.Scope.Add("openid");
opts.Scope.Add("profile");
opts.Scope.Add("email");
opts.Scope.Add("phone");
opts.Scope.Add("roles");
opts.SaveTokens = true;
opts.GetClaimsFromUserInfoEndpoint = true;
var mapAdditionalClaims = new[] { JwtClaimTypes.Role, ... };
foreach (string additionalClaim in mapAdditionalClaims)
{
opts.ClaimActions.MapJsonKey(additionalClaim, additionalClaim);
}
opts.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = JwtClaimTypes.Name,
RoleClaimType = JwtClaimTypes.Role
};
}
Насколько я понимаю, параметр id_token_hint
передан первому/ Connect / Endsession Endpoint «аутентифицирует» запрос на выход из системы, что позволяет нам обойти запрос, основываясь на свойстве ShowSignoutPrompt
, возвращаемом GetLogoutContextAsync
. Однако этого не происходит, когда пользователь перенаправляется на внешний поставщик. При вызове SignOut
создается второй URL-адрес / connect / endsession с параметром state
, но без id_token_hint
.
Код выхода из внешнего провайдера в основном совпадает с кодом, показанным выше. Когда он вызывает GetLogoutContextAsync
, этот метод не видит запрос как аутентифицированный, поэтому свойство ShowSignoutPrompt
имеет значение true.
Есть идеи, как аутентифицировать запрос к внешнему провайдеру?