🐛 Trying to fix connection issues

This commit is contained in:
LittleSheep 2025-06-17 20:01:20 +08:00
parent 1a5d0bbfc0
commit 50d8f74a98
3 changed files with 77 additions and 23 deletions

View File

@ -2,7 +2,7 @@ using DysonNetwork.Sphere.Account;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using DysonNetwork.Sphere.Auth.OpenId; using DysonNetwork.Sphere.Storage;
using NodaTime; using NodaTime;
namespace DysonNetwork.Sphere.Auth.OpenId; namespace DysonNetwork.Sphere.Auth.OpenId;
@ -14,9 +14,13 @@ public class ConnectionController(
AppDatabase db, AppDatabase db,
IEnumerable<OidcService> oidcServices, IEnumerable<OidcService> oidcServices,
AccountService accounts, AccountService accounts,
AuthService auth AuthService auth,
ICacheService cacheService
) : ControllerBase ) : ControllerBase
{ {
private const string StateCachePrefix = "oidc-state:";
private const string ReturnUrlCachePrefix = "oidc-returning:";
private static readonly TimeSpan StateExpiration = TimeSpan.FromMinutes(15);
[HttpGet] [HttpGet]
public async Task<ActionResult<List<AccountConnection>>> GetConnections() public async Task<ActionResult<List<AccountConnection>>> GetConnections()
{ {
@ -142,10 +146,12 @@ public class ConnectionController(
var state = Guid.NewGuid().ToString("N"); var state = Guid.NewGuid().ToString("N");
var nonce = Guid.NewGuid().ToString("N"); var nonce = Guid.NewGuid().ToString("N");
HttpContext.Session.SetString($"oidc_state_{state}", $"{currentUser.Id}|{request.Provider}|{nonce}"); var stateValue = $"{currentUser.Id}|{request.Provider}|{nonce}";
var finalReturnUrl = !string.IsNullOrEmpty(request.ReturnUrl) ? request.ReturnUrl : "/settings/connections"; var finalReturnUrl = !string.IsNullOrEmpty(request.ReturnUrl) ? request.ReturnUrl : "/settings/connections";
HttpContext.Session.SetString($"oidc_return_url_{state}", finalReturnUrl);
// Store state and return URL in cache
await cacheService.SetAsync($"{StateCachePrefix}{state}", stateValue, StateExpiration);
await cacheService.SetAsync($"{ReturnUrlCachePrefix}{state}", finalReturnUrl, StateExpiration);
var authUrl = oidcService.GetAuthorizationUrl(state, nonce); var authUrl = oidcService.GetAuthorizationUrl(state, nonce);
@ -169,14 +175,22 @@ public class ConnectionController(
if (callbackData.State == null) if (callbackData.State == null)
return BadRequest("State parameter is missing."); return BadRequest("State parameter is missing.");
var sessionState = HttpContext.Session.GetString($"oidc_state_{callbackData.State!}"); // Get and validate state from cache
HttpContext.Session.Remove($"oidc_state_{callbackData.State}"); var stateKey = $"{StateCachePrefix}{callbackData.State}";
var stateValue = await cacheService.GetAsync<string>(stateKey);
if (string.IsNullOrEmpty(stateValue))
{
return BadRequest("Invalid or expired state parameter");
}
// If sessionState is present, it's a manual connection flow for an existing user. // Remove state from cache to prevent replay attacks
if (sessionState == null) return await HandleLoginOrRegistration(provider, oidcService, callbackData); await cacheService.RemoveAsync(stateKey);
var stateParts = sessionState.Split('|');
if (stateParts.Length != 3 || !stateParts[1].Equals(provider, StringComparison.OrdinalIgnoreCase)) var stateParts = stateValue.Split('|');
return BadRequest("State mismatch."); if (stateParts.Length != 3)
{
return BadRequest("Invalid state format");
}
var accountId = Guid.Parse(stateParts[0]); var accountId = Guid.Parse(stateParts[0]);
return await HandleManualConnection(provider, oidcService, callbackData, accountId); return await HandleManualConnection(provider, oidcService, callbackData, accountId);
@ -259,9 +273,9 @@ public class ConnectionController(
} }
// Clean up and redirect // Clean up and redirect
var returnUrl = HttpContext.Session.GetString($"oidc_return_url_{callbackData.State}"); var returnUrlKey = $"{ReturnUrlCachePrefix}{callbackData.State}";
HttpContext.Session.Remove($"oidc_return_url_{callbackData.State}"); var returnUrl = await cacheService.GetAsync<string>(returnUrlKey);
HttpContext.Session.Remove($"oidc_state_{callbackData.State}"); await cacheService.RemoveAsync(returnUrlKey);
return Redirect(string.IsNullOrEmpty(returnUrl) ? "/settings/connections" : returnUrl); return Redirect(string.IsNullOrEmpty(returnUrl) ? "/settings/connections" : returnUrl);
} }

View File

@ -1,4 +1,5 @@
using DysonNetwork.Sphere.Account; using DysonNetwork.Sphere.Account;
using DysonNetwork.Sphere.Storage;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
@ -12,12 +13,16 @@ public class OidcController(
IServiceProvider serviceProvider, IServiceProvider serviceProvider,
AppDatabase db, AppDatabase db,
AccountService accounts, AccountService accounts,
AuthService authService AuthService auth,
ICacheService cache
) )
: ControllerBase : ControllerBase
{ {
private const string StateCachePrefix = "oidc-state:";
private static readonly TimeSpan StateExpiration = TimeSpan.FromMinutes(15);
[HttpGet("{provider}")] [HttpGet("{provider}")]
public ActionResult SignIn([FromRoute] string provider, [FromQuery] string? returnUrl = "/") public async Task<ActionResult> SignIn([FromRoute] string provider, [FromQuery] string? returnUrl = "/")
{ {
try try
{ {
@ -29,10 +34,11 @@ public class OidcController(
var state = Guid.NewGuid().ToString(); var state = Guid.NewGuid().ToString();
var nonce = Guid.NewGuid().ToString(); var nonce = Guid.NewGuid().ToString();
// Store user's ID, provider, and nonce in session. The callback will use this. // Store user's ID, provider, and nonce in cache. The callback will use this.
HttpContext.Session.SetString($"oidc_state_{state}", $"{currentUser.Id}|{provider}|{nonce}"); var stateValue = $"{currentUser.Id}|{provider}|{nonce}";
await cache.SetAsync($"{StateCachePrefix}{state}", stateValue, StateExpiration);
// The state parameter sent to the provider is the GUID key for the session state. // The state parameter sent to the provider is the GUID key for the cache.
var authUrl = oidcService.GetAuthorizationUrl(state, nonce); var authUrl = oidcService.GetAuthorizationUrl(state, nonce);
return Redirect(authUrl); return Redirect(authUrl);
} }
@ -88,7 +94,7 @@ public class OidcController(
); );
// Generate token using existing auth service // Generate token using existing auth service
var token = authService.CreateToken(session); var token = auth.CreateToken(session);
return Ok(new AuthController.TokenExchangeResponse { Token = token }); return Ok(new AuthController.TokenExchangeResponse { Token = token });
} }
@ -156,7 +162,7 @@ public class OidcController(
Meta = userInfo.ToMetadata() Meta = userInfo.ToMetadata()
}; };
db.AccountConnections.Add(connection); await db.AccountConnections.AddAsync(connection);
await db.SaveChangesAsync(); await db.SaveChangesAsync();
return existingAccount; return existingAccount;

View File

@ -1,4 +1,4 @@
/*! tailwindcss v4.1.7 | MIT License | https://tailwindcss.com */ /*! tailwindcss v4.1.10 | MIT License | https://tailwindcss.com */
@layer properties; @layer properties;
@layer theme, base, components, utilities; @layer theme, base, components, utilities;
@layer theme { @layer theme {
@ -311,6 +311,9 @@
.max-w-lg { .max-w-lg {
max-width: var(--container-lg); max-width: var(--container-lg);
} }
.border-collapse {
border-collapse: collapse;
}
.transform { .transform {
transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,); transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
} }
@ -332,9 +335,23 @@
.rounded-lg { .rounded-lg {
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
} }
.border {
border-style: var(--tw-border-style);
border-width: 1px;
}
.border-2 {
border-style: var(--tw-border-style);
border-width: 2px;
}
.border-gray-300 {
border-color: var(--color-gray-300);
}
.bg-blue-500 { .bg-blue-500 {
background-color: var(--color-blue-500); background-color: var(--color-blue-500);
} }
.bg-gray-100 {
background-color: var(--color-gray-100);
}
.bg-green-100 { .bg-green-100 {
background-color: var(--color-green-100); background-color: var(--color-green-100);
} }
@ -430,6 +447,12 @@
.text-yellow-800 { .text-yellow-800 {
color: var(--color-yellow-800); color: var(--color-yellow-800);
} }
.lowercase {
text-transform: lowercase;
}
.underline {
text-decoration-line: underline;
}
.opacity-80 { .opacity-80 {
opacity: 80%; opacity: 80%;
} }
@ -504,6 +527,11 @@
line-height: var(--tw-leading, var(--text-6xl--line-height)); line-height: var(--tw-leading, var(--text-6xl--line-height));
} }
} }
.dark\:border-gray-600 {
@media (prefers-color-scheme: dark) {
border-color: var(--color-gray-600);
}
}
.dark\:bg-gray-800 { .dark\:bg-gray-800 {
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
background-color: var(--color-gray-800); background-color: var(--color-gray-800);
@ -800,6 +828,11 @@
syntax: "*"; syntax: "*";
inherits: false; inherits: false;
} }
@property --tw-border-style {
syntax: "*";
inherits: false;
initial-value: solid;
}
@property --tw-leading { @property --tw-leading {
syntax: "*"; syntax: "*";
inherits: false; inherits: false;
@ -953,6 +986,7 @@
--tw-rotate-z: initial; --tw-rotate-z: initial;
--tw-skew-x: initial; --tw-skew-x: initial;
--tw-skew-y: initial; --tw-skew-y: initial;
--tw-border-style: solid;
--tw-leading: initial; --tw-leading: initial;
--tw-font-weight: initial; --tw-font-weight: initial;
--tw-tracking: initial; --tw-tracking: initial;