I've recently been learned how to implement a token based authentication with ASP.NET and I would love to get some input on how my code & structure is as well as how I can make it better.
The code I'd like to share consists of the ASP.NET web API controller, and the client Xamarin application.
One question I have been having is am I taking the proper approach with refreshing the token every time the app is launched (and the user has already entered their credentials prior?)
Web API Controller:
[HttpPost("token")]
[AllowAnonymous]
public async Task<IActionResult> GenerateToken([FromForm]LoginModel model)
{
var user = await _userManager.FindByNameAsync(model.Username);
if (user != null && await _userManager.CheckPasswordAsync(user, model.Password))
{
var token = _identityService.GenerateToken(user);
string tokenText = new JwtSecurityTokenHandler().WriteToken(token);
var refreshToken = _identityService.GenerateRefreshToken();
user.RefreshToken = refreshToken;
_context.Update(user);
_context.SaveChanges();
string expirationString = token.Claims.Single(x => x.Type == "exp").Value;
DateTimeOffset dateTimeOffset = DateTimeOffset.FromUnixTimeSeconds(long.Parse(expirationString));
return Ok(new
{
token = tokenText,
refreshToken,
expiration = dateTimeOffset
});
}
ModelState.AddModelError("", "Your email or password did not match any users. Please verify you have entered the right credentials.");
return Unauthorized(ModelState);
}
[HttpPost("RefreshToken")]
public IActionResult RefreshToken([FromForm]string token, [FromForm]string refreshToken)
{
var principal = _identityService.GetPrincipalFromExpiredToken(token);
var username = principal.Identity.Name;
var allClaims = principal.Claims.ToList();
var name = allClaims.First(c => c.Type.Contains("nameidentifier")).Value;
var user = _context.Users.Single(x => x.UserName == name);
var savedRefreshToken = user.RefreshToken;
if (savedRefreshToken != refreshToken)
throw new SecurityTokenException("Invalid refresh token");
var newJwtToken = _identityService.GenerateToken(user);
var newRefreshToken = _identityService.GenerateRefreshToken();
user.RefreshToken = newRefreshToken;
_context.Update(user);
_context.SaveChanges();
string tokenText = new JwtSecurityTokenHandler().WriteToken(newJwtToken);
string expirationString = newJwtToken.Claims.Single(x => x.Type == "exp").Value;
DateTimeOffset dateTimeOffset = DateTimeOffset.FromUnixTimeSeconds(long.Parse(expirationString));
return new ObjectResult(new
{
token = tokenText,
refreshToken = newRefreshToken,
expiration = dateTimeOffset
});
}
Xamarin Client
private async void RefreshToken()
{
// get the saved refresh token
string refreshToken = CrossSettings.Current.GetValueOrDefault("RefreshToken", "_");
string token = CrossSettings.Current.GetValueOrDefault("Token", "_");
// the IDatabaseManager implementation simply makes the HTTP calls
var db = TinyIoCContainer.Current.Resolve<IDatabaseManager>();
// returns http result from HTTP call to refresh token controller action
var response = await db.RefreshToken(token, refreshToken);
if (response.IsSuccessStatusCode)
{
var contentString = await response.Content.ReadAsStringAsync();
var content = JsonConvert.DeserializeObject<IdentityResponse>(contentString);
var newToken = content.Token;
var newRefreshToken = content.RefreshToken;
db.SetToken(token);
CrossSettings.Current.AddOrUpdateValue("RefreshToken", newRefreshToken);
CrossSettings.Current.AddOrUpdateValue("Token", newToken);
MainPage = new NavigationPage(new HomeMaster());
isLaunched = true;
}
else
{
// the refresh token is invalid
MainPage = new LoginPage();
await MainPage.DisplayAlert("Authentication Error", "You have been logged out", "Ok");
}
}
When the Xamarin app is launched, it will check for an existing token first. If a token is found, the RefreshToken method is called. If not, it brings the user to the login screen.
1 Answer 1
It is fine to refresh the token on every app start.
It will also prevent potential hijacked tokens to be usefull for a longer time, when the token is refreshed everytime the user starts the app and the old token becomes invalid.
Explore related questions
See similar questions with these tags.