In release environment, my mvc application has two set of cookies. Lets say the site name is test-solution.com, the cookies created are as follows:
1) test-solution.com
2) www.test-solution.com
Following is the code for writing cookie:
HttpCookie UserToken = HttpContext.Current.Request.Cookies["UserToken"];
if (UserToken == null)
{
UserToken = new HttpCookie("UserToken");
UserToken.Values["Token"] = Guid.NewGuid().ToString();
}
HttpContext.Current.Session["UserToken"] = UserToken.Values["Token"];
#if RELEASE
UserToken.Secure = true;
#endif
UserToken.Expires = DateTime.Now.AddYears(1);
#if RELEASE
HttpContext.Current.Response.Cookies["UserToken"].Path += ";SameSite=Lax;";
#endif
HttpContext.Current.Response.Cookies.Add(UserToken);
I just intend to have one (www.test-solution.com) cookie rather than two.
The reason for these two cookies is that my website was referring to a google-analytics script and this was implicitly creating another cookie.
Related
I have a standard .NET Core 2.1 (MVC and API) and Identity Server 4 project setup.
I am using reference tokens instead of jwt tokens.
The scenario is as follows:
Browse to my application
Redirected to Identity Server
Enter valid valid credentials
Redirected back to application with all claims (roles) and correct access to the application and API
Wait an undetermined amount of time (I think it's an hour, I don't have the exact timing)
Browse to my application
Redirected to Identity Server
I'm still logged into the IDP so I'm redirected immediately back to my
application
At this point the logged in .NET user is missing claims (roles) and no longer has access to the API
The same result happens if I delete all application cookies
It seems obvious to me that the access token has expired. How do I handle this scenario? I'm still logged into the IDP and the middleware automatically logged me into my application, however, with an expired (?) access token and missing claims.
Does this have anything to do with the use of reference tokens?
I'm digging through a huge mess of threads and articles, any guidance and/or solution to this scenario?
EDIT: It appears my access token is valid. I have narrowed my issue down to the missing user profile data. Specifically, the role claim.
When I clear both my application and IDP cookies, everything works fine. However, after "x" (1 hour?) time period, when I attempt to refresh or access the application I am redirected to the IDP then right back to the application.
At that point I have a valid and authenticated user, however, I am missing all my role claims.
How can I configure the AddOpenIdConnect Middleware to fetch the missing claims in this scenario?
I suppose in the OnUserInformationReceived event I can check for the missing "role" claim, if missing then call the UserInfoEndpoint...that seems like a very odd workflow. Especially since on a "fresh" login the "role" claim comes back fine. (Note: I do see the role claim missing from the context in the error scenario).
Here is my client application configuration:
services.AddAuthentication(authOpts =>
{
authOpts.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
authOpts.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, opts => { })
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, openIdOpts =>
{
openIdOpts.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
openIdOpts.Authority = settings.IDP.Authority;
openIdOpts.ClientId = settings.IDP.ClientId;
openIdOpts.ClientSecret = settings.IDP.ClientSecret;
openIdOpts.ResponseType = settings.IDP.ResponseType;
openIdOpts.GetClaimsFromUserInfoEndpoint = true;
openIdOpts.RequireHttpsMetadata = false;
openIdOpts.SaveTokens = true;
openIdOpts.ResponseMode = "form_post";
openIdOpts.Scope.Clear();
settings.IDP.Scope.ForEach(s => openIdOpts.Scope.Add(s));
// https://leastprivilege.com/2017/11/15/missing-claims-in-the-asp-net-core-2-openid-connect-handler/
// https://github.com/aspnet/Security/issues/1449
// https://github.com/IdentityServer/IdentityServer4/issues/1786
// Add Claim Mappings
openIdOpts.ClaimActions.MapUniqueJsonKey("preferred_username", "preferred_username"); /* SID alias */
openIdOpts.ClaimActions.MapJsonKey("role", "role", "role");
openIdOpts.TokenValidationParameters = new TokenValidationParameters
{
ValidAudience = settings.IDP.ClientId,
ValidIssuer = settings.IDP.Authority,
NameClaimType = "name",
RoleClaimType = "role"
};
openIdOpts.Events = new OpenIdConnectEvents
{
OnUserInformationReceived = context =>
{
Log.Info("Recieved user info from IDP.");
// check for missing roles? they are here on a fresh login but missing
// after x amount of time (1 hour?)
return Task.CompletedTask;
},
OnRedirectToIdentityProvider = context =>
{
Log.Info("Redirecting to identity provider.");
return Task.CompletedTask;
},
OnTokenValidated = context =>
{
Log.Debug("OnTokenValidated");
// this addressed the scenario where the Identity Server validates a user however that user does not
// exist in the currently configured source system.
// Can happen if there is a configuration mismatch between the local SID system and the IDP Client
var validUser = false;
int uid = 0;
var identity = context.Principal?.Identity as ClaimsIdentity;
if (identity != null)
{
var sub = identity.Claims.FirstOrDefault(c => c.Type == "sub");
Log.Debug($" Validating sub '{sub.Value}'");
if (sub != null && !string.IsNullOrWhiteSpace(sub.Value))
{
if (Int32.TryParse(sub.Value, out uid))
{
using (var configSvc = ApiServiceHelper.GetAdminService(settings))
{
try
{
var usr = configSvc.EaiUser.GetByID(uid);
if (usr != null && usr.ID.GetValueOrDefault(0) > 0)
validUser = true;
}
catch { }
}
}
}
Log.Debug($" Validated sub '{sub.Value}'");
}
if (!validUser)
{
// uhhh, does this work? Logout?
// TODO: test!
Log.Warn($"Unable to validate user is SID for ({uid}). Redirecting to '/Home/Logout'");
context.Response.Redirect("/Home/Logout?msg=User not validated in source system");
context.HandleResponse();
}
return Task.CompletedTask;
},
OnTicketReceived = context =>
{
// TODO: Is this necessary?
// added the below code because I thought my application access_token was expired
// however it turns out I'm actually misisng the role claims when I come back to the
// application from the IDP after about an hour
if (context.Properties != null &&
context.Properties.Items != null)
{
DateTime expiresAt = System.DateTime.MinValue;
foreach (var p in context.Properties.Items)
{
if (p.Key == ".Token.expires_at")
{
DateTime.TryParse(p.Value, null, DateTimeStyles.AdjustToUniversal, out expiresAt);
break;
}
}
if (expiresAt != DateTime.MinValue &&
expiresAt != DateTime.MaxValue)
{
// I did this to synch the .NET cookie timeout with the IDP access token timeout?
// This somewhat concerns me becuase I thought that part should be done auto-magically already
// I mean, refresh token?
context.Properties.IsPersistent = true;
context.Properties.ExpiresUtc = expiresAt;
}
}
return Task.CompletedTask;
}
};
});
I'm sorry folks, looks like I found the source of my issue.
Total fail on my side :(.
I had a bug in the ProfileService in my Identity Server implementation that was causing the roles to not be returned in all cases
humph, thanks!
how to restrict x amount of login on each client app in specific the SPA client with grant type - implicit
This is out of scope within Identity server
Solutions tried -
Access tokens persisted to DB, however this approach the client kept updating the access token without coming to code because the client browser request is coming with a valid token though its expired the silent authentication is renewing the token by issues a new reference token ( that can be seen in the table persistGrants token_type 'reference_token')
Cookie event - on validateAsync - not much luck though this only works for the server web, we can't put this logic on the oidc library on the client side for SPA's.
Custom signInManager by overriding SignInAsync - but the the executing is not reaching to this point in debug mode because the IDM kept recognising the user has a valid toke ( though expired) kept re issueing the token ( please note there is no refresh token here to manage it by storing and modifying!!!)
Any clues how the IDM re issue the token without taking user to login screen, even though the access token is expired??(Silent authentication. ??
implement profile service overrride activeasync
public override async Task IsActiveAsync(IsActiveContext context)
{
var sub = context.Subject.GetSubjectId();
var user = await userManager.FindByIdAsync(sub);
//Check existing sessions
if (context.Caller.Equals("AccessTokenValidation", StringComparison.OrdinalIgnoreCase))
{
if (user != null)
context.IsActive = !appuser.VerifyRenewToken(sub, context.Client.ClientId);
else
context.IsActive = false;
}
else
context.IsActive = user != null;
}
startup
services.AddTransient<IProfileService, ProfileService>();
while adding the identity server service to collection under configure services
.AddProfileService<ProfileService>();
Update
Session.Abandon(); //is only in aspnet prior versions not in core
Session.Clear();//clears the session doesn't mean that session expired this should be controlled by addSession life time when including service.
I have happened to found a better way i.e. using aspnetuser securitystamp, every time user log-in update the security stamp so that any prior active session/cookies will get invalidated.
_userManager.UpdateSecurityStampAsync(_userManager.FindByEmailAsync(model.Email).Result).Result
Update (final):
On sign-in:-
var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberLogin, false);
if (result.Succeeded)
{
//Update security stamp to invalidate existing sessions
var user = _userManager.FindByEmailAsync(model.Email).Result;
var test= _userManager.UpdateSecurityStampAsync(user).Result;
//Refresh the cookie to update securitystamp on authenticationmanager responsegrant to the current request
await _signInManager.RefreshSignInAsync(user);
}
Profile service implementation :-
public class ProfileService : ProfileService<ApplicationUser>
{
public override async Task IsActiveAsync(IsActiveContext context)
{
if (context == null) throw new ArgumentNullException(nameof(context));
if (context.Subject == null) throw new ArgumentNullException(nameof(context.Subject));
context.IsActive = false;
var subject = context.Subject;
var user = await userManager.FindByIdAsync(context.Subject.GetSubjectId());
if (user != null)
{
var security_stamp_changed = false;
if (userManager.SupportsUserSecurityStamp)
{
var security_stamp = (
from claim in subject.Claims
where claim.Type =="AspNet.Identity.SecurityStamp"
select claim.Value
).SingleOrDefault();
if (security_stamp != null)
{
var latest_security_stamp = await userManager.GetSecurityStampAsync(user);
security_stamp_changed = security_stamp != latest_security_stamp;
}
}
context.IsActive =
!security_stamp_changed &&
!await userManager.IsLockedOutAsync(user);
}
}
}
*
Hook in the service collection:-
*
services.AddIdentityServer()
.AddAspNetIdentity<ApplicationUser>()
.AddProfileService<ProfileService>();
i.e. on every login, the security stamp of the user gets updated and pushed to the cookie, when the token expires, the authorize end point will verify on the security change, If there is any then redirects the user to login. This way we are ensuring there will only be one active session
I am trying to add an attachment to a document - I am running a separate MVC application - not part of the same solution as the Kentico admin site.
I am getting error site not found for my UserInfo.
public Models.PartnerUpdateModel NewPartnerUpdate(Models.PartnerUpdateModel partnerupdatemodel)
{
CMS.DocumentEngine.TreeNode newpartnerupdatetreenode = CMS.DocumentEngine.TreeNode.New(partnerupdatemodel.KenticoPartnerUpdate.ClassName);
TreeProvider tree = new TreeProvider();
partnerupdatemodel.KenticoPartnerUpdate.Title = partnerupdatemodel.Title;
partnerupdatemodel.KenticoPartnerUpdate.Summary = partnerupdatemodel.Summary;
partnerupdatemodel.KenticoPartnerUpdate.Article = partnerupdatemodel.Article;
partnerupdatemodel.KenticoPartnerUpdate.Author = partnerupdatemodel.MarketConnectUser.UserID.ToString();
partnerupdatemodel.KenticoPartnerUpdate.DocumentCulture = partnerupdatemodel.DocumentCulture;
partnerupdatemodel.KenticoPartnerUpdate.Insert(TreeHelper.SelectSingleNode(partnerupdatemodel.MarketConnectUser.DefaultGroup.GroupChildNodes.Where(x => x.Key == partnerupdatemodel.ParentClassName).Select(x => x.Value).Single()), true);
partnerupdatemodel.KenticoPartnerUpdate.SubmitChanges(true);
foreach (var file in partnerupdatemodel.Files)
{
HttpPostedFile postedfile = FileHelper.ConstructHttpPostedFile(FileHelper.ToByteArray(file.InputStream), file.FileName, file.ContentType);
DocumentHelper.AddAttachment(newpartnerupdatetreenode, Models.PartnerUpdateModel.AttachmentColumnNames.Image.ToString(), postedfile, new TreeProvider(MembershipContext.AuthenticatedUser)); //GETTING ERROR HERE
newpartnerupdatetreenode.Update();
}
return partnerupdatemodel;
}
MembershipContext is not oficially supported in v9 (Supported and unsupported Kentico features on MVC sites) however I think this should work - to be honest I have tried it by myself and I`ve got UserInfo (for public user and global admin too). Could you please provide call stack of exception?
edit: I have also tried to get current SiteInfo like
var site = SiteContext.CurrentSite;
and it`s null if you have not specified Presentation URL - could you check this setting?
I am using Asp.Net with MVC 4 to build a web application. For authentication, I am using forms authentication. The login page is set correctly and login behaves properly. However, instead of using the default partial login view I am using my own and I use AJAX to log in.
The login controller works fine and here is the code for login.
Here is my code in login action. Here resp is my custom response object
resp.Status = true;
// sometimes used to persist user roles
string userData = "some user data";
FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(
1, // ticket version
login.username, // authenticated username
DateTime.Now, // issueDate
DateTime.Now.AddMinutes(30), // expiryDate
false, // true to persist across browser sessions
userData, // can be used to store additional user data
FormsAuthentication.FormsCookiePath); // the path for the cookie
// Encrypt the ticket using the machine key
string encryptedTicket = FormsAuthentication.Encrypt(ticket);
// Add the cookie to the request to save it
HttpCookie cookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket);
cookie.HttpOnly = true;
//Response.Cookies.Add(cookie);
Response.SetCookie(cookie);
return Json(resp);
Here is the code of cshtml page to handle this script response
function (respData) {
if (respData.Status) {
window.location.href = "/";
}
if (!respData.Status) {
if (respData.Errors[0].ErrorCode == 1) {
$('#invalid').show();
$('#username').val('');
$('#password').val('');
}
else if (respData.Errors[0].ErrorCode == -1) {
var msg = respData.Errors[0].ErrorDescription;
$('#error_email').text(msg);
}
else {
var msg = respData.Errors[0].ErrorDescription;
$('#error_pwd').text(msg);
}
}
$("#dialog").dialog("close");
},
Everything works fine and the user is successfully redirected to home page on successful login. Also gets a proper message on failure.
The problem is, when I browse any other page after this successful redirection, the subsequent requests are not authenticated.
I did a little bit research and found that the browser is not sending the forms authentication cookie in the subsequent requests and hence those requests are not authenticated.
Any idea on this behavior ? , Am I missing something ?
Try explicitly setting the expiry time on your cookie with:
Cookie.Expires(DateTime.Now.AddMinutes(30));
I'm using the PersistentTokenBasedRememberMeServices (Spring Security 2.04) in Grails App in conjunction with the OpenIDAuthenticationProcessingFilter. The configuration is as follows (This is Grails's DSL equivalent to Spring resource.xml but it should be quite easy to adapt):
customTokenRepository(JdbcTokenRepositoryImpl)
{
dataSource = ref('dataSource')
}
rememberMeServices(PersistentTokenBasedRememberMeServices) {
userDetailsService = ref('userDetailsService')
key = securityConf.rememberMeKey
cookieName = securityConf.cookieName
alwaysRemember = securityConf.alwaysRemember
tokenValiditySeconds = securityConf.tokenValiditySeconds
parameter = securityConf.parameter
tokenRepository = customTokenRepository
}
openIDAuthProvider(org.codehaus.groovy.grails.plugins.springsecurity.openid.GrailsOpenIdAuthenticationProvider) {
userDetailsService = ref('userDetailsService')
}
openIDStore(org.openid4java.consumer.InMemoryConsumerAssociationStore)
openIDNonceVerifier(org.openid4java.consumer.InMemoryNonceVerifier, securityConf.openIdNonceMaxSeconds) // 300 seconds
openIDConsumerManager(org.openid4java.consumer.ConsumerManager) {
nonceVerifier = openIDNonceVerifier
}
openIDConsumer(org.springframework.security.ui.openid.consumers.OpenID4JavaConsumer, openIDConsumerManager)
openIDAuthenticationProcessingFilter(org.springframework.security.ui.openid.OpenIDAuthenticationProcessingFilter) {
authenticationManager = ref('authenticationManager')
authenticationFailureUrl = securityConf.authenticationFailureUrl //'/login/authfail?login_error=1' // /spring_security_login?login_error
defaultTargetUrl = securityConf.defaultTargetUrl // '/'
filterProcessesUrl = '/j_spring_openid_security_check' // not configurable
rememberMeServices = ref('rememberMeServices')
consumer = openIDConsumer
targetUrlResolver = customTargetUrlResolver
}
After a user has authenticated everything is fine until the cookie issued to him is used for the first time for example after a container restart (see here).
The very first request using the cookie seems to be always fine but after the cookie has been updated with a new date and most importantly a new token, subsequent requests will crash in here. As if the browser would still request resources using the old version of the cookie containing the old token. I'm totally baffled why this happens. Any suggestions?