💥 The newly crafted Dyson Token
This commit is contained in:
parent
7e309bb5c7
commit
6a426efde9
@ -11,7 +11,7 @@ public class AccountService(
|
||||
ICacheService cache
|
||||
)
|
||||
{
|
||||
public const string AccountCachePrefix = "Account_";
|
||||
public const string AccountCachePrefix = "account:";
|
||||
|
||||
public async Task PurgeAccountCache(Account account)
|
||||
{
|
||||
|
@ -1,9 +1,12 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Encodings.Web;
|
||||
using DysonNetwork.Sphere.Account;
|
||||
using DysonNetwork.Sphere.Storage;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using SystemClock = NodaTime.SystemClock;
|
||||
|
||||
namespace DysonNetwork.Sphere.Auth;
|
||||
|
||||
@ -13,44 +16,104 @@ public static class AuthConstants
|
||||
public const string TokenQueryParamName = "tk";
|
||||
}
|
||||
|
||||
public enum TokenType
|
||||
{
|
||||
AuthKey,
|
||||
ApiKey,
|
||||
Unknown
|
||||
}
|
||||
|
||||
public class TokenInfo
|
||||
{
|
||||
public string Token { get; set; } = string.Empty;
|
||||
public TokenType Type { get; set; } = TokenType.Unknown;
|
||||
}
|
||||
|
||||
public class DysonTokenAuthOptions : AuthenticationSchemeOptions;
|
||||
|
||||
public class DysonTokenAuthHandler : AuthenticationHandler<DysonTokenAuthOptions>
|
||||
public class DysonTokenAuthHandler(
|
||||
IOptionsMonitor<DysonTokenAuthOptions> options,
|
||||
IConfiguration configuration,
|
||||
ILoggerFactory logger,
|
||||
UrlEncoder encoder,
|
||||
AppDatabase database,
|
||||
ICacheService cache
|
||||
)
|
||||
: AuthenticationHandler<DysonTokenAuthOptions>(options, logger, encoder)
|
||||
{
|
||||
private TokenValidationParameters _tokenValidationParameters;
|
||||
private const string AuthCachePrefix = "auth:";
|
||||
|
||||
public DysonTokenAuthHandler(
|
||||
IOptionsMonitor<DysonTokenAuthOptions> options,
|
||||
IConfiguration configuration,
|
||||
ILoggerFactory logger,
|
||||
UrlEncoder encoder)
|
||||
: base(options, logger, encoder)
|
||||
{
|
||||
var publicKey = File.ReadAllText(configuration["Jwt:PublicKeyPath"]!);
|
||||
var rsa = RSA.Create();
|
||||
rsa.ImportFromPem(publicKey);
|
||||
_tokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = true,
|
||||
ValidateAudience = false,
|
||||
ValidateLifetime = true,
|
||||
ValidateIssuerSigningKey = true,
|
||||
ValidIssuer = "solar-network",
|
||||
IssuerSigningKey = new RsaSecurityKey(rsa)
|
||||
};
|
||||
}
|
||||
|
||||
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||
{
|
||||
var token = _ExtractToken(Request);
|
||||
var tokenInfo = _ExtractToken(Request);
|
||||
|
||||
if (string.IsNullOrEmpty(token))
|
||||
if (tokenInfo == null || string.IsNullOrEmpty(tokenInfo.Token))
|
||||
return AuthenticateResult.Fail("No token was provided.");
|
||||
|
||||
try
|
||||
{
|
||||
var tokenHandler = new JwtSecurityTokenHandler();
|
||||
var principal = tokenHandler.ValidateToken(token, _tokenValidationParameters, out var validatedToken);
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
|
||||
// Validate token and extract session ID
|
||||
if (!ValidateToken(tokenInfo.Token, out var sessionId))
|
||||
return AuthenticateResult.Fail("Invalid token.");
|
||||
|
||||
// Try to get session from cache first
|
||||
var session = await cache.GetAsync<Session>($"{AuthCachePrefix}{sessionId}");
|
||||
|
||||
// If not in cache, load from database
|
||||
if (session is null)
|
||||
{
|
||||
session = await database.AuthSessions
|
||||
.Where(e => e.Id == sessionId)
|
||||
.Include(e => e.Challenge)
|
||||
.Include(e => e.Account)
|
||||
.ThenInclude(e => e.Profile)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (session is not null)
|
||||
{
|
||||
// Store in cache for future requests
|
||||
await cache.SetWithGroupsAsync(
|
||||
$"Auth_{sessionId}",
|
||||
session,
|
||||
[$"{AccountService.AccountCachePrefix}{session.Account.Id}"],
|
||||
TimeSpan.FromHours(1)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the session exists
|
||||
if (session == null)
|
||||
return AuthenticateResult.Fail("Session not found.");
|
||||
|
||||
// Check if the session is expired
|
||||
if (session.ExpiredAt.HasValue && session.ExpiredAt.Value < now)
|
||||
return AuthenticateResult.Fail("Session expired.");
|
||||
|
||||
// Store user and session in the HttpContext.Items for easy access in controllers
|
||||
Context.Items["CurrentUser"] = session.Account;
|
||||
Context.Items["CurrentSession"] = session;
|
||||
Context.Items["CurrentTokenType"] = tokenInfo.Type.ToString();
|
||||
|
||||
// Create claims from the session
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new("user_id", session.Account.Id.ToString()),
|
||||
new("session_id", session.Id.ToString()),
|
||||
new("token_type", tokenInfo.Type.ToString())
|
||||
};
|
||||
|
||||
// Add scopes as claims
|
||||
session.Challenge.Scopes.ForEach(scope => claims.Add(new Claim("scope", scope)));
|
||||
|
||||
// Add superuser claim if applicable
|
||||
if (session.Account.IsSuperuser)
|
||||
claims.Add(new Claim("is_superuser", "1"));
|
||||
|
||||
// Create the identity and principal
|
||||
var identity = new ClaimsIdentity(claims, AuthConstants.SchemeName);
|
||||
var principal = new ClaimsPrincipal(identity);
|
||||
|
||||
var ticket = new AuthenticationTicket(principal, AuthConstants.SchemeName);
|
||||
return AuthenticateResult.Success(ticket);
|
||||
@ -61,17 +124,97 @@ public class DysonTokenAuthHandler : AuthenticationHandler<DysonTokenAuthOptions
|
||||
}
|
||||
}
|
||||
|
||||
private static string? _ExtractToken(HttpRequest request)
|
||||
private bool ValidateToken(string token, out Guid sessionId)
|
||||
{
|
||||
sessionId = Guid.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
// Split the token
|
||||
var parts = token.Split('.');
|
||||
if (parts.Length != 2)
|
||||
return false;
|
||||
|
||||
// Decode the payload
|
||||
var payloadBytes = Base64UrlDecode(parts[0]);
|
||||
|
||||
// Extract session ID
|
||||
sessionId = new Guid(payloadBytes);
|
||||
|
||||
// Load public key for verification
|
||||
var publicKeyPem = File.ReadAllText(configuration["Jwt:PublicKeyPath"]!);
|
||||
using var rsa = RSA.Create();
|
||||
rsa.ImportFromPem(publicKeyPem);
|
||||
|
||||
// Verify signature
|
||||
var signature = Base64UrlDecode(parts[1]);
|
||||
return rsa.VerifyData(payloadBytes, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] Base64UrlDecode(string base64Url)
|
||||
{
|
||||
string padded = base64Url
|
||||
.Replace('-', '+')
|
||||
.Replace('_', '/');
|
||||
|
||||
switch (padded.Length % 4)
|
||||
{
|
||||
case 2: padded += "=="; break;
|
||||
case 3: padded += "="; break;
|
||||
}
|
||||
|
||||
return Convert.FromBase64String(padded);
|
||||
}
|
||||
|
||||
private static TokenInfo? _ExtractToken(HttpRequest request)
|
||||
{
|
||||
// Check for token in query parameters
|
||||
if (request.Query.TryGetValue(AuthConstants.TokenQueryParamName, out var queryToken))
|
||||
return queryToken;
|
||||
{
|
||||
return new TokenInfo
|
||||
{
|
||||
Token = queryToken,
|
||||
Type = TokenType.AuthKey
|
||||
};
|
||||
}
|
||||
|
||||
// Check for token in Authorization header
|
||||
var authHeader = request.Headers.Authorization.ToString();
|
||||
if (!string.IsNullOrEmpty(authHeader) && authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
|
||||
return authHeader["Bearer ".Length..].Trim();
|
||||
if (!string.IsNullOrEmpty(authHeader))
|
||||
{
|
||||
if (authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new TokenInfo
|
||||
{
|
||||
Token = authHeader["Bearer ".Length..].Trim(),
|
||||
Type = TokenType.AuthKey
|
||||
};
|
||||
}
|
||||
else if (authHeader.StartsWith("ApiKey ", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new TokenInfo
|
||||
{
|
||||
Token = authHeader["ApiKey ".Length..].Trim(),
|
||||
Type = TokenType.ApiKey
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return request.Cookies.TryGetValue(AuthConstants.TokenQueryParamName, out var cookieToken)
|
||||
? cookieToken
|
||||
: null;
|
||||
// Check for token in cookies
|
||||
if (request.Cookies.TryGetValue(AuthConstants.TokenQueryParamName, out var cookieToken))
|
||||
{
|
||||
return new TokenInfo
|
||||
{
|
||||
Token = cookieToken,
|
||||
Type = TokenType.AuthKey
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@ -182,8 +182,13 @@ public class AuthController(
|
||||
public string? Code { get; set; }
|
||||
}
|
||||
|
||||
public class TokenExchangeResponse
|
||||
{
|
||||
public string Token { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
[HttpPost("token")]
|
||||
public async Task<ActionResult<SignedTokenPair>> ExchangeToken([FromBody] TokenExchangeRequest request)
|
||||
public async Task<ActionResult<TokenExchangeResponse>> ExchangeToken([FromBody] TokenExchangeRequest request)
|
||||
{
|
||||
Session? session;
|
||||
switch (request.GrantType)
|
||||
@ -218,26 +223,11 @@ public class AuthController(
|
||||
db.AuthSessions.Add(session);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return auth.CreateToken(session);
|
||||
var tk = auth.CreateToken(session);
|
||||
return Ok(new TokenExchangeResponse { Token = tk });
|
||||
case "refresh_token":
|
||||
var handler = new JwtSecurityTokenHandler();
|
||||
var token = handler.ReadJwtToken(request.RefreshToken);
|
||||
var sessionIdClaim = token.Claims.FirstOrDefault(c => c.Type == "session_id")?.Value;
|
||||
|
||||
if (!Guid.TryParse(sessionIdClaim, out var sessionId))
|
||||
return Unauthorized("Invalid or missing session_id claim in refresh token.");
|
||||
|
||||
session = await db.AuthSessions
|
||||
.Include(e => e.Account)
|
||||
.Include(e => e.Challenge)
|
||||
.FirstOrDefaultAsync(s => s.Id == sessionId);
|
||||
if (session is null)
|
||||
return NotFound("Session not found or expired.");
|
||||
|
||||
session.LastGrantedAt = Instant.FromDateTimeUtc(DateTime.UtcNow);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return auth.CreateToken(session);
|
||||
// Since we no longer need the refresh token
|
||||
// This case is blank for now, thinking to mock it if the OIDC standard requires it
|
||||
default:
|
||||
return BadRequest("Unsupported grant type.");
|
||||
}
|
||||
|
@ -1,19 +1,8 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Auth;
|
||||
|
||||
public class SignedTokenPair
|
||||
{
|
||||
public string AccessToken { get; set; } = null!;
|
||||
public string RefreshToken { get; set; } = null!;
|
||||
public Instant ExpiredAt { get; set; }
|
||||
}
|
||||
|
||||
public class AuthService(IConfiguration config, IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
public async Task<bool> ValidateCaptcha(string token)
|
||||
@ -69,47 +58,88 @@ public class AuthService(IConfiguration config, IHttpClientFactory httpClientFac
|
||||
}
|
||||
}
|
||||
|
||||
public SignedTokenPair CreateToken(Session session)
|
||||
public string CreateToken(Session session)
|
||||
{
|
||||
// Load the private key for signing
|
||||
var privateKeyPem = File.ReadAllText(config["Jwt:PrivateKeyPath"]!);
|
||||
var rsa = RSA.Create();
|
||||
using var rsa = RSA.Create();
|
||||
rsa.ImportFromPem(privateKeyPem);
|
||||
var key = new RsaSecurityKey(rsa);
|
||||
|
||||
// Create and return a single token
|
||||
return CreateCompactToken(session.Id, rsa);
|
||||
}
|
||||
|
||||
var creds = new SigningCredentials(key, SecurityAlgorithms.RsaSha256);
|
||||
var claims = new List<Claim>
|
||||
private string CreateCompactToken(Guid sessionId, RSA rsa)
|
||||
{
|
||||
// Create the payload: just the session ID
|
||||
var payloadBytes = sessionId.ToByteArray();
|
||||
|
||||
// Base64Url encode the payload
|
||||
var payloadBase64 = Base64UrlEncode(payloadBytes);
|
||||
|
||||
// Sign the payload with RSA-SHA256
|
||||
var signature = rsa.SignData(payloadBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||
|
||||
// Base64Url encode the signature
|
||||
var signatureBase64 = Base64UrlEncode(signature);
|
||||
|
||||
// Combine payload and signature with a dot
|
||||
return $"{payloadBase64}.{signatureBase64}";
|
||||
}
|
||||
|
||||
public bool ValidateToken(string token, out Guid sessionId)
|
||||
{
|
||||
sessionId = Guid.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
new("user_id", session.Account.Id.ToString()),
|
||||
new("session_id", session.Id.ToString())
|
||||
};
|
||||
|
||||
var refreshTokenClaims = new JwtSecurityToken(
|
||||
issuer: "solar-network",
|
||||
audience: string.Join(',', session.Challenge.Audiences),
|
||||
claims: claims,
|
||||
expires: DateTime.Now.AddDays(30),
|
||||
signingCredentials: creds
|
||||
);
|
||||
|
||||
session.Challenge.Scopes.ForEach(c => claims.Add(new Claim("scope", c)));
|
||||
if (session.Account.IsSuperuser) claims.Add(new Claim("is_superuser", "1"));
|
||||
var accessTokenClaims = new JwtSecurityToken(
|
||||
issuer: "solar-network",
|
||||
audience: string.Join(',', session.Challenge.Audiences),
|
||||
claims: claims,
|
||||
expires: DateTime.Now.AddMinutes(30),
|
||||
signingCredentials: creds
|
||||
);
|
||||
|
||||
var handler = new JwtSecurityTokenHandler();
|
||||
var accessToken = handler.WriteToken(accessTokenClaims);
|
||||
var refreshToken = handler.WriteToken(refreshTokenClaims);
|
||||
|
||||
return new SignedTokenPair
|
||||
// Split the token
|
||||
var parts = token.Split('.');
|
||||
if (parts.Length != 2)
|
||||
return false;
|
||||
|
||||
// Decode the payload
|
||||
var payloadBytes = Base64UrlDecode(parts[0]);
|
||||
|
||||
// Extract session ID
|
||||
sessionId = new Guid(payloadBytes);
|
||||
|
||||
// Load public key for verification
|
||||
var publicKeyPem = File.ReadAllText(config["Jwt:PublicKeyPath"]!);
|
||||
using var rsa = RSA.Create();
|
||||
rsa.ImportFromPem(publicKeyPem);
|
||||
|
||||
// Verify signature
|
||||
var signature = Base64UrlDecode(parts[1]);
|
||||
return rsa.VerifyData(payloadBytes, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||
}
|
||||
catch
|
||||
{
|
||||
AccessToken = accessToken,
|
||||
RefreshToken = refreshToken,
|
||||
ExpiredAt = Instant.FromDateTimeUtc(DateTime.UtcNow.AddMinutes(30))
|
||||
};
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper methods for Base64Url encoding/decoding
|
||||
private static string Base64UrlEncode(byte[] data)
|
||||
{
|
||||
return Convert.ToBase64String(data)
|
||||
.TrimEnd('=')
|
||||
.Replace('+', '-')
|
||||
.Replace('/', '_');
|
||||
}
|
||||
|
||||
private static byte[] Base64UrlDecode(string base64Url)
|
||||
{
|
||||
string padded = base64Url
|
||||
.Replace('-', '+')
|
||||
.Replace('_', '/');
|
||||
|
||||
switch (padded.Length % 4)
|
||||
{
|
||||
case 2: padded += "=="; break;
|
||||
case 3: padded += "="; break;
|
||||
}
|
||||
|
||||
return Convert.FromBase64String(padded);
|
||||
}
|
||||
}
|
94
DysonNetwork.Sphere/Auth/CompactTokenService.cs
Normal file
94
DysonNetwork.Sphere/Auth/CompactTokenService.cs
Normal file
@ -0,0 +1,94 @@
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace DysonNetwork.Sphere.Auth;
|
||||
|
||||
public class CompactTokenService(IConfiguration config)
|
||||
{
|
||||
private readonly string _privateKeyPath = config["Jwt:PrivateKeyPath"]
|
||||
?? throw new InvalidOperationException("Jwt:PrivateKeyPath configuration is missing");
|
||||
|
||||
public string CreateToken(Session session)
|
||||
{
|
||||
// Load the private key for signing
|
||||
var privateKeyPem = File.ReadAllText(_privateKeyPath);
|
||||
using var rsa = RSA.Create();
|
||||
rsa.ImportFromPem(privateKeyPem);
|
||||
|
||||
// Create and return a single token
|
||||
return CreateCompactToken(session.Id, rsa);
|
||||
}
|
||||
|
||||
private string CreateCompactToken(Guid sessionId, RSA rsa)
|
||||
{
|
||||
// Create the payload: just the session ID
|
||||
var payloadBytes = sessionId.ToByteArray();
|
||||
|
||||
// Base64Url encode the payload
|
||||
var payloadBase64 = Base64UrlEncode(payloadBytes);
|
||||
|
||||
// Sign the payload with RSA-SHA256
|
||||
var signature = rsa.SignData(payloadBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||
|
||||
// Base64Url encode the signature
|
||||
var signatureBase64 = Base64UrlEncode(signature);
|
||||
|
||||
// Combine payload and signature with a dot
|
||||
return $"{payloadBase64}.{signatureBase64}";
|
||||
}
|
||||
|
||||
public bool ValidateToken(string token, out Guid sessionId)
|
||||
{
|
||||
sessionId = Guid.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
// Split the token
|
||||
var parts = token.Split('.');
|
||||
if (parts.Length != 2)
|
||||
return false;
|
||||
|
||||
// Decode the payload
|
||||
var payloadBytes = Base64UrlDecode(parts[0]);
|
||||
|
||||
// Extract session ID
|
||||
sessionId = new Guid(payloadBytes);
|
||||
|
||||
// Load public key for verification
|
||||
var publicKeyPem = File.ReadAllText(config["Jwt:PublicKeyPath"]!);
|
||||
using var rsa = RSA.Create();
|
||||
rsa.ImportFromPem(publicKeyPem);
|
||||
|
||||
// Verify signature
|
||||
var signature = Base64UrlDecode(parts[1]);
|
||||
return rsa.VerifyData(payloadBytes, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper methods for Base64Url encoding/decoding
|
||||
private static string Base64UrlEncode(byte[] data)
|
||||
{
|
||||
return Convert.ToBase64String(data)
|
||||
.TrimEnd('=')
|
||||
.Replace('+', '-')
|
||||
.Replace('/', '_');
|
||||
}
|
||||
|
||||
private static byte[] Base64UrlDecode(string base64Url)
|
||||
{
|
||||
string padded = base64Url
|
||||
.Replace('-', '+')
|
||||
.Replace('_', '/');
|
||||
|
||||
switch (padded.Length % 4)
|
||||
{
|
||||
case 2: padded += "=="; break;
|
||||
case 3: padded += "="; break;
|
||||
}
|
||||
|
||||
return Convert.FromBase64String(padded);
|
||||
}
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
using DysonNetwork.Sphere.Account;
|
||||
using DysonNetwork.Sphere.Storage;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DysonNetwork.Sphere.Auth;
|
||||
|
||||
public class UserInfoMiddleware(RequestDelegate next, ICacheService cache)
|
||||
{
|
||||
public async Task InvokeAsync(HttpContext context, AppDatabase db)
|
||||
{
|
||||
var sessionIdClaim = context.User.FindFirst("session_id")?.Value;
|
||||
if (sessionIdClaim is not null && Guid.TryParse(sessionIdClaim, out var sessionId))
|
||||
{
|
||||
var session = await cache.GetAsync<Session>($"Auth_{sessionId}");
|
||||
if (session is null)
|
||||
{
|
||||
session = await db.AuthSessions
|
||||
.Where(e => e.Id == sessionId)
|
||||
.Include(e => e.Challenge)
|
||||
.Include(e => e.Account)
|
||||
.ThenInclude(e => e.Profile)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (session is not null)
|
||||
{
|
||||
await cache.SetWithGroupsAsync($"Auth_{sessionId}", session,
|
||||
[$"{AccountService.AccountCachePrefix}{session.Account.Id}"], TimeSpan.FromHours(1));
|
||||
}
|
||||
}
|
||||
|
||||
if (session is not null)
|
||||
{
|
||||
context.Items["CurrentUser"] = session.Account;
|
||||
context.Items["CurrentSession"] = session;
|
||||
}
|
||||
}
|
||||
|
||||
await next(context);
|
||||
}
|
||||
}
|
@ -171,6 +171,7 @@ builder.Services.AddScoped<IWebSocketPacketHandler, MessageReadHandler>();
|
||||
builder.Services.AddScoped<IWebSocketPacketHandler, MessageTypingHandler>();
|
||||
|
||||
// Services
|
||||
builder.Services.AddScoped<CompactTokenService>();
|
||||
builder.Services.AddScoped<RazorViewRenderer>();
|
||||
builder.Services.Configure<GeoIpOptions>(builder.Configuration.GetSection("GeoIP"));
|
||||
builder.Services.AddScoped<GeoIpService>();
|
||||
@ -274,7 +275,6 @@ app.UseWebSockets();
|
||||
app.UseRateLimiter();
|
||||
app.UseHttpsRedirection();
|
||||
app.UseAuthorization();
|
||||
app.UseMiddleware<UserInfoMiddleware>();
|
||||
app.UseMiddleware<PermissionMiddleware>();
|
||||
|
||||
app.MapControllers().RequireRateLimiting("fixed");
|
||||
|
@ -62,6 +62,7 @@
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AQueryable_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F42d8f09d6a294d00a6f49efc989927492fe00_003F4e_003F26d1ee34_003FQueryable_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AResizeOptions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fef3339e864a448e2b1ec6fa7bbf4c6661fee00_003F48_003F0209e410_003FResizeOptions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AResourceManagerStringLocalizerFactory_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb62f365d06c44ad695ff75960cdf97a2a800_003Fe4_003Ff6ba93b7_003FResourceManagerStringLocalizerFactory_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ARSA_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fee4f989f6b8042b59b2654fdc188e287243600_003F8b_003F44e5f855_003FRSA_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASafeHandle_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb6f0571a6bc744b0b551fd4578292582e54c00_003F66_003Fde27c365_003FSafeHandle_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASecuritySchemeType_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F29898ce74e3763a786ac1bd9a6db2152e1af75769440b1e53b9cbdf1dda1bd99_003FSecuritySchemeType_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AServiceCollectionContainerBuilderExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fc0e30e11d8f5456cb7a11b21ebee6c5a35c00_003F60_003F78b485f5_003FServiceCollectionContainerBuilderExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
|
Loading…
x
Reference in New Issue
Block a user