Add support for cookie-based JWT authentication alongside the existing Authorization header.

This commit is contained in:
Felipe Cotti 2024-11-17 23:42:46 -03:00
parent 2f0d63a90f
commit eaecace48d
5 changed files with 62 additions and 6 deletions

View file

@ -0,0 +1,8 @@
using Guestbooky.API.Enums;
namespace Guestbooky.API.Configurations;
public class APISettings
{
public required RunningEnvironment RunningEnvironment { get; set; }
}

View file

@ -1,4 +1,5 @@
using Guestbooky.API.DTOs.Auth; using Guestbooky.API.Configurations;
using Guestbooky.API.DTOs.Auth;
using Guestbooky.Application.UseCases.AuthenticateUser; using Guestbooky.Application.UseCases.AuthenticateUser;
using Guestbooky.Application.UseCases.RefreshToken; using Guestbooky.Application.UseCases.RefreshToken;
using MediatR; using MediatR;
@ -12,11 +13,13 @@ public class AuthController : ControllerBase
{ {
private readonly IMediator _mediator; private readonly IMediator _mediator;
private readonly ILogger<AuthController> _logger; private readonly ILogger<AuthController> _logger;
private readonly APISettings _apiSettings;
public AuthController(IMediator mediator, ILogger<AuthController> logger) public AuthController(IMediator mediator, ILogger<AuthController> logger, APISettings apiSettings)
{ {
_mediator = mediator; _mediator = mediator;
_logger = logger; _logger = logger;
_apiSettings = apiSettings;
} }
[ProducesResponseType(typeof(LoginResponseDto), 200)] [ProducesResponseType(typeof(LoginResponseDto), 200)]
@ -35,6 +38,7 @@ public class AuthController : ControllerBase
if(result.IsAuthenticated) if(result.IsAuthenticated)
{ {
_logger.LogInformation("Authentication successful. Returning LoginResponse."); _logger.LogInformation("Authentication successful. Returning LoginResponse.");
SetJwtCookie(result.Token);
return Ok(new LoginResponseDto(result.Token, result.RefreshToken)); return Ok(new LoginResponseDto(result.Token, result.RefreshToken));
} }
else else
@ -80,4 +84,16 @@ public class AuthController : ControllerBase
return Problem($"An error occurred on the server: {e.Message}", statusCode: StatusCodes.Status500InternalServerError); return Problem($"An error occurred on the server: {e.Message}", statusCode: StatusCodes.Status500InternalServerError);
} }
} }
private void SetJwtCookie(string token)
{
var cookieOptions = new CookieOptions
{
HttpOnly = true,
Secure = _apiSettings.RunningEnvironment == Enums.RunningEnvironment.Production,
SameSite = SameSiteMode.Strict,
Expires = DateTimeOffset.UtcNow.AddHours(2)
};
Response.Cookies.Append("token", token, cookieOptions);
}
} }

View file

@ -0,0 +1,8 @@
namespace Guestbooky.API.Enums;
public enum RunningEnvironment
{
Unknown = 0,
Development,
Production
}

View file

@ -1,11 +1,13 @@
using Guestbooky.Application.DependencyInjection; using Guestbooky.Application.DependencyInjection;
using Guestbooky.Infrastructure.DependencyInjection; using Guestbooky.Infrastructure.DependencyInjection;
using Guestbooky.Infrastructure.Environment; using Guestbooky.Infrastructure.Environment;
using Guestbooky.API.Configurations;
using Guestbooky.API.Enums;
using Guestbooky.API.Validations;
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using System.Text; using System.Text;
using Serilog; using Serilog;
using Guestbooky.API.Validations;
namespace Guestbooky.API namespace Guestbooky.API
{ {
@ -74,9 +76,10 @@ namespace Guestbooky.API
var corsOrigins = builder.Configuration[Constants.CORS_ORIGINS]?.Split(',') ?? Array.Empty<string>(); var corsOrigins = builder.Configuration[Constants.CORS_ORIGINS]?.Split(',') ?? Array.Empty<string>();
cfg.AddPolicy(name: "local", policy => cfg.AddPolicy(name: "local", policy =>
{ {
policy.WithExposedHeaders("Content-Range", "Accept-Ranges") policy.WithExposedHeaders("Content-Range", "Accept-Ranges", "Set-Cookie")
.WithOrigins(corsOrigins) .WithOrigins(corsOrigins)
.AllowAnyHeader() .AllowAnyHeader()
.AllowCredentials()
.WithMethods("GET", "POST", "DELETE", "OPTIONS"); .WithMethods("GET", "POST", "DELETE", "OPTIONS");
}); });
}); });
@ -86,7 +89,11 @@ namespace Guestbooky.API
options.InvalidModelStateResponseFactory = InvalidModelStateResponseFactory.DefaultInvalidModelStateResponse; options.InvalidModelStateResponseFactory = InvalidModelStateResponseFactory.DefaultInvalidModelStateResponse;
}); });
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) builder.Services.AddAuthentication(o =>
{
o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(o => .AddJwtBearer(o =>
{ {
o.RequireHttpsMetadata = false; o.RequireHttpsMetadata = false;
@ -102,12 +109,22 @@ namespace Guestbooky.API
ValidateLifetime = true, ValidateLifetime = true,
ClockSkew = TimeSpan.Zero ClockSkew = TimeSpan.Zero
}; };
o.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
context.Token = context.Request.Cookies["token"];
return Task.CompletedTask;
}
};
}); });
builder.Services.AddInfrastructure(builder.Configuration); builder.Services.AddInfrastructure(builder.Configuration);
builder.Services.AddApplication(); builder.Services.AddApplication();
builder.Services.AddSingleton(new APISettings(){ RunningEnvironment = GetRunningEnvironment(builder.Configuration["ASPNETCORE_ENVIRONMENT"]!) });
if (builder.Environment.IsDevelopment()) if (builder.Environment.IsDevelopment())
{ {
@ -170,5 +187,12 @@ namespace Guestbooky.API
_ => conf.MinimumLevel.Information() _ => conf.MinimumLevel.Information()
}; };
public static RunningEnvironment GetRunningEnvironment(string env) => env.ToUpper() switch
{
"DEVELOPMENT" => RunningEnvironment.Development,
"PRODUCTION" => RunningEnvironment.Production,
_ => RunningEnvironment.Unknown
};
} }
} }

View file

@ -53,7 +53,7 @@ namespace Guestbooky.Infrastructure.Persistence.Repositories
var messageDtos = await _messages.Find(_ => true) var messageDtos = await _messages.Find(_ => true)
.SortBy(x => x.Timestamp) .SortBy(x => x.Timestamp)
.Skip((int?)offset) .Skip((int?)offset)
.Limit(50) .Limit(10)
.ToCursorAsync(cancellationToken); .ToCursorAsync(cancellationToken);
return messageDtos.ToEnumerable().Select(MapToDomainModel).ToArray(); return messageDtos.ToEnumerable().Select(MapToDomainModel).ToArray();
} }