Added auth

This commit is contained in:
2026-05-29 16:02:34 +02:00
parent 335e1a785e
commit 39be51304b
13 changed files with 281 additions and 1 deletions
+10
View File
@@ -5,6 +5,16 @@
"Microsoft.AspNetCore": "Warning" "Microsoft.AspNetCore": "Warning"
} }
}, },
"Authentication": {
"MetadataAddress": "http://localhost:8080/.well-known/openid-configuration",
"IssuerAddress": "http://localhost:8080",
"AudienceAddress": "372825689755287555",
"ClientId": "372825689755287555",
"AuthorizationEndpoint": "http://localhost:8080/oauth/v2/authorize?prompt=login",
"TokenEndpoint": "http://localhost:8080/oauth/v2/token",
"Scopes": [ "openid", "profile", "email" ],
"RequireHttpsMetadata": false
},
"Database": { "Database": {
"ConnectionString": "Host=localhost;Port=59000;Database=musify_db;Username=postgres;Password=postgres" "ConnectionString": "Host=localhost;Port=59000;Database=musify_db;Username=postgres;Password=postgres"
}, },
+10
View File
@@ -6,6 +6,16 @@
} }
}, },
"AllowedHosts": "*", "AllowedHosts": "*",
"Authentication": {
"MetadataAddress": "",
"IssuerAddress": "",
"AudienceAddress": "",
"ClientId": "",
"AuthorizationEndpoint": "",
"TokenEndpoint": "",
"Scopes": [],
"RequireHttpsMetadata": true
},
"Database": { "Database": {
"ConnectionString": "" "ConnectionString": ""
}, },
@@ -0,0 +1,37 @@
using System.ComponentModel.DataAnnotations;
namespace WebApi.Authentication;
public sealed class AuthenticationConfiguration
{
public const string SectionName = "Authentication";
[Required]
[Url]
public required string MetadataAddress { get; init; }
[Required]
public required string IssuerAddress { get; init; }
[Required]
public required string AudienceAddress { get; init; }
[Required]
public required string ClientId { get; init; }
public string? ClientSecret { get; init; }
[Required]
[Url]
public required string AuthorizationEndpoint { get; init; }
[Required]
[Url]
public required string TokenEndpoint { get; init; }
[Required]
[MinLength(1)]
public required string[] Scopes { get; init; }
public bool RequireHttpsMetadata { get; init; } = true;
}
@@ -0,0 +1,31 @@
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Options;
namespace WebApi.Authentication;
public static class AuthenticationExtensions
{
public static IServiceCollection AddAuthenticationConfiguration(
this IServiceCollection services,
IConfiguration configuration)
{
services
.AddOptionsWithValidateOnStart<AuthenticationConfiguration>()
.Bind(configuration.GetRequiredSection(AuthenticationConfiguration.SectionName))
.ValidateDataAnnotations();
services.AddSingleton(serviceProvider =>
serviceProvider.GetRequiredService<IOptions<AuthenticationConfiguration>>().Value);
services.AddScoped<JwtBearerEventsHandler>();
services.ConfigureOptions<JwtBearerOptionsSetup>();
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer();
services.AddAuthorization();
return services;
}
}
@@ -0,0 +1,25 @@
using Mediator;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Musify.Application.Users.Commands;
using System.Security.Claims;
namespace WebApi.Authentication;
public sealed class JwtBearerEventsHandler(IMediator mediator) : JwtBearerEvents
{
public override async Task TokenValidated(TokenValidatedContext tokenValidatedContext)
{
var principal = tokenValidatedContext.Principal!;
var id = principal.FindFirstValue(ClaimTypes.NameIdentifier)!;
var guidId = Guid.Parse(id);
var username = principal.FindFirstValue(ClaimTypes.Name)!;
var firstName = principal.FindFirstValue(ClaimTypes.GivenName);
var lastName = principal.FindFirstValue(ClaimTypes.Surname);
await mediator.Send(
new CreateUserCommand(guidId, username, firstName, lastName),
tokenValidatedContext.HttpContext.RequestAborted);
}
}
@@ -0,0 +1,37 @@
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using System.Security.Claims;
namespace WebApi.Authentication;
public sealed class JwtBearerOptionsSetup(IOptions<AuthenticationConfiguration> options)
: IConfigureNamedOptions<JwtBearerOptions>
{
private readonly AuthenticationConfiguration _configuration = options.Value;
public void Configure(string? name, JwtBearerOptions options)
{
if (name != JwtBearerDefaults.AuthenticationScheme)
{
return;
}
Configure(options);
}
public void Configure(JwtBearerOptions options)
{
options.MetadataAddress = _configuration.MetadataAddress;
options.RequireHttpsMetadata = _configuration.RequireHttpsMetadata;
options.Audience = _configuration.AudienceAddress;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = _configuration.IssuerAddress,
ValidAudience = _configuration.AudienceAddress,
RoleClaimType = ClaimTypes.Role,
NameClaimType = ClaimTypes.Name,
};
options.EventsType = typeof(JwtBearerEventsHandler);
}
}
@@ -3,7 +3,10 @@ using Musify.Infrastructure.MassTransit;
using Musify.Infrastructure.Persistence; using Musify.Infrastructure.Persistence;
using Musify.Application.Configuration; using Musify.Application.Configuration;
using Musify.Infrastructure.Services; using Musify.Infrastructure.Services;
using WebApi.Authentication;
using WebApi.DataTransferObjects.PlayLists; using WebApi.DataTransferObjects.PlayLists;
using WebApi.OpenApi;
using WebApi.Scalar;
namespace WebApi.Extensions; namespace WebApi.Extensions;
@@ -19,7 +22,9 @@ public static class ServiceCollectionExtensions
services.AddUploadIntentConfiguration(configuration); services.AddUploadIntentConfiguration(configuration);
services.AddMassTransitClient(configuration); services.AddMassTransitClient(configuration);
services.AddMediator(); services.AddMediator();
services.AddOpenApi(); services.AddAuthenticationConfiguration(configuration);
services.AddOpenApiConfiguration();
services.AddScalarConfiguration();
services.AddValidatorsFromAssemblyContaining<CreatePlayListRequest>(); services.AddValidatorsFromAssemblyContaining<CreatePlayListRequest>();
return services; return services;
+14
View File
@@ -0,0 +1,14 @@
using Microsoft.AspNetCore.OpenApi;
namespace WebApi.OpenApi;
public static class OpenApiExtensions
{
public static IServiceCollection AddOpenApiConfiguration(this IServiceCollection services)
{
services.AddOpenApi();
services.ConfigureOptions<OpenApiOptionsSetup>();
return services;
}
}
+66
View File
@@ -0,0 +1,66 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi;
using WebApi.Authentication;
namespace WebApi.OpenApi;
public sealed class OpenApiOptionsSetup(IOptions<AuthenticationConfiguration> options)
: IConfigureNamedOptions<OpenApiOptions>
{
private const string SecuritySchemeId = "OAuth2";
private readonly AuthenticationConfiguration _configuration = options.Value;
public void Configure(string? name, OpenApiOptions options) => Configure(options);
public void Configure(OpenApiOptions options)
{
options.AddDocumentTransformer((document, _, _) =>
{
document.Components ??= new OpenApiComponents();
document.Components.SecuritySchemes ??= new Dictionary<string, IOpenApiSecurityScheme>();
document.Components.SecuritySchemes[SecuritySchemeId] = new OpenApiSecurityScheme
{
Type = SecuritySchemeType.OAuth2,
Flows = new OpenApiOAuthFlows
{
AuthorizationCode = new OpenApiOAuthFlow
{
AuthorizationUrl = new Uri(_configuration.AuthorizationEndpoint),
TokenUrl = new Uri(_configuration.TokenEndpoint),
Scopes = _configuration.Scopes.ToDictionary(scope => scope, scope => scope),
},
},
};
return Task.CompletedTask;
});
options.AddOperationTransformer((operation, context, _) =>
{
var requiresAuthorization = context.Description.ActionDescriptor.EndpointMetadata
.OfType<IAuthorizeData>()
.Any();
var allowsAnonymous = context.Description.ActionDescriptor.EndpointMetadata
.OfType<IAllowAnonymous>()
.Any();
if (!requiresAuthorization || allowsAnonymous)
{
return Task.CompletedTask;
}
operation.Security ??= new List<OpenApiSecurityRequirement>();
operation.Security.Add(new OpenApiSecurityRequirement
{
[new OpenApiSecuritySchemeReference(SecuritySchemeId, context.Document)] =
_configuration.Scopes.ToList(),
});
return Task.CompletedTask;
});
}
}
+3
View File
@@ -17,6 +17,9 @@ if (app.Environment.IsDevelopment())
app.MapScalarApiReference(); app.MapScalarApiReference();
} }
app.UseAuthentication();
app.UseAuthorization();
app app
.MapPlayListEndpoints() .MapPlayListEndpoints()
.MapTrackEndpoints() .MapTrackEndpoints()
+11
View File
@@ -0,0 +1,11 @@
namespace WebApi.Scalar;
public static class ScalarExtensions
{
public static IServiceCollection AddScalarConfiguration(this IServiceCollection services)
{
services.ConfigureOptions<ScalarOptionsSetup>();
return services;
}
}
+30
View File
@@ -0,0 +1,30 @@
using Microsoft.Extensions.Options;
using Scalar.AspNetCore;
using WebApi.Authentication;
namespace WebApi.Scalar;
public sealed class ScalarOptionsSetup(IOptions<AuthenticationConfiguration> options)
: IConfigureOptions<ScalarOptions>
{
private const string SecuritySchemeId = "OAuth2";
private readonly AuthenticationConfiguration _configuration = options.Value;
public void Configure(ScalarOptions scalarOptions)
{
scalarOptions
.WithTitle("Musify API")
.AddPreferredSecuritySchemes(SecuritySchemeId)
.AddAuthorizationCodeFlow(SecuritySchemeId, flow =>
{
flow.ClientId = _configuration.ClientId;
flow.ClientSecret = _configuration.ClientSecret;
flow.AuthorizationUrl = _configuration.AuthorizationEndpoint;
flow.TokenUrl = _configuration.TokenEndpoint;
flow.Pkce = Pkce.Sha256;
flow.SelectedScopes = _configuration.Scopes;
})
.HideModels();
}
}
+1
View File
@@ -15,6 +15,7 @@
<PackageReference Include="MassTransit.EntityFrameworkCore" Version="8.5.9" /> <PackageReference Include="MassTransit.EntityFrameworkCore" Version="8.5.9" />
<PackageReference Include="MassTransit.RabbitMQ" Version="8.5.9" /> <PackageReference Include="MassTransit.RabbitMQ" Version="8.5.9" />
<PackageReference Include="Mediator.Abstractions" Version="3.0.2" /> <PackageReference Include="Mediator.Abstractions" Version="3.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.8" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.8" /> <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.8" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.8" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.8" />