Search code examples
oauth-2.0asp.net-identityaccess-tokenidentityserver4angular2-jwt

Implicit grant SPA with identity server4 concurrent login


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 -

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

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

  3. 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. ??


Solution

  • 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