Fix claims.
Use OID instead of email for microsoft identifier. Fix PlayerCount route. Add created date to user table. Create spectator icon.
This commit is contained in:
@@ -74,13 +74,7 @@ public class SessionsController : ControllerBase
|
||||
[HttpGet("PlayerCount")]
|
||||
public async Task<ActionResult<ReadSessionsPlayerCountResponse>> GetSessionsPlayerCount()
|
||||
{
|
||||
var sessions = await this.queryRespository.ReadSessionPlayerCount();
|
||||
|
||||
return Ok(new ReadSessionsPlayerCountResponse
|
||||
{
|
||||
PlayerHasJoinedSessions = Array.Empty<SessionMetadata>(),
|
||||
AllOtherSessions = sessions.ToList()
|
||||
});
|
||||
return Ok(await this.queryRespository.ReadSessionPlayerCount(this.User.GetShogiUserId()));
|
||||
}
|
||||
|
||||
[HttpGet("{name}")]
|
||||
|
||||
@@ -42,7 +42,7 @@ public class UserController : ControllerBase
|
||||
public ActionResult<CreateTokenResponse> GetWebSocketToken()
|
||||
{
|
||||
var userId = User.GetShogiUserId();
|
||||
var displayName = User.DisplayName();
|
||||
var displayName = User.GetShogiUserDisplayname();
|
||||
|
||||
var token = tokenCache.GenerateToken(userId);
|
||||
return new CreateTokenResponse
|
||||
@@ -74,7 +74,7 @@ public class UserController : ControllerBase
|
||||
{
|
||||
var signOutTask = HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
|
||||
var userId = User?.GetGuestUserId();
|
||||
var userId = User?.GetShogiUserId();
|
||||
if (!string.IsNullOrEmpty(userId))
|
||||
{
|
||||
connectionManager.Unsubscribe(userId);
|
||||
|
||||
@@ -1,42 +1,30 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.Identity.Web;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace Shogi.Api.Extensions;
|
||||
|
||||
public static class ClaimsExtensions
|
||||
{
|
||||
private static readonly string MsalUsernameClaim = "preferred_username";
|
||||
// https://learn.microsoft.com/en-us/azure/active-directory/develop/id-tokens#payload-claims
|
||||
|
||||
public static string? GetGuestUserId(this ClaimsPrincipal self)
|
||||
{
|
||||
return self.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value;
|
||||
}
|
||||
/// <summary>
|
||||
/// Get Id from claims after applying shogi-specific claims transformations.
|
||||
/// </summary>
|
||||
public static string GetShogiUserId(this ClaimsPrincipal self)
|
||||
{
|
||||
var id = self.GetNameIdentifierId();
|
||||
if (string.IsNullOrEmpty(id)) throw new InvalidOperationException("Shogi UserId not found in claims.");
|
||||
return id;
|
||||
}
|
||||
|
||||
public static string? DisplayName(this ClaimsPrincipal self)
|
||||
{
|
||||
return self.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Name)?.Value;
|
||||
}
|
||||
/// <summary>
|
||||
/// Get display name from claims after applying shogi-specific claims transformations.
|
||||
/// </summary>
|
||||
public static string GetShogiUserDisplayname(this ClaimsPrincipal self)
|
||||
{
|
||||
var displayName = self.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Name)?.Value;
|
||||
if (string.IsNullOrEmpty(displayName)) throw new InvalidOperationException("Shogi Display name not found in claims.");
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public static bool IsMicrosoft(this ClaimsPrincipal self)
|
||||
{
|
||||
return self.HasClaim(c => c.Type == MsalUsernameClaim);
|
||||
}
|
||||
|
||||
public static string? GetMicrosoftUserId(this ClaimsPrincipal self)
|
||||
{
|
||||
return self.Claims.FirstOrDefault(c => c.Type == MsalUsernameClaim)?.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the userId from claims after claims transformation has occurred.
|
||||
/// Throws if a shogi userid is not found.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException"></exception>
|
||||
public static string GetShogiUserId(this ClaimsPrincipal self)
|
||||
{
|
||||
var id = self.IsMicrosoft() ? self.GetMicrosoftUserId() : self.GetGuestUserId();
|
||||
|
||||
if (string.IsNullOrEmpty(id)) throw new InvalidOperationException("Shogi UserId not found in claims.");
|
||||
|
||||
return id;
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ public class User
|
||||
public static readonly ReadOnlyCollection<string> Subjects = new(new[] {
|
||||
"Hippo", "Basil", "Mouse", "Walnut", "Minstrel", "Lima Bean", "Koala", "Potato", "Penguin", "Cola", "Banana", "Egg", "Fish", "Yak"
|
||||
});
|
||||
public static User CreateMsalUser(string id) => new(id, id, WhichLoginPlatform.Microsoft);
|
||||
public static User CreateMsalUser(string id, string displayName) => new(id, displayName, WhichLoginPlatform.Microsoft);
|
||||
public static User CreateGuestUser(string id)
|
||||
{
|
||||
var random = new Random();
|
||||
|
||||
@@ -13,223 +13,224 @@ using System.Text;
|
||||
|
||||
namespace Shogi.Api
|
||||
{
|
||||
public class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
public class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
var allowedOrigins = builder.Configuration.GetSection("Cors:AllowedOrigins").Get<string[]>();
|
||||
Console.WriteLine(string.Join("\n", allowedOrigins));
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddDefaultPolicy(policy =>
|
||||
{
|
||||
policy
|
||||
.WithOrigins(allowedOrigins)
|
||||
.SetIsOriginAllowedToAllowWildcardSubdomains()
|
||||
.WithExposedHeaders("Set-Cookie")
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod()
|
||||
.AllowCredentials();
|
||||
});
|
||||
});
|
||||
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);
|
||||
ConfigureAuthentication(builder);
|
||||
ConfigureControllers(builder);
|
||||
ConfigureSwagger(builder);
|
||||
ConfigureDependencyInjection(builder);
|
||||
ConfigureLogging(builder);
|
||||
|
||||
var app = builder.Build();
|
||||
var app = builder.Build();
|
||||
|
||||
app.UseWhen(
|
||||
// Log anything that isn't related to swagger.
|
||||
context => ShouldLog(context),
|
||||
appBuilder => appBuilder.UseHttpLogging());
|
||||
app.UseWhen(
|
||||
// Log anything that isn't related to swagger.
|
||||
context => ShouldLog(context),
|
||||
appBuilder => appBuilder.UseHttpLogging());
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
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;
|
||||
});
|
||||
app.UseHttpsRedirection(); // Apache handles HTTPS in production.
|
||||
}
|
||||
// Configure the HTTP request pipeline.
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
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;
|
||||
});
|
||||
app.UseHttpsRedirection(); // Apache handles HTTPS in production.
|
||||
}
|
||||
|
||||
UseCorsAndWebSockets(app, allowedOrigins);
|
||||
UseCorsAndWebSockets(app, allowedOrigins);
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.Map("/", () => "OK");
|
||||
app.MapControllers();
|
||||
app.Map("/", () => "OK");
|
||||
app.MapControllers();
|
||||
|
||||
app.Run();
|
||||
app.Run();
|
||||
|
||||
static bool ShouldLog(HttpContext context)
|
||||
{
|
||||
var path = context.Request.GetEncodedPathAndQuery();
|
||||
static bool ShouldLog(HttpContext context)
|
||||
{
|
||||
var path = context.Request.GetEncodedPathAndQuery();
|
||||
|
||||
return !path.Contains("swagger")
|
||||
&& !path.Equals("/", StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
return !path.Contains("swagger")
|
||||
&& !path.Equals("/", StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
|
||||
private static void UseCorsAndWebSockets(WebApplication app, string[] allowedOrigins)
|
||||
{
|
||||
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);
|
||||
// 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);
|
||||
}
|
||||
await next();
|
||||
});
|
||||
}
|
||||
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 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);
|
||||
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 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 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
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 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.AddHttpClient("couchdb", c =>
|
||||
{
|
||||
var base64 = Convert.ToBase64String(Encoding.UTF8.GetBytes("admin:admin"));
|
||||
c.DefaultRequestHeaders.Add("Accept", "application/json");
|
||||
c.DefaultRequestHeaders.Add("Authorization", $"Basic {base64}");
|
||||
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.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 = $"{builder.Configuration["AppSettings:CouchDB:Url"]}/{builder.Configuration["AppSettings:CouchDB:Database"]}/";
|
||||
c.BaseAddress = new Uri(baseUrl);
|
||||
});
|
||||
services.AddTransient<IModelMapper, ModelMapper>();
|
||||
}
|
||||
var baseUrl = $"{builder.Configuration["AppSettings:CouchDB:Url"]}/{builder.Configuration["AppSettings:CouchDB:Database"]}/";
|
||||
c.BaseAddress = new Uri(baseUrl);
|
||||
});
|
||||
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
|
||||
{
|
||||
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" }
|
||||
}
|
||||
}
|
||||
},
|
||||
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>()
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
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
|
||||
{
|
||||
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>()
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Dapper;
|
||||
using Shogi.Contracts.Api;
|
||||
using Shogi.Contracts.Types;
|
||||
using System.Data.SqlClient;
|
||||
|
||||
@@ -6,23 +7,33 @@ namespace Shogi.Api.Repositories;
|
||||
|
||||
public class QueryRepository : IQueryRespository
|
||||
{
|
||||
private readonly string connectionString;
|
||||
private readonly string connectionString;
|
||||
|
||||
public QueryRepository(IConfiguration configuration)
|
||||
{
|
||||
connectionString = configuration.GetConnectionString("ShogiDatabase");
|
||||
}
|
||||
public QueryRepository(IConfiguration configuration)
|
||||
{
|
||||
connectionString = configuration.GetConnectionString("ShogiDatabase");
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<SessionMetadata>> ReadSessionPlayerCount()
|
||||
{
|
||||
using var connection = new SqlConnection(connectionString);
|
||||
return await connection.QueryAsync<SessionMetadata>(
|
||||
"session.ReadSessionPlayerCount",
|
||||
commandType: System.Data.CommandType.StoredProcedure);
|
||||
}
|
||||
public async Task<ReadSessionsPlayerCountResponse> ReadSessionPlayerCount(string playerName)
|
||||
{
|
||||
using var connection = new SqlConnection(connectionString);
|
||||
|
||||
var results = await connection.QueryMultipleAsync(
|
||||
"session.ReadSessionPlayerCount",
|
||||
new { PlayerName = playerName },
|
||||
commandType: System.Data.CommandType.StoredProcedure);
|
||||
|
||||
var joinedSessions = await results.ReadAsync<SessionMetadata>();
|
||||
var otherSessions = await results.ReadAsync<SessionMetadata>();
|
||||
return new ReadSessionsPlayerCountResponse
|
||||
{
|
||||
PlayerHasJoinedSessions = joinedSessions.ToList(),
|
||||
AllOtherSessions = otherSessions.ToList()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public interface IQueryRespository
|
||||
{
|
||||
Task<IEnumerable<SessionMetadata>> ReadSessionPlayerCount();
|
||||
Task<ReadSessionsPlayerCountResponse> ReadSessionPlayerCount(string playerName);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Shogi.Api.Extensions;
|
||||
using Microsoft.Identity.Web;
|
||||
using Shogi.Api.Models;
|
||||
using Shogi.Api.Repositories;
|
||||
using System.Security.Claims;
|
||||
@@ -13,76 +13,92 @@ namespace Shogi.Api;
|
||||
/// </summary>
|
||||
public class ShogiUserClaimsTransformer : IShogiUserClaimsTransformer
|
||||
{
|
||||
private readonly IUserRepository userRepository;
|
||||
private readonly IUserRepository userRepository;
|
||||
|
||||
public ShogiUserClaimsTransformer(IUserRepository userRepository)
|
||||
{
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
|
||||
{
|
||||
var newPrincipal = principal.IsMicrosoft()
|
||||
? await CreateClaimsFromMicrosoftPrincipal(principal)
|
||||
: await CreateClaimsFromGuestPrincipal(principal);
|
||||
|
||||
return newPrincipal;
|
||||
}
|
||||
|
||||
public async Task<ClaimsPrincipal> CreateClaimsFromGuestPrincipal(ClaimsPrincipal principal)
|
||||
{
|
||||
var id = principal.GetGuestUserId();
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
public ShogiUserClaimsTransformer(IUserRepository userRepository)
|
||||
{
|
||||
var newUser = User.CreateGuestUser(Guid.NewGuid().ToString());
|
||||
await this.userRepository.CreateUser(newUser);
|
||||
return new ClaimsPrincipal(CreateClaimsIdentity(newUser));
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
var user = await this.userRepository.ReadUser(id);
|
||||
if (user == null) throw new UnauthorizedAccessException("Guest account does not exist.");
|
||||
return new ClaimsPrincipal(CreateClaimsIdentity(user));
|
||||
}
|
||||
|
||||
private async Task<ClaimsPrincipal> CreateClaimsFromMicrosoftPrincipal(ClaimsPrincipal principal)
|
||||
{
|
||||
var id = principal.GetMicrosoftUserId();
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
|
||||
{
|
||||
throw new UnauthorizedAccessException("Found MSAL claims but no preferred_username.");
|
||||
var newPrincipal = IsMicrosoft(principal)
|
||||
? await CreateClaimsFromMicrosoftPrincipal(principal)
|
||||
: await CreateClaimsFromGuestPrincipal(principal);
|
||||
|
||||
return newPrincipal;
|
||||
}
|
||||
|
||||
var user = await this.userRepository.ReadUser(id);
|
||||
if (user == null)
|
||||
public async Task<ClaimsPrincipal> CreateClaimsFromGuestPrincipal(ClaimsPrincipal principal)
|
||||
{
|
||||
user = User.CreateMsalUser(id);
|
||||
await this.userRepository.CreateUser(user);
|
||||
var id = GetGuestUserId(principal);
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
{
|
||||
var newUser = User.CreateGuestUser(Guid.NewGuid().ToString());
|
||||
await this.userRepository.CreateUser(newUser);
|
||||
return new ClaimsPrincipal(CreateClaimsIdentity(newUser));
|
||||
}
|
||||
|
||||
var user = await this.userRepository.ReadUser(id);
|
||||
if (user == null) throw new UnauthorizedAccessException("Guest account does not exist.");
|
||||
return new ClaimsPrincipal(CreateClaimsIdentity(user));
|
||||
}
|
||||
return new ClaimsPrincipal(CreateClaimsIdentity(user));
|
||||
|
||||
}
|
||||
private async Task<ClaimsPrincipal> CreateClaimsFromMicrosoftPrincipal(ClaimsPrincipal principal)
|
||||
{
|
||||
var id = GetMicrosoftUserId(principal);
|
||||
var displayname = principal.GetDisplayName();
|
||||
if (string.IsNullOrWhiteSpace(id) || string.IsNullOrWhiteSpace(displayname))
|
||||
{
|
||||
throw new UnauthorizedAccessException("Unknown claim set.");
|
||||
}
|
||||
|
||||
private static ClaimsIdentity CreateClaimsIdentity(User user)
|
||||
{
|
||||
var claims = new List<Claim>(4)
|
||||
var user = await this.userRepository.ReadUser(id);
|
||||
if (user == null)
|
||||
{
|
||||
user = User.CreateMsalUser(id, displayname);
|
||||
await this.userRepository.CreateUser(user);
|
||||
}
|
||||
return new ClaimsPrincipal(CreateClaimsIdentity(user));
|
||||
|
||||
}
|
||||
|
||||
private static bool IsMicrosoft(ClaimsPrincipal self)
|
||||
{
|
||||
return self.GetObjectId() != null;
|
||||
}
|
||||
|
||||
private static string? GetMicrosoftUserId(ClaimsPrincipal self)
|
||||
{
|
||||
return self.GetObjectId();
|
||||
}
|
||||
|
||||
private static string? GetGuestUserId(ClaimsPrincipal self)
|
||||
{
|
||||
return self.GetNameIdentifierId();
|
||||
}
|
||||
|
||||
private static ClaimsIdentity CreateClaimsIdentity(User user)
|
||||
{
|
||||
var claims = new List<Claim>(4)
|
||||
{
|
||||
new Claim(ClaimTypes.NameIdentifier, user.Id),
|
||||
new Claim(ClaimTypes.Name, user.DisplayName),
|
||||
};
|
||||
if (user.LoginPlatform == WhichLoginPlatform.Guest)
|
||||
{
|
||||
if (user.LoginPlatform == WhichLoginPlatform.Guest)
|
||||
{
|
||||
|
||||
claims.Add(new Claim(ClaimTypes.Role, "Guest"));
|
||||
return new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
claims.Add(new Claim(ClaimTypes.Role, "Guest"));
|
||||
return new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ClaimsIdentity(claims, JwtBearerDefaults.AuthenticationScheme);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ClaimsIdentity(claims, JwtBearerDefaults.AuthenticationScheme);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface IShogiUserClaimsTransformer : IClaimsTransformation
|
||||
{
|
||||
Task<ClaimsPrincipal> CreateClaimsFromGuestPrincipal(ClaimsPrincipal principal);
|
||||
Task<ClaimsPrincipal> CreateClaimsFromGuestPrincipal(ClaimsPrincipal principal);
|
||||
}
|
||||
Reference in New Issue
Block a user