In my web application I need to know whether users are online or offline. I need to update the status in the database whenever users' sessions are created or destroyed, so SessionRegistry is not an option.
I implemented it using session counters - when a user logs in, his counter increments, when he logs out or his session times out, his counter decrements.
web.xml
<listener>
<listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class>
</listener>
Service
#Service
public class UsersLoginLogoutListener implements ApplicationListener<ApplicationEvent> {
#Autowired
private AccountService accountService;
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof InteractiveAuthenticationSuccessEvent) { // User log in
System.out.println("User logged in...");
InteractiveAuthenticationSuccessEvent iasEvent = (InteractiveAuthenticationSuccessEvent) event;
String userName = iasEvent.getAuthentication().getName();
if (userName != null) {
System.out.println("...username is " + userName + ".");
accountService.incrementSessionCounter(userName);
}
}
else if (event instanceof SessionDestroyedEvent) { // User log out / session timeout
System.out.println("User logged out or session timeout...");
SessionDestroyedEvent sdEvent = (SessionDestroyedEvent) event;
List<SecurityContext> lstSecurityContext = sdEvent.getSecurityContexts();
System.out.println("...SecurityContexts list size: " + lstSecurityContext.size() + "...");
for (SecurityContext securityContext : lstSecurityContext)
{
String userName = securityContext.getAuthentication().getName();
if (userName != null) {
System.out.println("...username is " + userName + ".");
accountService.decrementSessionCounter(userName);
}
}
}
}
}
It's been working just fine until recently, when I noticed that one counter was incremented, but never was decremented. Thanks to the logs I was able to reproduce it.
A user opens login pages in two browser tabs and logs in on one tab, then he logs in on another tab without logging out on the first tab. If he entered the correct credentials everything works fine:
User logged in... ...username is testuser.
User logged out or session timeout... ...SecurityContexts list size:
1... ...username is testuser.
User logged in... ...username is testuser.
Even though he logged in twice using the same browser, his first session was destroyed and my counter worked correctly.
But if he entered incorrect credentials, I see the following output:
User logged in... ...username is testuser.
(After session timeout)
User logged out or session timeout... ...SecurityContexts list size:
0...
As you can see, in this case SessionDestroyedEvent was fired, but only after the session timeout and SecurityContexts list was empty, so user's session counter wasn't decremented.
Related
I have a login page called LoginTrial, the user enters the username and password and presses enter to log in.
The code below does successfully test the validity of username/password combos and returns an error if it does not exist in the table User.
However, if the combo of username/password is correct, the login looks to work, but it is as if i had not logged in at all, i don't see anything under: if (User.Identity.IsAuthenticated)
I know it's a bad idea to not hash passwords, but this was made to test login features.
User appUser = dbs.FromSqlInterpolated($"SELECT * FROM [User] WHERE username = {uid} AND password = {pw}").FirstOrDefault();
principal = null;
if (appUser != null)
{
principal = new ClaimsPrincipal(
new ClaimsIdentity(
new Claim[] {
new Claim(ClaimTypes.Name, appUser.Username),
new Claim(ClaimTypes.Role, appUser.Role)
}, "Basic"
)
);
return true;
}
return false;
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 have troubles using the default template that implements Facebook login (and other providers).
I can get logged to the app, however logged I would like to arrive on a register form, pre-filled with Facebook data (in order to complete the profile).
I don't have issue to get the default claims (email, names), however I can't add birthday or location.
My startup has the following lines:
fo.Scope.Add("public_profile");
fo.Scope.Add("email");
fo.Scope.Add("user_birthday");
fo.Scope.Add("user_location");
...
app.UseFacebookAuthentication(fo);
This works, but does not ask for these permissions during the login process.
And if I add
fo.Fields.Add("location"); or
fo.Fields.Add("user_birthday");
The login process in the callback fails.
The external login call is the default one:
var redirectUrl = Url.Action("ExternalLoginCallback", Name, new { ReturnUrl = returnUrl });
var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
return Challenge(properties, provider);
And the callback is the following:
var info = await _signInManager.GetExternalLoginInfoAsync();
if (info == null)
return RedirectToAction(nameof(Login));
// Sign in the user with this external login provider if the user already has a login.
var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false);
if (result.Succeeded)
{
_logger.LogInformation(5, "User logged in with {Name} provider.", info.LoginProvider);
return RedirectToLocal(returnUrl);
}
if (result.RequiresTwoFactor)
return RedirectToAction(nameof(SendCode), new { ReturnUrl = returnUrl });
if (result.IsLockedOut)
return View("Lockout");
else
{
// If the user does not have an account, then ask the user to create an account.
ViewData["ReturnUrl"] = returnUrl;
ViewData["LoginProvider"] = info.LoginProvider;
Console.WriteLine("Debug claims!");
foreach (var c in info.Principal.Claims)
Console.WriteLine("---> " + c.Subject + ":" + c.Value + " " + c.ToString());
Console.WriteLine("End Debug claims!");
I never got anything more than the default claims and got some exceptions like this when adding the Fields on startup
An unhandled exception has occurred: Response status code does not indicate success: 400 (Bad Request).
System.Net.Http.HttpRequestException: Response status code does not indicate success: 400 (Bad Request).
at System.Net.Http.HttpResponseMessage.EnsureSuccessStatusCode()
at Microsoft.AspNetCore.Authentication.Facebook.FacebookHandler.<CreateTicketAsync>d__1.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at Microsoft.AspNetCore.Authentication.OAuth.OAuthHandler`1.<HandleRemoteAuthenticateAsync>d__5.MoveNext()
Any idea on how to do this? (not via the graph API, even if it is much easier)
I have been searching for hours and cannot find anything else than "email" issues.
The two issues are:
1. Why at login, the scopes set on startup are not required?
2. How to retreive the fields from Facebook? (birthday, gender, location)
I added Shiro session management (based on Kim's and Leif's webinar) to the Vaadin quick ticket dashboard demo application. When I do a browser reload in the application I get thrown back to the login page with no session. How / where can I prevent this.
I have a standard shiro.ini setup
Login button handler:
signin.addClickListener(new ClickListener() {
#Override
public void buttonClick(ClickEvent event) {
boolean loginOK = false;
Factory<SecurityManager> factory =
new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager((org.apache.shiro.mgt.SecurityManager)
securityManager);
Subject currentUser = SecurityUtils.getSubject();
//collect user principals and credentials in a gui specific manner
//such as username/password html form, X509 certificate, OpenID, etc.
//We'll use the username/password example here since it is the most common.
UsernamePasswordToken token =
new UsernamePasswordToken(username.getValue(), password.getValue());
//this is all you have to do to support 'remember me' (no config - built in!):
token.setRememberMe(true);
//currentUser.login(token);
try {
logger.log(Level.INFO, "trying login");
currentUser.login( token );
logger.log(Level.INFO, "login done");
//if no exception, that's it, we're done!
} catch ( Exception e ) {
logger.log(Level.INFO, "exception");
}
if ( currentUser.hasRole( "schwartz" ) ) {
loginOK = true;
} else {
loginOK = false;
}
if (loginOK) {
signin.removeShortcutListener(enter);
buildMainView();
} else {
if (loginPanel.getComponentCount() > 2) {
// Remove the previous error message
loginPanel.removeComponent(loginPanel.getComponent(2));
}
// Add new error message
Label error = new Label(
"Wrong username or password. <span>Hint: try empty values</span>",
ContentMode.HTML);
error.addStyleName("error");
error.setSizeUndefined();
error.addStyleName("light");
// Add animation
error.addStyleName("v-animate-reveal");
loginPanel.addComponent(error);
username.focus();
}
}
});
Use #preserveonRefresh annotation in UI init class
I recommend using Shiro's web filter. This way, your session will not be lost and you can prohibit unauthorized actions (e.g. Instantiating view objects) easily since Shiro's context is already set up when you display the login or any other view.
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));