diff options
| author | ivarlovlie <git@ivarlovlie.no> | 2022-01-23 11:41:42 +0100 |
|---|---|---|
| committer | ivarlovlie <git@ivarlovlie.no> | 2022-01-23 14:33:05 +0100 |
| commit | ce86d103039b22695b04714ee85e9ef3e1e032b5 (patch) | |
| tree | 557455780de06ceb95dd556ca5ffca0208a1f8ba /src/server | |
| parent | 89816382424e59ad953b433fbf82c925741b3136 (diff) | |
| download | bookmark-thing-ce86d103039b22695b04714ee85e9ef3e1e032b5.tar.xz bookmark-thing-ce86d103039b22695b04714ee85e9ef3e1e032b5.zip | |
feat(auth): Implements first draft of basic auth gen/validation
Diffstat (limited to 'src/server')
| -rw-r--r-- | src/server/Api/Internal/Account/CreateTokenRequest.cs | 6 | ||||
| -rw-r--r-- | src/server/Api/Internal/Account/CreateTokenRoute.cs | 28 | ||||
| -rw-r--r-- | src/server/Api/V1/BaseV1Route.cs | 21 | ||||
| -rw-r--r-- | src/server/Api/V1/Entries/CreateEntryRoute.cs | 4 | ||||
| -rw-r--r-- | src/server/Api/V1/Entries/DeleteEntryRoute.cs | 4 | ||||
| -rw-r--r-- | src/server/Api/V1/Entries/GetEntriesRoute.cs | 4 | ||||
| -rw-r--r-- | src/server/Api/V1/Entries/UpdateEntryRoute.cs | 5 | ||||
| -rw-r--r-- | src/server/Migrations/20220123102257_AccessTokens.Designer.cs | 141 | ||||
| -rw-r--r-- | src/server/Migrations/20220123102257_AccessTokens.cs | 79 | ||||
| -rw-r--r-- | src/server/Migrations/AppDbContextModelSnapshot.cs | 64 | ||||
| -rw-r--r-- | src/server/Models/Database/AccessToken.cs | 10 | ||||
| -rw-r--r-- | src/server/Startup.cs | 4 | ||||
| -rw-r--r-- | src/server/StaticData/AppJsonSettings.cs | 2 | ||||
| -rw-r--r-- | src/server/StaticData/Constants.cs | 4 | ||||
| -rw-r--r-- | src/server/Utilities/BasicAuthenticationHandler.cs | 58 | ||||
| -rw-r--r-- | src/server/Utilities/SnakeCaseNamingPolicy.cs | 15 |
16 files changed, 413 insertions, 36 deletions
diff --git a/src/server/Api/Internal/Account/CreateTokenRequest.cs b/src/server/Api/Internal/Account/CreateTokenRequest.cs index 399bdfc..6839092 100644 --- a/src/server/Api/Internal/Account/CreateTokenRequest.cs +++ b/src/server/Api/Internal/Account/CreateTokenRequest.cs @@ -2,5 +2,9 @@ namespace IOL.BookmarkThing.Server.Api.Internal.Account; public class CreateTokenRequest { - public string Name { get; set; } + public bool AllowRead { get; set; } + public bool AllowCreate { get; set; } + public bool AllowUpdate { get; set; } + public bool AllowDelete { get; set; } + public DateTime ExpiryDate { get; set; } } diff --git a/src/server/Api/Internal/Account/CreateTokenRoute.cs b/src/server/Api/Internal/Account/CreateTokenRoute.cs index ea0e01f..0b30cc0 100644 --- a/src/server/Api/Internal/Account/CreateTokenRoute.cs +++ b/src/server/Api/Internal/Account/CreateTokenRoute.cs @@ -1,11 +1,17 @@ +using System.Text; + namespace IOL.BookmarkThing.Server.Api.Internal.Account; public class CreateTokenRoute : RouteBaseInternalSync.WithRequest<CreateTokenRequest>.WithActionResult { private readonly AppDbContext _context; + private readonly IConfiguration _configuration; + private readonly ILogger<CreateTokenRoute> _logger; - public CreateTokenRoute(AppDbContext context) { + public CreateTokenRoute(AppDbContext context, IConfiguration configuration, ILogger<CreateTokenRoute> logger) { _context = context; + _configuration = configuration; + _logger = logger; } [ApiVersionNeutral] @@ -17,18 +23,24 @@ public class CreateTokenRoute : RouteBaseInternalSync.WithRequest<CreateTokenReq return NotFound(new ErrorResult("User does not exist")); } - if (request.Name.IsNullOrWhiteSpace()) { - return BadRequest(new ErrorResult("Token name is required")); + var token_entropy = _configuration.GetValue<string>("TOKEN_ENTROPY"); + if (token_entropy.IsNullOrWhiteSpace()) { + _logger.LogWarning("No token entropy is available in env:TOKEN_ENTROPY, Basic auth is disabled"); + return NotFound(); } - var token = new AccessToken { + var access_token = new AccessToken { Id = Guid.NewGuid(), - Name = request.Name, - User = user + User = user, + ExpiryDate = request.ExpiryDate.ToUniversalTime(), + AllowCreate = request.AllowCreate, + AllowRead = request.AllowRead, + AllowDelete = request.AllowDelete, + AllowUpdate = request.AllowUpdate }; - _context.AccessTokens.Add(token); + _context.AccessTokens.Add(access_token); _context.SaveChanges(); - return Ok(token); + return Ok(Convert.ToBase64String(Encoding.UTF8.GetBytes(access_token.Id.ToString().EncryptWithAes(token_entropy)))); } } diff --git a/src/server/Api/V1/BaseV1Route.cs b/src/server/Api/V1/BaseV1Route.cs index ba7d978..b1e2128 100644 --- a/src/server/Api/V1/BaseV1Route.cs +++ b/src/server/Api/V1/BaseV1Route.cs @@ -1,3 +1,5 @@ +using System.Net.Http.Headers; + namespace IOL.BookmarkThing.Server.Api.V1; /// <inheritdoc /> @@ -14,4 +16,23 @@ public class BaseV1Route : ControllerBase Username = User.Identity?.Name, Id = User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value.ToGuid() ?? default }; + + protected bool IsApiCall() { + if (!Request.Headers.ContainsKey("Authorization")) return false; + try { + var authHeader = AuthenticationHeaderValue.Parse(Request.Headers["Authorization"]); + if (authHeader.Parameter == null) return false; + } catch (Exception e) { + return false; + } + + return true; + } + + protected bool HasApiPermission(string permission_key) { + var permission_claim = User.Claims.SingleOrDefault(c => c.Type == permission_key); + return permission_claim is { + Value: "True" + }; + } } diff --git a/src/server/Api/V1/Entries/CreateEntryRoute.cs b/src/server/Api/V1/Entries/CreateEntryRoute.cs index ebe49fc..b502e4a 100644 --- a/src/server/Api/V1/Entries/CreateEntryRoute.cs +++ b/src/server/Api/V1/Entries/CreateEntryRoute.cs @@ -21,6 +21,10 @@ public class CreateEntryRoute : RouteBaseV1Sync.WithRequest<CreateEntryRequest>. [ApiVersion(ApiSpecV1.VERSION_STRING)] [HttpPost("~/v{version:apiVersion}/entries/create")] public override ActionResult<EntryDto> Handle(CreateEntryRequest entry) { + if (IsApiCall() && !HasApiPermission(Constants.TOKEN_ALLOW_CREATE)) { + return StatusCode(403, "Your token does not permit access to this resource"); + } + var errors = entry.GetErrors(); if (errors.Count != 0) { return BadRequest(errors); diff --git a/src/server/Api/V1/Entries/DeleteEntryRoute.cs b/src/server/Api/V1/Entries/DeleteEntryRoute.cs index fc79049..c979c1f 100644 --- a/src/server/Api/V1/Entries/DeleteEntryRoute.cs +++ b/src/server/Api/V1/Entries/DeleteEntryRoute.cs @@ -18,6 +18,10 @@ public class DeleteEntryRoute : RouteBaseV1Sync.WithRequest<Guid>.WithActionResu [ApiVersion(ApiSpecV1.VERSION_STRING)] [HttpDelete("~/v{version:apiVersion}/entries/{entryId:guid}")] public override ActionResult Handle(Guid entryId) { + if (IsApiCall() && !HasApiPermission(Constants.TOKEN_ALLOW_DELETE)) { + return StatusCode(403, "Your token does not permit access to this resource"); + } + var entry = _context.Entries.SingleOrDefault(c => c.Id == entryId && c.UserId == LoggedInUser.Id); if (entry == default) { return NotFound(new ErrorResult("Entry does not exist")); diff --git a/src/server/Api/V1/Entries/GetEntriesRoute.cs b/src/server/Api/V1/Entries/GetEntriesRoute.cs index adadf01..27905a2 100644 --- a/src/server/Api/V1/Entries/GetEntriesRoute.cs +++ b/src/server/Api/V1/Entries/GetEntriesRoute.cs @@ -16,6 +16,10 @@ public class GetEntriesRoute : RouteBaseV1Sync.WithoutRequest.WithActionResult<L [ApiVersion(ApiSpecV1.VERSION_STRING)] [HttpGet("~/v{version:apiVersion}/entries")] public override ActionResult<List<EntryDto>> Handle() { + if (IsApiCall() && !HasApiPermission(Constants.TOKEN_ALLOW_READ)) { + return StatusCode(403, "Your token does not permit access to this resource"); + } + return Ok(_context.Entries.Where(c => c.UserId == LoggedInUser.Id).Select(c => new EntryDto(c))); } } diff --git a/src/server/Api/V1/Entries/UpdateEntryRoute.cs b/src/server/Api/V1/Entries/UpdateEntryRoute.cs index 96c60fe..919364d 100644 --- a/src/server/Api/V1/Entries/UpdateEntryRoute.cs +++ b/src/server/Api/V1/Entries/UpdateEntryRoute.cs @@ -1,4 +1,3 @@ -using System.Security.Cryptography; using IOL.BookmarkThing.Server.Api.V1.Entries.Dtos; namespace IOL.BookmarkThing.Server.Api.V1.Entries; @@ -24,6 +23,10 @@ public class UpdateEntryRoute : RouteBaseV1Sync.WithRequest<UpdateEntryRequest>. [ApiVersion(ApiSpecV1.VERSION_STRING)] [HttpPost("~/v{version:apiVersion}/entries/update")] public override ActionResult<EntryDto> Handle(UpdateEntryRequest entryToUpdate) { + if (IsApiCall() && !HasApiPermission(Constants.TOKEN_ALLOW_UPDATE)) { + return StatusCode(403, "Your token does not permit access to this resource"); + } + var entry = _context.Entries.SingleOrDefault(c => c.Id == entryToUpdate.Id && c.UserId == LoggedInUser.Id); if (entry == default) { return NotFound(new ErrorResult("Entry does not exist")); diff --git a/src/server/Migrations/20220123102257_AccessTokens.Designer.cs b/src/server/Migrations/20220123102257_AccessTokens.Designer.cs new file mode 100644 index 0000000..0b8d3c0 --- /dev/null +++ b/src/server/Migrations/20220123102257_AccessTokens.Designer.cs @@ -0,0 +1,141 @@ +// <auto-generated /> +using System; +using IOL.BookmarkThing.Server.Models.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace IOL.BookmarkThing.Server.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20220123102257_AccessTokens")] + partial class AccessTokens + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("IOL.BookmarkThing.Server.Models.Database.AccessToken", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<bool>("AllowCreate") + .HasColumnType("boolean") + .HasColumnName("allow_create"); + + b.Property<bool>("AllowDelete") + .HasColumnType("boolean") + .HasColumnName("allow_delete"); + + b.Property<bool>("AllowRead") + .HasColumnType("boolean") + .HasColumnName("allow_read"); + + b.Property<bool>("AllowUpdate") + .HasColumnType("boolean") + .HasColumnName("allow_update"); + + b.Property<DateTime>("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property<DateTime>("ExpiryDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiry_date"); + + b.Property<Guid?>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_access_tokens"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_access_tokens_user_id"); + + b.ToTable("access_tokens", (string)null); + }); + + modelBuilder.Entity("IOL.BookmarkThing.Server.Models.Database.Entry", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<DateTime>("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property<string>("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property<string>("Tags") + .HasColumnType("text") + .HasColumnName("tags"); + + b.Property<string>("Url") + .HasColumnType("text") + .HasColumnName("url"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_entries"); + + b.ToTable("entries", (string)null); + }); + + modelBuilder.Entity("IOL.BookmarkThing.Server.Models.Database.User", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<DateTime>("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property<string>("Password") + .HasColumnType("text") + .HasColumnName("password"); + + b.Property<string>("Username") + .HasColumnType("text") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("IOL.BookmarkThing.Server.Models.Database.AccessToken", b => + { + b.HasOne("IOL.BookmarkThing.Server.Models.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .HasConstraintName("fk_access_tokens_users_user_id"); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/server/Migrations/20220123102257_AccessTokens.cs b/src/server/Migrations/20220123102257_AccessTokens.cs new file mode 100644 index 0000000..a29f944 --- /dev/null +++ b/src/server/Migrations/20220123102257_AccessTokens.cs @@ -0,0 +1,79 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace IOL.BookmarkThing.Server.Migrations +{ + public partial class AccessTokens : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn<DateTime>( + name: "created", + table: "users", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp without time zone"); + + migrationBuilder.AlterColumn<DateTime>( + name: "created", + table: "entries", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp without time zone"); + + migrationBuilder.CreateTable( + name: "access_tokens", + columns: table => new + { + id = table.Column<Guid>(type: "uuid", nullable: false), + user_id = table.Column<Guid>(type: "uuid", nullable: true), + expiry_date = table.Column<DateTime>(type: "timestamp with time zone", nullable: false), + allow_read = table.Column<bool>(type: "boolean", nullable: false), + allow_create = table.Column<bool>(type: "boolean", nullable: false), + allow_update = table.Column<bool>(type: "boolean", nullable: false), + allow_delete = table.Column<bool>(type: "boolean", nullable: false), + created = table.Column<DateTime>(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_access_tokens", x => x.id); + table.ForeignKey( + name: "fk_access_tokens_users_user_id", + column: x => x.user_id, + principalTable: "users", + principalColumn: "id"); + }); + + migrationBuilder.CreateIndex( + name: "ix_access_tokens_user_id", + table: "access_tokens", + column: "user_id"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "access_tokens"); + + migrationBuilder.AlterColumn<DateTime>( + name: "created", + table: "users", + type: "timestamp without time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn<DateTime>( + name: "created", + table: "entries", + type: "timestamp without time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + } + } +} diff --git a/src/server/Migrations/AppDbContextModelSnapshot.cs b/src/server/Migrations/AppDbContextModelSnapshot.cs index 5476b90..c48f478 100644 --- a/src/server/Migrations/AppDbContextModelSnapshot.cs +++ b/src/server/Migrations/AppDbContextModelSnapshot.cs @@ -17,12 +17,56 @@ namespace IOL.BookmarkThing.Server.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "5.0.7") + .HasAnnotation("ProductVersion", "6.0.1") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - modelBuilder.Entity("IOL.BookmarkThing.Server.Data.Database.Entry", b => + modelBuilder.Entity("IOL.BookmarkThing.Server.Models.Database.AccessToken", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<bool>("AllowCreate") + .HasColumnType("boolean") + .HasColumnName("allow_create"); + + b.Property<bool>("AllowDelete") + .HasColumnType("boolean") + .HasColumnName("allow_delete"); + + b.Property<bool>("AllowRead") + .HasColumnType("boolean") + .HasColumnName("allow_read"); + + b.Property<bool>("AllowUpdate") + .HasColumnType("boolean") + .HasColumnName("allow_update"); + + b.Property<DateTime>("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property<DateTime>("ExpiryDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiry_date"); + + b.Property<Guid?>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_access_tokens"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_access_tokens_user_id"); + + b.ToTable("access_tokens", (string)null); + }); + + modelBuilder.Entity("IOL.BookmarkThing.Server.Models.Database.Entry", b => { b.Property<Guid>("Id") .ValueGeneratedOnAdd() @@ -30,7 +74,7 @@ namespace IOL.BookmarkThing.Server.Migrations .HasColumnName("id"); b.Property<DateTime>("Created") - .HasColumnType("timestamp without time zone") + .HasColumnType("timestamp with time zone") .HasColumnName("created"); b.Property<string>("Description") @@ -55,7 +99,7 @@ namespace IOL.BookmarkThing.Server.Migrations b.ToTable("entries", (string)null); }); - modelBuilder.Entity("IOL.BookmarkThing.Server.Data.Database.User", b => + modelBuilder.Entity("IOL.BookmarkThing.Server.Models.Database.User", b => { b.Property<Guid>("Id") .ValueGeneratedOnAdd() @@ -63,7 +107,7 @@ namespace IOL.BookmarkThing.Server.Migrations .HasColumnName("id"); b.Property<DateTime>("Created") - .HasColumnType("timestamp without time zone") + .HasColumnType("timestamp with time zone") .HasColumnName("created"); b.Property<string>("Password") @@ -79,6 +123,16 @@ namespace IOL.BookmarkThing.Server.Migrations b.ToTable("users", (string)null); }); + + modelBuilder.Entity("IOL.BookmarkThing.Server.Models.Database.AccessToken", b => + { + b.HasOne("IOL.BookmarkThing.Server.Models.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .HasConstraintName("fk_access_tokens_users_user_id"); + + b.Navigation("User"); + }); #pragma warning restore 612, 618 } } diff --git a/src/server/Models/Database/AccessToken.cs b/src/server/Models/Database/AccessToken.cs index 51ada27..5094c5e 100644 --- a/src/server/Models/Database/AccessToken.cs +++ b/src/server/Models/Database/AccessToken.cs @@ -3,9 +3,11 @@ namespace IOL.BookmarkThing.Server.Models.Database; public class AccessToken : Base { public User User { get; set; } - public string Name { get; set; } + public DateTime ExpiryDate { get; set; } + public bool AllowRead { get; set; } + public bool AllowCreate { get; set; } + public bool AllowUpdate { get; set; } + public bool AllowDelete { get; set; } - public string PublicId() { - return Convert.ToBase64String(Id.ToByteArray()); - } + public bool HasExpired => ExpiryDate < DateTime.UtcNow; } diff --git a/src/server/Startup.cs b/src/server/Startup.cs index 8fd6955..f96d23b 100644 --- a/src/server/Startup.cs +++ b/src/server/Startup.cs @@ -83,10 +83,10 @@ public class Startup Name = "Authorization", Type = SecuritySchemeType.ApiKey, Scheme = "Basic", - BearerFormat = "Custom", + BearerFormat = "Basic", In = ParameterLocation.Header, Description = - "Enter your token in the text input below.\r\n\r\nExample: \"12345abcdef\"", + "Enter your token in the text input below.\r\n\r\nExample: \"Basic 12345abcdef\"", }); options.AddSecurityRequirement(new OpenApiSecurityRequirement { diff --git a/src/server/StaticData/AppJsonSettings.cs b/src/server/StaticData/AppJsonSettings.cs index 47c67a7..bed5ba9 100644 --- a/src/server/StaticData/AppJsonSettings.cs +++ b/src/server/StaticData/AppJsonSettings.cs @@ -6,6 +6,6 @@ public static class AppJsonSettings options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles; options.JsonSerializerOptions.PropertyNameCaseInsensitive = true; options.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowReadingFromString; - options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + options.JsonSerializerOptions.PropertyNamingPolicy = SnakeCaseNamingPolicy.Instance; }; } diff --git a/src/server/StaticData/Constants.cs b/src/server/StaticData/Constants.cs index 818004f..e0a46b1 100644 --- a/src/server/StaticData/Constants.cs +++ b/src/server/StaticData/Constants.cs @@ -4,4 +4,8 @@ public static class Constants { public const string API_NAME = "Bookmark API"; public const string BASIC_AUTH_SCHEME = "BasicAuthenticationScheme"; + public const string TOKEN_ALLOW_READ = "TOKEN_ALLOW_READ"; + public const string TOKEN_ALLOW_CREATE = "TOKEN_ALLOW_CREATE"; + public const string TOKEN_ALLOW_UPDATE = "TOKEN_ALLOW_UPDATE"; + public const string TOKEN_ALLOW_DELETE = "TOKEN_ALLOW_DELETE"; } diff --git a/src/server/Utilities/BasicAuthenticationHandler.cs b/src/server/Utilities/BasicAuthenticationHandler.cs index 7961b82..c4124e8 100644 --- a/src/server/Utilities/BasicAuthenticationHandler.cs +++ b/src/server/Utilities/BasicAuthenticationHandler.cs @@ -8,10 +8,21 @@ namespace IOL.BookmarkThing.Server.Utilities; public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions> { private readonly AppDbContext _context; + private readonly IConfiguration _configuration; + private readonly ILogger<BasicAuthenticationHandler> _logger; - public BasicAuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, AppDbContext context) : + public BasicAuthenticationHandler( + IOptionsMonitor<AuthenticationSchemeOptions> options, + ILoggerFactory logger, + UrlEncoder encoder, + ISystemClock clock, + AppDbContext context, + IConfiguration configuration + ) : base(options, logger, encoder, clock) { _context = context; + _configuration = configuration; + _logger = logger.CreateLogger<BasicAuthenticationHandler>(); } protected override Task<AuthenticateResult> HandleAuthenticateAsync() { @@ -23,26 +34,45 @@ public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSc return Task.FromResult(AuthenticateResult.Fail("Missing Authorization Header")); try { + var token_entropy = _configuration.GetValue<string>("TOKEN_ENTROPY"); + if (token_entropy.IsNullOrWhiteSpace()) { + _logger.LogWarning("No token entropy is available in env:TOKEN_ENTROPY, Basic auth is disabled"); + return Task.FromResult(AuthenticateResult.Fail("Invalid Authorization Header")); + } + var authHeader = AuthenticationHeaderValue.Parse(Request.Headers["Authorization"]); if (authHeader.Parameter == null) return Task.FromResult(AuthenticateResult.Fail("Invalid Authorization Header")); var credentialBytes = Convert.FromBase64String(authHeader.Parameter); - var token_is_guid = Guid.TryParse(Encoding.UTF8.GetString(credentialBytes), out var token_id); - if (token_is_guid) { - var token = _context.AccessTokens.Include(c => c.User).SingleOrDefault(c => c.Id == token_id); - if (token == default) { - return Task.FromResult(AuthenticateResult.Fail("Invalid Authorization Header")); - } + var decrypted_string = Encoding.UTF8.GetString(credentialBytes).DecryptWithAes(token_entropy); + var token_is_guid = Guid.TryParse(decrypted_string, out var token_id); - var claims = token.User.DefaultClaims(); - var identity = new ClaimsIdentity(claims, Scheme.Name); - var principal = new ClaimsPrincipal(identity); - var ticket = new AuthenticationTicket(principal, Scheme.Name); + if (!token_is_guid) { + return Task.FromResult(AuthenticateResult.Fail("Invalid Authorization Header")); + } - return Task.FromResult(AuthenticateResult.Success(ticket)); + var token = _context.AccessTokens.Include(c => c.User).SingleOrDefault(c => c.Id == token_id); + if (token == default) { + return Task.FromResult(AuthenticateResult.Fail("Invalid Authorization Header: Not Found")); } - return Task.FromResult(AuthenticateResult.Fail("Invalid Authorization Header")); - } catch { + if (token.HasExpired) { + return Task.FromResult(AuthenticateResult.Fail("Invalid Authorization Header: Expired")); + } + + var permissions = new List<Claim>() { + new(Constants.TOKEN_ALLOW_READ, token.AllowRead.ToString()), + new(Constants.TOKEN_ALLOW_UPDATE, token.AllowUpdate.ToString()), + new(Constants.TOKEN_ALLOW_CREATE, token.AllowCreate.ToString()), + new(Constants.TOKEN_ALLOW_DELETE, token.AllowDelete.ToString()), + }; + var claims = token.User.DefaultClaims().Concat(permissions); + var identity = new ClaimsIdentity(claims, Constants.BASIC_AUTH_SCHEME); + var principal = new ClaimsPrincipal(identity); + var ticket = new AuthenticationTicket(principal, Constants.BASIC_AUTH_SCHEME); + + return Task.FromResult(AuthenticateResult.Success(ticket)); + } catch (Exception e) { + _logger.LogError(e, $"An exception occured when challenging {Constants.BASIC_AUTH_SCHEME}"); return Task.FromResult(AuthenticateResult.Fail("Invalid Authorization Header")); } } diff --git a/src/server/Utilities/SnakeCaseNamingPolicy.cs b/src/server/Utilities/SnakeCaseNamingPolicy.cs new file mode 100644 index 0000000..9a7f1f3 --- /dev/null +++ b/src/server/Utilities/SnakeCaseNamingPolicy.cs @@ -0,0 +1,15 @@ +namespace IOL.BookmarkThing.Server.Utilities; + +public class SnakeCaseNamingPolicy : JsonNamingPolicy +{ + public static SnakeCaseNamingPolicy Instance { get; } = new SnakeCaseNamingPolicy(); + + public override string ConvertName(string name) { + // Conversion to other naming convention goes here. Like SnakeCase, KebabCase etc. + return ToSnakeCase(name); + } + + private static string ToSnakeCase(string str) { + return string.Concat(str.Select((x, i) => i > 0 && char.IsUpper(x) ? "_" + x.ToString() : x.ToString())).ToLower(); + } +} |
