diff --git a/Server/Configurations/SmtpCredentials.cs b/Server/Configurations/SmtpCredentials.cs index c501dae..b04c4c1 100644 --- a/Server/Configurations/SmtpCredentials.cs +++ b/Server/Configurations/SmtpCredentials.cs @@ -3,7 +3,7 @@ namespace Server.Configurations; public class SmtpCredentials { public string Host { get; set; } = null!; - public int Port { get; set; } + public string Port { get; set; } public string User { get; set; } = null!; public string Password { get; set; } = null!; } \ No newline at end of file diff --git a/Server/Controllers/AuthenticationController.cs b/Server/Controllers/AuthenticationController.cs index a828c77..f997aee 100644 --- a/Server/Controllers/AuthenticationController.cs +++ b/Server/Controllers/AuthenticationController.cs @@ -49,7 +49,7 @@ public class AuthenticationController : ControllerBase } [HttpPost("authenticate")] - public async Task GetTokenAsync(AuthenticationRequest authRequest) + public async Task Authenticate(AuthenticationRequest authRequest) { var (succeeded, authResponse, refreshToken) = await _authService.AuthenticateAsync(authRequest); @@ -64,6 +64,22 @@ public class AuthenticationController : ControllerBase return Ok(authResponse); } + [HttpPost("googleoauth")] + public async Task AuthenticateWithGoogle(GoogleAuthenticationRequest authRequest) + { + var (succeeded, authResponse, refreshToken) = + await _authService.AuthenticateWithGoogleAsync(authRequest); + + if (!succeeded) + { + return BadRequest(authResponse); + } + + SetRefreshTokenInCookie(refreshToken!); + + return Ok(authResponse); + } + [HttpPost("renew-session")] public async Task RenewTokens() { diff --git a/Server/Program.cs b/Server/Program.cs index 7c04f5f..77e50db 100644 --- a/Server/Program.cs +++ b/Server/Program.cs @@ -16,20 +16,22 @@ using Server.Services; using SharedModels.DataTransferObjects; var builder = WebApplication.CreateBuilder(args); +var services = builder.Services; +var configuration = builder.Configuration; // Add services to the container. -builder.Services.AddControllers().AddNewtonsoftJson(options => { +services.AddControllers().AddNewtonsoftJson(options => { options.SerializerSettings.Formatting = Formatting.Indented; options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Error; options.SerializerSettings.DateFormatHandling = DateFormatHandling.IsoDateFormat; }); -builder.Services.AddHttpContextAccessor(); +services.AddHttpContextAccessor(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(options => { +services.AddEndpointsApiExplorer(); +services.AddSwaggerGen(options => { options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { Scheme = "Bearer", BearerFormat = "JWT", @@ -53,22 +55,25 @@ builder.Services.AddSwaggerGen(options => { }); }); -builder.Services.AddCors(options => { +services.AddCors(options => { options.AddDefaultPolicy(policy => policy.AllowAnyOrigin() .AllowAnyHeader().AllowAnyMethod()); }); -builder.Services.AddIdentityCore(options => +services.AddIdentityCore(options => { options.User.RequireUniqueEmail = true; options.Password.RequiredLength = 8; }).AddRoles().AddEntityFrameworkStores().AddDefaultTokenProviders(); // Configuration from AppSettings -builder.Services.Configure(builder.Configuration.GetSection("Jwt")); +services.Configure(configuration.GetSection("SmtpCredentials")); +services.Configure(configuration.GetSection("Jwt")); + // Adding Authentication - JWT -builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) - .AddJwtBearer(options => { +services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { // options.RequireHttpsMetadata = false; // options.SaveToken = false; options.TokenValidationParameters = new TokenValidationParameters @@ -77,15 +82,14 @@ builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) ValidateAudience = false, ValidateIssuer = false, ValidateLifetime = true, - ValidIssuer = builder.Configuration["Jwt:Issuer"], - ValidAudience = builder.Configuration["Jwt:Audience"], + ValidIssuer = configuration["Jwt:Issuer"], + ValidAudience = configuration["Jwt:Audience"], IssuerSigningKey = new SymmetricSecurityKey( - Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"])) + Encoding.UTF8.GetBytes(configuration["Jwt:Key"])) }; }); -builder.Services.Configure(builder.Configuration.GetSection("SmtpCredentials")); -builder.Services.AddAuthorization(options => { +services.AddAuthorization(options => { // options.FallbackPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build(); // Policies for accessing endpoints on a top level based on user role @@ -100,62 +104,62 @@ builder.Services.AddAuthorization(options => { policy.RequireRole(Identity.Roles.Administrator.ToString())); }); -builder.Services.AddAutoMapper(typeof(MapperInitializer)); +services.AddAutoMapper(typeof(MapperInitializer)); -builder.Services.AddScoped(); -builder.Services.AddScoped(); +services.AddScoped(); +services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); +services.AddScoped(); +services.AddScoped(); +services.AddScoped(); +services.AddScoped(); +services.AddScoped(); +services.AddScoped(); +services.AddScoped(); +services.AddScoped(); +services.AddScoped(); +services.AddScoped(); +services.AddScoped(); +services.AddScoped(); -builder.Services.AddScoped, SortHelper>(); +services.AddScoped, SortHelper>(); -builder.Services.AddScoped, DataShaper>(); -builder.Services.AddScoped, DataShaper>(); -builder.Services.AddScoped, DataShaper>(); -builder.Services.AddScoped, DataShaper>(); -builder.Services.AddScoped, DataShaper>(); -builder.Services.AddScoped, DataShaper>(); -builder.Services.AddScoped, DataShaper>(); -builder.Services.AddScoped, DataShaper>(); -builder.Services.AddScoped, DataShaper>(); -builder.Services.AddScoped, DataShaper>(); -builder.Services.AddScoped, DataShaper>(); -builder.Services.AddScoped, DataShaper>(); -builder.Services.AddScoped, DataShaper>(); -builder.Services.AddScoped, DataShaper>(); -builder.Services.AddScoped, DataShaper>(); +services.AddScoped, DataShaper>(); +services.AddScoped, DataShaper>(); +services.AddScoped, DataShaper>(); +services.AddScoped, DataShaper>(); +services.AddScoped, DataShaper>(); +services.AddScoped, DataShaper>(); +services.AddScoped, DataShaper>(); +services.AddScoped, DataShaper>(); +services.AddScoped, DataShaper>(); +services.AddScoped, DataShaper>(); +services.AddScoped, DataShaper>(); +services.AddScoped, DataShaper>(); +services.AddScoped, DataShaper>(); +services.AddScoped, DataShaper>(); +services.AddScoped, DataShaper>(); -builder.Services.AddScoped, Pager>(); +services.AddScoped, Pager>(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); +services.AddScoped(); +services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped, DataShaper>(); -builder.Services.AddScoped, DataShaper>(); +services.AddScoped(); +services.AddScoped, DataShaper>(); +services.AddScoped, DataShaper>(); // Adding DB Context with PostgreSQL -var connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); -builder.Services.AddDbContext(options => +var connectionString = configuration.GetConnectionString("DefaultConnection"); +services.AddDbContext(options => options.UseNpgsql(connectionString)); var app = builder.Build(); // Data seeding using var scope = app.Services.CreateScope(); -var services = scope.ServiceProvider; -await SeedData.Initialize(services); +var serviceProvider = scope.ServiceProvider; +await SeedData.Initialize(serviceProvider); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) diff --git a/Server/Server.csproj b/Server/Server.csproj index bf4404a..e1bf303 100644 --- a/Server/Server.csproj +++ b/Server/Server.csproj @@ -8,7 +8,9 @@ + + diff --git a/Server/Services/AuthenticationService.cs b/Server/Services/AuthenticationService.cs index 78852a3..38c600e 100644 --- a/Server/Services/AuthenticationService.cs +++ b/Server/Services/AuthenticationService.cs @@ -3,6 +3,7 @@ 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; @@ -17,24 +18,28 @@ namespace Server.Services; public class AuthenticationService : IAuthenticationService { + private readonly Jwt _jwt; + private readonly UserManager _userManager; private readonly RoleManager _roleManager; - private readonly Jwt _jwt; private readonly IHttpContextAccessor _contextAccessor; private readonly LinkGenerator _linkGenerator; private readonly IEmailSenderService _emailSender; + private readonly IConfiguration _configuration; public AuthenticationService(UserManager userManager, RoleManager roleManager, IOptions jwt, IHttpContextAccessor contextAccessor, LinkGenerator linkGenerator, - IEmailSenderService emailSender) + IEmailSenderService emailSender, IConfiguration configuration) { + _jwt = jwt.Value; + _userManager = userManager; _roleManager = roleManager; - _jwt = jwt.Value; _contextAccessor = contextAccessor; _linkGenerator = linkGenerator; _emailSender = emailSender; + _configuration = configuration; _userManager.UserValidators.Clear(); } @@ -70,15 +75,23 @@ public class AuthenticationService : IAuthenticationService 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); - await _emailSender.SendMail(user.Email, "Email confirmation", confirmationLink); + try + { + await _emailSender.SendMail(user.Email, "Email confirmation", confirmationLink); + } + catch (Exception e) + { + throw; + } - await _userManager.AddToRoleAsync(user, Constants.Identity.DefaultRole.ToString()); return (true, $"User registered with email {user.Email}. Before signing in confirm your email" + $"by following a link sent to registered email address."); } @@ -100,8 +113,8 @@ public class AuthenticationService : IAuthenticationService return (true, $"Email {email} confirmed"); } - public async Task<(bool succeeded, AuthenticationResponse authResponse, - string? refreshToken)> AuthenticateAsync(AuthenticationRequest authRequest) + public async Task<(bool succeeded, AuthenticationResponse authResponse, string? refreshToken)> + AuthenticateAsync(AuthenticationRequest authRequest) { var authResponse = new AuthenticationResponse(); @@ -145,6 +158,56 @@ public class AuthenticationService : IAuthenticationService 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 { _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) { @@ -222,7 +285,7 @@ public class AuthenticationService : IAuthenticationService var claims = new[] { new Claim(JwtStandardClaimNames.Sub, user.Id), - new Claim(JwtStandardClaimNames.Name, user.LastName + user.FirstName + user.Patronymic), + 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), diff --git a/Server/Services/EmailSenderService.cs b/Server/Services/EmailSenderService.cs index bfbc142..478bb9f 100644 --- a/Server/Services/EmailSenderService.cs +++ b/Server/Services/EmailSenderService.cs @@ -34,18 +34,11 @@ public class EmailSenderService : IEmailSenderService mailMessage.Subject = $"{applicationName}. {subject}"; mailMessage.Body = new TextPart(MimeKit.Text.TextFormat.Text) { Text = message}; - try - { - await _smtpClient.ConnectAsync(_smtpCredentials.Host, _smtpCredentials.Port, true); - await _smtpClient.AuthenticateAsync(Encoding.ASCII, _smtpCredentials.User, _smtpCredentials.Password); - await _smtpClient.SendAsync(mailMessage); - await _smtpClient.DisconnectAsync(true); - return (true, "Letter has been sent successfully"); - } - catch (Exception e) - { - Console.WriteLine(e); - throw; - } + await _smtpClient.ConnectAsync(_smtpCredentials.Host, Int32.Parse(_smtpCredentials.Port), false); + await _smtpClient.AuthenticateAsync(Encoding.ASCII, _smtpCredentials.User, _smtpCredentials.Password); + await _smtpClient.SendAsync(mailMessage); + await _smtpClient.DisconnectAsync(true); + + return (true, "Letter has been sent successfully"); } } \ No newline at end of file diff --git a/Server/Services/IAuthenticationService.cs b/Server/Services/IAuthenticationService.cs index 011ec48..f68e1d4 100644 --- a/Server/Services/IAuthenticationService.cs +++ b/Server/Services/IAuthenticationService.cs @@ -11,6 +11,9 @@ public interface IAuthenticationService Task<(bool succeeded, AuthenticationResponse authResponse, string? refreshToken)> AuthenticateAsync(AuthenticationRequest authRequest); + + Task<(bool succeeded, AuthenticationResponse authResponse, string? refreshToken)> + AuthenticateWithGoogleAsync(GoogleAuthenticationRequest authRequest); Task<(bool succeeded, AuthenticationResponse authResponse, string? refreshToken)> RenewRefreshTokenAsync(string? token); diff --git a/Server/appsettings.Development.json b/Server/appsettings.Development.json index c45a1ab..5e9add1 100644 --- a/Server/appsettings.Development.json +++ b/Server/appsettings.Development.json @@ -16,6 +16,12 @@ "User": "", "Password": "" }, + "Authentication": { + "Google": { + "ClientId": "", + "ClientSecret": "" + } + }, "Jwt": { "Key": "Secret which will never be exposed", "Audience": "Application URL", diff --git a/Server/appsettings.json b/Server/appsettings.json index e9224d9..94fd6be 100644 --- a/Server/appsettings.json +++ b/Server/appsettings.json @@ -7,14 +7,21 @@ }, "AllowedHosts": "*", "ConnectionStrings": { - "DefaultConnection": "Host=localhost;Database=auto.bus;Username=postgres;Password=postgres;" + "DefaultConnection": "host=localhost;database=auto.bus;user id=postgres;password=postgres;Include Error Detail = true" }, + "ApplicationName": "auto.bus", "SmtpCredentials": { "Host": "", "Port": "", "User": "", "Password": "" }, + "Authentication": { + "Google": { + "ClientId": "", + "ClientSecret": "" + } + }, "Jwt": { "Key": "Secret which will never be exposed", "Audience": "Application URL", diff --git a/SharedModels/Requests/GoogleAuthenticationRequest.cs b/SharedModels/Requests/GoogleAuthenticationRequest.cs new file mode 100644 index 0000000..86d9927 --- /dev/null +++ b/SharedModels/Requests/GoogleAuthenticationRequest.cs @@ -0,0 +1,6 @@ +namespace SharedModels.Requests; + +public class GoogleAuthenticationRequest +{ + public string IdToken { get; set; } = null!; +} \ No newline at end of file