mirror of
https://github.com/OrchardCMS/Orchard.git
synced 2026-01-19 17:51:45 +08:00
User data providers and cookie expiration setting (#7945)
* Added providers to compute and validate the userdata string for authentication cookies * Added UserDataProvider based on the date when the password was changed last: this means that changing the password invalidates all extant authentication cookies * Added a service that allows configuring the lifespan of auth cookies, rather than always using a constant. The default implementation returns the same constant. * Added setting to control the life span of authentication cookies * Added setting to control the behaviour on changing password: logout every client, or none. Added code to prevent users that were authenticated with "old" cookies to be suddenly logged out when deploying this * Removed ISecurityService implementation from Orchard.Users. Changed FormsAuthenticationService and DefaultSecurityService so that property injection configured in sites.config still works the same as before, and takes priority over ISecurityService implementations. Changed serialization of UserData dictionary to use Newtonsoft.Json. Added check to upgrade the authentication cookie whenever an "old" one is received. * On cookie upgrade we do not give a fresh new cookie, but rather we keep the same expiration
This commit is contained in:
committed by
Sébastien Ros
parent
afb04c7d45
commit
0754e7ba65
@@ -19,6 +19,8 @@ namespace Orchard.OpenId.Services {
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly IMembershipValidationService _membershipValidationService;
|
||||
private readonly IEnumerable<IOpenIdProvider> _openIdProviders;
|
||||
private readonly IEnumerable<IUserDataProvider> _userDataProviders;
|
||||
private readonly ISecurityService _securityService;
|
||||
|
||||
private IUser _localAuthenticationUser;
|
||||
|
||||
@@ -26,7 +28,7 @@ namespace Orchard.OpenId.Services {
|
||||
private IAuthenticationService FallbackAuthenticationService {
|
||||
get {
|
||||
if (_fallbackAuthenticationService == null)
|
||||
_fallbackAuthenticationService = new FormsAuthenticationService(_settings, _clock, _membershipService, _httpContextAccessor, _sslSettingsProvider, _membershipValidationService);
|
||||
_fallbackAuthenticationService = new FormsAuthenticationService(_settings, _clock, _membershipService, _httpContextAccessor, _sslSettingsProvider, _membershipValidationService, _userDataProviders, _securityService);
|
||||
|
||||
return _fallbackAuthenticationService;
|
||||
}
|
||||
@@ -39,7 +41,9 @@ namespace Orchard.OpenId.Services {
|
||||
ISslSettingsProvider sslSettingsProvider,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IMembershipValidationService membershipValidationService,
|
||||
IEnumerable<IOpenIdProvider> openIdProviders) {
|
||||
IEnumerable<IOpenIdProvider> openIdProviders,
|
||||
IEnumerable<IUserDataProvider> userDataProviders,
|
||||
ISecurityService securityService) {
|
||||
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_membershipService = membershipService;
|
||||
@@ -48,6 +52,8 @@ namespace Orchard.OpenId.Services {
|
||||
_sslSettingsProvider = sslSettingsProvider;
|
||||
_membershipValidationService = membershipValidationService;
|
||||
_openIdProviders = openIdProviders;
|
||||
_userDataProviders = userDataProviders;
|
||||
_securityService = securityService;
|
||||
}
|
||||
|
||||
public void SignIn(IUser user, bool createPersistentCookie) {
|
||||
|
||||
@@ -227,6 +227,10 @@ namespace Orchard.Users.Controllers {
|
||||
var membershipSettings = _membershipService.GetSettings();
|
||||
ViewData["PasswordLength"] = membershipSettings.GetMinimumPasswordLength();
|
||||
|
||||
ViewData["InvalidateOnPasswordChange"] = _orchardServices.WorkContext
|
||||
.CurrentSite.As<SecuritySettingsPart>()
|
||||
.ShouldInvalidateAuthOnPasswordChanged;
|
||||
|
||||
return View();
|
||||
}
|
||||
|
||||
@@ -238,7 +242,10 @@ namespace Orchard.Users.Controllers {
|
||||
Justification = "Exceptions result in password not being changed.")]
|
||||
public ActionResult ChangePassword(string currentPassword, string newPassword, string confirmPassword) {
|
||||
var membershipSettings = _membershipService.GetSettings();
|
||||
ViewData["PasswordLength"] = membershipSettings.GetMinimumPasswordLength(); ;
|
||||
ViewData["PasswordLength"] = membershipSettings.GetMinimumPasswordLength();
|
||||
ViewData["InvalidateOnPasswordChange"] = _orchardServices.WorkContext
|
||||
.CurrentSite.As<SecuritySettingsPart>()
|
||||
.ShouldInvalidateAuthOnPasswordChanged;
|
||||
|
||||
if (!ValidateChangePassword(currentPassword, newPassword, confirmPassword)) {
|
||||
return View();
|
||||
@@ -296,7 +303,13 @@ namespace Orchard.Users.Controllers {
|
||||
if (validated != null) {
|
||||
_membershipService.SetPassword(validated, newPassword);
|
||||
_userEventHandler.ChangedPassword(validated);
|
||||
// if security settings tell to invalidate on password change fire the LoggedOut event
|
||||
if (_orchardServices.WorkContext
|
||||
.CurrentSite.As<SecuritySettingsPart>()
|
||||
.ShouldInvalidateAuthOnPasswordChanged) {
|
||||
|
||||
_userEventHandler.LoggedOut(validated);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -354,6 +367,9 @@ namespace Orchard.Users.Controllers {
|
||||
|
||||
[AlwaysAccessible]
|
||||
public ActionResult ChangePasswordSuccess() {
|
||||
ViewData["InvalidateOnPasswordChange"] = _orchardServices.WorkContext
|
||||
.CurrentSite.As<SecuritySettingsPart>()
|
||||
.ShouldInvalidateAuthOnPasswordChanged;
|
||||
return View();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
using Orchard.ContentManagement;
|
||||
using Orchard.ContentManagement.Handlers;
|
||||
using Orchard.Localization;
|
||||
using Orchard.Users.Models;
|
||||
|
||||
namespace Orchard.Users.Handlers {
|
||||
public class SecuritySettingsPartHandler : ContentHandler {
|
||||
|
||||
public SecuritySettingsPartHandler() {
|
||||
T = NullLocalizer.Instance;
|
||||
Filters.Add(new ActivatingFilter<SecuritySettingsPart>("Site"));
|
||||
Filters.Add(new TemplateFilterForPart<SecuritySettingsPart>("SecuritySettings", "Parts/Users.SecuritySettings", "users"));
|
||||
}
|
||||
|
||||
public Localizer T { get; set; }
|
||||
|
||||
protected override void GetItemMetadata(GetContentItemMetadataContext context) {
|
||||
if (context.ContentItem.ContentType != "Site")
|
||||
return;
|
||||
base.GetItemMetadata(context);
|
||||
context.Metadata.EditorGroupInfo.Add(new GroupInfo(T("Users")));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using Orchard.ContentManagement;
|
||||
|
||||
namespace Orchard.Users.Models {
|
||||
public class SecuritySettingsPart : ContentPart {
|
||||
|
||||
/// <summary>
|
||||
/// The way this setting works is that it controls the behaviour of
|
||||
/// PasswordChangedDateUserDataProvider. If the setting is true, when the password
|
||||
/// changes all extant authentication tokens using that provider to generate UserData
|
||||
/// become invalid. If this setting is set to true, all authenticated user who have
|
||||
/// changed their password between the moment the setting is enabled and their last login
|
||||
/// (i.e. the last time they have been provided an authentication cookie) will be logged out,
|
||||
/// because the information in the cookie will fail to validate, but the LoggedOut event
|
||||
/// will not have fired for them.
|
||||
/// </summary>
|
||||
public bool ShouldInvalidateAuthOnPasswordChanged {
|
||||
get { return this.Retrieve(x => x.ShouldInvalidateAuthOnPasswordChanged); }
|
||||
set { this.Store(x => x.ShouldInvalidateAuthOnPasswordChanged, value); }
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -114,6 +114,7 @@
|
||||
<Compile Include="Forms\SignInUserForm.cs" />
|
||||
<Compile Include="Forms\VerifyUserUnicityForm.cs" />
|
||||
<Compile Include="Forms\CreateUserForm.cs" />
|
||||
<Compile Include="Handlers\SecuritySettingsPartHandler.cs" />
|
||||
<Compile Include="Handlers\WorkflowUserEventHandler.cs" />
|
||||
<Compile Include="Extensions\ModelStateDictionaryExtensions.cs" />
|
||||
<Compile Include="Migrations.cs" />
|
||||
@@ -122,6 +123,7 @@
|
||||
<Compile Include="Events\IUserEventHandler.cs" />
|
||||
<Compile Include="Models\MessageTypes.cs" />
|
||||
<Compile Include="Models\RegistrationSettingsPart.cs" />
|
||||
<Compile Include="Models\SecuritySettingsPart.cs" />
|
||||
<Compile Include="Models\UserPart.cs" />
|
||||
<Compile Include="Handlers\UserPartHandler.cs" />
|
||||
<Compile Include="Models\UserPartRecord.cs" />
|
||||
@@ -131,6 +133,7 @@
|
||||
<Compile Include="Services\AuthenticationRedirectionFilter.cs" />
|
||||
<Compile Include="Services\IUserService.cs" />
|
||||
<Compile Include="Services\MembershipValidationService.cs" />
|
||||
<Compile Include="Services\PasswordChangedDateUserDataProvider.cs" />
|
||||
<Compile Include="Services\UserResolverSelector.cs" />
|
||||
<Compile Include="Services\MembershipService.cs" />
|
||||
<Compile Include="AdminMenu.cs" />
|
||||
@@ -249,6 +252,9 @@
|
||||
<ItemGroup>
|
||||
<Content Include="Views\Account\ChangeExpiredPassword.cshtml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="Views\EditorTemplates\Parts\Users.SecuritySettings.cshtml" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
|
||||
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
|
||||
@@ -293,4 +299,4 @@
|
||||
</FlavorProperties>
|
||||
</VisualStudio>
|
||||
</ProjectExtensions>
|
||||
</Project>
|
||||
</Project>
|
||||
@@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Orchard.ContentManagement;
|
||||
using Orchard.Security;
|
||||
using Orchard.Security.Providers;
|
||||
using Orchard.Settings;
|
||||
using Orchard.Users.Models;
|
||||
|
||||
namespace Orchard.Users.Services {
|
||||
public class PasswordChangedDateUserDataProvider : BaseUserDataProvider {
|
||||
|
||||
|
||||
private readonly ISiteService _siteService;
|
||||
|
||||
public PasswordChangedDateUserDataProvider(
|
||||
ISiteService siteService) : base(true) {
|
||||
// By calling base(true) we set DefaultValid to true. This means that cookies whose
|
||||
// UserData dictionary does not contain the entry from this provider will be valid.
|
||||
|
||||
_siteService = siteService;
|
||||
}
|
||||
|
||||
protected override bool DefaultValid {
|
||||
get {
|
||||
return !_siteService
|
||||
.GetSiteSettings()
|
||||
.As<SecuritySettingsPart>()
|
||||
.ShouldInvalidateAuthOnPasswordChanged;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool IsValid(IUser user, IDictionary<string, string> userData) {
|
||||
|
||||
return DefaultValid || base.IsValid(user, userData);
|
||||
}
|
||||
|
||||
protected override string Value(IUser user) {
|
||||
var part = user.As<UserPart>();
|
||||
if (part == null) {
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var date = GetDate(part);
|
||||
return date.ToString();
|
||||
}
|
||||
|
||||
private DateTime GetDate(UserPart part) {
|
||||
// CreatedUTC should never require a default value to fallback to.
|
||||
var created = DateOrDefault(part.CreatedUtc);
|
||||
// LastPasswordChangeUtc may require a value to fallback to for users that have not changed their
|
||||
// password since migration UpdateFrom4()
|
||||
var changed = DateOrDefault(part.LastPasswordChangeUtc);
|
||||
|
||||
// Return the most recent of the two dates
|
||||
return created > changed
|
||||
? created
|
||||
: changed;
|
||||
}
|
||||
|
||||
private DateTime DateOrDefault(DateTime? date) {
|
||||
return date.HasValue
|
||||
? date.Value
|
||||
: new DateTime(1990, 1, 1);// Just a default value.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,9 @@
|
||||
<h1>@Html.TitleForPage(T("Change Password").ToString()) </h1>
|
||||
<p>@T("Use the form below to change your password.")</p>
|
||||
<p>@T.Plural("The password can't be empty.", "New passwords are required to be a minimum of {0} characters in length.", (int)ViewData["PasswordLength"])</p>
|
||||
@if ((bool)ViewData["InvalidateOnPasswordChange"]) {
|
||||
<p>@T("After changing the password you will be required to login anew.")</p>
|
||||
}
|
||||
@Html.ValidationSummary(T("Password change was unsuccessful. Please correct the errors and try again.").ToString())
|
||||
@using (Html.BeginFormAntiForgeryPost()) {
|
||||
<fieldset>
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
@model dynamic
|
||||
<h1>@Html.TitleForPage(T("Change Password").ToString())</h1>
|
||||
<p>@T("Your password has been changed successfully.")</p>
|
||||
@if ((bool)ViewData["InvalidateOnPasswordChange"]) {
|
||||
<p>@T("Please, login using your new password.")</p>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
@model Orchard.Users.Models.SecuritySettingsPart
|
||||
|
||||
<fieldset>
|
||||
<legend>@T("Security settings")</legend>
|
||||
<div>
|
||||
@Html.CheckBoxFor(m => m.ShouldInvalidateAuthOnPasswordChanged)
|
||||
<label class="forcheckbox" for="@Html.FieldIdFor(m => m.ShouldInvalidateAuthOnPasswordChanged)">@T("Invalidate authentication cookies on password change")</label>
|
||||
@Html.ValidationMessageFor(m => m.ShouldInvalidateAuthOnPasswordChanged)
|
||||
<p class="hint">@T("When checked, the user will be disconnected from all clients when their password is changed.")</p>
|
||||
</div>
|
||||
</fieldset>
|
||||
@@ -222,8 +222,12 @@
|
||||
<Compile Include="Localization\Services\ILocalizationStreamParser.cs" />
|
||||
<Compile Include="Localization\Services\LocalizationStreamParser.cs" />
|
||||
<Compile Include="Mvc\Html\LinkExtensions.cs" />
|
||||
<Compile Include="Security\ISecurityService.cs" />
|
||||
<Compile Include="Security\ISslSettingsProvider.cs" />
|
||||
<Compile Include="Security\IUserDataProvider.cs" />
|
||||
<Compile Include="Security\NullMembershipService.cs" />
|
||||
<Compile Include="Security\Providers\BaseUserDataProvider.cs" />
|
||||
<Compile Include="Security\Providers\DefaultSecurityService.cs" />
|
||||
<Compile Include="Security\Providers\DefaultSslSettingsProvider.cs" />
|
||||
<Compile Include="Security\Providers\DefaultMembershipValidationService.cs" />
|
||||
<Compile Include="Caching\DefaultCacheContextAccessor.cs" />
|
||||
@@ -428,6 +432,7 @@
|
||||
<Compile Include="Security\IEncryptionService.cs" />
|
||||
<Compile Include="Security\CurrentUserWorkContext.cs" />
|
||||
<Compile Include="Security\Providers\DefaultEncryptionService.cs" />
|
||||
<Compile Include="Security\Providers\TenantNameUserDataProvider.cs" />
|
||||
<Compile Include="Services\Clock.cs" />
|
||||
<Compile Include="Services\ClientHostAddressAccessor.cs" />
|
||||
<Compile Include="Services\DefaultJsonConverter.cs" />
|
||||
|
||||
11
src/Orchard/Security/ISecurityService.cs
Normal file
11
src/Orchard/Security/ISecurityService.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using System;
|
||||
|
||||
namespace Orchard.Security {
|
||||
public interface ISecurityService : IDependency {
|
||||
/// <summary>
|
||||
/// Provides the TimeSpan telling how long an authentication cookie will be allowed to be valid.
|
||||
/// </summary>
|
||||
/// <returns>A <c>TimeSpan</c> with the value for the validity of an authentication cookie.</returns>
|
||||
TimeSpan GetAuthenticationCookieLifeSpan();
|
||||
}
|
||||
}
|
||||
36
src/Orchard/Security/IUserDataProvider.cs
Normal file
36
src/Orchard/Security/IUserDataProvider.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Orchard.Security {
|
||||
/// <summary>
|
||||
/// Implementations of this interface are used to generate the userdata for
|
||||
/// authentication cookies.
|
||||
/// </summary>
|
||||
public interface IUserDataProvider : IDependency {
|
||||
|
||||
/// <summary>
|
||||
/// The Key for the provider in the UserData Dictionary. If either this or the
|
||||
/// provider's computed value are null, the provider should add no item to the
|
||||
/// dictionary.
|
||||
/// </summary>
|
||||
/// <remarks>Implementations should generally ensure that their key is unique.</remarks>
|
||||
string Key { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Provides the Value for UserData for the given user.
|
||||
/// </summary>
|
||||
/// <param name="user">The user.</param>
|
||||
/// <returns>The string to be added in a Dictionary while building
|
||||
/// the UserData. If either this or the provider's Key are null, the provider
|
||||
/// should add no item to the dictionary.</returns>
|
||||
string ComputeUserDataElement(IUser user);
|
||||
|
||||
/// <summary>
|
||||
/// Processes a dictionary containing the UserData information and evaluates
|
||||
/// whether it is valid for the given user.
|
||||
/// </summary>
|
||||
/// <param name="user">The user.</param>
|
||||
/// <param name="userData">The dictionary of UserData.</param>
|
||||
/// <returns>true if the information matches what was expected by this dictionary.</returns>
|
||||
bool IsValid(IUser user, IDictionary<string, string> userData);
|
||||
}
|
||||
}
|
||||
63
src/Orchard/Security/Providers/BaseUserDataProvider.cs
Normal file
63
src/Orchard/Security/Providers/BaseUserDataProvider.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Orchard.Security.Providers {
|
||||
/// <summary>
|
||||
/// Base implementation for providers that generate the UserData for Authentication Cookies.
|
||||
/// To use this, inherit from this abstract class calling the ctor(bool) as follows:
|
||||
///
|
||||
/// public class MyImplementation : BaseUserDataProvider {
|
||||
/// public MyImplementation (
|
||||
/// // Inject any required IDependency
|
||||
/// ) : base (true/false) {
|
||||
/// // MyImplementation ctor body
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// That bool passed to the base ctor controls the behaviour when validating a UserData
|
||||
/// dictionary that does not contain the provider's key.
|
||||
/// On top of that, you will be required to implement the Value(IUser) method that computes
|
||||
/// the value provided by the implementation.
|
||||
/// </summary>
|
||||
public abstract class BaseUserDataProvider : IUserDataProvider {
|
||||
|
||||
public BaseUserDataProvider(bool defaultValid) {
|
||||
DefaultValid = defaultValid;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tells whether this provider should return true for validation when there is no
|
||||
/// element for it in the UserData dictionary. Setting this value to false means that
|
||||
/// if a new provider is added (e.g. if the feature that contains it is enabled) extant
|
||||
/// authentication cookies will be invalidated, because their UserData dictionary does
|
||||
/// not contain the element for the new provider.
|
||||
/// </summary>
|
||||
protected virtual bool DefaultValid { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This is the key that will be used in the UserData dictionary
|
||||
/// </summary>
|
||||
public virtual string Key {
|
||||
get { return GetType().FullName; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is the value that this provider will compute for the given user. This is
|
||||
/// used both to generate the element of the dictionary for this provider, and to
|
||||
/// validate a given dictionary, unless ComputeUserDataElement and IsValid are being
|
||||
/// overridden as well.
|
||||
/// </summary>
|
||||
protected abstract string Value(IUser user);
|
||||
|
||||
public virtual string ComputeUserDataElement(IUser user) {
|
||||
return Value(user);
|
||||
}
|
||||
|
||||
public virtual bool IsValid(IUser user, IDictionary<string, string> userData) {
|
||||
if (userData.ContainsKey(Key)) {
|
||||
return string.Equals(userData[Key], Value(user), StringComparison.Ordinal);
|
||||
}
|
||||
return DefaultValid;
|
||||
}
|
||||
}
|
||||
}
|
||||
13
src/Orchard/Security/Providers/DefaultSecurityService.cs
Normal file
13
src/Orchard/Security/Providers/DefaultSecurityService.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
|
||||
namespace Orchard.Security.Providers {
|
||||
public class DefaultSecurityService : ISecurityService {
|
||||
public TimeSpan GetAuthenticationCookieLifeSpan() {
|
||||
return TimeSpan.FromDays(30);
|
||||
// The default value for the lifespan of authentication cookies used to be 30 days, or the
|
||||
// value from the Sites.config file (or Sites.MyTenant.config). The "choice" between the value
|
||||
// from the service and the one from the config file is done in the IAuthenticationService
|
||||
// implementations.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Web;
|
||||
using System.Web.Security;
|
||||
using Newtonsoft.Json;
|
||||
using Orchard.Environment.Configuration;
|
||||
using Orchard.Logging;
|
||||
using Orchard.Mvc;
|
||||
@@ -10,7 +13,7 @@ using Orchard.Utility.Extensions;
|
||||
|
||||
namespace Orchard.Security.Providers {
|
||||
public class FormsAuthenticationService : IAuthenticationService {
|
||||
private const int _cookieVersion = 3;
|
||||
private const int _cookieVersion = 4;
|
||||
|
||||
private readonly ShellSettings _settings;
|
||||
private readonly IClock _clock;
|
||||
@@ -18,6 +21,8 @@ namespace Orchard.Security.Providers {
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly ISslSettingsProvider _sslSettingsProvider;
|
||||
private readonly IMembershipValidationService _membershipValidationService;
|
||||
private readonly IEnumerable<IUserDataProvider> _userDataProviders;
|
||||
private readonly ISecurityService _securityService;
|
||||
|
||||
private IUser _signedInUser;
|
||||
private bool _isAuthenticated;
|
||||
@@ -35,62 +40,48 @@ namespace Orchard.Security.Providers {
|
||||
IMembershipService membershipService,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
ISslSettingsProvider sslSettingsProvider,
|
||||
IMembershipValidationService membershipValidationService) {
|
||||
IMembershipValidationService membershipValidationService,
|
||||
IEnumerable<IUserDataProvider> userDataProviders,
|
||||
ISecurityService securityService) {
|
||||
|
||||
_settings = settings;
|
||||
_clock = clock;
|
||||
_membershipService = membershipService;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_sslSettingsProvider = sslSettingsProvider;
|
||||
_membershipValidationService = membershipValidationService;
|
||||
_userDataProviders = userDataProviders;
|
||||
_securityService = securityService;
|
||||
|
||||
Logger = NullLogger.Instance;
|
||||
|
||||
ExpirationTimeSpan = TimeSpan.FromDays(30);
|
||||
ExpirationTimeSpan = TimeSpan.Zero;
|
||||
}
|
||||
|
||||
public ILogger Logger { get; set; }
|
||||
|
||||
public TimeSpan ExpirationTimeSpan { get; set; }
|
||||
public TimeSpan ExpirationTimeSpan {
|
||||
get; set;
|
||||
// The public setter allows injecting this from Sites.MyTenant.Config or Sites.config, by using
|
||||
// an AutoFac component
|
||||
}
|
||||
|
||||
public TimeSpan GetExpirationTimeSpan() {
|
||||
if (ExpirationTimeSpan != TimeSpan.Zero) {
|
||||
// Basically here we are checking whether a value has been injected. If that is the case
|
||||
// that takes priority over possible services. The idea is to make the existence of those
|
||||
// services not-breaking, so that the introduction of new ones will not affect tenants where
|
||||
// the value from the Sites.Config file has been used. Implementers of those services should
|
||||
// take care of noting this in whatever UI they provide for their configuration, for the sake
|
||||
// of clarity towards whoever handles the tenant's configuration.
|
||||
return ExpirationTimeSpan;
|
||||
}
|
||||
return _securityService.GetAuthenticationCookieLifeSpan();
|
||||
}
|
||||
|
||||
public void SignIn(IUser user, bool createPersistentCookie) {
|
||||
var now = _clock.UtcNow.ToLocalTime();
|
||||
|
||||
// The cookie user data is "{userName.Base64};{tenant}".
|
||||
// The username is encoded to Base64 to prevent collisions with the ';' seprarator.
|
||||
var userData = String.Concat(user.UserName.ToBase64(), ";", _settings.Name);
|
||||
|
||||
var ticket = new FormsAuthenticationTicket(
|
||||
_cookieVersion,
|
||||
user.UserName,
|
||||
now,
|
||||
now.Add(ExpirationTimeSpan),
|
||||
createPersistentCookie,
|
||||
userData,
|
||||
FormsAuthentication.FormsCookiePath);
|
||||
|
||||
var encryptedTicket = FormsAuthentication.Encrypt(ticket);
|
||||
|
||||
var cookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket) {
|
||||
HttpOnly = true,
|
||||
Secure = _sslSettingsProvider.GetRequiresSSL(),
|
||||
Path = FormsAuthentication.FormsCookiePath
|
||||
};
|
||||
|
||||
var httpContext = _httpContextAccessor.Current();
|
||||
|
||||
if (!String.IsNullOrEmpty(_settings.RequestUrlPrefix)) {
|
||||
cookie.Path = GetCookiePath(httpContext);
|
||||
}
|
||||
|
||||
if (FormsAuthentication.CookieDomain != null) {
|
||||
cookie.Domain = FormsAuthentication.CookieDomain;
|
||||
}
|
||||
|
||||
if (createPersistentCookie) {
|
||||
cookie.Expires = ticket.Expiration;
|
||||
}
|
||||
|
||||
httpContext.Response.Cookies.Add(cookie);
|
||||
CreateAndAddAuthCookie(user, createPersistentCookie);
|
||||
|
||||
_isAuthenticated = true;
|
||||
_isNonOrchardUser = false;
|
||||
@@ -135,39 +126,158 @@ namespace Orchard.Security.Providers {
|
||||
}
|
||||
|
||||
var formsIdentity = (FormsIdentity)httpContext.User.Identity;
|
||||
|
||||
var userData = formsIdentity.Ticket.UserData ?? "";
|
||||
var userDataDictionary = new Dictionary<string, string>();
|
||||
|
||||
if (formsIdentity.Ticket.Version == 3) {
|
||||
var userDataSegments = userData.Split(';');
|
||||
|
||||
// The cookie user data is {userName.Base64};{tenant}.
|
||||
var userDataSegments = userData.Split(';');
|
||||
if (userDataSegments.Length < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (userDataSegments.Length < 2) {
|
||||
return null;
|
||||
var userDataName = userDataSegments[0];
|
||||
var userDataTenant = userDataSegments[1];
|
||||
|
||||
try {
|
||||
userDataName = userDataName.FromBase64();
|
||||
}
|
||||
catch {
|
||||
return null;
|
||||
}
|
||||
userDataDictionary.Add("UserName", userDataName);
|
||||
userDataDictionary.Add("TenantName", userDataTenant);
|
||||
}
|
||||
else { //we assume that the version here will be 4
|
||||
try {
|
||||
userDataDictionary = DeserializeUserData(userData);
|
||||
}
|
||||
catch (Exception) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
var userDataName = userDataSegments[0];
|
||||
var userDataTenant = userDataSegments[1];
|
||||
|
||||
try {
|
||||
userDataName = userDataName.FromBase64();
|
||||
// 1. Take the username
|
||||
if (!userDataDictionary.ContainsKey("UserName")) {
|
||||
return null; // should never happen, unless the cookie has been tampered with
|
||||
}
|
||||
catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!String.Equals(userDataTenant, _settings.Name, StringComparison.Ordinal)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
_signedInUser = _membershipService.GetUser(userDataName);
|
||||
var userName = userDataDictionary["UserName"];
|
||||
_signedInUser = _membershipService.GetUser(userName);
|
||||
if (_signedInUser == null || !_membershipValidationService.CanAuthenticateWithCookie(_signedInUser)) {
|
||||
_isNonOrchardUser = true;
|
||||
return null;
|
||||
}
|
||||
// 2. Check the other stuff from the dictionary
|
||||
var validLogin = _userDataProviders.All(udp => udp.IsValid(_signedInUser, userDataDictionary));
|
||||
if (!validLogin) {
|
||||
_signedInUser = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
// Upgrade old cookies
|
||||
if (formsIdentity.Ticket.Version < 4) {
|
||||
UpgradeAndAddAuthCookie(_signedInUser, formsIdentity.Ticket);
|
||||
}
|
||||
|
||||
_isAuthenticated = true;
|
||||
return _signedInUser;
|
||||
}
|
||||
|
||||
private HttpCookie UpgradeAndAddAuthCookie(IUser user, FormsAuthenticationTicket oldTicket) {
|
||||
var ticket = UpgradeAuthenticationTicket(user, oldTicket);
|
||||
|
||||
var cookie = CreateCookieFromTicket(ticket);
|
||||
|
||||
var httpContext = _httpContextAccessor.Current();
|
||||
httpContext.Response.Cookies.Add(cookie);
|
||||
|
||||
return cookie;
|
||||
}
|
||||
|
||||
private HttpCookie CreateAndAddAuthCookie(IUser user, bool createPersistentCookie) {
|
||||
var ticket = NewAuthenticationTicket(user, createPersistentCookie);
|
||||
|
||||
var cookie = CreateCookieFromTicket(ticket);
|
||||
|
||||
var httpContext = _httpContextAccessor.Current();
|
||||
httpContext.Response.Cookies.Add(cookie);
|
||||
|
||||
return cookie;
|
||||
}
|
||||
|
||||
private HttpCookie CreateCookieFromTicket(FormsAuthenticationTicket ticket) {
|
||||
var encryptedTicket = FormsAuthentication.Encrypt(ticket);
|
||||
|
||||
var cookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket) {
|
||||
HttpOnly = true,
|
||||
Secure = _sslSettingsProvider.GetRequiresSSL(),
|
||||
Path = FormsAuthentication.FormsCookiePath
|
||||
};
|
||||
|
||||
var httpContext = _httpContextAccessor.Current();
|
||||
|
||||
if (!String.IsNullOrEmpty(_settings.RequestUrlPrefix)) {
|
||||
cookie.Path = GetCookiePath(httpContext);
|
||||
}
|
||||
|
||||
if (FormsAuthentication.CookieDomain != null) {
|
||||
cookie.Domain = FormsAuthentication.CookieDomain;
|
||||
}
|
||||
|
||||
if (ticket.IsPersistent) {
|
||||
cookie.Expires = ticket.Expiration;
|
||||
}
|
||||
|
||||
return cookie;
|
||||
}
|
||||
|
||||
private FormsAuthenticationTicket NewAuthenticationTicket(IUser user, bool createPersistentCookie) {
|
||||
var now = _clock.UtcNow.ToLocalTime();
|
||||
|
||||
var userData = ComputeUserData(user);
|
||||
|
||||
return new FormsAuthenticationTicket(
|
||||
_cookieVersion,
|
||||
user.UserName,
|
||||
now,
|
||||
now.Add(GetExpirationTimeSpan()),
|
||||
createPersistentCookie,
|
||||
userData,
|
||||
FormsAuthentication.FormsCookiePath);
|
||||
}
|
||||
|
||||
private FormsAuthenticationTicket UpgradeAuthenticationTicket(IUser user, FormsAuthenticationTicket oldTicket) {
|
||||
var userData = ComputeUserData(user);
|
||||
|
||||
return new FormsAuthenticationTicket(
|
||||
_cookieVersion,
|
||||
user.UserName,
|
||||
oldTicket.IssueDate,
|
||||
oldTicket.Expiration,
|
||||
oldTicket.IsPersistent,
|
||||
userData,
|
||||
FormsAuthentication.FormsCookiePath);
|
||||
}
|
||||
|
||||
private Dictionary<string, string> ComputeUserDataDictionary(IUser user) {
|
||||
var userDataDictionary = new Dictionary<string, string>();
|
||||
userDataDictionary.Add("UserName", user.UserName);
|
||||
foreach (var userDataProvider in _userDataProviders) {
|
||||
var key = userDataProvider.Key;
|
||||
var value = userDataProvider.ComputeUserDataElement(user);
|
||||
if (key != null && value != null) {
|
||||
userDataDictionary.Add(key, value);
|
||||
}
|
||||
}
|
||||
return userDataDictionary;
|
||||
}
|
||||
|
||||
private string ComputeUserData(IUser user) {
|
||||
// serialize dictionary to userData string
|
||||
return SerializeUserDataDictionary(ComputeUserDataDictionary(user));
|
||||
}
|
||||
|
||||
private string GetCookiePath(HttpContextBase httpContext) {
|
||||
var cookiePath = httpContext.Request.ApplicationPath;
|
||||
if (cookiePath != null && cookiePath.Length > 1) {
|
||||
@@ -178,5 +288,17 @@ namespace Orchard.Security.Providers {
|
||||
|
||||
return cookiePath;
|
||||
}
|
||||
|
||||
#region Serialization of UserData Dictionary
|
||||
// Use Newtonsoft.Json to handle this
|
||||
private string SerializeUserDataDictionary(IDictionary<string, string> userDataDictionary) {
|
||||
return JsonConvert.SerializeObject(userDataDictionary, Formatting.None);
|
||||
}
|
||||
|
||||
private Dictionary<string, string> DeserializeUserData(string userData) {
|
||||
return JsonConvert.DeserializeObject<Dictionary<string, string>>(userData);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
22
src/Orchard/Security/Providers/TenantNameUserDataProvider.cs
Normal file
22
src/Orchard/Security/Providers/TenantNameUserDataProvider.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using Orchard.Environment.Configuration;
|
||||
|
||||
namespace Orchard.Security.Providers {
|
||||
public class TenantNameUserDataProvider : BaseUserDataProvider {
|
||||
|
||||
private readonly ShellSettings _settings;
|
||||
|
||||
public TenantNameUserDataProvider(
|
||||
ShellSettings settings) : base(false) {
|
||||
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
public override string Key {
|
||||
get { return "TenantName"; }
|
||||
}
|
||||
|
||||
protected override string Value(IUser user) {
|
||||
return _settings.Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user