Created
December 5, 2020 17:07
-
-
Save manoj-choudhari-git/a704d77c029c2798adb6d3fcdbe2f0b5 to your computer and use it in GitHub Desktop.
.NET 5 Web API controller to generate access tokens and refresh tokens
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[Route("api/[controller]")] | |
[ApiController] | |
public class AuthController : ControllerBase | |
{ | |
private readonly JwtBearerTokenSettings jwtBearerTokenSettings; | |
private readonly UserManager<ApplicationUser> userManager; | |
private readonly ApplicationDbContext dbContext; | |
public AuthController(IOptions<JwtBearerTokenSettings> jwtTokenOptions, UserManager<ApplicationUser> userManager, ApplicationDbContext dbContext) | |
{ | |
this.jwtBearerTokenSettings = jwtTokenOptions.Value; | |
this.userManager = userManager; | |
this.dbContext = dbContext; | |
} | |
[HttpPost] | |
[Route("Register")] | |
public async Task<IActionResult> Register([FromBody] UserDetails userDetails) | |
{ | |
if (!ModelState.IsValid || userDetails == null) | |
{ | |
return new BadRequestObjectResult(new { Message = "User Registration Failed" }); | |
} | |
var identityUser = new ApplicationUser() { UserName = userDetails.UserName, Email = userDetails.Email }; | |
var result = await userManager.CreateAsync(identityUser, userDetails.Password); | |
if (!result.Succeeded) | |
{ | |
var dictionary = new ModelStateDictionary(); | |
foreach (IdentityError error in result.Errors) | |
{ | |
dictionary.AddModelError(error.Code, error.Description); | |
} | |
return new BadRequestObjectResult(new { Message = "User Registration Failed", Errors = dictionary }); | |
} | |
return Ok(new { Message = "User Reigstration Successful" }); | |
} | |
[HttpPost] | |
[Route("Login")] | |
public async Task<IActionResult> Login([FromBody] LoginCredentials credentials) | |
{ | |
ApplicationUser identityUser; | |
if (!ModelState.IsValid | |
|| credentials == null | |
|| (identityUser = await ValidateUser(credentials)) == null) | |
{ | |
return new BadRequestObjectResult(new { Message = "Login failed" }); | |
} | |
var token = GenerateTokens(identityUser); | |
return Ok(new { Token = token, Message = "Success" }); | |
} | |
[AllowAnonymous] | |
[HttpPost] | |
[Route("RefreshToken")] | |
public IActionResult RefreshToken() | |
{ | |
var token = HttpContext.Request.Cookies["refreshToken"]; | |
var identityUser = dbContext.Users.Include(x => x.RefreshTokens) | |
.FirstOrDefault(x => x.RefreshTokens.Any(y => y.Token == token && y.UserId == x.Id)); | |
// Get existing refresh token if it is valid and revoke it | |
var existingRefreshToken = GetValidRefreshToken(token, identityUser); | |
if(existingRefreshToken == null) | |
{ | |
return new BadRequestObjectResult(new { Message = "Failed" }); | |
} | |
existingRefreshToken.RevokedByIp = HttpContext.Connection.RemoteIpAddress.ToString(); | |
existingRefreshToken.RevokedOn = DateTime.UtcNow; | |
// Generate new tokens | |
var newToken = GenerateTokens(identityUser); | |
return Ok(new { Token = newToken, Message = "Success" }); | |
} | |
[HttpPost] | |
[Route("RevokeToken")] | |
public IActionResult RevokeToken(string token) | |
{ | |
// If user found, then revoke | |
if (RevokeRefreshToken(token)) | |
{ | |
return Ok(new { Message = "Success" }); | |
} | |
// Otherwise, return error | |
return new BadRequestObjectResult(new { Message = "Failed" }); | |
} | |
[HttpPost] | |
[Route("Logout")] | |
public async Task<IActionResult> Logout() | |
{ | |
// Revoke Refresh Token | |
RevokeRefreshToken(); | |
return Ok(new { Token = "", Message = "Logged Out" }); | |
} | |
private RefreshToken GetValidRefreshToken(string token, ApplicationUser identityUser) | |
{ | |
if (identityUser == null) | |
{ | |
return null; | |
} | |
var existingToken = identityUser.RefreshTokens.FirstOrDefault(x => x.Token == token); | |
return IsRefreshTokenValid(existingToken) ? existingToken : null; | |
} | |
private bool RevokeRefreshToken(string token = null) | |
{ | |
token = token == null ? HttpContext.Request.Cookies["refreshToken"] : token; | |
var identityUser = dbContext.Users.Include(x => x.RefreshTokens) | |
.FirstOrDefault(x => x.RefreshTokens.Any(y => y.Token == token && y.UserId == x.Id)); | |
if (identityUser == null) | |
{ | |
return false; | |
} | |
// Revoke Refresh token | |
var existingToken = identityUser.RefreshTokens.FirstOrDefault(x => x.Token == token); | |
existingToken.RevokedByIp = HttpContext.Connection.RemoteIpAddress.ToString(); | |
existingToken.RevokedOn = DateTime.UtcNow; | |
dbContext.Update(identityUser); | |
dbContext.SaveChanges(); | |
return true; | |
} | |
private async Task<ApplicationUser> ValidateUser(LoginCredentials credentials) | |
{ | |
var identityUser = await userManager.FindByNameAsync(credentials.Username); | |
if (identityUser != null) | |
{ | |
var result = userManager.PasswordHasher.VerifyHashedPassword(identityUser, identityUser.PasswordHash, credentials.Password); | |
return result == PasswordVerificationResult.Failed ? null : identityUser; | |
} | |
return null; | |
} | |
private string GenerateTokens(ApplicationUser identityUser) | |
{ | |
// Generate access token | |
string accessToken = GenerateAccessToken(identityUser); | |
// Generate refresh token and set it to cookie | |
var ipAddress = HttpContext.Connection.RemoteIpAddress.ToString(); | |
var refreshToken = GenerateRefreshToken(ipAddress, identityUser.Id); | |
// Set Refresh Token Cookie | |
var cookieOptions = new CookieOptions | |
{ | |
HttpOnly = true, | |
Expires = DateTime.UtcNow.AddDays(7) | |
}; | |
HttpContext.Response.Cookies.Append("refreshToken", refreshToken.Token, cookieOptions); | |
// Save refresh token to database | |
if (identityUser.RefreshTokens == null) | |
{ | |
identityUser.RefreshTokens = new List<RefreshToken>(); | |
} | |
identityUser.RefreshTokens.Add(refreshToken); | |
dbContext.Update(identityUser); | |
dbContext.SaveChanges(); | |
return accessToken; | |
} | |
private string GenerateAccessToken(ApplicationUser identityUser) | |
{ | |
var tokenHandler = new JwtSecurityTokenHandler(); | |
var key = Encoding.ASCII.GetBytes(jwtBearerTokenSettings.SecretKey); | |
var tokenDescriptor = new SecurityTokenDescriptor | |
{ | |
Subject = new ClaimsIdentity(new Claim[] | |
{ | |
new Claim(ClaimTypes.Name, identityUser.UserName.ToString()), | |
new Claim(ClaimTypes.Email, identityUser.Email) | |
}), | |
Expires = DateTime.Now.AddSeconds(jwtBearerTokenSettings.ExpiryTimeInSeconds), | |
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature), | |
Audience = jwtBearerTokenSettings.Audience, | |
Issuer = jwtBearerTokenSettings.Issuer | |
}; | |
var token = tokenHandler.CreateToken(tokenDescriptor); | |
return tokenHandler.WriteToken(token); | |
} | |
private RefreshToken GenerateRefreshToken(string ipAddress, string userId) | |
{ | |
using (var rngCryptoServiceProvider = new RNGCryptoServiceProvider()) | |
{ | |
var randomBytes = new byte[64]; | |
rngCryptoServiceProvider.GetBytes(randomBytes); | |
return new RefreshToken | |
{ | |
Token = Convert.ToBase64String(randomBytes), | |
ExpiryOn = DateTime.UtcNow.AddDays(jwtBearerTokenSettings.RefreshTokenExpiryInDays), | |
CreatedOn = DateTime.UtcNow, | |
CreatedByIp = ipAddress, | |
UserId = userId | |
}; | |
} | |
} | |
private bool IsRefreshTokenValid(RefreshToken existingToken) | |
{ | |
// Is token already revoked, then return false | |
if (existingToken.RevokedByIp != null && existingToken.RevokedOn != DateTime.MinValue) | |
{ | |
return false; | |
} | |
// Token already expired, then return false | |
if (existingToken.ExpiryOn <= DateTime.UtcNow) | |
{ | |
return false; | |
} | |
return true; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment