auto.bus_api/Server/Services/AuthenticationService.cs

327 lines
12 KiB
C#

using System.Globalization;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using Google.Apis.Auth;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using Server.Configurations;
using Server.Constants;
using Server.Models;
using SharedModels.Requests;
using SharedModels.Responses;
namespace Server.Services;
public class AuthenticationService : IAuthenticationService
{
private readonly Jwt _jwt;
private readonly UserManager<User> _userManager;
private readonly RoleManager<IdentityRole> _roleManager;
private readonly IHttpContextAccessor _contextAccessor;
private readonly LinkGenerator _linkGenerator;
private readonly IEmailSenderService _emailSender;
private readonly IConfiguration _configuration;
public AuthenticationService(UserManager<User> userManager,
RoleManager<IdentityRole> roleManager, IOptions<Jwt> jwt,
IHttpContextAccessor contextAccessor, LinkGenerator linkGenerator,
IEmailSenderService emailSender, IConfiguration configuration)
{
_jwt = jwt.Value;
_userManager = userManager;
_roleManager = roleManager;
_contextAccessor = contextAccessor;
_linkGenerator = linkGenerator;
_emailSender = emailSender;
_configuration = configuration;
_userManager.UserValidators.Clear();
}
public async Task<(bool succeeded, string message)> RegisterAsync(RegistrationRequest regRequest)
{
var userWithSameEmail = await _userManager.FindByEmailAsync(regRequest.Email);
if (userWithSameEmail != null)
{
return (false, "Email is already registered.");
}
var userWithSamePhone = await _userManager.Users
.SingleOrDefaultAsync(u => u.PhoneNumber == regRequest.PhoneNumber);
if (userWithSamePhone != null)
{
return (false, "Phone is already registered.");
}
var user = new User
{
UserName = "temp",
FirstName = regRequest.FirstName,
LastName = regRequest.LastName,
Patronymic = regRequest.Patronymic,
Email = regRequest.Email,
PhoneNumber = regRequest.PhoneNumber
};
var createUserResult = await _userManager.CreateAsync(user, regRequest.Password);
if (!createUserResult.Succeeded)
{
return (false, $"{createUserResult.Errors?.First().Description}");
}
await _userManager.AddToRoleAsync(user, Constants.Identity.DefaultRole.ToString());
var token = await _userManager.GenerateEmailConfirmationTokenAsync(user);
var confirmationLink = _linkGenerator.GetUriByAction(_contextAccessor.HttpContext,
"confirmEmail", "authentication",
new { email = user.Email, token = token, redirectionUrl = regRequest.EmailConfirmationRedirectUrl },
_contextAccessor.HttpContext.Request.Scheme);
try
{
await _emailSender.SendMail(user.Email, "Email confirmation", confirmationLink);
}
catch (Exception e)
{
throw;
}
return (true, $"User registered with email {user.Email}. Before signing in confirm your email" +
$"by following a link sent to registered email address.");
}
public async Task<(bool succeeded, string? message)> ConfirmEmailAsync(string email, string token)
{
var user = await _userManager.FindByEmailAsync(email);
if (user == null)
{
return (false, $"Email {email} not registered");
}
var result = await _userManager.ConfirmEmailAsync(user, token);
if (!result.Succeeded)
{
return (false, $"Error confirming email {email} with token {token}");
}
return (true, $"Email {email} confirmed");
}
public async Task<(bool succeeded, AuthenticationResponse authResponse, string? refreshToken)>
AuthenticateAsync(AuthenticationRequest authRequest)
{
var authResponse = new AuthenticationResponse();
User user;
user = await _userManager.FindByEmailAsync(authRequest.Email);
if (user == null)
{
authResponse.Message = $"No accounts registered with {authRequest.Email}.";
return (false, authResponse, null);
}
if (!await _userManager.CheckPasswordAsync(user, authRequest.Password))
{
authResponse.Message = $"Incorrect email or password.";
return (false, authResponse, null);
}
var jwtSecurityToken = await CreateJwtToken(user);
authResponse.Token = new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken);
string refreshTokenString;
if (user.RefreshTokens.Any(t => t.IsActive))
{
var activeRefreshToken =
user.RefreshTokens.First(t => t.IsActive);
refreshTokenString = activeRefreshToken.Token;
authResponse.RefreshTokenExpirationDate = activeRefreshToken.ExpiryDateTime;
}
else
{
var refreshToken = CreateRefreshToken();
refreshTokenString = refreshToken.Token;
authResponse.RefreshTokenExpirationDate = refreshToken.ExpiryDateTime;
user.RefreshTokens.Add(refreshToken);
await _userManager.UpdateAsync(user);
}
return (true, authResponse, refreshTokenString);
}
public async Task<(bool succeeded, AuthenticationResponse authResponse, string? refreshToken)>
AuthenticateWithGoogleAsync(GoogleAuthenticationRequest authRequest)
{
GoogleJsonWebSignature.ValidationSettings settings = new GoogleJsonWebSignature.ValidationSettings();
settings.Audience = new List<string> { _configuration["Authentication:Google:ClientId"] };
GoogleJsonWebSignature.Payload payload = GoogleJsonWebSignature.ValidateAsync(authRequest.IdToken, settings).Result;
var authResponse = new AuthenticationResponse();
var user = await _userManager.FindByEmailAsync(payload.Email);
if (user == null)
{
var createUserResult = await _userManager.CreateAsync(new User
{
Email = payload.Email,
EmailConfirmed = payload.EmailVerified,
FirstName = payload.GivenName,
LastName = payload.FamilyName
});
if (!createUserResult.Succeeded)
{
authResponse.Message = $"{createUserResult.Errors?.First().Description}";
return (false, authResponse, null);
}
}
string refreshTokenString;
if (user.RefreshTokens.Any(t => t.IsActive))
{
var activeRefreshToken =
user.RefreshTokens.First(t => t.IsActive);
refreshTokenString = activeRefreshToken.Token;
authResponse.RefreshTokenExpirationDate = activeRefreshToken.ExpiryDateTime;
}
else
{
var refreshToken = CreateRefreshToken();
refreshTokenString = refreshToken.Token;
authResponse.RefreshTokenExpirationDate = refreshToken.ExpiryDateTime;
user.RefreshTokens.Add(refreshToken);
await _userManager.UpdateAsync(user);
}
return (true, authResponse, refreshTokenString);
}
public async Task<(bool succeeded, AuthenticationResponse authResponse,
string? refreshToken)> RenewRefreshTokenAsync(string? token)
{
var authResponse = new AuthenticationResponse();
var user = await _userManager.Users.SingleOrDefaultAsync(u =>
u.RefreshTokens.Any(t => t.Token == token));
if (user == null)
{
authResponse.Message = "Refresh token did not mach any user.";
return (false, authResponse, null);
}
var refreshToken = user.RefreshTokens.Single(t => t.Token == token);
if (!refreshToken.IsActive)
{
authResponse.Message = "Refresh token expired.";
return (false, authResponse, null);
}
//Revoke Current Refresh Token
refreshToken.Revoked = DateTime.UtcNow;
//Generate new Refresh Token and save to Database
var newRefreshToken = CreateRefreshToken();
user.RefreshTokens.Add(newRefreshToken);
await _userManager.UpdateAsync(user);
//Generates new jwt
var jwtSecurityToken = await CreateJwtToken(user);
authResponse.Token = new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken);
authResponse.RefreshTokenExpirationDate = newRefreshToken.ExpiryDateTime;
return (true, authResponse, newRefreshToken.Token);
}
public async Task<bool> RevokeRefreshToken(string? token)
{
var user = await _userManager.Users.SingleOrDefaultAsync(u =>
u.RefreshTokens.Any(t => t.Token == token));
if (user == null)
{
return false;
}
var refreshToken = user.RefreshTokens.Single(x => x.Token == token);
if (!refreshToken.IsActive)
{
return false;
}
refreshToken.Revoked = DateTime.UtcNow;
await _userManager.UpdateAsync(user);
return true;
}
private async Task<JwtSecurityToken> CreateJwtToken(User user)
{
var userClaims = await _userManager.GetClaimsAsync(user);
var roles = await _userManager.GetRolesAsync(user);
var roleClaims = new List<Claim>();
foreach (var role in roles)
{
roleClaims.Add(new Claim("roles", role));
}
var claims = new[]
{
new Claim(JwtStandardClaimNames.Sub, user.Id),
new Claim(JwtStandardClaimNames.Name, $"{user.LastName} {user.FirstName} {user.Patronymic}"),
new Claim(JwtStandardClaimNames.GivenName, user.FirstName),
new Claim(JwtStandardClaimNames.FamilyName, user.LastName),
new Claim(JwtStandardClaimNames.MiddleName, user.Patronymic),
new Claim(JwtStandardClaimNames.Email, user.Email),
new Claim(JwtStandardClaimNames.EmailVerified, user.EmailConfirmed.ToString()),
new Claim(JwtRegisteredClaimNames.Exp, DateTime.UtcNow.AddMinutes(_jwt.ValidityInMinutes).ToString(CultureInfo.InvariantCulture))
}
.Union(userClaims)
.Union(roleClaims);
var symmetricSecurityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwt.Key));
var signingCredentials = new SigningCredentials(symmetricSecurityKey, SecurityAlgorithms.HmacSha256);
var jwtSecurityToken = new JwtSecurityToken(
issuer: _jwt.Issuer,
audience: _jwt.Audience,
claims: claims,
expires: DateTime.UtcNow.AddMinutes(_jwt.ValidityInMinutes),
signingCredentials: signingCredentials);
return jwtSecurityToken;
}
private RefreshToken CreateRefreshToken()
{
var randomNumber = new byte[32];
using var rng = RandomNumberGenerator.Create();
rng.GetNonZeroBytes(randomNumber);
return new RefreshToken
{
Token = Convert.ToBase64String(randomNumber),
CreationDateTime = DateTime.UtcNow,
ExpiryDateTime = DateTime.UtcNow.AddDays(_jwt.RefreshTokenValidityInDays)
};
}
}