From 9d57c3d52348ff94d3b01fbbe4b909d0371cd5d7 Mon Sep 17 00:00:00 2001 From: Enis Mulic Date: Fri, 17 Nov 2023 12:51:16 +0100 Subject: [PATCH] feat: add auth option --- .template.config/dotnetcli.host.json | 4 ++ .template.config/ide.host.json | 12 +++- .template.config/template.json | 24 ++++++++ README.md | 12 ++-- src/Api/Api.csproj | 3 + src/Api/ConfigureMicrosoftEntraAuth.cs | 26 +++++++++ src/Api/Options/MicrosoftEntraOptions.cs | 12 ++++ ...crosoftEntraSwaggerConfigurationOptions.cs | 55 +++++++++++++++++++ .../Options/SwaggerConfigurationOptions.cs | 29 ---------- src/Api/Program.cs | 19 +++++-- src/Api/appsettings.MsSql.json | 13 ----- src/Api/appsettings.PostgreSql.json | 13 ----- src/Api/appsettings.json | 9 ++- src/Application/Authorization/Scopes.cs | 6 ++ 14 files changed, 172 insertions(+), 65 deletions(-) create mode 100644 src/Api/ConfigureMicrosoftEntraAuth.cs create mode 100644 src/Api/Options/MicrosoftEntraOptions.cs create mode 100644 src/Api/Options/MicrosoftEntraSwaggerConfigurationOptions.cs create mode 100644 src/Application/Authorization/Scopes.cs diff --git a/.template.config/dotnetcli.host.json b/.template.config/dotnetcli.host.json index e812939..4fc56a3 100644 --- a/.template.config/dotnetcli.host.json +++ b/.template.config/dotnetcli.host.json @@ -7,6 +7,10 @@ "Database": { "longName": "database", "shortName": "db" + }, + "Auth": { + "longName": "auth", + "shortName": "oa" } } } diff --git a/.template.config/ide.host.json b/.template.config/ide.host.json index 298d3b2..cf298fa 100644 --- a/.template.config/ide.host.json +++ b/.template.config/ide.host.json @@ -9,7 +9,7 @@ "text": "Git Repository Host" }, "description": { - "text": "Select the git respository host" + "text": "Select the git repository host" }, "isVisible": true }, @@ -22,6 +22,16 @@ "text": "Select the database" }, "isVisible": true + }, + { + "id": "Auth", + "name": { + "text": "Auth" + }, + "description": { + "text": "Select the auth provider" + }, + "isVisible": true } ] } diff --git a/.template.config/template.json b/.template.config/template.json index 8a04635..6cf94c6 100644 --- a/.template.config/template.json +++ b/.template.config/template.json @@ -79,6 +79,26 @@ "UseDatabase": { "type": "computed", "value": "(Database != \"None\")" + }, + "Auth": { + "type": "parameter", + "datatype": "choice", + "choices": [ + { + "choice": "Entra", + "description": "Use Microsoft Entra as the auth provider" + }, + { + "choice": "None", + "description": "" + } + ], + "defaultValue": "None", + "description": "The type of auth provider to use" + }, + "UseEntra": { + "type": "computed", + "value": "(Auth == \"Entra\")" } }, "sources": [ @@ -130,6 +150,10 @@ "appsettings.PostgreSql.json": "appsettings.Development.json", "docker-compose.postgresql.yml": "docker-compose.yml" } + }, + { + "condition": "(!UseEntra)", + "exclude": ["**MicrosoftEntra**", "**Scopes**"] } ] } diff --git a/README.md b/README.md index e00b000..d9e7c9b 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ dotnet new vsma --name ProjectName dotnet new vsma --name [ProjectName] [-gh|--git-host [Github|AzureDevOps|None]] [-db|--database [MsSql|PostgreSql|None]] + [-oa|--auth [Entra|None]] ``` ### Options @@ -39,6 +40,9 @@ dotnet new vsma --name [ProjectName] Choose the platform you will host your projects git repository, this will give you a base CI workflow, pull request template, and anything specific to the platform that might be of use. The default value is `None`. - `-db|--database [MsSql|PostgreSql|None]` Choose what database to use for your project. The default is `None` +- `-oa|--auth [Entra|None]` + Choose a auth provider to use for your project. + The default is `None` which will configure a JwtBearer Auth that you can use with `dotnet user-jwts`. ## Configuration @@ -57,7 +61,7 @@ To test/develop the template with specific options add a `` blo net7.0 enable enable - + 7-recommended true @@ -81,9 +85,9 @@ When you run the application the database will be created (if it doesn't exist) To run the migrations you will need to add the following flags to your ef commands. -* `-p | --project src/Application` -* `-s | --startup-project src/Api` -* `-o | --output-dir Infrastructure/Persistance/Migrations` +- `-p | --project src/Application` +- `-s | --startup-project src/Api` +- `-o | --output-dir Infrastructure/Persistance/Migrations` For example, to add a new migration: diff --git a/src/Api/Api.csproj b/src/Api/Api.csproj index 14b01d5..ff8d3f6 100644 --- a/src/Api/Api.csproj +++ b/src/Api/Api.csproj @@ -23,6 +23,9 @@ + + + diff --git a/src/Api/ConfigureMicrosoftEntraAuth.cs b/src/Api/ConfigureMicrosoftEntraAuth.cs new file mode 100644 index 0000000..94c8f78 --- /dev/null +++ b/src/Api/ConfigureMicrosoftEntraAuth.cs @@ -0,0 +1,26 @@ +using Api.Options; + +using Microsoft.Extensions.Options; +using Microsoft.Identity.Web; + +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace Api; + +public static class ConfigureMicrosoftEntraAuth +{ + public static IServiceCollection AddMicrosoftEntraAuth(this IServiceCollection services, IConfiguration configuration) + { + services.Configure(options => + configuration.Bind(MicrosoftEntraOptions.SectionName, options)); + + services.AddTransient, MicrosoftEntraSwaggerConfigurationOptions>(); + + services.AddAuthentication() + .AddMicrosoftIdentityWebApi(configuration, MicrosoftEntraOptions.SectionName) + .EnableTokenAcquisitionToCallDownstreamApi() + .AddInMemoryTokenCaches(); + + return services; + } +} \ No newline at end of file diff --git a/src/Api/Options/MicrosoftEntraOptions.cs b/src/Api/Options/MicrosoftEntraOptions.cs new file mode 100644 index 0000000..2c37193 --- /dev/null +++ b/src/Api/Options/MicrosoftEntraOptions.cs @@ -0,0 +1,12 @@ +using Microsoft.Identity.Web; + +namespace Api.Options; + +public class MicrosoftEntraOptions : MicrosoftIdentityOptions +{ + public const string SectionName = "Entra"; + + public string BaseUrl => $"{Instance}/{TenantId}/oauth2/v2.0"; + public string AuthorizationUrl => $"{BaseUrl}/authorize"; + public string TokenUrl => $"{BaseUrl}/token"; +} \ No newline at end of file diff --git a/src/Api/Options/MicrosoftEntraSwaggerConfigurationOptions.cs b/src/Api/Options/MicrosoftEntraSwaggerConfigurationOptions.cs new file mode 100644 index 0000000..68963da --- /dev/null +++ b/src/Api/Options/MicrosoftEntraSwaggerConfigurationOptions.cs @@ -0,0 +1,55 @@ +using Application.Authorization; + +using Microsoft.Extensions.Options; +using Microsoft.OpenApi.Models; + +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace Api.Options; + +public class MicrosoftEntraSwaggerConfigurationOptions : IConfigureOptions +{ + private readonly MicrosoftEntraOptions _options; + + public MicrosoftEntraSwaggerConfigurationOptions(IOptions options) + { + _options = options.Value; + } + + public void Configure(SwaggerGenOptions options) + { + var authorizationUrl = _options?.AuthorizationUrl ?? string.Empty; + var tokenUrl = _options?.TokenUrl ?? string.Empty; + var clientId = _options?.ClientId ?? string.Empty; + + options.AddSecurityDefinition("Entra", new OpenApiSecurityScheme + { + Type = SecuritySchemeType.OAuth2, + Flows = new OpenApiOAuthFlows + { + AuthorizationCode = new OpenApiOAuthFlow + { + AuthorizationUrl = new Uri(authorizationUrl), + TokenUrl = new Uri(tokenUrl), + Scopes = new[] { $"{AuthorizationScopes.AccessAsUser}" } + .ToDictionary(p => $"api://{clientId}/{p}") + } + } + }); + options.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Entra" + }, + UnresolvedReference = true + }, + Array.Empty() + } + }); + } +} \ No newline at end of file diff --git a/src/Api/Options/SwaggerConfigurationOptions.cs b/src/Api/Options/SwaggerConfigurationOptions.cs index 90ea949..ac916cb 100644 --- a/src/Api/Options/SwaggerConfigurationOptions.cs +++ b/src/Api/Options/SwaggerConfigurationOptions.cs @@ -7,9 +7,6 @@ namespace Api.Options; public class SwaggerConfigurationOptions : IConfigureOptions { - public SwaggerConfigurationOptions() - { - } public void Configure(SwaggerGenOptions options) { @@ -36,31 +33,5 @@ public void Configure(SwaggerGenOptions options) Array.Empty() } }); - - //options.AddSecurityDefinition("aad-jwt", new OpenApiSecurityScheme - //{ - // Type = SecuritySchemeType.OAuth2, - // Flows = new OpenApiOAuthFlows - // { - // AuthorizationCode = new OpenApiOAuthFlow - // { - // } - // } - //}); - //options.AddSecurityRequirement(new OpenApiSecurityRequirement - //{ - // { - // new OpenApiSecurityScheme - // { - // Reference = new OpenApiReference - // { - // Type = ReferenceType.SecurityScheme, - // Id = "aad-jwt" - // }, - // UnresolvedReference = true - // }, - // Array.Empty() - // } - //}); } } \ No newline at end of file diff --git a/src/Api/Program.cs b/src/Api/Program.cs index 246c8a0..38850df 100644 --- a/src/Api/Program.cs +++ b/src/Api/Program.cs @@ -39,14 +39,19 @@ .AllowAnyHeader() .AllowAnyMethod())); +builder.Services.AddAndConfigureProblemDetails(); -builder.Services.AddTransient, SwaggerConfigurationOptions>(); -builder.Services.AddAndConfigureProblemDetails(); -builder.Services.AddAuthorization(); -builder.Services.AddAuthentication().AddJwtBearer(); +#if UseEntra +builder.Services.AddMicrosoftEntraAuth(builder.Configuration); +#else +builder.Services.AddTransient, SwaggerConfigurationOptions>(); +builder.Services.AddAuthentication() + .AddJwtBearer(); +#endif +builder.Services.AddAuthorization(); builder.Services.AddApiServices(); builder.Services.AddCommonServices(); @@ -66,6 +71,8 @@ db.Database.Migrate(); } +var microsoftIdentityOptions = app.Services.GetService>()?.Value; + // Enable middleware to serve generated Swagger as a JSON endpoint. app.UseSwagger(); @@ -74,6 +81,10 @@ { options.SwaggerEndpoint("/swagger/v1/swagger.json", "v1"); options.RoutePrefix = string.Empty; +#if UseEntra + options.OAuthClientId(microsoftIdentityOptions?.ClientId); + options.OAuthUsePkce(); +#endif }); app.UseCors(); diff --git a/src/Api/appsettings.MsSql.json b/src/Api/appsettings.MsSql.json index 0928f5b..b73ea92 100644 --- a/src/Api/appsettings.MsSql.json +++ b/src/Api/appsettings.MsSql.json @@ -7,18 +7,5 @@ "Default": "Information", "Microsoft.AspNetCore": "Warning" } - }, - "Authentication": { - "Schemes": { - "Bearer": { - "ValidAudiences": [ - "http://localhost:44986", - "https://localhost:44337", - "http://localhost:5191", - "https://localhost:7079" - ], - "ValidIssuer": "dotnet-user-jwts" - } - } } } diff --git a/src/Api/appsettings.PostgreSql.json b/src/Api/appsettings.PostgreSql.json index 8729e10..23aec1e 100644 --- a/src/Api/appsettings.PostgreSql.json +++ b/src/Api/appsettings.PostgreSql.json @@ -7,18 +7,5 @@ "Default": "Information", "Microsoft.AspNetCore": "Warning" } - }, - "Authentication": { - "Schemes": { - "Bearer": { - "ValidAudiences": [ - "http://localhost:44986", - "https://localhost:44337", - "http://localhost:5191", - "https://localhost:7079" - ], - "ValidIssuer": "dotnet-user-jwts" - } - } } } diff --git a/src/Api/appsettings.json b/src/Api/appsettings.json index 5bead84..e7fcba7 100644 --- a/src/Api/appsettings.json +++ b/src/Api/appsettings.json @@ -10,5 +10,12 @@ } } }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "Entra": { + "Instance": "https://login.microsoftonline.com", + "ClientId": "", + "ClientSecret": "", + "Domain": "", + "TenantId": "" + } } diff --git a/src/Application/Authorization/Scopes.cs b/src/Application/Authorization/Scopes.cs new file mode 100644 index 0000000..f05acb0 --- /dev/null +++ b/src/Application/Authorization/Scopes.cs @@ -0,0 +1,6 @@ +namespace Application.Authorization; + +public static class AuthorizationScopes +{ + public const string AccessAsUser = "access_as_user"; +} \ No newline at end of file