From f3d29bf124c395c503718957c02895b80f2885ce Mon Sep 17 00:00:00 2001 From: Rick van Dam Date: Mon, 3 Jun 2024 22:54:12 +0200 Subject: [PATCH] use microsoft identity --- .../Features/Employees/CreateEmployeeTests.cs | 4 +- .../Employees/DeleteEmployeeByIdTests.cs | 4 +- .../Employees/GetEmployeeByIdTests.cs | 4 +- .../Features/Employees/GetEmployeesTests.cs | 6 +-- .../Employees/UpdateEmployeeByIdTests.cs | 14 ++----- .../TestSetup/ClaimConstants.cs | 4 +- CleanAspCore/AppConfiguration.cs | 41 +++++++++++++++---- CleanAspCore/CleanAspCore.csproj | 1 + .../Employees/EmployeeEndpointConfig.cs | 4 +- CleanAspCore/appsettings.json | 5 +++ Readme.md | 5 ++- 11 files changed, 58 insertions(+), 34 deletions(-) diff --git a/CleanAspCore.Api.Tests/Features/Employees/CreateEmployeeTests.cs b/CleanAspCore.Api.Tests/Features/Employees/CreateEmployeeTests.cs index 70f8080..74b70fb 100644 --- a/CleanAspCore.Api.Tests/Features/Employees/CreateEmployeeTests.cs +++ b/CleanAspCore.Api.Tests/Features/Employees/CreateEmployeeTests.cs @@ -17,7 +17,7 @@ public async Task CreateEmployee_IsAdded() }); //Act - var response = await Sut.CreateClientFor(ClaimConstants.WriteEmployeesRole).CreateEmployee(createEmployeeRequest); + var response = await Sut.CreateClientFor(ClaimConstants.WriteRole).CreateEmployee(createEmployeeRequest); //Assert await response.AssertStatusCode(HttpStatusCode.Created); @@ -64,7 +64,7 @@ public async Task CreateEmployee_InvalidRequest_ReturnsBadRequest(TestScenario<( }); //Act - var response = await Sut.CreateClientFor(ClaimConstants.WriteEmployeesRole).CreateEmployee(createEmployeeRequest); + var response = await Sut.CreateClientFor(ClaimConstants.WriteRole).CreateEmployee(createEmployeeRequest); //Assert await response.AssertBadRequest(scenario.Input.expectedErrors); diff --git a/CleanAspCore.Api.Tests/Features/Employees/DeleteEmployeeByIdTests.cs b/CleanAspCore.Api.Tests/Features/Employees/DeleteEmployeeByIdTests.cs index 9696fb5..d7860e9 100644 --- a/CleanAspCore.Api.Tests/Features/Employees/DeleteEmployeeByIdTests.cs +++ b/CleanAspCore.Api.Tests/Features/Employees/DeleteEmployeeByIdTests.cs @@ -15,7 +15,7 @@ public async Task DeleteEmployeeById_IsDeleted() }); //Act - var response = await Sut.CreateClientFor(ClaimConstants.WriteEmployeesRole).DeleteEmployeeById(employee.Id); + var response = await Sut.CreateClientFor(ClaimConstants.WriteRole).DeleteEmployeeById(employee.Id); //Assert await response.AssertStatusCode(HttpStatusCode.NoContent); @@ -29,7 +29,7 @@ public async Task DeleteEmployeeById_DoesNotExist_ReturnsNotFound() var id = Guid.NewGuid(); //Act - var response = await Sut.CreateClientFor(ClaimConstants.WriteEmployeesRole).DeleteEmployeeById(id); + var response = await Sut.CreateClientFor(ClaimConstants.WriteRole).DeleteEmployeeById(id); //Assert await response.AssertStatusCode(HttpStatusCode.NotFound); diff --git a/CleanAspCore.Api.Tests/Features/Employees/GetEmployeeByIdTests.cs b/CleanAspCore.Api.Tests/Features/Employees/GetEmployeeByIdTests.cs index 707b6d5..3dc17bc 100644 --- a/CleanAspCore.Api.Tests/Features/Employees/GetEmployeeByIdTests.cs +++ b/CleanAspCore.Api.Tests/Features/Employees/GetEmployeeByIdTests.cs @@ -15,7 +15,7 @@ public async Task GetEmployeeById_ReturnsExpectedEmployee() }); //Act - var response = await Sut.CreateClientFor(ClaimConstants.ReadEmployeesRole).GetEmployeeById(employee.Id); + var response = await Sut.CreateClientFor(ClaimConstants.ReadRole).GetEmployeeById(employee.Id); //Assert await response.AssertStatusCode(HttpStatusCode.OK); @@ -29,7 +29,7 @@ public async Task GetEmployeeById_DoesNotExist_ReturnsNotFound() var employee = new EmployeeFaker().Generate(); //Act - var response = await Sut.CreateClientFor(ClaimConstants.ReadEmployeesRole).GetEmployeeById(employee.Id); + var response = await Sut.CreateClientFor(ClaimConstants.ReadRole).GetEmployeeById(employee.Id); //Assert await response.AssertStatusCode(HttpStatusCode.NotFound); diff --git a/CleanAspCore.Api.Tests/Features/Employees/GetEmployeesTests.cs b/CleanAspCore.Api.Tests/Features/Employees/GetEmployeesTests.cs index 7a1f040..9dd4eab 100644 --- a/CleanAspCore.Api.Tests/Features/Employees/GetEmployeesTests.cs +++ b/CleanAspCore.Api.Tests/Features/Employees/GetEmployeesTests.cs @@ -8,7 +8,7 @@ public class GetEmployees : TestBase public async Task? GetEmployees_NoEmployees_ReturnsEmptyPage() { //Act - var response = await Sut.CreateClientFor(ClaimConstants.ReadEmployeesRole).GetEmployees(1, 10); + var response = await Sut.CreateClientFor(ClaimConstants.ReadRole).GetEmployees(1, 10); //Assert await response.AssertStatusCode(HttpStatusCode.OK); @@ -37,7 +37,7 @@ public async Task GetEmployees_FirstPage_ReturnsExpectedEmployees() }); //Act - var response = await Sut.CreateClientFor(ClaimConstants.ReadEmployeesRole).GetEmployees(1, 10); + var response = await Sut.CreateClientFor(ClaimConstants.ReadRole).GetEmployees(1, 10); //Assert await response.AssertStatusCode(HttpStatusCode.OK); @@ -70,7 +70,7 @@ public async Task GetEmployees_SecondPage_ReturnsExpectedEmployees() }); //Act - var response = await Sut.CreateClientFor(ClaimConstants.ReadEmployeesRole).GetEmployees(2, 10); + var response = await Sut.CreateClientFor(ClaimConstants.ReadRole).GetEmployees(2, 10); //Assert await response.AssertStatusCode(HttpStatusCode.OK); diff --git a/CleanAspCore.Api.Tests/Features/Employees/UpdateEmployeeByIdTests.cs b/CleanAspCore.Api.Tests/Features/Employees/UpdateEmployeeByIdTests.cs index 9fd4bf9..9d49564 100644 --- a/CleanAspCore.Api.Tests/Features/Employees/UpdateEmployeeByIdTests.cs +++ b/CleanAspCore.Api.Tests/Features/Employees/UpdateEmployeeByIdTests.cs @@ -12,13 +12,10 @@ public async Task UpdateEmployeeById_IsUpdated() var employee = new EmployeeFaker().Generate(); Sut.SeedData(context => { context.Employees.Add(employee); }); - UpdateEmployeeRequest updateEmployeeRequest = new() - { - FirstName = "Updated" - }; + UpdateEmployeeRequest updateEmployeeRequest = new() { FirstName = "Updated" }; //Act - var response = await Sut.CreateClientFor(ClaimConstants.WriteEmployeesRole).UpdateEmployeeById(employee.Id, updateEmployeeRequest); + var response = await Sut.CreateClientFor(ClaimConstants.WriteRole).UpdateEmployeeById(employee.Id, updateEmployeeRequest); //Assert await response.AssertStatusCode(HttpStatusCode.NoContent); @@ -41,13 +38,10 @@ public async Task UpdateEmployeeById_DoesNotExist_ReturnsNotFound() //Arrange var employee = new EmployeeFaker().Generate(); - UpdateEmployeeRequest updateEmployeeRequest = new() - { - FirstName = "Updated" - }; + UpdateEmployeeRequest updateEmployeeRequest = new() { FirstName = "Updated" }; //Act - var response = await Sut.CreateClientFor(ClaimConstants.WriteEmployeesRole).UpdateEmployeeById(employee.Id, updateEmployeeRequest); + var response = await Sut.CreateClientFor(ClaimConstants.WriteRole).UpdateEmployeeById(employee.Id, updateEmployeeRequest); //Assert await response.AssertStatusCode(HttpStatusCode.NotFound); diff --git a/CleanAspCore.Api.Tests/TestSetup/ClaimConstants.cs b/CleanAspCore.Api.Tests/TestSetup/ClaimConstants.cs index afaac06..b3c0c1d 100644 --- a/CleanAspCore.Api.Tests/TestSetup/ClaimConstants.cs +++ b/CleanAspCore.Api.Tests/TestSetup/ClaimConstants.cs @@ -4,6 +4,6 @@ namespace CleanAspCore.Api.Tests.TestSetup; public static class ClaimConstants { - public static readonly Claim ReadEmployeesRole = new(ClaimTypes.Role, "reademployees"); - public static readonly Claim WriteEmployeesRole = new(ClaimTypes.Role, "writeemployees"); + public static readonly Claim ReadRole = new(ClaimTypes.Role, "read"); + public static readonly Claim WriteRole = new(ClaimTypes.Role, "write"); } diff --git a/CleanAspCore/AppConfiguration.cs b/CleanAspCore/AppConfiguration.cs index ca27919..02cdec4 100644 --- a/CleanAspCore/AppConfiguration.cs +++ b/CleanAspCore/AppConfiguration.cs @@ -6,6 +6,7 @@ using MicroElements.Swashbuckle.FluentValidation.AspNetCore; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; +using Microsoft.Identity.Web; using Microsoft.OpenApi.Models; namespace CleanAspCore; @@ -31,11 +32,15 @@ internal static void AddAuthServices(this WebApplicationBuilder builder) .RequireAuthenticatedUser() .Build()); - builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) - .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, x => - { - x.TokenValidationParameters.ClockSkew = TimeSpan.FromSeconds(5); - }); + var authBuilder = builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme); + if (builder.Environment.IsDevelopment()) + { + authBuilder.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme); + } + else + { + authBuilder.AddMicrosoftIdentityWebApi(builder.Configuration); + } } internal static void AddOpenApiServices(this WebApplicationBuilder builder) @@ -43,6 +48,7 @@ internal static void AddOpenApiServices(this WebApplicationBuilder builder) if (builder.Configuration.GetValue("DisableOpenApi") == true) return; + var config = builder.Configuration.GetRequiredSection(Constants.AzureAd).Get()!; builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(options => { @@ -50,19 +56,30 @@ internal static void AddOpenApiServices(this WebApplicationBuilder builder) var xmlDocPath = Path.Combine(AppContext.BaseDirectory, $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"); options.IncludeXmlComments(xmlDocPath); - var jwtSecurityScheme = new OpenApiSecurityScheme { BearerFormat = "JWT", Name = "JWT Authentication", In = ParameterLocation.Header, - Type = SecuritySchemeType.Http, + Type = builder.Environment.IsDevelopment() ? SecuritySchemeType.Http : SecuritySchemeType.OAuth2, Scheme = JwtBearerDefaults.AuthenticationScheme, Description = "Put **_ONLY_** your JWT Bearer token on textbox below!", Reference = new OpenApiReference { Id = JwtBearerDefaults.AuthenticationScheme, Type = ReferenceType.SecurityScheme + }, + Flows = new OpenApiOAuthFlows() + { + AuthorizationCode = new OpenApiOAuthFlow() + { + AuthorizationUrl = new Uri($"https://login.microsoftonline.com/{config.TenantId}/oauth2/v2.0/authorize"), + TokenUrl = new Uri($"https://login.microsoftonline.com/{config.TenantId}/oauth2/v2.0/token"), + Scopes = new Dictionary + { + { $"api://{config.ClientId}/default", "read" }, + }, + } } }; @@ -77,9 +94,15 @@ internal static void UseOpenApi(this WebApplication app) { if (app.Configuration.GetValue("DisableOpenApi") == true) return; - + var config = app.Configuration.GetRequiredSection(Constants.AzureAd).Get()!; app.UseSwagger(); - app.UseSwaggerUI(); + app.UseSwaggerUI(setup => + { + setup.ConfigObject.AdditionalItems.Add("persistAuthorization", "true"); + setup.OAuthClientId(config.ClientId); + setup.OAuthUsePkce(); + setup.OAuthScopes($"api://{config.ClientId}/default"); + }); } internal static void RunMigrations(this WebApplication app) diff --git a/CleanAspCore/CleanAspCore.csproj b/CleanAspCore/CleanAspCore.csproj index 57a31ae..49259a7 100644 --- a/CleanAspCore/CleanAspCore.csproj +++ b/CleanAspCore/CleanAspCore.csproj @@ -21,6 +21,7 @@ + diff --git a/CleanAspCore/Endpoints/Employees/EmployeeEndpointConfig.cs b/CleanAspCore/Endpoints/Employees/EmployeeEndpointConfig.cs index c9e8bd9..8335de8 100644 --- a/CleanAspCore/Endpoints/Employees/EmployeeEndpointConfig.cs +++ b/CleanAspCore/Endpoints/Employees/EmployeeEndpointConfig.cs @@ -10,8 +10,8 @@ internal static class EmployeeEndpointConfig internal static void AddEmployeeServices(this WebApplicationBuilder builder) { builder.Services.AddAuthorizationBuilder() - .AddPolicy(ReadEmployeesPolicy, policy => policy.RequireRole("reademployees")) - .AddPolicy(WriteEmployeesPolicy, policy => policy.RequireRole("writeemployees")); + .AddPolicy(ReadEmployeesPolicy, policy => policy.RequireRole("read")) + .AddPolicy(WriteEmployeesPolicy, policy => policy.RequireRole("write")); } internal static void AddEmployeesRoutes(this IEndpointRouteBuilder host) diff --git a/CleanAspCore/appsettings.json b/CleanAspCore/appsettings.json index 4282acb..a7b60a0 100644 --- a/CleanAspCore/appsettings.json +++ b/CleanAspCore/appsettings.json @@ -5,5 +5,10 @@ "Microsoft.AspNetCore": "Information" } }, + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "88d823a1-6334-422c-8b78-1665d9b4cbac", + "ClientId": "1a338460-39f1-42e4-9b68-6988d33741cd" + }, "AllowedHosts": "*" } diff --git a/Readme.md b/Readme.md index 3440915..a4eb590 100644 --- a/Readme.md +++ b/Readme.md @@ -1,6 +1,7 @@ # A productive ASP .NET minimal api template -This is a template repository showing how one can implement a clean api with ASP.NET using minimal apis. The focus on 'features' in this template is on dev productivity, the actual features of the api itself has been kept basic on purpose. Feel free to copy this repository or reuse parts of it, don't forget to give a star if you do. +This is a template repository showing how one can implement a clean api with ASP.NET using minimal apis. The focus on 'features' in this template is on dev productivity, the actual features +of the api itself has been kept basic on purpose. Feel free to copy this repository or reuse parts of it, don't forget to give a star if you do. Some features in this template: @@ -26,7 +27,7 @@ dotnet test 1. First generate a jwt that you can use for local testing: ```cmd -dotnet user-jwts create --role "reademployees" --role "writeemployees" +dotnet user-jwts create --role "read" --role "write" ``` NOTE: The jobs and department endpoints only require authentication but the employee endpoints require that you have the correct claims in the jwt token.