User data providers and cookie expiration setting#7945
Conversation
…ntication cookies
…d last: this means that changing the password invalidates all extant authentication cookies
… rather than always using a constant. The default implementation returns the same constant.
| private readonly IMembershipValidationService _membershipValidationService; | ||
| private readonly IEnumerable<IOpenIdProvider> _openIdProviders; | ||
| private readonly IEnumerable<IUserDataProvider> _userDataProviders; | ||
| private readonly ISecurityService _securityService; |
There was a problem hiding this comment.
I had to make changes here because of the new services being injected in FormsAuthenticationService
| using Orchard.Users.Models; | ||
|
|
||
| namespace Orchard.Users.Services { | ||
| public class PasswordChangedDateUserDataProvider : BaseUserDataProvider { |
There was a problem hiding this comment.
With this provider the auth cookies is considered invalid on password changes.
Because of this, when we change password we are automatically logged out of the tenant (from all clients, including the one from which we changed password), and have to sign in anew. I prefer this to having the user remain logged in, but it is different than the current UX.
There was a problem hiding this comment.
Note: in this PR right now (2018/01/18 15.30) I am not firing the LoggedOut event when this happens.
| return cookiePath; | ||
| } | ||
|
|
||
| #region Serialization of UserData Dictionary |
There was a problem hiding this comment.
I wrote this serialization/deserialization to avoid introducing new dependencies in Orchard.Framework.
There was a problem hiding this comment.
Don't even try, otherwise there is a risk that user data conflict with the format if you are not escaping correctly.
There was a problem hiding this comment.
The way it's written there should be no escaping concerns.
To safely serialize to json, I would feel safer to use JsonSerializer, or Newtonsoft.Json, or something like that. I haven't done that directly to avoid introducing new dependencies in Orchard.Framework. However, if you tell me that is fine, I would go that route.
There was a problem hiding this comment.
I think there is already a dependency on it.
There was a problem hiding this comment.
there should be no escaping concerns
Until there is
There was a problem hiding this comment.
Ok. I will use Newtonsoft.Json
| /// This is the key that will be used in the UserData dictionary | ||
| /// </summary> | ||
| public virtual string Key { | ||
| get { return GetType().FullName; } |
There was a problem hiding this comment.
This should make the Key unique to each provider, right?
|
Note: |
…very client, or none. Added code to prevent users that were authenticated with "old" cookies to be suddenly logged out when deploying this
| <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> |
| @model dynamic | ||
| <h1>@Html.TitleForPage(T("Change Password").ToString())</h1> | ||
| <p>@T("Your password has been changed successfully.")</p> | ||
| @if ((bool)ViewData["InvalidateOnPasswordChange"]) { |
There was a problem hiding this comment.
Unnecessary. Users are redirected to the login page, it's obvious they need to use the new one.
There was a problem hiding this comment.
While trying the feature, I was not being redirected anywhere, so I added this paragraph. Could be the redirect is not in place anymore?
| <label>@T("Life span of authentication cookies")</label> | ||
| @Html.TextBoxFor(m => m.AuthCookieLifeSpan, new { @class = "text medium" }) | ||
| @Html.ValidationMessageFor(m => m.AuthCookieLifeSpan) | ||
| <p class="hint">@T("The time after signing in after which authentication cookies will be invalidated, in standard TimeSpan format (dd.hh:mm:ss).")</p> |
There was a problem hiding this comment.
Don't mention TimeSpan, just the format.
There was a problem hiding this comment.
There was a problem hiding this comment.
I went to the URL, but I am not sure what I am looking for in relation with the TimeSpan
| @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("Select this flag if you want that changing a user's password will require them to sign in anew from all clients.")</p> |
There was a problem hiding this comment.
When checked, the user will be disconnected from all clients when their password is changed.
| public ILogger Logger { get; set; } | ||
|
|
||
| public TimeSpan ExpirationTimeSpan { get; set; } | ||
| public TimeSpan ExpirationTimeSpan { |
There was a problem hiding this comment.
This is breaking what users currently have to do to change this value (host.config).
Not sure it's wise to make it editable in the UI. More code, not usually used, accessible to more users, ... lots of reasons to keep it the way it is today.
There was a problem hiding this comment.
The value for this previously was contant and 30 days. I don't see where it was being read from host.config. Unless writing the form authentication settings there directly overrides anything that is done in code here for the auth cookie.
In this PR, the value is in a setting that is editable from the UI (that is the implementation I put in Orchard.Users, not the default one from Orchard.Framework, even though I realize that close to 100% tenants have Orchard.Users enabled), but here in the service we are getting it from a service, rather than from the setting. It's straightforward to write a service that reads from a config file, or that does something else entirely. DIfferent calls could lead to different values for this.
There was a problem hiding this comment.
The value is assigned automatically by autofac when the service is resolve. It's called "Property Injection". I don't think we need a service for this.
There was a problem hiding this comment.
I did not know that.
I am trying now to see how to set this up in a per-tenant way using the .config files ("Sites.MyTenant.config", right?)
There was a problem hiding this comment.
We could have both the services and property injection:
If I moved that ExpirationTimeSpan to DefaultSecurityService I could still do Property Injection into there.
Alternatively, in the constructor of FormsAuthenticationService I could initialize the value of ExpirationTimeSpan to a negative value. Then rather than using the property in the rest of the class, I could have a method like:
public TimeSpan GetExpirationTime() {
if (ExpirationTimeSpan < TimeSpan.Zero) {
// We have not set expiration from Sites.Tenant.config
return _securityService.GetAuthenticationCookieLifeSpan();
}
// we have set the expiration from the config file
return ExpirationTimeSpan;
}
As long as the UI for each of those services clearly stated that having a value in the config file would take precedence, that behaviour would not be obscure.
There was a problem hiding this comment.
The question, I guess, is more of a "should we" than a "could we" nature, at this point, right?
There was a problem hiding this comment.
Personally I think there may be value in having the option, for example, to set different ExpirationTimeSpan depending on the "source" of the SignIn. For example, if you SignIn from a browser I may set a short validity, but if you SignIn from my app on your phone I may decide that that may be ok with a longer-lived cookie (of course that requires me to identify the source in my service, but that is a separate issue).
There was a problem hiding this comment.
Another scenario may be:
- Cookie expires in a bunch of days for admin
- Cookie expires in a shorter timeframe for "normal" users
| var userDataDictionary = new Dictionary<string, string>(); | ||
|
|
||
| if (formsIdentity.Ticket.Version == 3) { | ||
| var userDataSegments = userData.Split(';'); |
There was a problem hiding this comment.
It's what I initially thought of doing. However that would have meant that deploying this update to how the cookies are processed would have caused all users to effectively be logged out.
That would have been jarring in terms of UX, and no LoggedOut event would have been fired for anyone.
Something else I thought would be adding at the end of this method a condition to replace V3 cookies with new ones by releasing it (I'd just call CreateAndAddCookie)
There was a problem hiding this comment.
I like the idea of replacing as a good compromise.
| return cookiePath; | ||
| } | ||
|
|
||
| #region Serialization of UserData Dictionary |
| return cookiePath; | ||
| } | ||
|
|
||
| #region Serialization of UserData Dictionary |
There was a problem hiding this comment.
Don't even try, otherwise there is a risk that user data conflict with the format if you are not escaping correctly.
| using Orchard.Users.Models; | ||
|
|
||
| namespace Orchard.Users.Services { | ||
| public class PasswordChangedDateUserDataProvider : BaseUserDataProvider { |
There was a problem hiding this comment.
Does it need to be extensible? Isn't the setting already available in the form authentication service?
There was a problem hiding this comment.
Could you clarify what you mean? I don't understand what you are referring to.
There was a problem hiding this comment.
I don't see why we need to be able to extend this. The forms auth logic should be self sufficient, and store in the auth cookie only what it needs. If other modules need to store stuff for the user in a cookie, they are free to create their own.
There was a problem hiding this comment.
The point is not to store information in the auth cookie per se, but to stamp it with information to be able to invalidate it if/when required by something. In the specific case of PasswordChangedDateUserDataProvider, the idea is that a password change can make all auth cookies, for the user and from any client, invalid. That information we are storing in the cookie (the datetime in this case) is not for the user/client.
|
I'll let you do all the changes and compromises that you deem necessary, and trust your judgement as you are the main user of this feature and it's not breaking. |
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.
…ep the same expiration
|
Thanks for the trust in my code. I think with the last couple commits this feature is good for me. As it is, nothing should break. Let me know what do you think. |
|
I trust you more than your code. |
AS mentioned in #6014 I have implemented providers for the UserData string that is used in authentication cookies. Adding this, any extant cookie wil immeditely be considered invalid. Upon first sign in, the new UserData will be used.
I have also made the life span of said cookies configurable (the default is still 30 days).
I'll add comments to the PR to explain/discuss my changes.