From 12fef7cbaf2073f9cc349ed765ea140be0259d8e Mon Sep 17 00:00:00 2001 From: Paweł Bernaciak Date: Sat, 21 Oct 2023 11:55:33 +0200 Subject: Basic API auth and Google login --- .../Elements.Backend/Controllers/AuthController.cs | 106 +++++++++++++++++++++ .../Elements.Backend/Controllers/UserController.cs | 41 ++++++++ .../Controllers/WeatherForecastController.cs | 32 ------- backend/Elements.Backend/Elements.Backend.csproj | 41 ++++---- backend/Elements.Backend/Program.cs | 53 ++++++++--- backend/Elements.Backend/WeatherForecast.cs | 12 --- 6 files changed, 214 insertions(+), 71 deletions(-) create mode 100644 backend/Elements.Backend/Controllers/AuthController.cs create mode 100644 backend/Elements.Backend/Controllers/UserController.cs delete mode 100644 backend/Elements.Backend/Controllers/WeatherForecastController.cs delete mode 100644 backend/Elements.Backend/WeatherForecast.cs (limited to 'backend/Elements.Backend') diff --git a/backend/Elements.Backend/Controllers/AuthController.cs b/backend/Elements.Backend/Controllers/AuthController.cs new file mode 100644 index 0000000..56e7c3b --- /dev/null +++ b/backend/Elements.Backend/Controllers/AuthController.cs @@ -0,0 +1,106 @@ +using System.Runtime.Serialization; +using System.Security.Claims; +using System.Text.Json; +using Elements.Data; +using Elements.Data.Models; +using Google.Apis.Auth; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Elements.Backend.Controllers; + +[ApiController] +[Route("[controller]/[action]")] +public class AuthController : ControllerBase +{ + private readonly IConfiguration _config; + private readonly ApplicationDbContext _dbContext; + + public AuthController(IConfiguration config, ApplicationDbContext dbContext) + { + _config = config; + _dbContext = dbContext; + } + + public class LoginModel + { + public required string GoogleToken { get; init; } + } + + [HttpPost] + public async Task Login([FromBody] LoginModel model) + { + GoogleJsonWebSignature.Payload? payload = await VerifyGoogleIdToken(model.GoogleToken); + if (payload == null) + return Unauthorized(); + + User? user = await _dbContext.Users.SingleOrDefaultAsync(u => u.GoogleId == payload.Subject); + if (user != null) + { + //Check if user's name changed and update if it did + if (user.Name != payload.Name) + user.Name = payload.Name; + } + else + { + user = new User() + { + Name = payload.Name, + GoogleId = payload.Subject, + Elements = new List() + }; + + await _dbContext.Users.AddAsync(user); + } + + await _dbContext.SaveChangesAsync(); + + List claims = new() + { + new Claim("id", user.Id.ToString()), + new Claim(ClaimTypes.Role, "User") + }; + ClaimsIdentity claimsIdentity = new(claims, CookieAuthenticationDefaults.AuthenticationScheme); + + AuthenticationProperties authProperties = new() + { + IsPersistent = true, + AllowRefresh = true + }; + + await HttpContext.SignInAsync( + CookieAuthenticationDefaults.AuthenticationScheme, + new ClaimsPrincipal(claimsIdentity), + authProperties); + + var response = new + { + Id = user.Id.ToString() + }; + + return Ok(JsonSerializer.Serialize(response)); + } + + [HttpPost] + public async Task Logout() + { + await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + return Ok(); + } + + private async Task VerifyGoogleIdToken(string token) + { + try + { + GoogleJsonWebSignature.Payload? payload = await GoogleJsonWebSignature.ValidateAsync(token); + return payload; + } + catch (InvalidJwtException) + { + return null; + } + } +} \ No newline at end of file diff --git a/backend/Elements.Backend/Controllers/UserController.cs b/backend/Elements.Backend/Controllers/UserController.cs new file mode 100644 index 0000000..bde93aa --- /dev/null +++ b/backend/Elements.Backend/Controllers/UserController.cs @@ -0,0 +1,41 @@ +using System.Security.Claims; +using System.Text.Json; +using Elements.Data; +using Elements.Data.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Elements.Backend.Controllers; + +[ApiController] +[Route("[controller]/[action]")] +public class UserController: ControllerBase +{ + private readonly ApplicationDbContext _dbContext; + + public UserController(ApplicationDbContext dbContext) + { + _dbContext = dbContext; + } + + [HttpGet] + [Authorize] + [Route("/user/{id:int}")] + public async Task Users(int id) + { + IEnumerable claims = User.Claims; + string? currentUserId = claims.FirstOrDefault(claim => claim.Type == "id")?.Value; + if (currentUserId == null) + return StatusCode(StatusCodes.Status500InternalServerError); + if (currentUserId != id.ToString()) + return Unauthorized(); + + User? user = await _dbContext.Users.FirstOrDefaultAsync(user => user.Id == id); + if (user == null) + return StatusCode(StatusCodes.Status500InternalServerError); + + string userJson = JsonSerializer.Serialize(user); + return Ok(userJson); + } +} \ No newline at end of file diff --git a/backend/Elements.Backend/Controllers/WeatherForecastController.cs b/backend/Elements.Backend/Controllers/WeatherForecastController.cs deleted file mode 100644 index 9a70e36..0000000 --- a/backend/Elements.Backend/Controllers/WeatherForecastController.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Microsoft.AspNetCore.Mvc; - -namespace Elements.Backend.Controllers; - -[ApiController] -[Route("[controller]")] -public class WeatherForecastController : ControllerBase -{ - private static readonly string[] Summaries = new[] - { - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" - }; - - private readonly ILogger _logger; - - public WeatherForecastController(ILogger logger) - { - _logger = logger; - } - - [HttpGet(Name = "GetWeatherForecast")] - public IEnumerable Get() - { - return Enumerable.Range(1, 5).Select(index => new WeatherForecast - { - Date = DateOnly.FromDateTime(DateTime.Now), - TemperatureC = Random.Shared.Next(-20, 55), - Summary = Summaries[Random.Shared.Next(Summaries.Length)] - }) - .ToArray(); - } -} diff --git a/backend/Elements.Backend/Elements.Backend.csproj b/backend/Elements.Backend/Elements.Backend.csproj index 0c664a6..787d048 100644 --- a/backend/Elements.Backend/Elements.Backend.csproj +++ b/backend/Elements.Backend/Elements.Backend.csproj @@ -1,18 +1,27 @@ - - - - net7.0 - enable - enable - - - - - - - + + + + net7.0 + enable + enable + dae266ca-b349-4b87-a992-5470f9dd635d + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + - - - + + + diff --git a/backend/Elements.Backend/Program.cs b/backend/Elements.Backend/Program.cs index 15eacee..50e5255 100644 --- a/backend/Elements.Backend/Program.cs +++ b/backend/Elements.Backend/Program.cs @@ -1,23 +1,54 @@ +using System.Text.Json; +using Elements.Data; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.Google; +using Microsoft.EntityFrameworkCore; + var builder = WebApplication.CreateBuilder(args); // Add services to the container. +builder.Services.AddRouting(options => options.LowercaseUrls = true); +builder.Services.AddControllers().AddJsonOptions(options => +{ + options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; +}); -builder.Services.AddControllers(); -// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); - -var app = builder.Build(); - -// Configure the HTTP request pipeline. -if (app.Environment.IsDevelopment()) +if (builder.Environment.IsDevelopment()) { - app.UseSwagger(); - app.UseSwaggerUI(); + builder.Services.AddDbContext(options => + { + options.UseSqlite("Data Source=elements.db"); + }); } +builder.Services +.AddAuthentication(options => +{ + options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; +}) +.AddCookie(options => +{ + options.Events = new() + { + OnRedirectToLogin = (ctx) => + { + ctx.Response.StatusCode = 401; + return Task.CompletedTask; + }, + OnRedirectToAccessDenied = (ctx) => + { + ctx.Response.StatusCode = 401; + return Task.CompletedTask; + } + + }; +}); + +var app = builder.Build(); + app.UseHttpsRedirection(); +app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); diff --git a/backend/Elements.Backend/WeatherForecast.cs b/backend/Elements.Backend/WeatherForecast.cs deleted file mode 100644 index 7c8962e..0000000 --- a/backend/Elements.Backend/WeatherForecast.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Elements.Backend; - -public class WeatherForecast -{ - public DateOnly Date { get; set; } - - public int TemperatureC { get; set; } - - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); - - public string? Summary { get; set; } -} -- cgit v1.2.3