Skip to content

User data providers and cookie expiration setting#7945

Merged
sebastienros merged 9 commits into
OrchardCMS:devfrom
LaserSrl:UserDataProviders
Feb 5, 2018
Merged

User data providers and cookie expiration setting#7945
sebastienros merged 9 commits into
OrchardCMS:devfrom
LaserSrl:UserDataProviders

Conversation

@MatteoPiovanelli

Copy link
Copy Markdown
Contributor

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.

…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;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {

@MatteoPiovanelli MatteoPiovanelli Jan 18, 2018

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wrote this serialization/deserialization to avoid introducing new dependencies in Orchard.Framework.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Json ?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't even try, otherwise there is a risk that user data conflict with the format if you are not escaping correctly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there is already a dependency on it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there should be no escaping concerns

Until there is

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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; }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should make the Key unique to each provider, right?

@MatteoPiovanelli

Copy link
Copy Markdown
Contributor Author

Note:
I'm changing the code a bit so that extant auth cookies arent't automatically invalid

…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>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo

@model dynamic
<h1>@Html.TitleForPage(T("Change Password").ToString())</h1>
<p>@T("Your password has been changed successfully.")</p>
@if ((bool)ViewData["InvalidateOnPasswordChange"]) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessary. Users are redirected to the login page, it's obvious they need to use the new one.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While trying the feature, I was not being redirected anywhere, so I added this paragraph. Could be the redirect is not in place anymore?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good then.

<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>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't mention TimeSpan, just the format.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When checked, the user will be disconnected from all clients when their password is changed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

public ILogger Logger { get; set; }

public TimeSpan ExpirationTimeSpan { get; set; }
public TimeSpan ExpirationTimeSpan {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The question, I guess, is more of a "should we" than a "could we" nature, at this point, right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(';');

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just revoke the cookie.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea of replacing as a good compromise.

return cookiePath;
}

#region Serialization of UserData Dictionary

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Json ?

return cookiePath;
}

#region Serialization of UserData Dictionary

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it need to be extensible? Isn't the setting already available in the form authentication service?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you clarify what you mean? I don't understand what you are referring to.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@sebastienros

Copy link
Copy Markdown
Member

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.
Once you are happy just let me know and I will merge.

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.
@MatteoPiovanelli

Copy link
Copy Markdown
Contributor Author

Thanks for the trust in my code.

I think with the last couple commits this feature is good for me.
I removed the service implementations I previously added in Orchard.Users to override the cookie ExpirationTimeSpan. I also wrote the code in FormsAuthenticationService so that it should behave exactly as it does right now (wrt the ExpirationTimeSpan): if autofac injects a value on that property, it will take priority on services trying to affect that; otherwise, the value form services will be used (with the default implementation giving 30days, that is the current default).
I used Newtonsoft.Json to serialize/deserialize the userdata dictionary. I had missed the fact that the dependency on that was already in place.
A call to GetAuthenticatedUser for a request that carries an "old" cookie will cause an upgrade of the cookie: the way it's set up, the new cookie that will be released will have the same expiration as the "old" cookie from the request.

As it is, nothing should break. Let me know what do you think.

@sebastienros

Copy link
Copy Markdown
Member

I trust you more than your code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants