В ASP.NET Identity я могу безопасно кэшировать пароль пользователя после входа в систему?
У меня есть приложение для интрасети, где все пользовательские операции выполняются вызовами API в удаленной системе (без локальных таблиц). Для нескольких вызовов API требуется пароль пользователя. Я не могу попросить пользователей продолжать повторно вводить пароль, поскольку они используют сайт (иногда через несколько секунд после того, как они только вошли в систему).
Таким образом, не сохраняя свой пароль в базе данных, где я могу безопасно кэшировать пароль на время входа пользователя (примечание: "login", а не "session"). Я попытался сохранить их в состоянии сеанса, но проблема в том, что сеанс длится 20 минут, но токен входа действителен в течение 24 часов.
В идеале я хочу, чтобы он был связан (как-то) напрямую с.AspNet.ApplicationCookie, поэтому логин и кешированный пароль не могут выйти из синхронизации, но он не видит, как можно добавлять пользовательские значения в этот файл cookie. Он может быть зашифрован, если этот файл cookie еще не зашифрован.
EDIT: из-за функции "запомнить меня" логины могут длиться намного дольше, чем значение Session.TimeOut, поэтому я не хочу использовать Session для этого.
Ответы
Ответ 1
У меня был проект, в котором мне пришлось реализовать точно то же самое, и в итоге была создана пользовательская реализация интерфейсов ASP.NET Identity
. (В моем случае имена пользователей и пароли управлялись внешней системой с API.)
Я объясню идею и основные части кода.
Необходимая пользовательская информация (например, имя пользователя и пароль) сохраняется в памяти в ConcurrentDictionary
в пользовательском IUserStore
, по определению место, по которому извлекается userinfo.
Заметка; Я собираюсь пропустить рекомендации по безопасности.
Единственное место для доступа к паролю пользователя - это метод PasswordSignInAsync
пользовательского SignInManager
.
Здесь все по-другому!
В стандартном/регулярном потоке SignInManager
использует IUserStore
для извлечения userinfo для проверки пароля. Но поскольку роль IUserStore
превратилась в хранилище пассивной памяти, которое больше невозможно; этот первоначальный поиск должен быть выполнен, например. поиск базы данных.
Затем SignInManager
выполняет проверку пароля.
Если он действителен, пользовательская информация добавляется или обновляется в пользовательский IUserStore
(с помощью настраиваемого метода в CustomUserStore
.)
Важно также делать обновление каждый раз, когда пользователь подписывается, иначе пароль остается устаревшим, поскольку он хранится в памяти в течение всего срока действия приложения.
В случае, если веб-приложение будет переработано и пользовательская информация в Dictionary
будет потеряна, структура идентификации ASP.NET позаботится об этом, перенаправив пользователя снова на страницу входа, с помощью которой приведенный выше поток начнется снова.
Следующим требованием является пользовательский UserManager
, поскольку мой IUserStore
не реализует все интерфейсы, требуемые идентификатором ASP.NET; см. комментарии в коде. Это может быть иначе для вашего дела.
При всем этом вы получаете CustomUser
через UserManager
; с объектом пользователя, содержащим пароль:
CustomUser user = this._userManager.FindById(userName);
Ниже приведены некоторые выдержки из реализации.
Данные, которые хранятся в памяти:
public class UserInfo
{
String Password { get; set; }
String Id { get; set; }
String UserName { get; set; }
}
Пользовательский IUser
:
public class CustomUser : IUser<String>
{
public String Id { get; }
public String Password { get; set; }
public String UserName { get; set; }
}
Пользовательский IUserStore
с помощью метода для написания:
public interface ICustomUserStore : IUserStore<CustomUser>
{
void CreateOrUpdate(UserInfo user);
}
Пользовательский UserStore
:
public class CustomUserStore : ICustomUserStore
{
private readonly ConcurrentDictionary<String, CustomUser> _users = new ConcurrentDictionary<String, CustomUser>(StringComparer.OrdinalIgnoreCase);
public Task<CustomUser> FindByIdAsync(String userId)
{
// UserId and userName are being treated as the same.
return this.FindByNameAsync(userId);
}
public Task<CustomUser> FindByNameAsync(String userName)
{
if (!this._users.ContainsKey(userName))
{
return Task.FromResult(null as CustomUser);
}
CustomUser user;
if (!this._users.TryGetValue(userName, out user))
{
return Task.FromResult(null as CustomUser);
}
return Task.FromResult(user);
}
public void CreateOrUpdate(UserInfo userInfo)
{
if (userInfo != null)
{
this._users.AddOrUpdate(userInfo.UserName,
// Add.
key => new CustomUser { Id = userInfo.Id, UserName = userInfo.UserName, Password = userInfo.Password) }
// Update; prevent stale password.
(key, value) => {
value.Password = userInfo.Password;
return value
});
}
}
}
Пользовательский UserManager
:
public class CustomUserManager : UserManager<CustomUser>
{
public CustomUserManager(ICustomUserStore userStore)
: base(userStore)
{}
/// Must be overridden because ICustomUserStore does not implement IUserPasswordStore<CustomUser>.
public override Task<Boolean> CheckPasswordAsync(CustomUser user, String password)
{
return Task.FromResult(true);
}
/// Must be overridden because ICustomUserStore does not implement IUserTwoFactorStore<CustomUser>.
public override Task<Boolean> GetTwoFactorEnabledAsync(String userId)
{
return Task.FromResult(false);
}
/// Must be overridden because ICustomUserStore does not implement IUserLockoutStore<CustomUser>.
public override Task<Boolean> IsLockedOutAsync(String userId)
{
return Task.FromResult(false);
}
/// Must be overridden because ICustomUserStore does not implement IUserLockoutStore<CustomUser>.
public override Task<IdentityResult> ResetAccessFailedCountAsync(String userId)
{
Task.FromResult(IdentityResult.Success);
}
}
Пользовательский SignInManager:
public class CustomSignInManager : SignInManager<CustomUser, String>
{
private readonly ICustomUserStore _userStore;
public CustomSignInManager(
CustomUserManager userManager,
IAuthenticationManager authenticationManager
ICustomUserStore userStore
)
: base(userManager, authenticationManager)
{
this._userStore = userStore;
}
/// Provided by the ASP.NET MVC template.
public override Task<ClaimsIdentity> CreateUserIdentityAsync(CustomUser user)
{
return user.GenerateUserIdentityAsync(this.UserManager);
}
public override Task<SignInStatus> PasswordSignInAsync(String userName, String password, Boolean isPersistent, Boolean shouldLockout)
{
UserInfo userInfo = // Call method the retrieve user info from eg. the database.
if (null == userInfo)
{
return Task.FromResult(SignInStatus.Failure);
}
// Do password check; if not OK:
// return Task.FromResult(SignInStatus.Failure);
// Password is OK; set data to the store.
this._userStore.CreateOrUpdate(userInfo);
// Execute the default flow, which will now use the IUserStore with the user present.
return base.PasswordSignInAsync(userName, password, isPersistent, shouldLockout);
}
}
Ответ 2
Отказ от ответственности: здесь вы вводите пароль в файл cookie. Зашифрованный файл cookie, но пароль. Это не лучшая практика с точки зрения безопасности. Поэтому принимайте решение самостоятельно, если это приемлемо для вашей системы или нет.
Я думаю, что лучшим способом для этого было бы хранить пароль в качестве претензий к файлу cookie аутентификации. Auth cookie шифруется при передаче, но вам не нужно разбираться с самим шифрованием - это делается OWIN для вас. И это требует гораздо меньше сантехники.
Сначала перепишите действие для входа в систему следующим образом:
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
if (!ModelState.IsValid)
{
return View(model);
}
var user = await UserManager.FindAsync(model.Email, model.Password);
if (user == null)
{
// user with this username/password not found
ModelState.AddModelError("", "Invalid login attempt.");
return View(model);
}
// BEWARE this does not check if user is disabled, locked or does not have a confirmed user
// I'll leave this for you to implement if needed.
var userIdentity = await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);
userIdentity.AddClaim(new Claim("MyApplication:Password", model.Password));
AuthenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = true }, userIdentity);
return RedirectToLocal(returnUrl);
}
Это берет пароль при входе в систему и добавляет его в качестве требования к Identity, которое, в свою очередь, становится сериализованным и зашифрованным в файл cookie.
Обратите внимание, что здесь не указана много логики - если вам нужно проверить, отключен ли пользователь, заблокирован или нет подтвержденного письма, вам нужно добавить это самостоятельно. Я подозреваю, что вам это не понадобится, поскольку вы упомянули, что это внутренний сайт.
Затем вам понадобится метод расширения для извлечения пароля:
using System;
using System.Security.Claims;
using System.Security.Principal;
public static class PrincipalExtensions
{
public static String GetStoredPassword(this IPrincipal principal)
{
var claimsPrincipal = principal as ClaimsPrincipal;
if (claimsPrincipal == null)
{
throw new Exception("Expecting ClaimsPrincipal");
}
var passwordClaim = claimsPrincipal.FindFirst("MyApplication:Password");
if (passwordClaim == null)
{
throw new Exception("Password is not stored");
}
var password = passwordClaim.Value;
return password;
}
}
Это в значительной степени. Теперь в каждом действии вы можете применить этот метод к свойству User
:
[Authorize]
public ActionResult MyPassword()
{
var myPassword = User.GetStoredPassword();
return View((object)myPassword);
}
Соответствующий вид будет таким:
@model String
<h2>Password is @Model</h2>
Однако, в зависимости от ваших требований, эта заявка на пароль может быть убита с течением времени или сохранена. Шаблон Identity по умолчанию позволяет SecurityStampInvalidator
который выполняется каждые 30 минут в файле cookie и перезаписывает его из базы данных. Обычно ad-hoc заявления, подобные этому, не выдерживают этого переписывания.
Чтобы сохранить значение пароля за последние 30 минут в возрасте печенья, возьмите этот класс:
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.Owin.Security.Cookies;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
// This is mostly copy of original security stamp validator, only with addition to keep hold of password claim
// https://github.com/aspnet/AspNetIdentity/blob/a24b776676f12cf7f0e13944783cf8e379b3ef70/src/Microsoft.AspNet.Identity.Owin/SecurityStampValidator.cs#L1
public class MySecurityStampValidator
{
/// <summary>
/// Can be used as the ValidateIdentity method for a CookieAuthenticationProvider which will check a user security
/// stamp after validateInterval
/// Rejects the identity if the stamp changes, and otherwise will call regenerateIdentity to sign in a new
/// ClaimsIdentity
/// </summary>
/// <typeparam name="TManager"></typeparam>
/// <typeparam name="TUser"></typeparam>
/// <param name="validateInterval"></param>
/// <param name="regenerateIdentity"></param>
/// <returns></returns>
public static Func<CookieValidateIdentityContext, Task> OnValidateIdentity<TManager, TUser>(
TimeSpan validateInterval, Func<TManager, TUser, Task<ClaimsIdentity>> regenerateIdentity)
where TManager : UserManager<TUser, string>
where TUser : class, IUser<string>
{
return OnValidateIdentity(validateInterval, regenerateIdentity, id => id.GetUserId());
}
/// <summary>
/// Can be used as the ValidateIdentity method for a CookieAuthenticationProvider which will check a user security
/// stamp after validateInterval
/// Rejects the identity if the stamp changes, and otherwise will call regenerateIdentity to sign in a new
/// ClaimsIdentity
/// </summary>
/// <typeparam name="TManager"></typeparam>
/// <typeparam name="TUser"></typeparam>
/// <typeparam name="TKey"></typeparam>
/// <param name="validateInterval"></param>
/// <param name="regenerateIdentityCallback"></param>
/// <param name="getUserIdCallback"></param>
/// <returns></returns>
public static Func<CookieValidateIdentityContext, Task> OnValidateIdentity<TManager, TUser, TKey>(
TimeSpan validateInterval, Func<TManager, TUser, Task<ClaimsIdentity>> regenerateIdentityCallback,
Func<ClaimsIdentity, TKey> getUserIdCallback)
where TManager : UserManager<TUser, TKey>
where TUser : class, IUser<TKey>
where TKey : IEquatable<TKey>
{
if (getUserIdCallback == null)
{
throw new ArgumentNullException("getUserIdCallback");
}
return async context =>
{
var currentUtc = DateTimeOffset.UtcNow;
if (context.Options != null && context.Options.SystemClock != null)
{
currentUtc = context.Options.SystemClock.UtcNow;
}
var issuedUtc = context.Properties.IssuedUtc;
// Only validate if enough time has elapsed
var validate = (issuedUtc == null);
if (issuedUtc != null)
{
var timeElapsed = currentUtc.Subtract(issuedUtc.Value);
validate = timeElapsed > validateInterval;
}
if (validate)
{
var manager = context.OwinContext.GetUserManager<TManager>();
var userId = getUserIdCallback(context.Identity);
if (manager != null && userId != null)
{
var user = await manager.FindByIdAsync(userId);
var reject = true;
// Refresh the identity if the stamp matches, otherwise reject
if (user != null && manager.SupportsUserSecurityStamp)
{
var securityStamp =
context.Identity.FindFirstValue(Constants.DefaultSecurityStampClaimType);
if (securityStamp == await manager.GetSecurityStampAsync(userId))
{
reject = false;
// Regenerate fresh claims if possible and resign in
if (regenerateIdentityCallback != null)
{
var identity = await regenerateIdentityCallback.Invoke(manager, user);
if (identity != null)
{
var passwordClaim = context.Identity.FindFirst("MyApplication:Password");
if (passwordClaim != null)
{
identity.AddClaim(passwordClaim);
}
// Fix for regression where this value is not updated
// Setting it to null so that it is refreshed by the cookie middleware
context.Properties.IssuedUtc = null;
context.Properties.ExpiresUtc = null;
context.OwinContext.Authentication.SignIn(context.Properties, identity);
}
}
}
}
if (reject)
{
context.RejectIdentity();
context.OwinContext.Authentication.SignOut(context.Options.AuthenticationType);
}
}
}
};
}
}
Обратите внимание, что это прямая копия исходного кода Identity с незначительной модификацией для сохранения заявки на получение пароля.
И чтобы активировать этот класс, в вашем Startup.Auth.cs выполните следующее:
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider
{
// use MySecurityStampValidator here
OnValidateIdentity = MySecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
validateInterval: TimeSpan.FromMinutes(10), // adjust time as required
regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
}
});
Вот пример рабочего кода