Skip to content

Instantly share code, notes, and snippets.

@jsauve
Last active December 1, 2020 21:09
Show Gist options
  • Save jsauve/ba322d330dc68f4c8bffb20e168a92f9 to your computer and use it in GitHub Desktop.
Save jsauve/ba322d330dc68f4c8bffb20e168a92f9 to your computer and use it in GitHub Desktop.
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