Added auth
This commit is contained in:
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,9 @@ if (app.Environment.IsDevelopment())
|
|||||||
app.MapScalarApiReference();
|
app.MapScalarApiReference();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
app.UseAuthentication();
|
||||||
|
app.UseAuthorization();
|
||||||
|
|
||||||
app
|
app
|
||||||
.MapPlayListEndpoints()
|
.MapPlayListEndpoints()
|
||||||
.MapTrackEndpoints()
|
.MapTrackEndpoints()
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
namespace WebApi.Scalar;
|
||||||
|
|
||||||
|
public static class ScalarExtensions
|
||||||
|
{
|
||||||
|
public static IServiceCollection AddScalarConfiguration(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.ConfigureOptions<ScalarOptionsSetup>();
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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" />
|
||||||
|
|||||||
Reference in New Issue
Block a user