using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Http.Json; using Microsoft.AspNetCore.HttpLogging; using Microsoft.Identity.Web; using Microsoft.OpenApi.Models; using Shogi.Api.Managers; using Shogi.Api.Repositories; using Shogi.Api.Services; namespace Shogi.Api { public class Program { public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); var allowedOrigins = builder.Configuration.GetSection("Cors:AllowedOrigins").Get() ?? throw new InvalidOperationException("Configuration for allowed origins is missing."); builder.Services.AddCors(options => { options.AddDefaultPolicy(policy => { policy .WithOrigins(allowedOrigins) .SetIsOriginAllowedToAllowWildcardSubdomains() .WithExposedHeaders("Set-Cookie") .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials(); }); }); ConfigureAuthentication(builder); ConfigureControllers(builder); ConfigureSwagger(builder); ConfigureDependencyInjection(builder); ConfigureLogging(builder); var app = builder.Build(); app.UseWhen( // Log anything that isn't related to swagger. context => IsNotSwaggerUI(context), appBuilder => appBuilder.UseHttpLogging()); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseHttpsRedirection(); // Apache handles HTTPS in production. } app.UseSwagger(); app.UseSwaggerUI(options => { options.OAuthScopes("api://c1e94676-cab0-42ba-8b6c-9532b8486fff/DefaultScope"); options.OAuthConfigObject.ClientId = builder.Configuration["AzureAd:SwaggerUIClientId"]; options.OAuthConfigObject.UsePkceWithAuthorizationCodeGrant = true; }); UseCorsAndWebSockets(app, allowedOrigins); app.UseAuthentication(); app.UseAuthorization(); app.Map("/", () => "OK"); app.MapControllers(); app.Run(); static bool IsNotSwaggerUI(HttpContext context) { var path = context.Request.GetEncodedPathAndQuery(); return !path.Contains("swagger") && !path.Equals("/", StringComparison.Ordinal); } } private static void UseCorsAndWebSockets(WebApplication app, string[] allowedOrigins) { // TODO: Figure out how to make a middleware for sockets? var socketService = app.Services.GetRequiredService(); var socketOptions = new WebSocketOptions(); foreach (var origin in allowedOrigins) socketOptions.AllowedOrigins.Add(origin); app.UseCors(); app.UseWebSockets(socketOptions); app.Use(async (context, next) => { if (context.WebSockets.IsWebSocketRequest) { await socketService.HandleSocketRequest(context); } await next(); }); } private static void ConfigureLogging(WebApplicationBuilder builder) { builder.Services.AddHttpLogging(options => { options.LoggingFields = HttpLoggingFields.RequestProperties | HttpLoggingFields.RequestBody | HttpLoggingFields.ResponseStatusCode | HttpLoggingFields.ResponseBody; }); } private static void ConfigureAuthentication(WebApplicationBuilder builder) { AddJwtAuth(builder); AddCookieAuth(builder); SetupAuthSwitch(builder); static void AddJwtAuth(WebApplicationBuilder builder) { builder.Services .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd")); } static void AddCookieAuth(WebApplicationBuilder builder) { builder.Services .AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(options => { options.Cookie.Name = "session-id"; options.Cookie.SameSite = SameSiteMode.None; options.Cookie.SecurePolicy = CookieSecurePolicy.Always; options.SlidingExpiration = true; options.LoginPath = new PathString("/User/LoginAsGuest"); }); } static void SetupAuthSwitch(WebApplicationBuilder builder) { var defaultScheme = "CookieOrJwt"; builder.Services .AddAuthentication(defaultScheme) .AddPolicyScheme("CookieOrJwt", "Either cookie or jwt", options => { options.ForwardDefaultSelector = context => { var bearerAuth = context.Request.Headers["Authorization"].FirstOrDefault()?.StartsWith("Bearer ") ?? false; return bearerAuth ? JwtBearerDefaults.AuthenticationScheme : CookieAuthenticationDefaults.AuthenticationScheme; }; }); builder .Services .AddAuthentication(options => { options.DefaultAuthenticateScheme = defaultScheme; }); } } private static void ConfigureControllers(WebApplicationBuilder builder) { builder.Services.AddControllers(); builder.Services.Configure(options => { options.SerializerOptions.WriteIndented = true; }); } private static void ConfigureDependencyInjection(WebApplicationBuilder builder) { var services = builder.Services; services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); } private static void ConfigureSwagger(WebApplicationBuilder builder) { // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(options => { var bearerKey = "Bearer"; options.AddSecurityDefinition(bearerKey, new OpenApiSecurityScheme { Type = SecuritySchemeType.OAuth2, Flows = new OpenApiOAuthFlows { Implicit = new OpenApiOAuthFlow { // These urls might be why only my email can login. // TODO: Try testing with tenantId in the url instead of "common". AuthorizationUrl = new Uri("https://login.microsoftonline.com/common/oauth2/v2.0/authorize"), TokenUrl = new Uri("https://login.microsoftonline.com/common/oauth2/v2.0/token"), Scopes = new Dictionary { { "api://c1e94676-cab0-42ba-8b6c-9532b8486fff/DefaultScope", "Default Scope" }, { "profile", "profile" }, { "openid", "openid" } } } }, Scheme = "Bearer", BearerFormat = "JWT", In = ParameterLocation.Header, }); // This adds the lock symbol next to every route in SwaggerUI. options.AddSecurityRequirement(new OpenApiSecurityRequirement { { new OpenApiSecurityScheme{ Reference = new OpenApiReference{ Type = ReferenceType.SecurityScheme, Id = bearerKey } }, Array.Empty() } }); }); } } }