Replace custom socket implementation with SignalR.
Replace MSAL and custom cookie auth with Microsoft.Identity.EntityFramework Also some UI redesign to accommodate different login experience.
This commit is contained in:
@@ -1,232 +1,106 @@
|
||||
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 Microsoft.AspNetCore.Identity.UI.Services;
|
||||
using Microsoft.AspNetCore.ResponseCompression;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Shogi.Api;
|
||||
using Shogi.Api.Application;
|
||||
using Shogi.Api.Identity;
|
||||
using Shogi.Api.Repositories;
|
||||
using Shogi.Api.Services;
|
||||
|
||||
namespace Shogi.Api
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
var allowedOrigins = builder
|
||||
.Configuration
|
||||
.GetSection("Cors:AllowedOrigins")
|
||||
.Get<string[]>() ?? throw new InvalidOperationException("Configuration for allowed origins is missing.");
|
||||
|
||||
builder.Services
|
||||
.AddControllers()
|
||||
.AddJsonOptions(options =>
|
||||
{
|
||||
options.JsonSerializerOptions.WriteIndented = true;
|
||||
});
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen();
|
||||
builder.Services.AddTransient<SessionRepository>();
|
||||
builder.Services.AddTransient<QueryRepository>();
|
||||
builder.Services.AddTransient<ShogiApplication>();
|
||||
builder.Services.AddTransient<GameHubContext>();
|
||||
builder.Services.AddHttpClient<IEmailSender, EmailSender>();
|
||||
builder.Services.Configure<ApiKeys>(builder.Configuration.GetSection("ApiKeys"));
|
||||
|
||||
AddIdentity(builder, builder.Configuration);
|
||||
builder.Services.AddSignalR();
|
||||
builder.Services.AddResponseCompression(opts =>
|
||||
{
|
||||
public class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(["application/octet-stream"]);
|
||||
});
|
||||
var app = builder.Build();
|
||||
|
||||
var allowedOrigins = builder.Configuration.GetSection("Cors:AllowedOrigins").Get<string[]>() ?? 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);
|
||||
app.MapIdentityApi<ShogiUser>();
|
||||
|
||||
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<ISocketService>();
|
||||
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);
|
||||
}
|
||||
else
|
||||
{
|
||||
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<JsonOptions>(options =>
|
||||
{
|
||||
options.SerializerOptions.WriteIndented = true;
|
||||
});
|
||||
}
|
||||
|
||||
private static void ConfigureDependencyInjection(WebApplicationBuilder builder)
|
||||
{
|
||||
var services = builder.Services;
|
||||
services.AddSingleton<ISocketConnectionManager, SocketConnectionManager>();
|
||||
services.AddSingleton<ISocketTokenCache, SocketTokenCache>();
|
||||
services.AddSingleton<ISocketService, SocketService>();
|
||||
services.AddTransient<IClaimsTransformation, ShogiUserClaimsTransformer>();
|
||||
services.AddTransient<IShogiUserClaimsTransformer, ShogiUserClaimsTransformer>();
|
||||
services.AddTransient<IUserRepository, UserRepository>();
|
||||
services.AddTransient<ISessionRepository, SessionRepository>();
|
||||
services.AddTransient<IQueryRespository, QueryRepository>();
|
||||
services.AddTransient<IModelMapper, ModelMapper>();
|
||||
}
|
||||
|
||||
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<string, string>
|
||||
{
|
||||
{ "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<string>()
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseHttpsRedirection(); // Apache handles HTTPS in production.
|
||||
}
|
||||
else
|
||||
{
|
||||
app.UseResponseCompression();
|
||||
}
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI(options => options.DocumentTitle = "Shogi.Api");
|
||||
app.UseAuthorization();
|
||||
app.Map("/", () => "OK");
|
||||
app.MapControllers();
|
||||
app.UseCors(policy =>
|
||||
{
|
||||
policy.WithOrigins(allowedOrigins).AllowAnyHeader().AllowAnyMethod().AllowCredentials();
|
||||
});
|
||||
|
||||
app.MapHub<GameHub>("/gamehub").RequireAuthorization();
|
||||
|
||||
app.Run();
|
||||
|
||||
static void AddIdentity(WebApplicationBuilder builder, ConfigurationManager configuration)
|
||||
{
|
||||
builder.Services
|
||||
.AddAuthorizationBuilder()
|
||||
.AddPolicy("Admin", policy =>
|
||||
{
|
||||
policy.RequireAuthenticatedUser();
|
||||
policy.RequireAssertion(context => context.User?.Identity?.Name switch
|
||||
{
|
||||
"Hauth@live.com" => true,
|
||||
"aat-account" => true,
|
||||
_ => false
|
||||
});
|
||||
});
|
||||
|
||||
builder.Services
|
||||
.AddDbContext<ApplicationDbContext>(options =>
|
||||
{
|
||||
var cs = configuration.GetConnectionString("ShogiDatabase") ?? throw new InvalidOperationException("Database not configured.");
|
||||
options.UseSqlServer(cs);
|
||||
|
||||
// This is helpful to debug account issues without affecting the database.
|
||||
//options.UseInMemoryDatabase("AppDb");
|
||||
})
|
||||
.AddIdentityApiEndpoints<ShogiUser>(options =>
|
||||
{
|
||||
options.SignIn.RequireConfirmedEmail = true;
|
||||
options.User.RequireUniqueEmail = true;
|
||||
})
|
||||
.AddEntityFrameworkStores<ApplicationDbContext>();
|
||||
|
||||
// I shouldn't this because I have it above, right?
|
||||
//builder.Services.Configure<IdentityOptions>(options =>
|
||||
//{
|
||||
// options.SignIn.RequireConfirmedEmail = true;
|
||||
// options.User.RequireUniqueEmail = true;
|
||||
//});
|
||||
|
||||
builder.Services.ConfigureApplicationCookie(options =>
|
||||
{
|
||||
options.SlidingExpiration = true;
|
||||
options.ExpireTimeSpan = TimeSpan.FromDays(3);
|
||||
});
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user