✨ Magic spell for one time code
🗑️ Drop the usage of casbin ♻️ Refactor the permission service ♻️ Refactor the flow of creating an account 🧱 Email infra structure
This commit is contained in:
@ -15,8 +15,7 @@ public class AuthController(
|
||||
AppDatabase db,
|
||||
AccountService accounts,
|
||||
AuthService auth,
|
||||
IConfiguration configuration,
|
||||
IHttpClientFactory httpClientFactory
|
||||
IConfiguration configuration
|
||||
) : ControllerBase
|
||||
{
|
||||
public class ChallengeRequest
|
||||
@ -218,49 +217,7 @@ public class AuthController(
|
||||
[HttpPost("captcha")]
|
||||
public async Task<ActionResult> ValidateCaptcha([FromBody] string token)
|
||||
{
|
||||
var provider = configuration.GetSection("Captcha")["Provider"]?.ToLower();
|
||||
var apiKey = configuration.GetSection("Captcha")["ApiKey"];
|
||||
var apiSecret = configuration.GetSection("Captcha")["ApiSecret"];
|
||||
|
||||
var client = httpClientFactory.CreateClient();
|
||||
|
||||
switch (provider)
|
||||
{
|
||||
case "cloudflare":
|
||||
var content = new StringContent($"secret={apiSecret}&response={token}", System.Text.Encoding.UTF8,
|
||||
"application/x-www-form-urlencoded");
|
||||
var response = await client.PostAsync("https://challenges.cloudflare.com/turnstile/v0/siteverify",
|
||||
content);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var json = await response.Content.ReadAsStringAsync();
|
||||
var cfResult = JsonSerializer.Deserialize<CloudflareVerificationResponse>(json);
|
||||
|
||||
if (cfResult?.Success == true)
|
||||
return Ok(new { success = true });
|
||||
|
||||
return BadRequest(new { success = false, errors = cfResult?.ErrorCodes });
|
||||
case "google":
|
||||
var secretKey = configuration.GetSection("CaptchaSettings")["GoogleRecaptchaSecretKey"];
|
||||
if (string.IsNullOrEmpty(secretKey))
|
||||
{
|
||||
return StatusCode(500, "Google reCaptcha secret key is not configured.");
|
||||
}
|
||||
|
||||
content = new StringContent($"secret={secretKey}&response={token}", System.Text.Encoding.UTF8,
|
||||
"application/x-www-form-urlencoded");
|
||||
response = await client.PostAsync("https://www.google.com/recaptcha/api/siteverify", content);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
json = await response.Content.ReadAsStringAsync();
|
||||
var capResult = JsonSerializer.Deserialize<GoogleVerificationResponse>(json);
|
||||
|
||||
if (capResult?.Success == true)
|
||||
return Ok(new { success = true });
|
||||
|
||||
return BadRequest(new { success = false, errors = capResult?.ErrorCodes });
|
||||
default:
|
||||
return StatusCode(500, "The server misconfigured for the captcha.");
|
||||
}
|
||||
var result = await auth.ValidateCaptcha(token);
|
||||
return result ? Ok() : BadRequest();
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
using Casbin;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
@ -15,68 +16,43 @@ public class SignedTokenPair
|
||||
public Instant ExpiredAt { get; set; }
|
||||
}
|
||||
|
||||
public class AuthService(AppDatabase db, IConfiguration config, IEnforcer enforcer)
|
||||
public class AuthService(IConfiguration config, IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
public async Task<bool> AssignRoleToUserAsync(string user, string role, string domain = "global")
|
||||
public async Task<bool> ValidateCaptcha(string token)
|
||||
{
|
||||
var added = await enforcer.AddGroupingPolicyAsync(user, role, domain);
|
||||
if (added) await enforcer.SavePolicyAsync();
|
||||
return added;
|
||||
}
|
||||
var provider = config.GetSection("Captcha")["Provider"]?.ToLower();
|
||||
var apiSecret = config.GetSection("Captcha")["ApiSecret"];
|
||||
|
||||
public async Task<bool> AddPermissionToUserAsync(string user, string domain, string obj, string act)
|
||||
{
|
||||
var added = await enforcer.AddPolicyAsync(user, domain, obj, act);
|
||||
if (added) await enforcer.SavePolicyAsync();
|
||||
return added;
|
||||
}
|
||||
|
||||
public async Task<bool> RemovePermissionFromUserAsync(string user, string domain, string obj, string act)
|
||||
{
|
||||
var removed = await enforcer.RemovePolicyAsync(user, domain, obj, act);
|
||||
if (removed) await enforcer.SavePolicyAsync();
|
||||
return removed;
|
||||
}
|
||||
var client = httpClientFactory.CreateClient();
|
||||
|
||||
public async Task<bool> CreateRoleAsync(string role, string domain, IEnumerable<(string obj, string act)> permissions)
|
||||
{
|
||||
bool anyAdded = false;
|
||||
foreach (var (obj, act) in permissions)
|
||||
switch (provider)
|
||||
{
|
||||
var added = await enforcer.AddPolicyAsync(role, domain, obj, act);
|
||||
if (added) anyAdded = true;
|
||||
}
|
||||
case "cloudflare":
|
||||
var content = new StringContent($"secret={apiSecret}&response={token}", System.Text.Encoding.UTF8,
|
||||
"application/x-www-form-urlencoded");
|
||||
var response = await client.PostAsync("https://challenges.cloudflare.com/turnstile/v0/siteverify",
|
||||
content);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
if (anyAdded) await enforcer.SavePolicyAsync();
|
||||
return anyAdded;
|
||||
var json = await response.Content.ReadAsStringAsync();
|
||||
var cfResult = JsonSerializer.Deserialize<CloudflareVerificationResponse>(json);
|
||||
|
||||
return cfResult?.Success == true;
|
||||
case "google":
|
||||
content = new StringContent($"secret={apiSecret}&response={token}", System.Text.Encoding.UTF8,
|
||||
"application/x-www-form-urlencoded");
|
||||
response = await client.PostAsync("https://www.google.com/recaptcha/api/siteverify", content);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
json = await response.Content.ReadAsStringAsync();
|
||||
var capResult = JsonSerializer.Deserialize<GoogleVerificationResponse>(json);
|
||||
|
||||
return capResult?.Success == true;
|
||||
default:
|
||||
throw new ArgumentException("The server misconfigured for the captcha.");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> AddPermissionsToRoleAsync(string role, string domain, IEnumerable<(string obj, string act)> permissions)
|
||||
{
|
||||
bool anyAdded = false;
|
||||
foreach (var (obj, act) in permissions)
|
||||
{
|
||||
var added = await enforcer.AddPolicyAsync(role, domain, obj, act);
|
||||
if (added) anyAdded = true;
|
||||
}
|
||||
|
||||
if (anyAdded) await enforcer.SavePolicyAsync();
|
||||
return anyAdded;
|
||||
}
|
||||
|
||||
public async Task<bool> RemovePermissionsFromRoleAsync(string role, string domain, IEnumerable<(string obj, string act)> permissions)
|
||||
{
|
||||
bool anyRemoved = false;
|
||||
foreach (var (obj, act) in permissions)
|
||||
{
|
||||
var removed = await enforcer.RemovePolicyAsync(role, domain, obj, act);
|
||||
if (removed) anyRemoved = true;
|
||||
}
|
||||
|
||||
if (anyRemoved) await enforcer.SavePolicyAsync();
|
||||
return anyRemoved;
|
||||
}
|
||||
|
||||
public SignedTokenPair CreateToken(Session session)
|
||||
{
|
||||
var privateKeyPem = File.ReadAllText(config["Jwt:PrivateKeyPath"]!);
|
||||
@ -98,9 +74,9 @@ public class AuthService(AppDatabase db, IConfiguration config, IEnforcer enforc
|
||||
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"));
|
||||
if (session.Account.IsSuperuser) claims.Add(new Claim("is_superuser", "1"));
|
||||
var accessTokenClaims = new JwtSecurityToken(
|
||||
issuer: "solar-network",
|
||||
audience: string.Join(',', session.Challenge.Audiences),
|
||||
|
@ -1,34 +0,0 @@
|
||||
using Casbin;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace DysonNetwork.Sphere.Auth;
|
||||
|
||||
public class CasbinRequirement(string domain, string obj, string act) : IAuthorizationRequirement
|
||||
{
|
||||
public string Domain { get; } = domain;
|
||||
public string Object { get; } = obj;
|
||||
public string Action { get; } = act;
|
||||
}
|
||||
|
||||
public class CasbinAuthorizationHandler(IEnforcer enforcer)
|
||||
: AuthorizationHandler<CasbinRequirement>
|
||||
{
|
||||
protected override async Task HandleRequirementAsync(
|
||||
AuthorizationHandlerContext context,
|
||||
CasbinRequirement requirement)
|
||||
{
|
||||
var userId = context.User.FindFirst("user_id")?.Value;
|
||||
if (userId == null) return;
|
||||
var isSuperuser = context.User.FindFirst("is_superuser")?.Value == "1";
|
||||
if (isSuperuser) userId = "super:" + userId;
|
||||
|
||||
var allowed = await enforcer.EnforceAsync(
|
||||
userId,
|
||||
requirement.Domain,
|
||||
requirement.Object,
|
||||
requirement.Action
|
||||
);
|
||||
|
||||
if (allowed) context.Succeed(requirement);
|
||||
}
|
||||
}
|
@ -21,6 +21,7 @@ public class Challenge : ModelBase
|
||||
public Instant? ExpiredAt { get; set; }
|
||||
public int StepRemain { get; set; }
|
||||
public int StepTotal { get; set; }
|
||||
public int FailedAttempts { get; set; }
|
||||
[Column(TypeName = "jsonb")] public List<long> BlacklistFactors { get; set; } = new();
|
||||
[Column(TypeName = "jsonb")] public List<string> Audiences { get; set; } = new();
|
||||
[Column(TypeName = "jsonb")] public List<string> Scopes { get; set; } = new();
|
||||
|
Reference in New Issue
Block a user