IdentityServer4 как внешний поставщик, как избежать выхода из системы? - PullRequest
1 голос
/ 01 октября 2019

Я работаю с двумя поставщиками удостоверений, оба реализованы с использованием 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.

Есть идеи, как аутентифицировать запрос к внешнему провайдеру?

1 Ответ

1 голос
/ 01 октября 2019

Последний блок кода, который вы ненавидите, но, к счастью, добавил, содержит одну значимую строку:

opts.SaveTokens = true;

Позволяет позже восстановить id_token, полученный от внешнего провайдера.
Тогда вы можете использовать его как «подсказку второго уровня».

if (vm.TriggerExternalSignout)
{
    var url = Url.Action("Logout", new { logoutId = vm.LogoutId });
    var props = new AuthenticationProperties {RedirectUri = url};
    props.SetParameter("id_token_hint", HttpContext.GetTokenAsync("id_token"));
    return SignOut(props, vm.ExternalAuthenticationScheme);
}
...