I get problems with the silent-refresh mechanism of my angular app, because the cookie expiration will not set correctly by the identity server.
After a successful login, the following cookies will be set for the domain of the identity server:
As you can see on the picture, the "idsrv.session" cookie has the expiration "session". In my old IdentityServer3 this cookie has a correct expiration date and all works fine.
Could anyone help me, to solve this problem. I need a way to set a expiration-date for the "idsrv.session" cookie.
Here are the current configuration/statements, which I added to achieve my goal:
// Startup.cs - ConfigureServices()
services
.AddIdentityServer(options =>
{
options.EmitStaticAudienceClaim = true;
options.AccessTokenJwtType = "JWT";
options.Authentication.CookieLifetime = TimeSpan.FromDays(30);
})
services
.AddAuthentication("Cookies")
.AddCookie("Cookies", options =>
{
options.ExpireTimeSpan = TimeSpan.FromDays(30);
})
// AccountController.cs - Login()
await HttpContext.SignInAsync(isuser, new AuthenticationProperties
{
IsPersistent = true,
ExpiresUtc = DateTimeOffset.UtcNow.Add(TimeSpan.FromDays(30))
});
But this settings has no effekt on the "idsrv.session" cookie.
I use .net core 3.1 and IdentityServer4 4.0.4.
Thanks!!
SOLUTION:
In my case the following statement helps me, that a user will not logout after 30 minutes. The expiration "idsrv.session" cookie is although still set to session, but it will not removed after 30 minutes.
services.Configure<SecurityStampValidatorOptions>(o => o.ValidationInterval = TimeSpan.FromDays(30));
Related
[DEVELOPMENT] - All fine (no issues as cookies are set on same domain 'localhost')
[PROD / LIVE] - Link below
The issue: The cookie are not being set properly or not getting persisted, I don't know why so if anyone can give me an idea what might be the cause as you can see in network tab, it sets the cookie but it doesn't put it in Application > Storage > Cookies you can have a look if I am not explaining it very well.
https://gta-open-q99pjtak6-patricksubang.vercel.app/
username: demo
password: demo
Front end, POST/GET request
const sendRequest = async (method, endpoint, custom) => {
const isProd =
process.env.NODE_ENV === "development"
? "http://localhost:8000/"
: "https://gta-open.ga/";
const url = isProd + endpoint;
const response = await fetch(url, {
method: method,
mode: "cors",
credentials: "include",
...custom,
});
return response;
};
Setting session cookie using gorrilla\sessions
Cookie.Options.Path = "/"
Cookie.Options.HttpOnly = true
Cookie.Options.SameSite = http.SameSiteNoneMode
state := false
if os.Getenv("ENV") == "PROD" {
state = true
}
Cookie.Options.Secure = state
func GenerateSession(w http.ResponseWriter, r *http.Request, uid int) (err error) {
session, _ := Cookie.Get(r, "sessionid")
session.Values["accountID"] = uid
// Save it before we write to the response/return from the handler.
err = session.Save(r, w)
if err != nil {
return
}
return
}
If anyone can give me a rough idea why, or the cause of the issue would be helpful
Many thanks!
Not familiar with a Golang backend nor gorrilla sessions, but I'm pretty sure it's how you're configuring your cookie settings. Specifically, I believe the domain name being set to gta-open.ga doesn't match vercel.app so it's either being blocked by the browser's third party cookie settings or not being utilized because the domain names don't match.
For example, cookie blocked by 3rd party (user's browser preference):
As a result, no cookie is set:
However, if I allow 3rd party cookies from cross-domains, then the cookie is set:
However, since it has a different domain from the app, it's not being utilized:
On that note, I'd highly suggest purchasing your own domain. They're pretty cheap (10usd/7gbp or less per year), then you can set up your vercel app to use the custom domain. Then, set your cookie to use that custom domain name and it should be smooth sailing.
Otherwise, you'll have to set up the cookie to use subdomain's .vercel.app in production (which isn't recommended because vercel hosts a lot of apps on their subdomains) or use domain gta-open-q99pjtak6-patricksubang.vercel.app, but that's kind of silly.
Using Identity Server 4 server.
In client’s Startup.Auth.cs :
private static void ConfigureAuth(IAppBuilder app)
{
ISettingsReader settingsReader = Services.Resolve<ISettingsReader>();
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
ExpireTimeSpan = new TimeSpan(1, 0, 0),
CookieSecure = CookieSecureOption.Always,
CookieHttpOnly = true,
SlidingExpiration = true
});
var platformUri = settingsReader.GetSetting("PlatformUri")?.TrimEnd('/');
var platformApiKey = settingsReader.GetSetting("PlatformApiKey");
var deploymentURL = settingsReader.GetSetting("deploymentURL")?.TrimEnd('/');
var authority = $"{platformUri}/identity";
string clientSecret;
string clientId = SplitApiKey(platformApiKey, out clientSecret);
var options = new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
ClientSecret = clientSecret,
Authority = authority,
RedirectUri = $"{deploymentURL}/signin/callback",
ResponseType = "id_token token",
Scope = "platform openid",
UseTokenLifetime = false,
SignInAsAuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = SecurityTokenValidatedHandler,
RedirectToIdentityProvider = RedirectToIdentityProviderHandler,
}
};
app.UseOpenIdConnectAuthentication(options);
AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.NameIdentifier;
}
Note – the cookie’s expire timespan has been set to 5 mins for debugging, normally would be set to an hour.
And then stash the access_token in the validated handler (as per several articles) so that we can use it later for api calls:
private static async Task SecurityTokenValidatedHandler(SecurityTokenValidatedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
{
var jwtDetails = JsonWebToken.Parse(notification.ProtocolMessage.AccessToken);
notification.AuthenticationTicket.Identity.AddClaims(
jwtDetails.Claims.Where(c => DesiredAccessTokenClaims.Contains(c.Type)));
notification.AuthenticationTicket.Identity.AddClaim(new Claim("access_token", notification.ProtocolMessage.AccessToken));
notification.AuthenticationTicket.Identity.AddClaim(new Claim("id_token", notification.ProtocolMessage.IdToken));
}
This works fine. However, although the cookies do auto refresh themselves, with sliding expiration (typically after 2-3 minutes, rather than the full 5), there doesn’t seem to be a way to refresh the access token which is held in the claim, so although the user will remain logged in, the access token will be useless after it expires.
Is this the right way to be going about this? And if so, is there a way to update the access token in the claim, in the background without disturbing the user? It seems that the ideal solution would be to have the cookie refresh also trigger the SecurityTokenValidatedHandler so that a renewed claim can be added into the new cookie, although despite looking in CookieManager, etc. there doesn't seem to be an event which is triggered when the cookie sliding refreshes itself. Does anyone know a way to do this?
Many thanks for your time!
If anyone comes across this issue, the answer was to change from implicit to hybrid. Hybrid allows refresh tokens, whereas implicit, not so much. In our case we needed Hybrid and Client Creds
How to implement a two factor authentication using Identity Server 4? The token end point returns a token with a username and password / client credentials.
Can we customize those end points?
Both the methods as per the sample does not allow to customize the end point:
> var tokenClient = new TokenClient(disco.TokenEndpoint, "ro.client", "secret");
> var tokenResponse = await tokenClient.RequestResourceOwnerPasswordAsync("brockallen#gmail.com",
> "Pass123$", "api1");
Is it possible to achieve 2 factor authentication using either asp.net identity Or EF Core implementation?
This shouldn't be a problem at all. When a user is redirected to the Identity Server for login in, if 2FA is enabled then he/she would have to enter the authenticator's code before the Identity Server returns the response back. I have created a repository and blog post series that explain in detail the related concepts. In the AccountController of the IdentityServer you have to check if 2FA is enabled and ask the user to proceed by providing an authenticator code before returning the response.
var signInResult = await _signInManager.PasswordSignInAsync(model.UserName, model.Password, true,
lockoutOnFailure: false);
if (signInResult.RequiresTwoFactor)
{
result.Status = Status.Success;
result.Message = "Enter the code generated by your authenticator app";
result.Data = new {requires2FA = true};
return result;
}
You will also need a TwoFactorAuthenticationController that supports all the 2FA tasks (enable/disable 2FA, sign in with authenticator code/recovery tokens, reset authenticator, etc...)
Is there a way to renew the authentication timeout of a cookie as part of a particular web request? I have an Angular app on top of my MVC 5 project and I need it to keep my server session alive in between requests. I've got the Angular part working, but it appears that hitting a URL on my server is not sufficient to reset the Auth timeout. I am new to Identity so I am probably missing something simple?
My Startup.Auth.cs code:
public void ConfigureAuth(IAppBuilder app)
{
// Configure the db context, user manager and signin manager to use a single instance per request
app.CreatePerOwinContext(ApplicationDbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);
// Enable the application to use a cookie to store information for the signed in user
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
ExpireTimeSpan = TimeSpan.FromSeconds(30),
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider
{
// Enables the application to validate the security stamp when the user logs in.
// This is a security feature which is used when you change a password or add an external login to your account.
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
validateInterval: TimeSpan.FromMinutes(20),
regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
}
});
}
And my simple method (authorization is set up globally for all requests that do not have [AllowAnonymous]):
[HttpGet]
public HttpResponseMessage KeepAuthAlive()
{
// Renew Auth Cookie - how?
}
Re-SignIn the User (this code assumes you have UserManager and SignInManager available as per the default Account controller and an async ActionResult). I haven't tested this, but it should hopefully work:
ApplicationUser user = await UserManager.FindByIdAsync(User.Identity.GetUserId());
if (user != null)
{
await SignInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false);
}
I understand the flow of JWT and a single page application in terms of login and JWT issuance. However, if the JWT has a baked in expiry, AND the server isn't issuing a new JWT on each request, what is the best way for renewing? There is a concept of refresh tokens, but storing such a thing in a web browser sounds like a golden ticket.
IE I could easily go into a browsers local storage and steal a refresh token. Then I could go to another computer and issue myself a new token. I feel like there would need to be a server session in a db that's referenced in the JWT. Therefore the server could see if the session ID is still active or invalidated by a refresh token.
What are the secure ways to implement JWT in a SPA and handling new token issuance whilst the user is active?
Renewing the token every 15 minutes (if it lives for 30) works if you don't have another restriction in your server in which you need to check for 1 hour inactivity to log the user out. If you just want this short lived JWT and keep on updating it, it'd work.
I think one of the big advantages of using JWT is to actually NOT need a server session and therefore not use the JTI. That way, you don't need syncing at all so that'd be the approach I'd recommend you following.
If you want to forcibly logout the user if he's inactive, just set a JWT with an expiration in one hour. Have a $interval which every ~50 minutes it automatically gets a new JWT based on the old one IF there was at least one operation done in the last 50 minutes (You could have a request interceptor that just counts requests to check if he's active) and that's it.
That way you don't have to save JTI in DB, you don't have to have a server session and it's not a much worse approach than the other one.
What do you think?
I think for my implementation I'm going to go with, after a bit of search, is...
Use case:
JWT is only valid for 15 minutes
User session will timeout after 1 hour of inactivity
Flow:
User logs in and is issued a JWT
JWT has a 15 minute expiration with claim 'exp'
JWT JTI is recorded in db has a session of 1 hour
After a JWT expires (after 15 min):
Current expired JWT will be used # a /refresh URI to exchange for a new one. The expired JWT will only work at the refresh endpoint. IE API calls will not accept an expired JWT. Also the refresh endpoint will not accept unexpired JWT's.
JTI will be checked to see if its been revoked
JTI will be checked to see if its still within 1 hour
JTI session will be deleted from DB
New JWT will be issued and new JTI entry will be added to the db
If a user logs out:
JWT is deleted from client
JTI is deleted from db so JWT cannot be refreshed
With that said, there will be database calls every 15 minutes to check a JTI is valid. The sliding session will be extended on the DB that tracks the JWT's JTI. If the JTI is expired then the entry is removed thus forcing the user to reauth.
This does expose a vulnerability that a token is active for 15 minutes. However, without tracking state every API request I'm not sure how else to do it.
I can offer a different approach for refreshing the jwt token.
I am using Angular with Satellizer and Spring Boot for the server side.
This is the code for the client side:
var app = angular.module('MyApp',[....]);
app.factory('jwtRefreshTokenInterceptor', ['$rootScope', '$q', '$timeout', '$injector', function($rootScope, $q, $timeout, $injector) {
const REQUEST_BUFFER_TIME = 10 * 1000; // 10 seconds
const SESSION_EXPIRY_TIME = 3600 * 1000; // 60 minutes
const REFRESH_TOKEN_URL = '/auth/refresh/';
var global_request_identifier = 0;
var requestInterceptor = {
request: function(config) {
var authService = $injector.get('$auth');
// No need to call the refresh_token api if we don't have a token.
if(config.url.indexOf(REFRESH_TOKEN_URL) == -1 && authService.isAuthenticated()) {
config.global_request_identifier = $rootScope.global_request_identifier = global_request_identifier;
var deferred = $q.defer();
if(!$rootScope.lastTokenUpdateTime) {
$rootScope.lastTokenUpdateTime = new Date();
}
if((new Date() - $rootScope.lastTokenUpdateTime) >= SESSION_EXPIRY_TIME - REQUEST_BUFFER_TIME) {
// We resolve immediately with 0, because the token is close to expiration.
// That's why we cannot afford a timer with REQUEST_BUFFER_TIME seconds delay.
deferred.resolve(0);
} else {
$timeout(function() {
// We update the token if we get to the last buffered request.
if($rootScope.global_request_identifier == config.global_request_identifier) {
deferred.resolve(REQUEST_BUFFER_TIME);
} else {
deferred.reject('This is not the last request in the queue!');
}
}, REQUEST_BUFFER_TIME);
}
var promise = deferred.promise;
promise.then(function(result){
$rootScope.lastTokenUpdateTime = new Date();
// we use $injector, because the $http creates a circular dependency.
var httpService = $injector.get('$http');
httpService.get(REFRESH_TOKEN_URL + result).success(function(data, status, headers, config) {
authService.setToken(data.token);
});
});
}
return config;
}
};
return requestInterceptor;
}]);
app.config(function($stateProvider, $urlRouterProvider, $httpProvider, $authProvider) {
.............
.............
$httpProvider.interceptors.push('jwtRefreshTokenInterceptor');
});
Let me explain what it does.
Let's say we want the "session timeout" (token expiry) to be 1 hour.
The server creates the token with 1 hour expiration date.
The code above creates a http inteceptor, that intercepts each request and sets a request identifier. Then we create a future promise that will be resolved in 2 cases:
1) If we create for example a 3 requests and in 10 seconds no other request are made, only the last request will trigger an token refresh GET request.
2) If we are "bombarded" with request so that there is no "last request", we check if we are close to the SESSION_EXPIRY_TIME in which case we start an immediate token refresh.
Last but not least, we resolve the promise with a parameter. This is the delta in seconds, so that when we create a new token in the server side, we should create it with the expiration time (60 minutes - 10 seconds). We subtract 10 seconds, because of the $timeout with 10 seconds delay.
The server side code looks something like this:
#RequestMapping(value = "auth/refresh/{delta}", method = RequestMethod.GET)
#ResponseBody
public ResponseEntity<?> refreshAuthenticationToken(HttpServletRequest request, #PathVariable("delta") Long delta, Device device) {
String authToken = request.getHeader(tokenHeader);
if(authToken != null && authToken.startsWith("Bearer ")) {
authToken = authToken.substring(7);
}
String username = jwtTokenUtil.getUsernameFromToken(authToken);
boolean isOk = true;
if(username == null) {
isOk = false;
} else {
final UserDetails userDetails = userDetailsService.loadUserByUsername(username);
isOk = jwtTokenUtil.validateToken(authToken, userDetails);
}
if(!isOk) {
Map<String, String> errorMap = new HashMap<>();
errorMap.put("message", "You are not authorized");
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(errorMap);
}
// renew the token
final String token = jwtTokenUtil.generateToken(username, device, delta);
return ResponseEntity.ok(new JwtAuthenticationResponse(token));
}
Hope that helps someone.