I've implemented sliding sessions in my Relying Party application, as described in Sliding Sessions for WIF 4.5. That works great as far as it goes, but there's one problem that it seems nobody talks about.
As the linked blog post points out, when the RP token expires, the next time make a request the token is re-issued from the STS. Assuming, of course, that the STS session lifetime is longer than the RP's session lifetime, which is almost certainly the case if you're implementing sliding sessions.
In any event, that completely defeats the whole point of sliding sessions.
What nobody seems to talk about is what to do when the RP session expires. What I want is, if the RP session times out (usually because somebody walked away from his desk for 10 minutes), is for my application to redirect to the STS login page where the user can re-authenticate, and then be redirected back to the page I had requested; or perhaps to the page that I was on when I made the request.
I'm almost certain that this is possible, but I have absolutely no idea how it's done.
Here's my code from global.asax:
private const int InactivityTimeout = 5; // minutes
void SessionAuthenticationModule_SessionSecurityTokenReceived
(object sender, SessionSecurityTokenReceivedEventArgs e)
{
var now = DateTime.UtcNow;
var validFrom = e.SessionToken.ValidFrom;
var validTo = e.SessionToken.ValidTo;
double halfSpan = (validTo - validFrom).TotalMinutes/2;
if (validFrom.AddMinutes(halfSpan) < now && now < validTo)
{
// add more time
var sam = sender as SessionAuthenticationModule;
e.SessionToken = sam.CreateSessionSecurityToken(
e.SessionToken.ClaimsPrincipal,
e.SessionToken.Context,
now,
now.AddMinutes(InactivityTimeout),
e.SessionToken.IsPersistent);
e.ReissueCookie = true;
}
else
{
// re-authenticate with STS
}
}
My questions:
Is the else clause the proper place to put the re-authentication logic?
If so, please provide an example, 'cause I have no idea.
If the answer to #1 is no, then is there a separate event I need to subscribe to that will tell me "Hey, your session security token has expired!"?
I'd recommend you sync the session lifetimes on the STS and the RP(s).
You can set the session lifetime to 10 minutes on the STS and 10 minutes on the RP and use the sliding session approach on the RP. After 10 minutes of inactivity both sessions would expire and the user should be required to re-authenticate.
If you have multiple RPs you could implement a form of keep-alive from the RP to the STS - e.g. load a resource from the STS in every webpage on the RPs. Whenever a page is loaded on an RP, the keep-alive resource would be loaded from the STS - refreshing the STS session. After 10 minutes of inactivity they would both time out and the user would have to re-authenticate.
"A resource from the STS" could mean a web page (Web Forms/MVC) loaded in an invisible iframe. The important thing is that it's a managed handler so the request is handled by ASP.NET.
As for your questions, if you sync the session lifetimes so they time out together:
No, you don't need to add any code in the else clause. If the token is expired, WIF will redirect to the STS.
Just remove the else clause.
Let WIF handle this for you.
For completeness, if you can't sync the session lifetimes you could trigger a federated sign-out when the RP session expires. The following snippet triggers a signout at the configured Issuer (STS). You could put this in the else clause to trigger a signout on the first request after the RP session expires:
using System.IdentityModel.Services; //WIF 4.5
var stsAddress = new Uri(FederatedAuthentication.FederationConfiguration.WsFederationConfiguration.Issuer);
WSFederationAuthenticationModule.FederatedSignOut(stsAddress, null); //Optional replyUrl set to null
Hope that helps!
Related
I have a dotnet 7 Blazor WASM app, using Azure AD B2C (via AddMsalAuthentication in Program.cs).
The homepage of the app allows anonymous access, and features a call-to-action to login if the user is not authenticated.
In the layout used by the homepage, I have a dropdown that is populated via an API call. This will attempt to make the API call if User.Identity?.IsAuthenticated == true. The API call uses an HTTP client using BaseAddressAuthorizationMessageHandler (which in turn inherits AuthorizationMessageHandler) which is responsible for silently obtaining an access token before making the call.
If obtaining an access token fails, AccessTokenNotAvailableException is thrown and I call Redirect() on the exception, which redirects to the B2C login screen. This is not the behaviour I want as users are redirected without responding to the call-to-action to log in.
Delving into the library code, this seems to be where Blazor WASM creates the ClaimsIdentity. It calls a JS method in MSAL.js, AuthenticationService.getUser:
If I call the AuthenticationService.getUser method in my browser, I can see an object returned:
I note the exp claim for the user is a few days ago - in my mind this has expired.
I also notice ClaimsIdentity.IsAuthenticated returns true when the AuthenticationType is non-null. In my case, the AuthenticationType is a Guid matching my B2C app's ClientId.
So my question is: what should I be calling, other than User.Identity?.IsAuthenticated == true to determine whether the user is authenticated, and an access token can be provisioned without the user having to re-authenticate? Should I be using Blazor custom policies?
After some more investigation, my conclusion is this behaviour is by design.
The default policy out-of-the box simply adds RequireAuthenticatedUser:
This doesn't necessarily do what you'd expect - it adds DenyAnonymousAuthorizationRequirement:
Which in turn simply checks that the Identity is authenticated (which as I noted before just checks for a non-null authentication type):
My solution was to use a custom policy - in services.AddAuthorizationCore - to check the exp claim:
new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.RequireClaim("email")
.AddAuthenticationSchemes("bearer")
.RequireAssertion(ctx =>
{
var exp = ctx.User.Claims.SingleOrDefault(c => c.Type.Equals("exp" , StringComparison.OrdinalIgnoreCase));
if (exp is null)
return false;
var datetime = DateTimeOffset.FromUnixTimeSeconds(long.Parse(exp.Value));
return datetime.UtcDateTime > Clock.Instance.UtcNow;
});
We are creating a Blazor WASM application for usage on unstable and possibly slow connections. We have successfully implemented authentication with OpenIdConnect.
We noticed that on every refresh (F5) of the page, the token is being validated against the Identity Provider again:
We think this is normal/desired behaviour, but is there any way around this?
We know this is a tiny amount of data, but it would be optimal to not have this every time.
The websites are for 'internal' usage only (through a VPN).
Thank you
I have personally run into this issue as well. For us, it was even worse since the IdP would take quite some time since the authorization endpoint would ignore the prompt=none parameter and try to challenge the user every time Blazor WASM Authentication tried to refresh its authentication state. This forced me to do some digging so hopefully, my findings are useful to you.
The OIDC in Blazor WASM makes use of their RemoteAuthenticationService class which implements the AuthenticationStateProvider to provide an authentication state to Blazor WASM on top of the Access Token.
I think this is the key problem here. That they are separating the AuthState and AccessToken which (at least for me) was unintuitive since in the past I would determine whether a user is "logged in", purely based on if they have a valid access token or not.
So the fact that you already have an "AccessToken" is irrelevant to the AuthState which begs the question: How do they determine your AuthState?
Lets checkout this key function in the RemoteAuthenticationService:
...
public override async Task<AuthenticationState> GetAuthenticationStateAsync() => new AuthenticationState(await GetUser(useCache: true));
...
private async Task<ClaimsPrincipal> GetUser(bool useCache = false)
{
var now = DateTimeOffset.Now;
if (useCache && now < _userLastCheck + _userCacheRefreshInterval)
{
return _cachedUser;
}
_cachedUser = await GetAuthenticatedUser();
_userLastCheck = now;
return _cachedUser;
}
In the above code snippet you can see that the AuthState is determined by this GetUser function which first checks some cache for the user which is currently hardcoded to expire every 60 seconds. This means that if you check the user's AuthState, then every 60 seconds it would have to query the IdP to determine the AuthState. This is how it does that:
Uses JSInterop to call trySilentSignIn on the oidc-client typescript library.
SilentSignIn opens a hidden iframe to the IdP authorization endpoint to see if you are in fact signed in at the IdP. If successful then it reports the signed-in user to the AuthState provider.
The problem here is that this could happen every time you refresh the page or even every 60 seconds whenever you query the current AuthState where the user cache is expired. There is no persistence of the access token or the AuthState in any way.
Ok so then how do I fix this?
The only way I can think of is to implement your own RemoteAuthenticationService with some slight modifications from the one in the Authentication Library.
Specifically to
Potentially persist the access token.
Reimplement the getUser call to check the validity/presence of the persisted access token to get the user rather than using the silentSignin function on the oidc-client library.
When user role is changed or rest password then security stamp is an update of that user by using "UpdateSecurityStampAsync" method then also user didn't kick out to log in.
Note:
-We are using the Entity framework core, Identity, .net core, Jwt Configurations and angular in frontend.
-We are using Authorize(AuthenticationSchemes = "Bearer") on controller.
It won't. The security stamp has to be revalidated first before that will happen, which by default happens every 30 minutes. You can lower this interval if you like. However, the lower the interval, the more your database will be queried. You can lower it all the way to zero, to have it always immediately revalidated, but that will then require a database query with every request.
services.Configure<SecurityStampValidatorOptions>(o => {
o.ValidationInterval = TimeSpan.Zero;
});
is it possible to specify the sticky session duration in mod_cluster?
I mean that the stuck session is cleared when there isn't activity for a period of time.
We have a distributable application that keeps a reference to logged user in the web session. But at login time, the web session replication isn't enough fast as requests that follow the login request. So, if, for those requests, the balancer choose a node that doesn't have been replicated yet, the user wouldn't be in session and an error occur.
Another use of this functionality would be when you use cached information on a server. If you don't use sticky session you would load to cache several times the same information in different servers. But if you use sticky session you would be tied to the same server for all session life.
Thanks in advance
Leandro
Answer
It is not possible to switch session stickiness in mod_cluster On and Off for a certain time duration. One either has it On or Off.
Further comments
IIUC, you are in fact after session expiration or session invalidation. You can programatically decide to invalidate your session at any moment, or you might let it expire by setting expiration timeout.
Could you perhaps elaborate more on how would you use the "session stickiness timeout"? We could create a JIRA and and implement a new feature if it makes sense...
I´ve found a workaround for our needs.
I configure different stickysession attribute at balancer (BALANCER_SESSION_ID_HEADER_NAME) and manage balancer sticky session duration at client side.
First time, I set counter + JSESSIONID to BALANCER_SESSION_ID_HEADER_NAME. Every time the STICKY_SESSION_TIMEOUT is spent, I set ++counter + JSESSIONID to BALANCER_SESSION_ID_HEADER_NAME.
Client code:
if (USE_STICKY_SESSION_TIMEOUT && this.getjSessionId() != null) {
if (this.getLastResponseTime() != 0
&& new Date().getTime() - this.getLastResponseTime() > STICKY_SESSION_TIMEOUT) {
balancerSubsessionCounter++;
}
final String cookie = BALANCER_SESSION_ID_HEADER_NAME + "=" + balancerSubsessionCounter + "-"
+ this.getjSessionId();
this.addCookie(httpPost, cookie);
}
//invoke service
if (USE_STICKY_SESSION_TIMEOUT) {
this.setLastResponseTime(new Date().getTime());
}
Perhaps I am misunderstanding how this works, but I thought the OnValidateIdentity would be called on every HTTP request to the server. I see it get hit on initial login (a bunch of times, so it looks like it is every HTTP request on initially), but then it doesn't seem to get called again. I let my app sit there until it should be expired, but it never gets hit again on subsequent requests to the server, until I logout and login again.
I have set my expiry very low and turned off sliding expiration to see if I could get it to fail, but to no avail.
Should I not see the OnValidateIdentity get called on every HTTP request? Ultimately, I really just want a cookie expiry to result in a logout. I assumed I had to check the expiry on each request in the OnValidateIdentity, but if this is not the way to do it please let me know!
Am I misunderstanding how this work, or how I should be using cookie expiries to force a logout?
Here is my basic setup:
app.UseCookieAuthentication(New CookieAuthenticationOptions() With
{
.AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
.AuthenticationMode = AuthenticationMode.Active,
.ExpireTimeSpan = TimeSpan.FromMinutes(1.0),
.SlidingExpiration = False,
.Provider = New CookieAuthenticationProvider() With
{
.OnValidateIdentity = SecurityStampValidator.OnValidateIdentity(Of ApplicationUserManager, ApplicationUser, Integer)(
validateInterval:=TimeSpan.FromMinutes(1.0),
regenerateIdentityCallback:=Async Function(manager, user)
Return Await user.GenerateUserIdentityAsync(manager, DefaultAuthenticationTypes.ApplicationCookie)
End Function,
getUserIdCallback:=Function(id) id.GetUserId(Of Integer)())
},
.LoginPath = New PathString("/Account/Login")
})
Probably a bit late...
To use cookie Expiry, that's delegated to the browser. If a cookie expires, then the browser wouldn't send it.
OnValidateIdentity is called on every request. If you want to debug it, pull it out into a new method and debug from that method instead.
However, the identity itself is only validated against the database when the validationInterval is passed.