IdentityServer4 теряет первоначальный returnUrl при использовании сервера внешнего входа - PullRequest
0 голосов
/ 25 сентября 2018

Я бьюсь головой об этом уже несколько месяцев.До недавнего времени это не стало серьезной проблемой, и я надеюсь, что кто-нибудь сможет мне помочь.К сожалению, учитывая размер проекта, я не могу легко поделиться воспроизводимой версией.Тем не менее, надеюсь, что то, что у меня есть ниже, прояснит мою проблему, и вы увидите, где я допустил ошибку.

У меня есть два сайта: приложение ASP.Net Core MVC и сервер входа в систему, а также ASP.Net Core MVC.Давайте назовем их http://mvc.mysite.com и http://login.mysite.com. Ни то, ни другое существенно не отличается от IdentityServer4 Quickstart # 6.Единственная реальная разница в том, что я реализовал внешний поставщик входа в систему для AzureAd.Мой код для этого ниже.

Сценарий 1

Учитывая внутренний поток входа в систему, где пользователь использует внутреннюю страницу входа в систему в http://login.mysite.com все работаетштраф.

  1. посещения пользователя http://mvc.mysite.com/clients/client-page-1
  2. Пользователь перенаправлен на http://login.mysite.com/Account/Login
  3. Пользователь входит в систему с правильным именем пользователя / паролем
  4. Пользовательперенаправляется на http://mvc.mysite.com/clients/client-page-1

Сценарий 2

Однако, если метод AccountController :: Login () сервера входа в систему определяет, что существует один ExternalLoginProvider, и выполняетв строке «возвращать в ожидании ExternalLogin (vm.ExternalLoginScheme, returnUrl);», тогда первоначальный redirectUrl теряется.

  1. посещения пользователя http://mvc.mysite.com/clients/client-page-1

  2. Пользовательперенаправлен на http://login.mysite.com/Account/Login (получение вывода AccountController :: ExternalLogin)

  3. Пользователь перенаправлен на внешний поставщик OIDC AzureAd

  4. Пользователь входит в систему с правильным именем пользователя / паролем

  5. Пользователь переадресованhttp://login.mysite.com/Account/ExternalLoginCallback

  6. Пользователь перенаправлен на http://mvc.mysite.com (обратите внимание, что пользователь перенаправляется в корневой каталог сайта MVC вместо / clients / client-page-1)

Для сценария 1:

  • С учетом сайта MVC
  • При использовании отладчика для проверки контекста, предоставленного дляOpenIdConnectEvents (например, OnMessageReceived, OnUserInformationReceived и т. д.)
  • Тогда все контексты имеют объект Properties, который содержит RedirectUri == «http://mvc.mysite.com/clients/client-page-1

Для сценария 2:

  • С учетом сайта MVC
  • При использовании отладчика для проверки контекста, предоставленного OpenIdConnectEvents (например, OnMessageReceived, OnUserInformationReceived и т. Д.)
  • Тогдавсе контексты имеют объект Properties, который содержит RedirectUri == «http://mvc.mysite.com” (отсутствует /client.client-page-1)

В Startup.cs моего сервера входа в систему я добавилэто для ConfigureServices:

services.AddAuthentication()
      .AddAzureAd(options =>
      {
            Configuration.Bind("AzureAd", options);
            AzureAdOptions.Settings = options;
      });

РеализацияОписание AddAzureAd выглядит следующим образом: (Вы увидите объекты опций, которые были переданы, я заменил все варианты использования постоянными значениями, кроме ClientId и ClientSecret).

public static class AzureAdAuthenticationBuilderExtensions
{
    public static AuthenticationBuilder AddAzureAd(this AuthenticationBuilder builder, Action<AzureAdOptions> configureOptions)
    {
        builder.AddOpenIdConnect("AzureAd", "Azure AD", options =>
        {
            var opts = new AzureAdOptions();
            configureOptions(opts);
            var config = new ConfigureAzureOptions(opts);
            config.Configure(options);
        });
        return builder;
    }

