Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Refresh user cookie ticket in ASP.Net Core Identity

In a controller in an ASP.NET Core web application I want to refresh the user and claims in the cookie ticket stored on the client.

The client is authenticated and authorized, ASP.NET Core Identity stores this Information in the cookie ticket - now in some Controller actions I want to refresh the data in the cookie.

The SignInManager has a function to refresh RefreshSignInAsync, but it does not accept HttpContext.User as parameter.

[HttpPost("[action]")]
[Authorize]
public async Task<IActionResult> Validate()
{
  // todo: update the Client Cookie
  await _signInManager.RefreshSignInAsync(User); // wrong type
}

How do I refresh the cookie?

like image 282
Sam Avatar asked Jan 26 '17 09:01

Sam


2 Answers

public static class HttpContextExtensions
{
    public static async Task RefreshLoginAsync(this HttpContext context)
    {
        if (context.User == null)
            return;

        // The example uses base class, IdentityUser, yours may be called 
        // ApplicationUser if you have added any extra fields to the model
        var userManager = context.RequestServices
            .GetRequiredService<UserManager<IdentityUser>>();
        var signInManager = context.RequestServices
            .GetRequiredService<SignInManager<IdentityUser>>();

        IdentityUser user = await userManager.GetUserAsync(context.User);

        if(signInManager.IsSignedIn(context.User))
        {
            await signInManager.RefreshSignInAsync(user);
        }
    }
}

Then use it in your controller

[HttpPost("[action]")]
[Authorize]
public async Task<IActionResult> Validate()
{
    await HttpContext.RefreshLoginAsync();
}

Or abstract it in an action filter

public class RefreshLoginAttribute : ActionFilterAttribute
{
    public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        await context.HttpContext.RefreshLoginAsync();

        await next();
    }
}

Then use it like this in your controller

[HttpPost("[action]")]
[Authorize]
[RefreshLogin] // or simpler [Authorize, RefreshLogin]
public async Task<IActionResult> Validate()
{
    // your normal controller code
}
like image 155
Tseng Avatar answered Oct 19 '22 23:10

Tseng


This is also possible if the user is already logged out (their access token has expired but their refresh token is still valid).

Important note: the following only works if you have "Do you want to remember your user's devices?" set to "No" in the cognito config. If anyone knows how to get it to work with it on, please let me know.

We use the following flow (js client app connecting to .NET Core API):

  1. User signs in using username/password (CognitoSignInManager<CognitoUser>.PasswordSignInAsync)
  2. The client receives the token, userID, and refreshToken and stores them in localStorage.
  3. When the original token expires (1 hour), the client gets a 401 error from the API.
  4. The client calls another API endpoint with the userID and refreshToken which then in turn calls the code below on our user service.
  5. If the refresh result is successful, we return the new token (AuthenticationResult.IdToken).
  6. The client the repeats the call that originally errored in a 401 with the new token.

Here is the code we added to the User Service:

public async Task<UserLoginResult> SignInRefreshAsync(string uid, string refreshToken)
{
    try
    {
        var result = await _cognitoIdentityProvider.InitiateAuthAsync(
            new InitiateAuthRequest
            {
                AuthFlow = AuthFlowType.REFRESH_TOKEN_AUTH,
                ClientId = _pool.ClientID,
                AuthParameters = new Dictionary<string, string>
                {
                    { "REFRESH_TOKEN", refreshToken },
                    { "SECRET_HASH", HmacSHA256(uid + _pool.ClientID, _options.UserPoolClientSecret) }
                }
            });

        if (!result.HttpStatusCode.Successful() || string.IsNullOrEmpty(result.AuthenticationResult?.IdToken))
            return new UserLoginResult(UserLoginStatus.Failed);

        return new UserLoginResult(UserLoginStatus.Success, uid, null, null, result.AuthenticationResult.IdToken, null);
    }
    catch
    {
        return new UserLoginResult(UserLoginStatus.Failed);
    }
}

private static string HmacSHA256(string data, string key)
{
    using (var sha = new System.Security.Cryptography.HMACSHA256(System.Text.Encoding.UTF8.GetBytes(key)))
    {
        var result = sha.ComputeHash(System.Text.Encoding.UTF8.GetBytes(data));
        return Convert.ToBase64String(result);
    }
}

IAmazonCognitoIdentityProvider _cognitoIdentityProvider is resolved from DI.

AWSCognitoClientOptions _options = configuration.GetAWSCognitoClientOptions(); and IConfiguration configuration is also resolved from DI.

UserLoginResult is our class to hold the token and refresh token. Obviously, adjust accordingly.

Please note that setting SECRET_HASH may not be required based on your config is Cognito.

like image 24
Oleg Fridman Avatar answered Oct 19 '22 23:10

Oleg Fridman