diff --git a/Gameboard.ShogiUI.Sockets/AnonymousSessionMiddleware.cs b/Gameboard.ShogiUI.Sockets/AnonymousSessionMiddleware.cs new file mode 100644 index 0000000..f39e649 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/AnonymousSessionMiddleware.cs @@ -0,0 +1,39 @@ +namespace Gameboard.ShogiUI.Sockets +{ + namespace anonymous_session.Middlewares + { + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Authentication; + using System.Security.Claims; + + /// + /// TODO: Use this example in the guest session logic instead of custom claims. + /// + public class AnonymousSessionMiddleware + { + private readonly RequestDelegate _next; + + public AnonymousSessionMiddleware(RequestDelegate next) + { + _next = next; + } + + public async System.Threading.Tasks.Task InvokeAsync(HttpContext context) + { + if (!context.User.Identity.IsAuthenticated) + { + if (string.IsNullOrEmpty(context.User.FindFirstValue(ClaimTypes.Anonymous))) + { + var claim = new Claim(ClaimTypes.Anonymous, System.Guid.NewGuid().ToString()); + context.User.AddIdentity(new ClaimsIdentity(new[] { claim })); + + string scheme = Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationDefaults.AuthenticationScheme; + await context.SignInAsync(scheme, context.User, new AuthenticationProperties { IsPersistent = false }); + } + } + + await _next(context); + } + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs b/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs index db5f8b9..0f76e3b 100644 --- a/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs +++ b/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs @@ -241,6 +241,21 @@ namespace Gameboard.ShogiUI.Sockets.Controllers return Ok(); } + [Authorize(Roles = "Admin, Shogi")] + [HttpDelete("{gameName}")] + public async Task DeleteSession([FromRoute] string gameName) + { + var user = await ReadUserOrThrow(); + if (user.IsAdmin) + { + return Ok(); + } + else + { + return Unauthorized(); + } + } + private async Task ReadUserOrThrow() { var user = await gameboardManager.ReadUser(User); diff --git a/Gameboard.ShogiUI.Sockets/Models/User.cs b/Gameboard.ShogiUI.Sockets/Models/User.cs index 7c1a2fa..e633524 100644 --- a/Gameboard.ShogiUI.Sockets/Models/User.cs +++ b/Gameboard.ShogiUI.Sockets/Models/User.cs @@ -37,6 +37,8 @@ namespace Gameboard.ShogiUI.Sockets.Models public bool IsGuest => LoginPlatform == WhichLoginPlatform.Guest; + public bool IsAdmin => LoginPlatform == WhichLoginPlatform.Microsoft && Id == "Hauth@live.com"; + public User(string id, string displayName, WhichLoginPlatform platform) { Id = id; diff --git a/Gameboard.ShogiUI.Sockets/Startup.cs b/Gameboard.ShogiUI.Sockets/Startup.cs index 5650206..c735d0e 100644 --- a/Gameboard.ShogiUI.Sockets/Startup.cs +++ b/Gameboard.ShogiUI.Sockets/Startup.cs @@ -30,169 +30,188 @@ using System.Threading.Tasks; namespace Gameboard.ShogiUI.Sockets { - public class Startup - { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } - public IConfiguration Configuration { get; } + 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.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton, JoinByCodeRequestValidator>(); - services.AddSingleton, JoinGameRequestValidator>(); - services.AddSingleton(); - services.AddTransient(); - services.AddSingleton(); - services.AddHttpClient("couchdb", c => - { - var base64 = Convert.ToBase64String(Encoding.UTF8.GetBytes("admin:admin")); - c.DefaultRequestHeaders.Add("Accept", "application/json"); - c.DefaultRequestHeaders.Add("Authorization", $"Basic {base64}"); + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton, JoinByCodeRequestValidator>(); + services.AddSingleton, JoinGameRequestValidator>(); + services.AddSingleton(); + services.AddTransient(); + services.AddSingleton(); + services.AddHttpClient("couchdb", c => + { + var base64 = Convert.ToBase64String(Encoding.UTF8.GetBytes("admin:admin")); + c.DefaultRequestHeaders.Add("Accept", "application/json"); + c.DefaultRequestHeaders.Add("Authorization", $"Basic {base64}"); - var baseUrl = $"{Configuration["AppSettings:CouchDB:Url"]}/{Configuration["AppSettings:CouchDB:Database"]}/"; - c.BaseAddress = new Uri(baseUrl); - }); + var baseUrl = $"{Configuration["AppSettings:CouchDB:Url"]}/{Configuration["AppSettings:CouchDB:Database"]}/"; + c.BaseAddress = new Uri(baseUrl); + }); - services - .AddControllers() - .AddNewtonsoftJson(options => - { - options.SerializerSettings.Formatting = Formatting.Indented; - options.SerializerSettings.ContractResolver = new DefaultContractResolver - { - NamingStrategy = new CamelCaseNamingStrategy { ProcessDictionaryKeys = true } - }; - options.SerializerSettings.Converters = new[] { new StringEnumConverter() }; - options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; - }); + services + .AddControllers() + .AddNewtonsoftJson(options => + { + options.SerializerSettings.Formatting = Formatting.Indented; + options.SerializerSettings.ContractResolver = new DefaultContractResolver + { + NamingStrategy = new CamelCaseNamingStrategy { ProcessDictionaryKeys = true } + }; + options.SerializerSettings.Converters = new[] { new StringEnumConverter() }; + options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; + }); - services.AddAuthentication("CookieOrJwt") - .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; - }; - }) - .AddCookie(options => - { - options.Cookie.Name = "session-id"; - options.Cookie.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.None; - options.Cookie.SecurePolicy = Microsoft.AspNetCore.Http.CookieSecurePolicy.Always; - options.SlidingExpiration = true; - }) - .AddMicrosoftIdentityWebApi(Configuration); + services.AddAuthentication("CookieOrJwt") + .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; + }; + }) + .AddCookie(options => + { + options.Cookie.Name = "session-id"; + options.Cookie.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.None; + options.Cookie.SecurePolicy = Microsoft.AspNetCore.Http.CookieSecurePolicy.Always; + options.SlidingExpiration = true; + }) + .AddMicrosoftIdentityWebApi(Configuration); - services.AddSwaggerDocument(config => - { - config.AddSecurity("Bearer", new NSwag.OpenApiSecurityScheme - { - Type = NSwag.OpenApiSecuritySchemeType.OAuth2, - Flow = NSwag.OpenApiOAuth2Flow.AccessCode, - AuthorizationUrl = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize", - TokenUrl = "https://login.microsoftonline.com/common/oauth2/v2.0/token", - Scopes = new Dictionary { { "api://c1e94676-cab0-42ba-8b6c-9532b8486fff/access_as_user", "The scope" } }, - Scheme = "Bearer" - }); - config.PostProcess = document => - { - document.Info.Title = "Gameboard.ShogiUI.Sockets"; - }; - }); + services.AddSwaggerDocument(config => + { + //config.AddSecurity("bearer", Enumerable.Empty(), new NSwag.OpenApiSecurityScheme + //{ + // Type = NSwag.OpenApiSecuritySchemeType.OAuth2, + // Flow = NSwag.OpenApiOAuth2Flow.Implicit, + // Flows = new NSwag.OpenApiOAuthFlows + // { + // Implicit = new NSwag.OpenApiOAuthFlow + // { + // Scopes = + // } + // } + //}); - // Remove default HttpClient logging. - services.RemoveAll(); - } + // This just ensures anyone with a microsoft account can make API calls. + config.AddSecurity("bearer", new NSwag.OpenApiSecurityScheme + { + Type = NSwag.OpenApiSecuritySchemeType.OAuth2, + Flow = NSwag.OpenApiOAuth2Flow.Implicit, + AuthorizationUrl = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize", + TokenUrl = "https://login.microsoftonline.com/common/oauth2/v2.0/token", + Scopes = new Dictionary { + { "api://c1e94676-cab0-42ba-8b6c-9532b8486fff/access_as_user", "The scope" }, + { "api://c1e94676-cab0-42ba-8b6c-9532b8486fff/ShogiAdmin", "Admin scope" } + }, + Scheme = "bearer", + BearerFormat = "JWT", + In = NSwag.OpenApiSecurityApiKeyLocation.Header, + }); + config.PostProcess = document => + { + document.Info.Title = "Gameboard.ShogiUI.Sockets"; + }; + }); - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ISocketService socketConnectionManager) - { - var origins = new[] { - "http://localhost:3000", "https://localhost:3000", - "http://127.0.0.1:3000", "https://127.0.0.1:3000", - "https://dev.lucaserver.space", "https://lucaserver.space" - }; - var socketOptions = new WebSocketOptions(); - foreach (var o in origins) - socketOptions.AllowedOrigins.Add(o); + // Remove default HttpClient logging. + services.RemoveAll(); + } - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - var client = PublicClientApplicationBuilder - .Create(Configuration["AzureAd:ClientId"]) - .WithLogging( - (level, message, pii) => - { + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ISocketService socketConnectionManager) + { + var origins = new[] { + "http://localhost:3000", "https://localhost:3000", + "http://127.0.0.1:3000", "https://127.0.0.1:3000", + "https://dev.lucaserver.space", "https://lucaserver.space" + }; + var socketOptions = new WebSocketOptions(); + foreach (var o in origins) + socketOptions.AllowedOrigins.Add(o); - }, - LogLevel.Verbose, - true, - true - ) - .Build(); - } - else - { - app.UseHsts(); - } - app - .UseRequestResponseLogging() - .UseCors(opt => opt.WithOrigins(origins).AllowAnyMethod().AllowAnyHeader().WithExposedHeaders("Set-Cookie").AllowCredentials()) - .UseRouting() - .UseAuthentication() - .UseAuthorization() - .UseOpenApi() - .UseSwaggerUi3(config => - { - config.OAuth2Client = new NSwag.AspNetCore.OAuth2ClientSettings() - { - ClientId = "c1e94676-cab0-42ba-8b6c-9532b8486fff", - UsePkceWithAuthorizationCodeGrant = true - }; - //config.WithCredentials = true; - }) - .UseWebSockets(socketOptions) - .UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }) - .Use(async (context, next) => - { - if (context.WebSockets.IsWebSocketRequest) - { - await socketConnectionManager.HandleSocketRequest(context); - } - else - { - await next(); - } - }); + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + var client = PublicClientApplicationBuilder + .Create(Configuration["AzureAd:ClientId"]) + .WithLogging( + (level, message, pii) => + { + Console.WriteLine(message); + }, + LogLevel.Verbose, + true, + true + ) + .Build(); + } + else + { + app.UseHsts(); + } + app + .UseRequestResponseLogging() + .UseCors(opt => opt.WithOrigins(origins).AllowAnyMethod().AllowAnyHeader().WithExposedHeaders("Set-Cookie").AllowCredentials()) + .UseRouting() + .UseAuthentication() + .UseAuthorization() + .UseOpenApi() + .UseSwaggerUi3(config => + { + config.OAuth2Client = new NSwag.AspNetCore.OAuth2ClientSettings() + { + ClientId = "c1e94676-cab0-42ba-8b6c-9532b8486fff", + UsePkceWithAuthorizationCodeGrant = true + }; + //config.WithCredentials = true; + }) + .UseWebSockets(socketOptions) + .UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }) + .Use(async (context, next) => + { + if (context.WebSockets.IsWebSocketRequest) + { + await socketConnectionManager.HandleSocketRequest(context); + } + else + { + await next(); + } + }); - JsonConvert.DefaultSettings = () => new JsonSerializerSettings - { - Formatting = Formatting.Indented, - ContractResolver = new DefaultContractResolver - { - NamingStrategy = new CamelCaseNamingStrategy - { - ProcessDictionaryKeys = true - } - }, - Converters = new[] { new StringEnumConverter() }, - NullValueHandling = NullValueHandling.Ignore, - }; - } - } + JsonConvert.DefaultSettings = () => new JsonSerializerSettings + { + Formatting = Formatting.Indented, + ContractResolver = new DefaultContractResolver + { + NamingStrategy = new CamelCaseNamingStrategy + { + ProcessDictionaryKeys = true + } + }, + Converters = new[] { new StringEnumConverter() }, + NullValueHandling = NullValueHandling.Ignore, + }; + } + } } diff --git a/Gameboard.ShogiUI.Sockets/appsettings.json b/Gameboard.ShogiUI.Sockets/appsettings.json index 7abd699..119c533 100644 --- a/Gameboard.ShogiUI.Sockets/appsettings.json +++ b/Gameboard.ShogiUI.Sockets/appsettings.json @@ -16,7 +16,8 @@ "Instance": "https://login.microsoftonline.com/", "ClientId": "c1e94676-cab0-42ba-8b6c-9532b8486fff", "TenantId": "common", - "Audience": "c1e94676-cab0-42ba-8b6c-9532b8486fff" + "Audience": "c1e94676-cab0-42ba-8b6c-9532b8486fff", + "ClientSecret": "" }, "AllowedHosts": "*" } \ No newline at end of file