Recaptcha

This commit is contained in:
LittleSheep 2025-04-23 01:18:12 +08:00
parent 31db3d5388
commit a008a74d77
6 changed files with 243 additions and 6 deletions

View File

@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Mvc;
using NodaTime;
using Microsoft.EntityFrameworkCore;
using System.IdentityModel.Tokens.Jwt;
using System.Text.Json;
namespace DysonNetwork.Sphere.Auth;
@ -14,7 +15,8 @@ public class AuthController(
AppDatabase db,
AccountService accounts,
AuthService auth,
IHttpContextAccessor httpContext
IConfiguration configuration,
IHttpClientFactory httpClientFactory
) : ControllerBase
{
public class ChallengeRequest
@ -31,8 +33,8 @@ public class AuthController(
var account = await accounts.LookupAccount(request.Account);
if (account is null) return NotFound("Account was not found.");
var ipAddress = httpContext.HttpContext?.Connection.RemoteIpAddress?.ToString();
var userAgent = httpContext.HttpContext?.Request.Headers.UserAgent.ToString();
var ipAddress = HttpContext.Connection.RemoteIpAddress?.ToString();
var userAgent = HttpContext.Request.Headers.UserAgent.ToString();
var now = Instant.FromDateTimeUtc(DateTime.UtcNow);
@ -186,7 +188,7 @@ public class AuthController(
[HttpGet("test")]
public async Task<ActionResult> Test()
{
var sessionIdClaim = httpContext.HttpContext?.User.FindFirst("session_id")?.Value;
var sessionIdClaim = HttpContext.User.FindFirst("session_id")?.Value;
if (!Guid.TryParse(sessionIdClaim, out var sessionId))
return Unauthorized();
@ -195,4 +197,53 @@ public class AuthController(
return Ok(session);
}
[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.");
}
}
}

View File

@ -0,0 +1,17 @@
namespace DysonNetwork.Sphere.Auth;
public class CloudflareVerificationResponse
{
public bool Success { get; set; }
public string[]? ErrorCodes { get; set; }
}
public class GoogleVerificationResponse
{
public bool Success { get; set; }
public float Score { get; set; }
public string Action { get; set; }
public DateTime ChallengeTs { get; set; }
public string Hostname { get; set; }
public string[]? ErrorCodes { get; set; }
}

View File

@ -0,0 +1,149 @@
@page "/auth/captcha"
@model DysonNetwork.Sphere.Pages.CheckpointPage
@{
Layout = null;
var cfg = ViewData.Model.Configuration;
var provider = cfg.GetSection("Captcha")["Provider"]?.ToLower();
var apiKey = cfg.GetSection("Captcha")["ApiKey"];
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Solar Network Captcha</title>
<link
href="https://fonts.googleapis.com/css2?family=Roboto+Mono&display=swap"
rel="stylesheet"
/>
<style>
body {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #2d2d2d;
font-family: "Roboto Mono", monospace;
color: #c9d1d9;
}
.parent {
padding: 20px;
max-width: 480px;
margin: 0 auto;
}
h2 {
font-size: 18px;
font-weight: 300;
color: #ffffff;
margin-bottom: 15px;
}
.footer {
margin-top: 20px;
font-size: 11px;
opacity: 0.6;
}
.footer-product {
font-size: 12px;
font-weight: bold;
margin-bottom: 5px;
opacity: 0.8;
}
.g-recaptcha {
display: inline-block; /* Adjust as needed */
}
</style>
@switch (provider)
{
case "recaptcha":
<script src="https://www.recaptcha.net/recaptcha/api.js" async defer></script>
break;
case "cloudflare":
<script
src="https://challenges.cloudflare.com/turnstile/v0/api.js"
async
defer
></script>
break;
}
</head>
<body>
<div class="parent">
<div class="container">
<h1>reCaptcha</h1>
@switch (provider)
{
case "cloudflare":
<div
class="cf-turnstile"
data-sitekey="@apiKey"
data-callback="onSuccess"
></div>
break;
case "recaptcha":
<div
class="g-recaptcha"
data-sitekey="@apiKey"
data-callback="onSuccess"
></div>
break;
default:
<p style="color: yellow;">Captcha provider not configured correctly.</p>
break;
}
</div>
<div class="footer">
<div class="footer-product">Solar Network Anti-Robot</div>
<a
href="https://solsynth.dev"
style="color: #c9d1d9; text-decoration: none"
>Solsynth LLC</a>
&copy; @DateTime.Now.Year<br/>
Powered by
@switch (provider)
{
case "cloudflare":
<a href="https://www.cloudflare.com/turnstile/" style="color: #c9d1d9"
>Cloudflare Turnstile</a>
break;
case "recaptcha":
<a href="https://www.google.com/recaptcha/" style="color: #c9d1d9"
>Google reCaptcha</a>
break;
default:
<span>Nothing</span>
break;
}
<br/>
Hosted by
<a
@* TODO Update the project link here *@
href="https://github.com/Solsynth/HyperNet.Nexus"
style="color: #c9d1d9"
>DysonNetwork.Sphere</a>
</div>
</div>
<script>
function getQueryParam(name) {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get(name);
}
function onSuccess(token) {
window.parent.postMessage("captcha_tk=" + token, "*");
const redirectUri = getQueryParam("redirect_uri");
if (redirectUri) {
window.location.href = `${redirectUri}?captcha_tk=${encodeURIComponent(token)}`;
}
}
</script>
</body>
</html>

View File

@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace DysonNetwork.Sphere.Pages;
public class CheckpointPage(IConfiguration configuration) : PageModel
{
[BindProperty] public IConfiguration Configuration { get; set; } = configuration;
public void OnGet()
{
}
}

View File

@ -40,7 +40,7 @@ builder.Services.AddControllers().AddJsonOptions(options =>
options.JsonSerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
});
builder.Services.AddHttpContextAccessor();
builder.Services.AddRazorPages();
// Casbin permissions
@ -155,7 +155,7 @@ using (var scope = app.Services.CreateScope())
db.Database.Migrate();
}
if (app.Environment.IsDevelopment()) app.MapOpenApi();
app.MapOpenApi();
app.UseSwagger();
app.UseSwaggerUI();
@ -179,6 +179,8 @@ app.UseAuthorization();
app.UseMiddleware<UserInfoMiddleware>();
app.MapControllers();
app.MapStaticAssets();
app.MapRazorPages();
var tusDiskStore = new tusdotnet.Stores.TusDiskStore(
builder.Configuration.GetSection("Tus").GetValue<string>("StorePath")!

View File

@ -44,5 +44,10 @@
"EnableSsl": true
}
]
},
"Captcha": {
"Provider": "recaptcha",
"ApiKey": "6LfIzSArAAAAAN413MtycDcPlKa636knBSAhbzj-",
"ApiSecret": ""
}
}