2026-06-01 05:09:20 +10:00
|
|
|
using System;
|
|
|
|
|
using System.Security.Claims;
|
|
|
|
|
using System.IdentityModel.Tokens.Jwt;
|
|
|
|
|
using Microsoft.AspNetCore.Http;
|
|
|
|
|
using Microsoft.IdentityModel.Tokens;
|
2026-06-01 07:46:03 +10:00
|
|
|
using Microsoft.Extensions.Configuration;
|
|
|
|
|
using Microsoft.Extensions.DependencyInjection;
|
2026-06-01 05:09:20 +10:00
|
|
|
using SNote.Server.Security;
|
2026-06-01 07:46:03 +10:00
|
|
|
using SNote.Server.Endpoints;
|
2026-06-01 05:09:20 +10:00
|
|
|
|
|
|
|
|
namespace SNote.Server.Security;
|
|
|
|
|
|
|
|
|
|
public static class AuthHelper
|
|
|
|
|
{
|
|
|
|
|
public static string? GetAuthenticatedUser(HttpContext context, CertificateManager certificateManager)
|
|
|
|
|
{
|
|
|
|
|
var token = GetBearerToken(context);
|
|
|
|
|
if (string.IsNullOrEmpty(token)) return null;
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var principal = ValidateToken(token, certificateManager);
|
|
|
|
|
// Verify it has a Name claim and is not just a server token
|
|
|
|
|
var isServer = principal.HasClaim(c => c.Type == "IsServer" && c.Value == "true");
|
|
|
|
|
if (isServer) return null;
|
|
|
|
|
|
|
|
|
|
return principal.Identity?.Name;
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static bool IsServerAuthenticated(HttpContext context, CertificateManager certificateManager)
|
|
|
|
|
{
|
|
|
|
|
var token = GetBearerToken(context);
|
|
|
|
|
if (string.IsNullOrEmpty(token)) return false;
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var principal = ValidateToken(token, certificateManager);
|
|
|
|
|
return principal.HasClaim(c => c.Type == "IsServer" && c.Value == "true");
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-01 05:49:08 +10:00
|
|
|
public static bool IsServerTokenValid(HttpContext context, PeerCache peerCache)
|
|
|
|
|
{
|
2026-06-01 17:11:09 +10:00
|
|
|
var serverId = context.Request.Headers["X-Server-Id"].ToString().Trim();
|
2026-06-01 05:49:08 +10:00
|
|
|
var serverToken = context.Request.Headers["X-Server-Token"].ToString();
|
|
|
|
|
|
2026-06-01 17:11:09 +10:00
|
|
|
if (string.IsNullOrEmpty(serverId) || string.IsNullOrEmpty(serverToken))
|
2026-06-01 05:49:08 +10:00
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-01 07:46:03 +10:00
|
|
|
// 1. Verify if it's a handshaked downstream peer calling us (upstream verification)
|
2026-06-01 17:11:09 +10:00
|
|
|
if (peerCache.VerifySessionToken(serverId, serverToken))
|
2026-06-01 07:46:03 +10:00
|
|
|
{
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. Verify if it's our configured upstream calling us (downstream verification)
|
2026-06-01 17:11:09 +10:00
|
|
|
if (!string.IsNullOrEmpty(SyncEndpoints.UpstreamServerGuid) &&
|
|
|
|
|
string.Equals(serverId, SyncEndpoints.UpstreamServerGuid, StringComparison.OrdinalIgnoreCase))
|
2026-06-01 07:46:03 +10:00
|
|
|
{
|
|
|
|
|
// The request is coming from our upstream. Verify the token matches the one we received during handshake!
|
|
|
|
|
if (!string.IsNullOrEmpty(SyncEndpoints.UpstreamSessionToken) &&
|
|
|
|
|
string.Equals(SyncEndpoints.UpstreamSessionToken, serverToken, StringComparison.Ordinal))
|
|
|
|
|
{
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
2026-06-01 05:49:08 +10:00
|
|
|
}
|
|
|
|
|
|
2026-06-01 05:09:20 +10:00
|
|
|
// Helper to generate a server token for outgoing sync requests
|
|
|
|
|
public static string GenerateServerToken(CertificateManager certificateManager)
|
|
|
|
|
{
|
|
|
|
|
var cert = certificateManager.GetCertificate();
|
|
|
|
|
var privateKey = new X509SecurityKey(cert);
|
|
|
|
|
|
|
|
|
|
var claims = new[]
|
|
|
|
|
{
|
|
|
|
|
new Claim(ClaimTypes.Name, "SNoteServerNetwork"),
|
|
|
|
|
new Claim("IsServer", "true")
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var tokenDescriptor = new SecurityTokenDescriptor
|
|
|
|
|
{
|
|
|
|
|
Subject = new ClaimsIdentity(claims),
|
|
|
|
|
Expires = DateTime.UtcNow.AddMinutes(15), // Short-lived for security
|
|
|
|
|
SigningCredentials = new SigningCredentials(privateKey, SecurityAlgorithms.RsaSha256)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var tokenHandler = new JwtSecurityTokenHandler();
|
|
|
|
|
var token = tokenHandler.CreateToken(tokenDescriptor);
|
|
|
|
|
return tokenHandler.WriteToken(token);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string? GetBearerToken(HttpContext context)
|
|
|
|
|
{
|
|
|
|
|
var authHeader = context.Request.Headers["Authorization"].ToString();
|
|
|
|
|
if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
{
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
return authHeader.Substring("Bearer ".Length).Trim();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static ClaimsPrincipal ValidateToken(string token, CertificateManager certificateManager)
|
|
|
|
|
{
|
|
|
|
|
var cert = certificateManager.GetCertificate();
|
|
|
|
|
var publicKey = new X509SecurityKey(cert);
|
|
|
|
|
|
|
|
|
|
var validationParameters = new TokenValidationParameters
|
|
|
|
|
{
|
|
|
|
|
ValidateIssuerSigningKey = true,
|
|
|
|
|
IssuerSigningKey = publicKey,
|
|
|
|
|
ValidateIssuer = false,
|
|
|
|
|
ValidateAudience = false,
|
|
|
|
|
ClockSkew = TimeSpan.FromMinutes(5)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var handler = new JwtSecurityTokenHandler();
|
|
|
|
|
return handler.ValidateToken(token, validationParameters, out _);
|
|
|
|
|
}
|
|
|
|
|
}
|