User.Identity колеблется между ClaimsIdentity и WindowsIdentity.

У меня есть сайт MVC, который позволяет входить в систему как с помощью входа в формы, так и с помощью проверки подлинности Windows. Я использую собственный MembershipProvider, который аутентифицирует пользователей в Active Directory, класс System.Web.Helpers AntiForgery для защиты от CSRF и промежуточное ПО для проверки подлинности файлов cookie Owin.

Во время входа в систему, когда пользователь прошел аутентификацию в Active Directory, я делаю следующее:

IAuthenticationManager authenticationManager = HttpContext.Current.GetOwinContext().Authentication;
authenticationManager.SignOut(StringConstants.ApplicationCookie);
var identity = new ClaimsIdentity(StringConstants.ApplicationCookie,
    ClaimsIdentity.DefaultNameClaimType,
    ClaimsIdentity.DefaultRoleClaimType);
if(HttpContext.Current.User.Identity is WindowsIdentity)
{
    identity.AddClaims(((WindowsIdentity)HttpContext.Current.User.Identity).Claims);
}
else
{
    identity.AddClaim(new Claim(ClaimTypes.Name, userData.Name));
}
identity.AddClaim(new Claim("http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider", "Active Directory"));
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, userData.userGuid));
authenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = false }, identity);

Моя функция SignOut выглядит так:

IAuthenticationManager authenticationManager = HttpContext.Current.GetOwinContext().Authentication;
authenticationManager.SignOut(StringConstants.ApplicationCookie);

Вход осуществляется через запрос jQuery.ajax. В случае успеха Window.location обновляется до главной страницы сайта.

Вход с помощью Forms и IntegratedWindowsAuthentication (IWA) работает, но я столкнулся с проблемой при входе в IWA. Вот что происходит:

  1. Пользователь выбирает IWA на странице входа и нажимает кнопку отправки. Это отправляется в обычное действие входа в систему через запрос ajax.
  2. Сайт получает запрос, видит опцию «использовать IWA» и перенаправляет на соответствующее действие. 302 ответ отправлен.
  3. Браузер автоматически обрабатывает ответ 302 и вызывает цель перенаправления.
  4. Фильтр видит, что запрос направлен на действие входа в IWA и что User.Identity.IsAuthenticated == false. Отправляется ответ 401.
  5. Браузер автоматически обрабатывает ответ 401. Если пользователь еще не прошел аутентификацию с помощью IWA в браузере, для этого появится всплывающее окно (поведение браузера по умолчанию). После получения учетных данных браузер выполняет тот же запрос с учетными данными пользователя.
  6. Сайт получает аутентифицированный запрос и олицетворяет пользователя, чтобы выполнить проверку Active Directory. Если пользователь проходит аутентификацию, мы завершаем SignIn, используя приведенный выше код.
  7. Пользователь перенаправляется на главную страницу сайта.
  8. Сайт получает запрос на загрузку главной страницы. Именно здесь вещи иногда идут наперекосяк.
    User.Identity в этот момент имеет тип WindowsIdentity с AuthenticationType установленным на Negotiate и НЕ как Я ожидаю, что ClaimsIdentity создан в методе SignIn выше.
    Сайт подготавливает главную страницу для пользователя, вызывая @AntiForgery.GetHtml() в представлении. Это делается для создания нового токена AntiForgery с данными вошедшего в систему пользователя. Токен создается с помощью WindowsIdentity
  9. Когда главная страница загружается, ajax-запросы к серверу приходят с ClaimsIdentity! Таким образом, первый поступивший запрос POST неизбежно вызывает AntiForgeryException, где отправленный им токен защиты от подделки предназначен «для другого пользователя».

Обновление страницы приводит к загрузке главной страницы с ClaimsIdentity и разрешает выполнение POST запросов.

Вторая проблема, связанная с этим: в любой момент после обновления, когда все предположительно работает нормально, запрос POST может прийти с WindowsIdentity, а не с ClaimsIdentity, снова выдавая AntiForgeryException.

  • Это не какой-либо конкретный почтовый запрос,
  • это нет через какое-то определенное время (может быть первый/второй запрос, может быть сотый),
  • это необязательно, когда во время этого сеанса вызывается конкретный запрос на публикацию в первый раз.

Я чувствую, что либо что-то упускаю из виду в отношении User.Identity, либо что-то сделал не так в процессе входа в систему... Есть идеи?

Примечание. Параметр AntiForgeryConfig.SuppressIdentityHeuristicChecks = true; позволяет выполнить действие AntiForgery.Validate независимо от того, получены ли WindowsIdentity или ClaimsIdentity, но, как указано в MSDN:

Будьте осторожны при установке этого значения. Неправильное использование может открыть уязвимости безопасности в приложении.

Без каких-либо дополнительных объяснений, я не знаю, какие уязвимости безопасности на самом деле открываются здесь, и поэтому мне не хочется использовать это в качестве решения.


