diff --git a/src/MyRecipes.API/Controllers/LoginController.cs b/src/MyRecipes.API/Controllers/LoginController.cs index 84bf0ff..baf6b8a 100644 --- a/src/MyRecipes.API/Controllers/LoginController.cs +++ b/src/MyRecipes.API/Controllers/LoginController.cs @@ -1,5 +1,11 @@ -using Microsoft.AspNetCore.Mvc; +using System.Security.Claims; + +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Google; +using Microsoft.AspNetCore.Mvc; + using MyRecipes.Application.UseCases.Login.DoLogin; +using MyRecipes.Application.UseCases.Login.External; using MyRecipes.Communication.Requests; using MyRecipes.Communication.Responses; @@ -19,4 +25,22 @@ public async Task Login( return Ok(response); } + + [HttpGet("google")] + public async Task GoogleLogin(string returnUrl, [FromServices] IExternalLoginUseCase useCase) + { + var authenticate = await Request.HttpContext.AuthenticateAsync(GoogleDefaults.AuthenticationScheme); + + if (IsNotAuthenticated(authenticate)) + return Challenge(GoogleDefaults.AuthenticationScheme); + + var claims = authenticate.Principal!.Identities.First().Claims; + + var name = claims.First(c => c.Type == ClaimTypes.Name).Value; + var email = claims.First(c => c.Type == ClaimTypes.Email).Value; + + var token = await useCase.Execute(name, email); + + return Redirect($"{returnUrl}/{token}"); + } } diff --git a/src/MyRecipes.API/Controllers/MyRecipesBaseController.cs b/src/MyRecipes.API/Controllers/MyRecipesBaseController.cs index 510fa82..48b13eb 100644 --- a/src/MyRecipes.API/Controllers/MyRecipesBaseController.cs +++ b/src/MyRecipes.API/Controllers/MyRecipesBaseController.cs @@ -1,8 +1,15 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Mvc; namespace MyRecipes.API.Controllers; [ApiController] public class MyRecipesBaseController : ControllerBase { + protected static bool IsNotAuthenticated(AuthenticateResult authenticate) + { + return !authenticate.Succeeded + || authenticate.Principal is null + || !authenticate.Principal.Identities.Any(id => id.IsAuthenticated); + } } diff --git a/src/MyRecipes.API/MyRecipes.API.csproj b/src/MyRecipes.API/MyRecipes.API.csproj index 92b0827..cdcfc5b 100644 --- a/src/MyRecipes.API/MyRecipes.API.csproj +++ b/src/MyRecipes.API/MyRecipes.API.csproj @@ -8,8 +8,9 @@ + - + diff --git a/src/MyRecipes.API/Program.cs b/src/MyRecipes.API/Program.cs index 55dac46..c1964ab 100644 --- a/src/MyRecipes.API/Program.cs +++ b/src/MyRecipes.API/Program.cs @@ -1,3 +1,5 @@ +using Microsoft.AspNetCore.Authentication.Cookies; + using MyRecipes.API.BackgroudServices; using MyRecipes.API.Converters; using MyRecipes.API.Filters; @@ -36,6 +38,8 @@ if (!builder.Configuration.IsUnitTestEnvironment()) { builder.Services.AddHostedService(); + + AddGoogleAuthentication(); } var app = builder.Build(); @@ -69,6 +73,21 @@ void MigrateDatabase() DatabaseMigration.Migrate(connectionString, serviceScope.ServiceProvider); } +void AddGoogleAuthentication() +{ + var clientId = builder.Configuration.GetValue("Settings:Google:ClientId"); + var clientSecret = builder.Configuration.GetValue("Settings:Google:ClientSecret"); + + builder.Services + .AddAuthentication(config => config.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme) + .AddCookie() + .AddGoogle(options => + { + options.ClientId = clientId!; + options.ClientSecret = clientSecret!; + }); +} + public partial class Program { protected Program() { } diff --git a/src/MyRecipes.API/appsettings.Development.json b/src/MyRecipes.API/appsettings.Development.json index 49dc144..6af058d 100644 --- a/src/MyRecipes.API/appsettings.Development.json +++ b/src/MyRecipes.API/appsettings.Development.json @@ -25,6 +25,10 @@ }, "BlobStorage": { "Azure": "" + }, + "Google": { + "ClientId": "", + "ClientSecret": "" } } } diff --git a/src/MyRecipes.Application/UseCases/Login/External/ExternalLoginUseCase.cs b/src/MyRecipes.Application/UseCases/Login/External/ExternalLoginUseCase.cs new file mode 100644 index 0000000..56270b7 --- /dev/null +++ b/src/MyRecipes.Application/UseCases/Login/External/ExternalLoginUseCase.cs @@ -0,0 +1,45 @@ +using MyRecipes.Domain.Repositories; +using MyRecipes.Domain.Repositories.User; +using MyRecipes.Domain.Security.Tokens; + +namespace MyRecipes.Application.UseCases.Login.External; + +public class ExternalLoginUseCase : IExternalLoginUseCase +{ + private readonly IUserReadOnlyRepository _userReadOnlyRepository; + private readonly IUserWriteOnlyRepository _userWriteOnlyRepository; + private readonly IUnitOfWork _unitOfWork; + private readonly IAccessTokenGenerator _accessTokenGenerator; + + public ExternalLoginUseCase( + IUserReadOnlyRepository userReadOnlyRepository, + IUserWriteOnlyRepository userWriteOnlyRepository, + IUnitOfWork unitOfWork, + IAccessTokenGenerator accessTokenGenerator) + { + _userReadOnlyRepository = userReadOnlyRepository; + _userWriteOnlyRepository = userWriteOnlyRepository; + _unitOfWork = unitOfWork; + _accessTokenGenerator = accessTokenGenerator; + } + + public async Task Execute(string name, string email) + { + var user = await _userReadOnlyRepository.GetByEmail(email); + + if (user is null) + { + user = new Domain.Entities.User + { + Name = name, + Email = email, + Password = "-" + }; + + await _userWriteOnlyRepository.Add(user); + await _unitOfWork.Commit(); + } + + return _accessTokenGenerator.Generate(user.UserIdentifier); + } +} diff --git a/src/MyRecipes.Application/UseCases/Login/External/IExternalLoginUseCase.cs b/src/MyRecipes.Application/UseCases/Login/External/IExternalLoginUseCase.cs new file mode 100644 index 0000000..3bec4e1 --- /dev/null +++ b/src/MyRecipes.Application/UseCases/Login/External/IExternalLoginUseCase.cs @@ -0,0 +1,5 @@ +namespace MyRecipes.Application.UseCases.Login.External; +public interface IExternalLoginUseCase +{ + Task Execute(string name, string email); +} diff --git a/src/MyRecipes.Domain/Entities/User.cs b/src/MyRecipes.Domain/Entities/User.cs index ce7eb88..71776a3 100644 --- a/src/MyRecipes.Domain/Entities/User.cs +++ b/src/MyRecipes.Domain/Entities/User.cs @@ -5,5 +5,5 @@ public class User : EntityBase public string Name { get; set; } = string.Empty; public string Email { get; set; } = string.Empty; public string Password { get; set; } = string.Empty; - public Guid UserIdentifier { get; set; } + public Guid UserIdentifier { get; set; } = Guid.NewGuid(); } diff --git a/src/MyRecipes.Domain/Repositories/User/IUserReadOnlyRepository.cs b/src/MyRecipes.Domain/Repositories/User/IUserReadOnlyRepository.cs index d2f5f47..4adcf72 100644 --- a/src/MyRecipes.Domain/Repositories/User/IUserReadOnlyRepository.cs +++ b/src/MyRecipes.Domain/Repositories/User/IUserReadOnlyRepository.cs @@ -5,4 +5,5 @@ public interface IUserReadOnlyRepository public Task ExistsActiveUserWithWithEmail(string email); public Task ExistsActiveUserWithIdentifier(Guid userIdentifier); public Task GetByEmailAndPassword(string email, string password); + public Task GetByEmail(string email); } diff --git a/src/MyRecipes.Infrastructure/DataAccess/Repositories/UserRepository.cs b/src/MyRecipes.Infrastructure/DataAccess/Repositories/UserRepository.cs index f255961..9a04294 100644 --- a/src/MyRecipes.Infrastructure/DataAccess/Repositories/UserRepository.cs +++ b/src/MyRecipes.Infrastructure/DataAccess/Repositories/UserRepository.cs @@ -44,4 +44,11 @@ public async Task DeleteAccount(Guid userIdentifier) _dbContext.Recipes.RemoveRange(recipes); _dbContext.Users.Remove(user); } + + public Task GetByEmail(string email) + { + return _dbContext.Users + .AsNoTracking() + .FirstOrDefaultAsync(u => u.Active && u.Email.Equals(email)); + } }