    private class ConfigureAzureOptions : IConfigureNamedOptions<OpenIdConnectOptions>
    {
        private readonly AzureAdOptions _azureOptions;

        public ConfigureAzureOptions(AzureAdOptions azureOptions)
        {
            _azureOptions = azureOptions;
        }

        public ConfigureAzureOptions(IOptions<AzureAdOptions> azureOptions) : this(azureOptions.Value) {}

        public void Configure(string name, OpenIdConnectOptions options)
        {
            Configure(options);
        }

        public void Configure(OpenIdConnectOptions options)
        {
            options.ClientId = _azureOptions.ClientId;
            options.Authority = "https://login.microsoftonline.com/common"; //_azureOptions.Authority;
            options.UseTokenLifetime = true;
            options.CallbackPath = "/signin-oidc"; // _azureOptions.CallbackPath;
            options.RequireHttpsMetadata = false; // true in production // _azureOptions.RequireHttps;
            options.ClientSecret = _azureOptions.ClientSecret;

            // Add code for hybridflow
            options.ResponseType = "id_token code";

            options.TokenValidationParameters = new IdentityModel.Tokens.TokenValidationParameters
            {
                // instead of using the default validation (validating against a single issuer value, as we do in line of business apps), 
                // we inject our own multitenant validation logic
                ValidateIssuer = false,
            };

            // Subscribing to the OIDC events
            options.Events.OnAuthorizationCodeReceived = OnAuthorizationCodeReceived;
            options.Events.OnAuthenticationFailed = OnAuthenticationFailed;
        }


        /// <summary>
        /// Redeems the authorization code by calling AcquireTokenByAuthorizationCodeAsync in order to ensure
        /// that the cache has a token for the signed-in user.
        /// </summary>
        private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedContext context)
        {
            string userObjectId = (context.Principal.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier"))?.Value;
            var authContext = new AuthenticationContext(context.Options.Authority, new NaiveSessionCache(userObjectId, context.HttpContext.Session));
            var credential = new ClientCredential(context.Options.ClientId, context.Options.ClientSecret);

            var authResult = await authContext.AcquireTokenByAuthorizationCodeAsync(context.TokenEndpointRequest.Code,
                new Uri(context.TokenEndpointRequest.RedirectUri, UriKind.RelativeOrAbsolute), credential, context.Options.Resource);

            // Notify the OIDC middleware that we already took care of code redemption.
            context.HandleCodeRedemption(authResult.AccessToken, context.ProtocolMessage.IdToken);
        }

        private Task OnAuthenticationFailed(AuthenticationFailedContext context)
        {
            throw context.Exception;
        }
    }
}

public class NaiveSessionCache : TokenCache
{
    private static readonly object FileLock = new object();
    string UserObjectId = string.Empty;
    string CacheId = string.Empty;
    ISession Session = null;

    public NaiveSessionCache(string userId, ISession session)
    {
        UserObjectId = userId;
        CacheId = UserObjectId + "_TokenCache";
        Session = session;
        this.AfterAccess = AfterAccessNotification;
        this.BeforeAccess = BeforeAccessNotification;
        Load();
    }

    public void Load()
    {
        lock (FileLock)
            this.Deserialize(Session.Get(CacheId));
    }

    public void Persist()
    {
        lock (FileLock)
        {
            // reflect changes in the persistent store
            Session.Set(CacheId, this.Serialize());
            // once the write operation took place, restore the HasStateChanged bit to false
            this.HasStateChanged = false;
        }
    }

    // Empties the persistent store.
    public override void Clear()
    {
        base.Clear();
        Session.Remove(CacheId);
    }

    public override void DeleteItem(TokenCacheItem item)
    {
        base.DeleteItem(item);
        Persist();
    }

    // Triggered right before ADAL 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 ADAL accessed the cache.
    void AfterAccessNotification(TokenCacheNotificationArgs args)
    {
        // if the access operation resulted in a cache update
        if (this.HasStateChanged)
            Persist();
    }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...