person Guy Passy    schedule 08.05.2018    source источник
comment
Почему вы вообще удосужились бросить. Вы могли бы сделать HttpContext.Current.User.Identity.Claims вообще без актерского состава.   -  person Scott Chamberlain    schedule 10.05.2018
comment
@ScottChamberlain IIdentity не имеет свойства Claims   -  person Guy Passy    schedule 13.05.2018
comment
Какую версию ASP.net вы используете?   -  person Scott Chamberlain    schedule 13.05.2018
comment
@ Скотт Чемберлен 4.5.2   -  person Guy Passy    schedule 13.05.2018


Ответы (2)


Оказывается, проблема заключалась в том, что ClaimsPrincipal поддерживает несколько идентификаторов. Если вы находитесь в ситуации, когда у вас есть несколько идентификаторов, он выбирает один самостоятельно. Я не знаю, что определяет порядок идентификаторов в IEnumerable, но что бы это ни было, оно, по-видимому, обязательно приводит к постоянному порядку в течение жизненного цикла сеанса пользователя.

Как упоминалось в разделе «Проблемы» asp.net/Security git, проверка подлинности NTLM и cookie #1467:

Identities содержит как идентификатор Windows, так и идентификатор файла cookie.

а также

Похоже, что с помощью ClaimsPrincipals вы можете установить static Func<IEnumerable<ClaimsIdentity>, ClaimsIdentity> с именем PrimaryIdentitySelector, которое вы можете использовать для выбора основного идентификатора для работы.

Для этого создадим статический метод с сигнатурой:

static ClaimsIdentity MyPrimaryIdentitySelectorFunc(IEnumerable<ClaimsIdentity> identities)

Этот метод будет использоваться для просмотра списка ClaimsIdentity и выбора того, который вам больше нравится.
Затем в файле Global.asax.cs установите этот метод как PrimaryIdentitySelector, например:

System.Security.Claims.ClaimsPrincipal.PrimaryIdentitySelector = MyPrimaryIdentitySelectorFunc;

Мой метод PrimaryIdentitySelector в итоге выглядел так:

public static ClaimsIdentity PrimaryIdentitySelector(IEnumerable<ClaimsIdentity> identities)
{
    //check for null (the default PIS also does this)
    if (identities == null) throw new ArgumentNullException(nameof(identities));

    //if there is only one, there is no need to check further
    if (identities.Count() == 1) return identities.First();

    //Prefer my cookie identity. I can recognize it by the IdentityProvider
    //claim. This doesn't need to be a unique value, simply one that I know
    //belongs to the cookie identity I created. AntiForgery will use this
    //identity in the anti-CSRF check.
    var primaryIdentity = identities.FirstOrDefault(identity => {
        return identity.Claims.FirstOrDefault(c => {
            return c.Type.Equals(StringConstants.ClaimTypes_IdentityProvider, StringComparison.Ordinal) &&
                   c.Value == StringConstants.Claim_IdentityProvider;
        }) != null;
    });

    //if none found, default to the first identity
    if (primaryIdentity == null) return identities.First();

    return primaryIdentity;
}

[Изменить]
Теперь этого оказалось недостаточно, так как PrimaryIdentitySelector не запускается, когда в списке Identities есть только один Identity. Это вызывало проблемы на странице входа в систему, когда иногда браузер пропускал WindowsIdentity при загрузке страницы, но не передал его при запросе входа {раздраженно вздыхает}. Чтобы решить это, я создал ClaimsIdentity для страницы входа, а затем вручную перезаписал участника потока, как описано в это ТАК вопрос.

Это создает проблему с проверкой подлинности Windows, поскольку OnAuthenticate не будет отправлять 401 для запроса удостоверения Windows. Чтобы решить эту проблему, вы должны выйти из своей учетной записи. Если не удается войти в систему, обязательно заново создайте пользователя для входа. (Вам также может потребоваться воссоздать токен CSRF)

person Guy Passy    schedule 14.05.2018

Я не уверен, поможет ли это, но вот как я решил эту проблему для меня.

Когда я добавил аутентификацию Windows, она начала колебаться между удостоверениями Windows и Claims. Я заметил, что GET запросы получают ClaimsIdentity, а POST запросы получают WindowsIdentity. Это было очень неприятно, и я решил отладить и поставить точку останова на DefaultHttpContext.set_User. IISMiddleware устанавливал свойство User, затем я заметил, что у него есть AutomaticAuthentication, по умолчанию true, который устанавливает свойство User. Я изменил это значение на false, поэтому HttpContext.User все время становилось ClaimsPrincipal, ура.

Не моя проблема заключается в том, как я могу использовать проверку подлинности Windows. К счастью, даже если я установлю AutomaticAuthentication на false, IISMiddleware обновит HttpContext.Features на WindowsPrincipal, поэтому var windowsUser = HttpContext.Features.Get<WindowsPrincipal>(); вернет пользователя Windows на мою страницу единого входа.

Сейчас все работает нормально, без заминок, ни флуктуаций, ничего. База форм и проверка подлинности Windows работают вместе.

services.Configure<IISOptions>(opts =>
{
    opts.AutomaticAuthentication = false;
});
person Erdogan Kurtur    schedule 03.09.2020