-
-
Save jsauve/ba322d330dc68f4c8bffb20e168a92f9 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.IO; | |
using System.Reflection; | |
using System.Security.Claims; | |
using System.Threading.Tasks; | |
using Microsoft.AspNetCore.Authentication.Cookies; | |
using Microsoft.AspNetCore.Authentication.OpenIdConnect; | |
using Microsoft.AspNetCore.Authorization; | |
using Microsoft.AspNetCore.Builder; | |
using Microsoft.AspNetCore.Diagnostics; | |
using Microsoft.AspNetCore.Hosting; | |
using Microsoft.AspNetCore.Http; | |
using Microsoft.AspNetCore.Mvc.RazorPages; | |
using Microsoft.EntityFrameworkCore; | |
using Microsoft.Extensions.Configuration; | |
using Microsoft.Extensions.DependencyInjection; | |
using Microsoft.Extensions.DependencyInjection.Extensions; | |
using Microsoft.Extensions.Hosting; | |
using Microsoft.Extensions.Logging; | |
using Microsoft.Extensions.Options; | |
using Microsoft.IdentityModel.Protocols.OpenIdConnect; | |
using Microsoft.IdentityModel.Tokens; | |
using Microsoft.Net.Http.Headers; | |
using Newtonsoft.Json; | |
using TwinPortsPulse.Core; | |
using TwinPortsPulse.Core.Config.Api; | |
using TwinPortsPulse.Data; | |
using TwinPortsPulse.Services; | |
using TwinPortsPulse.SharedValues; | |
using TwinPortsPulse.WebApi.Web.Data; | |
using ILogger = Microsoft.Extensions.Logging.ILogger; | |
namespace TwinPortsPulse.WebApi | |
{ | |
public class Startup | |
{ | |
readonly ILogger _Logger; | |
public IConfiguration Configuration { get; } | |
public Startup(IConfiguration configuration) | |
{ | |
Configuration = configuration; | |
_Logger = GetEarlyInitializationLogger(); | |
} | |
public void ConfigureServices(IServiceCollection services) | |
{ | |
services.AddOptions(); | |
services.AddRazorPages(); | |
services.AddServerSideBlazor(); | |
services | |
.Configure<ApiConfig>(Configuration) | |
.Configure<RazorPagesOptions>(options => options.RootDirectory = "/Web/Pages") | |
.Configure<CookiePolicyOptions>(options => | |
{ | |
// This lambda determines whether user consent for non-essential cookies is needed for a given request. | |
options.CheckConsentNeeded = context => true; | |
options.MinimumSameSitePolicy = Microsoft.AspNetCore.Http.SameSiteMode.None; | |
}); | |
services | |
.AddSignalR() | |
.AddAzureSignalR(); | |
services.AddSingleton<TppApiKeyHub>(); | |
services.AddSingleton<SearchApiKeyHub>(); | |
// Replace System.Text.Json with Newtonsoft Json.NET, because System.Text.Json does not yet support preventing serializing circular references. | |
services.AddControllers().AddNewtonsoftJson(options => | |
{ | |
options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; | |
}); | |
// Register the DB context with DI | |
services.AddDbContext<TwinPortsPulseDbContext>(options => | |
{ | |
string databaseConnectionString = Configuration.GetConnectionString("DBConnectionString"); | |
if (string.IsNullOrWhiteSpace(databaseConnectionString)) | |
throw new Exception($"{nameof(Startup)}.{nameof(ConfigureServices)}(): connection string cannot be null or empty"); | |
options.UseSqlServer(Configuration.GetConnectionString("DBConnectionString")); | |
#if DEBUG | |
options.EnableDetailedErrors(); | |
options.EnableSensitiveDataLogging(); | |
#endif | |
}, ServiceLifetime.Transient); | |
services.AddSwaggerDocument(config => | |
{ | |
config.Title = "Twin Ports Pulse API"; | |
}); | |
// Register all ~Services from given assemblies | |
services.RegisterServices(ServiceLifetime.Transient, Assembly.GetAssembly(typeof(AggregateService))); | |
// Replaces the auto-registered IAuthManagementService transient with a singleton. | |
// Necessary because we want to hold onto the Management API's token for as long as possible in memory, not transiently. | |
// ...because the above RegisterServices() extension method that I wrote automatically transiently registers all ~Service-suffixed classes that implement interfaces. | |
var authManagementServiceDescriptor = new ServiceDescriptor( | |
typeof(IAuthManagementService), | |
_ => new AuthManagementService(_.GetRequiredService<IOptionsMonitor<ApiConfig>>().CurrentValue, _.GetRequiredService<ILogger<AuthManagementService>>()), | |
ServiceLifetime.Singleton); | |
services.Replace(authManagementServiceDescriptor); | |
string domain = $"{Configuration["Auth0:Domain"]}"; | |
string authority = $"https://{domain}/"; | |
string audience = Configuration["Auth0:Audience"]; | |
string clientId = Configuration["Auth0:ClientId"]; | |
string clientSecret = Configuration["Auth0:ClientSecret"]; | |
services.AddAuthentication(options => | |
{ | |
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme; | |
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; | |
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme; | |
}) | |
.AddJwtBearer(options => | |
{ | |
options.Authority = authority; | |
options.Audience = clientId; | |
options.TokenValidationParameters = new TokenValidationParameters { NameClaimType = ClaimTypes.NameIdentifier }; | |
}) | |
.AddCookie() | |
.AddOpenIdConnect("Auth0", options => | |
{ | |
// Set the authority to your Auth0 domain | |
options.Authority = authority; | |
// Configure the Auth0 Client ID and Client Secret | |
options.ClientId = clientId; | |
options.ClientSecret = clientSecret; | |
// Set response type to code | |
options.ResponseType = OpenIdConnectResponseType.Code; | |
// Configure the scope | |
options.Scope.Clear(); | |
options.Scope.Add("openid"); | |
// Set the callback path, so Auth0 will call back to http://localhost:3000/callback (in my case, https://localhost:5001/callback) | |
// Also ensure that you have added the URL as an Allowed Callback URL in your Auth0 dashboard | |
options.CallbackPath = new PathString("/callback"); | |
// Configure the Claims Issuer to be Auth0 | |
options.ClaimsIssuer = "Auth0"; | |
options.Events = new OpenIdConnectEvents | |
{ | |
// handle the logout redirection | |
OnRedirectToIdentityProviderForSignOut = (context) => | |
{ | |
var logoutUri = $"https://{domain}/v2/logout?client_id={clientId}"; | |
var postLogoutUri = context.Properties.RedirectUri; | |
if (!string.IsNullOrEmpty(postLogoutUri)) | |
{ | |
if (postLogoutUri.StartsWith("/")) | |
{ | |
// transform to absolute | |
var request = context.Request; | |
postLogoutUri = request.Scheme + "://" + request.Host + request.PathBase + postLogoutUri; | |
} | |
logoutUri += $"&returnTo={ Uri.EscapeDataString(postLogoutUri)}"; | |
} | |
context.Response.Redirect(logoutUri); | |
context.HandleResponse(); | |
return Task.CompletedTask; | |
} | |
}; | |
// Set the correct name claim type | |
options.TokenValidationParameters = new TokenValidationParameters | |
{ | |
NameClaimType = "name", | |
RoleClaimType = "https://schemas.twinportspulse.com/roles" | |
}; | |
}); | |
services.AddHttpContextAccessor(); | |
services.AddDatabaseDeveloperPageExceptionFilter(); | |
services.AddSingleton<WeatherForecastService>(); | |
services.AddAuthorization(options => | |
{ | |
foreach (var scope in GetScopes()) | |
options.AddPolicy(scope, policy => policy.Requirements.Add(new HasScopeRequirement(scope, domain))); | |
}); | |
// Add the scope handler | |
services.AddSingleton<IAuthorizationHandler, HasScopeHandler>(); | |
// add caching | |
services.AddResponseCaching(); | |
// add response compression | |
services.AddResponseCompression(); | |
} | |
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. | |
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) | |
{ | |
app.UseHttpsRedirection(); | |
app.UseStaticFiles(); | |
app.UseRouting(); | |
app.UseCookiePolicy(); | |
app.UseAuthentication(); | |
app.UseAuthorization(); | |
app.UseFileServer(); | |
app.UseResponseBuffering(); | |
app.UseResponseCompression(); | |
//app.UseMiddleware<RequestResponseLoggingMiddleware>(); | |
app.Use(async (ctx, next) => | |
{ | |
ctx.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() | |
{ | |
Public = true, | |
MaxAge = TimeSpan.FromSeconds(600) | |
}; | |
await next(); | |
}); | |
app.UseResponseCaching(); | |
app.UseCors(x => x | |
.AllowAnyOrigin() | |
//.WithOrigins( | |
// Configuration["CorsOrigins:Local"], | |
// Configuration["CorsOrigins:Production"], | |
// Configuration["CorsOrigins:Ngrok"]) | |
.WithMethods("GET", "POST", "DELETE", "PUT", "PATCH") | |
.AllowAnyHeader()); | |
if (env.IsDevelopment()) | |
{ | |
app.UseDeveloperExceptionPage(); | |
// add NSwag | |
app.UseOpenApi(); | |
app.UseSwaggerUi3(); | |
} | |
else | |
{ | |
app.UseExceptionHandler(errorApp => | |
{ | |
errorApp.Run(async context => | |
{ | |
var exceptionHandlerPathFeature = context.Features.Get<IExceptionHandlerPathFeature>(); | |
var ex = exceptionHandlerPathFeature?.Error; | |
_Logger.LogError($"{ex.Message} {ex.StackTrace}"); | |
if (ex is TPPException se) | |
{ | |
_Logger.LogError(se.DiagnosticMessage); | |
switch (se.ErrorType) | |
{ | |
case TPPErrorType.Unauthorized: | |
context.Response.StatusCode = 401; | |
break; | |
case TPPErrorType.NotFound: | |
context.Response.StatusCode = 404; | |
break; | |
case TPPErrorType.Conflict: | |
context.Response.StatusCode = 409; | |
break; | |
case TPPErrorType.Unrecoverable: | |
default: | |
context.Response.StatusCode = 500; | |
break; | |
} | |
await context.Response.WriteAsync(se.UserFriendlyMessage); | |
return; | |
} | |
context.Response.StatusCode = 500; | |
await context.Response.WriteAsync("An error has occurred"); | |
}); | |
}); | |
app.UseHsts(); | |
} | |
app.UseEndpoints(endpoints => | |
{ | |
endpoints.MapControllers(); | |
endpoints.MapBlazorHub(); | |
endpoints.MapHub<TppApiKeyHub>($"/{SignalRHubs.TppApiKeyHub}"); | |
endpoints.MapHub<SearchApiKeyHub>($"/{SignalRHubs.SearchApiKeyHub}"); | |
endpoints.MapFallbackToPage("/_Host"); | |
}); | |
} | |
private ILogger GetEarlyInitializationLogger() | |
{ | |
var instrumentationKey = Configuration.GetValue<string>("ApplicationInsights:InstrumentationKey"); | |
using var loggerFactory = LoggerFactory.Create(builder => | |
{ | |
builder.ClearProviders(); | |
builder.AddConsole(); | |
builder.AddDebug(); | |
builder.AddApplicationInsights(instrumentationKey); | |
}); | |
return loggerFactory.CreateLogger("Initialization"); | |
} | |
static List<string> GetScopes() | |
{ | |
var result = new List<string>(); | |
var scopesJson = string.Empty; | |
using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("TwinPortsPulse.WebApi.Resources.scopes.json")) | |
using (var reader = new StreamReader(stream)) | |
scopesJson = reader.ReadToEnd(); | |
if (!string.IsNullOrWhiteSpace(scopesJson)) | |
result.AddRange(JsonConvert.DeserializeObject<List<string>>(scopesJson)); | |
return result; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment