diff --git a/.gitignore b/.gitignore index ae5681e..b0b6a46 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,5 @@ /ClientCredentialsFlows/LogsServiceApi.txt /AngularAzureADMultipleApis/_logs-ApiWithMutlipleApis.txt /AngularAzureADMultipleApis/_logs-ServiceApi.txt +/ConfidentialClientCredentialsCertificate/_logs-MyServerRenderedPortal.txt +/ConfidentialClientCredentialsCertificate/_logs-ServiceApi.txt diff --git a/ConfidentialClientCredentialsCertificate/MyServerRenderedPortal/HostingExtensions.cs b/ConfidentialClientCredentialsCertificate/MyServerRenderedPortal/HostingExtensions.cs new file mode 100644 index 0000000..d26776a --- /dev/null +++ b/ConfidentialClientCredentialsCertificate/MyServerRenderedPortal/HostingExtensions.cs @@ -0,0 +1,62 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc.Authorization; +using Microsoft.Identity.Web; +using Microsoft.Identity.Web.UI; +using Microsoft.IdentityModel.Logging; +using Serilog; +using System.IdentityModel.Tokens.Jwt; + +namespace MyServerRenderedPortal; + +internal static class HostingExtensions +{ + public static WebApplication ConfigureServices(this WebApplicationBuilder builder) + { + var services = builder.Services; + var configuration = builder.Configuration; + + services.AddTransient(); + services.AddTransient(); + services.AddHttpClient(); + services.AddOptions(); + + services.AddMicrosoftIdentityWebAppAuthentication(configuration); + + services.AddRazorPages().AddMvcOptions(options => + { + var policy = new AuthorizationPolicyBuilder() + .RequireAuthenticatedUser() + .Build(); + options.Filters.Add(new AuthorizeFilter(policy)); + }).AddMicrosoftIdentityUI(); + return builder.Build(); + } + + public static WebApplication ConfigurePipeline(this WebApplication app) + { + IdentityModelEventSource.ShowPII = true; + JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); + + app.UseSerilogRequestLogging(); + + if (app.Environment.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseSerilogRequestLogging(); + + app.UseHttpsRedirection(); + app.UseStaticFiles(); + + app.UseRouting(); + + app.UseAuthentication(); + app.UseAuthorization(); + + app.MapRazorPages(); + app.MapControllers(); + + return app; + } +} diff --git a/ConfidentialClientCredentialsCertificate/MyServerRenderedPortal/MyServerRenderedPortal.csproj b/ConfidentialClientCredentialsCertificate/MyServerRenderedPortal/MyServerRenderedPortal.csproj index dc924c5..26a62b4 100644 --- a/ConfidentialClientCredentialsCertificate/MyServerRenderedPortal/MyServerRenderedPortal.csproj +++ b/ConfidentialClientCredentialsCertificate/MyServerRenderedPortal/MyServerRenderedPortal.csproj @@ -9,15 +9,21 @@ - - - - - - - - - + + + + + + + + + + + + + + + diff --git a/ConfidentialClientCredentialsCertificate/MyServerRenderedPortal/Program.cs b/ConfidentialClientCredentialsCertificate/MyServerRenderedPortal/Program.cs index dd1180d..2bc41b8 100644 --- a/ConfidentialClientCredentialsCertificate/MyServerRenderedPortal/Program.cs +++ b/ConfidentialClientCredentialsCertificate/MyServerRenderedPortal/Program.cs @@ -1,48 +1,40 @@ +using MyServerRenderedPortal; + +using ServiceApi; +using Azure.Identity; using Serilog; -using Serilog.Events; -using Serilog.Sinks.SystemConsole.Themes; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using System; -namespace MyServerRenderedPortal; +Log.Logger = new LoggerConfiguration() + .WriteTo.Console() + .WriteTo.AzureApp() + .CreateBootstrapLogger(); -public class Program +try { - public static int Main(string[] args) - { - Log.Logger = new LoggerConfiguration() - .MinimumLevel.Debug() - .MinimumLevel.Override("Microsoft", LogEventLevel.Information) - .Enrich.FromLogContext() - .WriteTo.Console() - .CreateLogger(); + Log.Information("Starting WebApi"); + + var builder = WebApplication.CreateBuilder(args); + + builder.Host.UseSerilog((context, loggerConfiguration) => loggerConfiguration + .ReadFrom.Configuration(context.Configuration)); - try - { - Log.Information("Starting web host"); - CreateHostBuilder(args).Build().Run(); - return 0; - } - catch (Exception ex) - { - Log.Fatal(ex, "Host terminated unexpectedly"); - return 1; - } - finally - { - Log.CloseAndFlush(); - } - } + var app = builder + .ConfigureServices() + .ConfigurePipeline(); - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .UseSerilog((hostingContext, loggerConfiguration) => loggerConfiguration - .ReadFrom.Configuration(hostingContext.Configuration) - .Enrich.FromLogContext() - .MinimumLevel.Verbose() - .WriteTo.File("../LogsMyServerRenderedPortal.txt") - .WriteTo.Console(theme: AnsiConsoleTheme.Code) - ) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); -} \ No newline at end of file + app.Run(); +} +catch (Exception ex) when (ex.GetType().Name is not "StopTheHostException" + && ex.GetType().Name is not "HostAbortedException") +{ + Log.Fatal(ex, "Unhandled exception"); +} +finally +{ + Log.Information("Shut down complete"); + Log.CloseAndFlush(); +} diff --git a/ConfidentialClientCredentialsCertificate/MyServerRenderedPortal/Properties/launchSettings.json b/ConfidentialClientCredentialsCertificate/MyServerRenderedPortal/Properties/launchSettings.json index 1a536a8..20bc000 100644 --- a/ConfidentialClientCredentialsCertificate/MyServerRenderedPortal/Properties/launchSettings.json +++ b/ConfidentialClientCredentialsCertificate/MyServerRenderedPortal/Properties/launchSettings.json @@ -1,29 +1,12 @@ { - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:60192", - "sslPort": 44377 - } - }, "profiles": { - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "environmentVariables": { - "AZURE_CLIENT_ID": "64ecb044-417b-4892-83d4-5c03e8c977b9", - "ASPNETCORE_ENVIRONMENT": "Development", - "AZURE_TENANT_ID": "7ff95b15-dc21-4ba6-bc92-824856578fc1" - } - }, "MyServerRenderedPortal": { "commandName": "Project", "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, - "applicationUrl": "https://localhost:5001" + "applicationUrl": "https://localhost:44377" } } } \ No newline at end of file diff --git a/ConfidentialClientCredentialsCertificate/MyServerRenderedPortal/Startup.cs b/ConfidentialClientCredentialsCertificate/MyServerRenderedPortal/Startup.cs deleted file mode 100644 index cdb51b2..0000000 --- a/ConfidentialClientCredentialsCertificate/MyServerRenderedPortal/Startup.cs +++ /dev/null @@ -1,74 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc.Authorization; -using Microsoft.Identity.Web; -using Microsoft.Identity.Web.UI; -using Microsoft.IdentityModel.Logging; -using Serilog; - -namespace MyServerRenderedPortal; - -public class Startup -{ - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } - - public IConfiguration Configuration { get; } - - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - services.AddTransient(); - services.AddTransient(); - - services.AddHttpClient(); - - services.AddOptions(); - - services.AddMicrosoftIdentityWebAppAuthentication(Configuration); - - services.AddRazorPages().AddMvcOptions(options => - { - var policy = new AuthorizationPolicyBuilder() - .RequireAuthenticatedUser() - .Build(); - options.Filters.Add(new AuthorizeFilter(policy)); - }).AddMicrosoftIdentityUI(); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - IdentityModelEventSource.ShowPII = true; - - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - else - { - app.UseExceptionHandler("/Error"); - // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. - app.UseHsts(); - } - - // https://nblumhardt.com/2019/10/serilog-in-aspnetcore-3/ - // https://nblumhardt.com/2019/10/serilog-mvc-logging/ - app.UseSerilogRequestLogging(); - - app.UseHttpsRedirection(); - app.UseStaticFiles(); - - app.UseRouting(); - - app.UseAuthentication(); - app.UseAuthorization(); - - app.UseEndpoints(endpoints => - { - endpoints.MapRazorPages(); - endpoints.MapControllers(); - }); - } -} \ No newline at end of file diff --git a/ConfidentialClientCredentialsCertificate/MyServerRenderedPortal/appsettings.json b/ConfidentialClientCredentialsCertificate/MyServerRenderedPortal/appsettings.json index 057a2b5..8940d35 100644 --- a/ConfidentialClientCredentialsCertificate/MyServerRenderedPortal/appsettings.json +++ b/ConfidentialClientCredentialsCertificate/MyServerRenderedPortal/appsettings.json @@ -22,12 +22,36 @@ } ] }, - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } + "Serilog": { + "Using": [ "Serilog.Sinks.Console" ], + "MinimumLevel": { + "Default": "Debug", + "Override": { + "Microsoft": "Debug", + "Microsoft.EntityFrameworkCore": "Warning", + "System": "Debug" + } + }, + "Enrich": [ "FromLogContext", "WithMachineName", "WithThreadId" ], + "WriteTo": [ + { + "Name": "Console", + "Args": { + "theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console", + "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} ({SourceContext}){NewLine}{Exception}" + } + }, + { + "Name": "File", + "Args": { + "path": "../_logs-MyServerRenderedPortal.txt", + "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level}] [{SourceContext}] [{EventId}] {Message}{NewLine}{Exception}", + "rollOnFileSizeLimit": true, + "fileSizeLimitBytes": 4194304, + "retainedFileCountLimit": 5 + } + } + ] }, "AllowedHosts": "*" } diff --git a/ConfidentialClientCredentialsCertificate/ServiceApi/Startup.cs b/ConfidentialClientCredentialsCertificate/ServiceApi/HostingExtensions.cs similarity index 65% rename from ConfidentialClientCredentialsCertificate/ServiceApi/Startup.cs rename to ConfidentialClientCredentialsCertificate/ServiceApi/HostingExtensions.cs index a4c09b5..e22cc65 100644 --- a/ConfidentialClientCredentialsCertificate/ServiceApi/Startup.cs +++ b/ConfidentialClientCredentialsCertificate/ServiceApi/HostingExtensions.cs @@ -1,72 +1,70 @@ -using Microsoft.Identity.Web; -using System.IdentityModel.Tokens.Jwt; -using Microsoft.AspNetCore.Authorization; -using Microsoft.IdentityModel.Logging; -using Serilog; - -namespace ServiceApi; - -public class Startup -{ - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } - - public IConfiguration Configuration { get; } - - public void ConfigureServices(IServiceCollection services) - { - JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); - - services.AddSingleton(); - - services.AddMicrosoftIdentityWebApiAuthentication(Configuration); - - services.AddAuthorization(options => - { - options.AddPolicy("ValidateAccessTokenPolicy", validateAccessTokenPolicy => - { - validateAccessTokenPolicy.Requirements.Add(new HasServiceApiRoleRequirement()); - - // Validate ClientId from token - validateAccessTokenPolicy.RequireClaim("azp", Configuration["AzureAd:ClientId"]); - - // only allow tokens which used "Private key JWT Client authentication" - // // https://docs.microsoft.com/en-us/azure/active-directory/develop/access-tokens - // Indicates how the client was authenticated. For a public client, the value is "0". - // If client ID and client secret are used, the value is "1". - // If a client certificate was used for authentication, the value is "2". - validateAccessTokenPolicy.RequireClaim("azpacr", "2"); - }); - }); - - services.AddControllers(); - } - - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - IdentityModelEventSource.ShowPII = true; - - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - // https://nblumhardt.com/2019/10/serilog-in-aspnetcore-3/ - // https://nblumhardt.com/2019/10/serilog-mvc-logging/ - app.UseSerilogRequestLogging(); - - app.UseHttpsRedirection(); - - app.UseRouting(); - - app.UseAuthentication(); - app.UseAuthorization(); - - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); - } -} \ No newline at end of file +using Microsoft.AspNetCore.Authorization; +using Microsoft.Identity.Web; +using Microsoft.IdentityModel.Logging; +using Serilog; +using System.IdentityModel.Tokens.Jwt; + +namespace ServiceApi; + +internal static class HostingExtensions +{ + public static WebApplication ConfigureServices(this WebApplicationBuilder builder) + { + var services = builder.Services; + var configuration = builder.Configuration; + + services.AddSingleton(); + + services.AddMicrosoftIdentityWebApiAuthentication(builder.Configuration); + + services.AddControllers(); + + services.AddSingleton(); + + services.AddAuthorization(options => + { + options.AddPolicy("ValidateAccessTokenPolicy", validateAccessTokenPolicy => + { + validateAccessTokenPolicy.Requirements.Add(new HasServiceApiRoleRequirement()); + + // Validate ClientId from token + validateAccessTokenPolicy.RequireClaim("azp", builder.Configuration["AzureAd:ClientId"]!); + + // only allow tokens which used "Private key JWT Client authentication" + // // https://docs.microsoft.com/en-us/azure/active-directory/develop/access-tokens + // Indicates how the client was authenticated. For a public client, the value is "0". + // If client ID and client secret are used, the value is "1". + // If a client certificate was used for authentication, the value is "2". + validateAccessTokenPolicy.RequireClaim("azpacr", "2"); + }); + }); + + return builder.Build(); + } + + public static WebApplication ConfigurePipeline(this WebApplication app) + { + IdentityModelEventSource.ShowPII = true; + JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); + + app.UseSerilogRequestLogging(); + + if (app.Environment.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseSerilogRequestLogging(); + + app.UseHttpsRedirection(); + + app.UseRouting(); + + app.UseAuthentication(); + app.UseAuthorization(); + + app.MapControllers(); + + return app; + } +} diff --git a/ConfidentialClientCredentialsCertificate/ServiceApi/Program.cs b/ConfidentialClientCredentialsCertificate/ServiceApi/Program.cs index 1f6fa1e..6531f82 100644 --- a/ConfidentialClientCredentialsCertificate/ServiceApi/Program.cs +++ b/ConfidentialClientCredentialsCertificate/ServiceApi/Program.cs @@ -1,48 +1,57 @@ +using ServiceApi; +using Azure.Identity; using Serilog; -using Serilog.Events; -using Serilog.Sinks.SystemConsole.Themes; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using System; -namespace ServiceApi; +Log.Logger = new LoggerConfiguration() + .WriteTo.Console() + .WriteTo.AzureApp() + .CreateBootstrapLogger(); -public class Program +try { - public static int Main(string[] args) - { - Log.Logger = new LoggerConfiguration() - .MinimumLevel.Debug() - .MinimumLevel.Override("Microsoft", LogEventLevel.Information) - .Enrich.FromLogContext() - .WriteTo.Console() - .CreateLogger(); + Log.Information("Starting WebApi"); - try - { - Log.Information("Starting web host"); - CreateHostBuilder(args).Build().Run(); - return 0; - } - catch (Exception ex) - { - Log.Fatal(ex, "Host terminated unexpectedly"); - return 1; - } - finally - { - Log.CloseAndFlush(); - } - } + var builder = WebApplication.CreateBuilder(args); - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .UseSerilog((hostingContext, loggerConfiguration) => loggerConfiguration - .ReadFrom.Configuration(hostingContext.Configuration) - .Enrich.FromLogContext() - .MinimumLevel.Verbose() - .WriteTo.File("../LogsServiceApi.txt") - .WriteTo.Console(theme: AnsiConsoleTheme.Code) - ) - .ConfigureWebHostDefaults(webBuilder => + builder.WebHost + .ConfigureKestrel(serverOptions => { serverOptions.AddServerHeader = false; }) + .ConfigureAppConfiguration((context, configurationBuilder) => + { + var config = configurationBuilder.Build(); + var azureKeyVaultEndpoint = config["AzureKeyVaultEndpoint"]; + if (!string.IsNullOrEmpty(azureKeyVaultEndpoint)) + { + // Add Secrets from KeyVault + Log.Information("Use secrets from {AzureKeyVaultEndpoint}", azureKeyVaultEndpoint); + configurationBuilder.AddAzureKeyVault(new Uri(azureKeyVaultEndpoint), new DefaultAzureCredential()); + } + else { - webBuilder.UseStartup(); - }); -} \ No newline at end of file + // Add Secrets from UserSecrets for local development + configurationBuilder.AddUserSecrets("9f17b08c-435a-4f50-ba7a-802e68ca8d80"); + } + }); + + builder.Host.UseSerilog((context, loggerConfiguration) => loggerConfiguration + .ReadFrom.Configuration(context.Configuration)); + + var app = builder + .ConfigureServices() + .ConfigurePipeline(); + + app.Run(); +} +catch (Exception ex) when (ex.GetType().Name is not "StopTheHostException" + && ex.GetType().Name is not "HostAbortedException") +{ + Log.Fatal(ex, "Unhandled exception"); +} +finally +{ + Log.Information("Shut down complete"); + Log.CloseAndFlush(); +} diff --git a/ConfidentialClientCredentialsCertificate/ServiceApi/Properties/launchSettings.json b/ConfidentialClientCredentialsCertificate/ServiceApi/Properties/launchSettings.json index 7610fa1..7ad22d0 100644 --- a/ConfidentialClientCredentialsCertificate/ServiceApi/Properties/launchSettings.json +++ b/ConfidentialClientCredentialsCertificate/ServiceApi/Properties/launchSettings.json @@ -1,22 +1,5 @@ { - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "https://localhost:44390", - "sslPort": 44390 - } - }, - "$schema": "http://json.schemastore.org/launchsettings.json", "profiles": { - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "launchUrl": "weatherforecast", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, "MyApi": { "commandName": "Project", "launchBrowser": true, @@ -24,7 +7,7 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, - "applicationUrl": "https://localhost:5001" + "applicationUrl": "https://localhost:44390" } } } \ No newline at end of file diff --git a/ConfidentialClientCredentialsCertificate/ServiceApi/ServiceApi.csproj b/ConfidentialClientCredentialsCertificate/ServiceApi/ServiceApi.csproj index f24ad22..170825e 100644 --- a/ConfidentialClientCredentialsCertificate/ServiceApi/ServiceApi.csproj +++ b/ConfidentialClientCredentialsCertificate/ServiceApi/ServiceApi.csproj @@ -8,12 +8,18 @@ - - - - - - + + + + + + + + + + + + diff --git a/ConfidentialClientCredentialsCertificate/ServiceApi/appsettings.Development.json b/ConfidentialClientCredentialsCertificate/ServiceApi/appsettings.Development.json deleted file mode 100644 index dba68eb..0000000 --- a/ConfidentialClientCredentialsCertificate/ServiceApi/appsettings.Development.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } - } -} diff --git a/ConfidentialClientCredentialsCertificate/ServiceApi/appsettings.json b/ConfidentialClientCredentialsCertificate/ServiceApi/appsettings.json index 0b0959a..97d1db8 100644 --- a/ConfidentialClientCredentialsCertificate/ServiceApi/appsettings.json +++ b/ConfidentialClientCredentialsCertificate/ServiceApi/appsettings.json @@ -12,12 +12,36 @@ } ] }, - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } + "Serilog": { + "Using": [ "Serilog.Sinks.Console" ], + "MinimumLevel": { + "Default": "Debug", + "Override": { + "Microsoft": "Debug", + "Microsoft.EntityFrameworkCore": "Warning", + "System": "Debug" + } + }, + "Enrich": [ "FromLogContext", "WithMachineName", "WithThreadId" ], + "WriteTo": [ + { + "Name": "Console", + "Args": { + "theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console", + "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} ({SourceContext}){NewLine}{Exception}" + } + }, + { + "Name": "File", + "Args": { + "path": "../_logs-ServiceApi.txt", + "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level}] [{SourceContext}] [{EventId}] {Message}{NewLine}{Exception}", + "rollOnFileSizeLimit": true, + "fileSizeLimitBytes": 4194304, + "retainedFileCountLimit": 5 + } + } + ] }, "AllowedHosts": "*" }