🐛 Dozen of bugs fixes
This commit is contained in:
@ -9,16 +9,14 @@ using DysonNetwork.Sphere.Developer;
|
||||
|
||||
namespace DysonNetwork.Sphere.Pages.Auth;
|
||||
|
||||
[Authorize]
|
||||
public class AuthorizeModel(OidcProviderService oidcService) : PageModel
|
||||
{
|
||||
[BindProperty(SupportsGet = true)]
|
||||
public string? ReturnUrl { get; set; }
|
||||
[BindProperty(SupportsGet = true)] public string? ReturnUrl { get; set; }
|
||||
|
||||
[BindProperty(SupportsGet = true, Name = "client_id")]
|
||||
[Required(ErrorMessage = "The client_id parameter is required")]
|
||||
public string? ClientIdString { get; set; }
|
||||
|
||||
|
||||
public Guid ClientId { get; set; }
|
||||
|
||||
[BindProperty(SupportsGet = true, Name = "response_type")]
|
||||
@ -27,22 +25,19 @@ public class AuthorizeModel(OidcProviderService oidcService) : PageModel
|
||||
[BindProperty(SupportsGet = true, Name = "redirect_uri")]
|
||||
public string? RedirectUri { get; set; }
|
||||
|
||||
[BindProperty(SupportsGet = true)]
|
||||
public string? Scope { get; set; }
|
||||
[BindProperty(SupportsGet = true)] public string? Scope { get; set; }
|
||||
|
||||
[BindProperty(SupportsGet = true)]
|
||||
public string? State { get; set; }
|
||||
[BindProperty(SupportsGet = true)] public string? State { get; set; }
|
||||
|
||||
[BindProperty(SupportsGet = true)] public string? Nonce { get; set; }
|
||||
|
||||
[BindProperty(SupportsGet = true)]
|
||||
public string? Nonce { get; set; }
|
||||
|
||||
|
||||
[BindProperty(SupportsGet = true, Name = "code_challenge")]
|
||||
public string? CodeChallenge { get; set; }
|
||||
|
||||
|
||||
[BindProperty(SupportsGet = true, Name = "code_challenge_method")]
|
||||
public string? CodeChallengeMethod { get; set; }
|
||||
|
||||
|
||||
[BindProperty(SupportsGet = true, Name = "response_mode")]
|
||||
public string? ResponseMode { get; set; }
|
||||
|
||||
@ -50,21 +45,21 @@ public class AuthorizeModel(OidcProviderService oidcService) : PageModel
|
||||
public string? AppLogo { get; set; }
|
||||
public string? AppUri { get; set; }
|
||||
public string[]? RequestedScopes { get; set; }
|
||||
|
||||
|
||||
public async Task<IActionResult> OnGetAsync()
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Sphere.Account.Account)
|
||||
{
|
||||
var returnUrl = Uri.EscapeDataString($"{Request.Path}{Request.QueryString}");
|
||||
return RedirectToPage($"/Auth/Login?returnUrl={returnUrl}");
|
||||
return RedirectToPage("/Auth/Login", new { returnUrl });
|
||||
}
|
||||
|
||||
|
||||
if (string.IsNullOrEmpty(ClientIdString) || !Guid.TryParse(ClientIdString, out var clientId))
|
||||
{
|
||||
ModelState.AddModelError("client_id", "Invalid client_id format");
|
||||
return BadRequest("Invalid client_id format");
|
||||
}
|
||||
|
||||
|
||||
ClientId = clientId;
|
||||
|
||||
var client = await oidcService.FindClientByIdAsync(ClientId);
|
||||
@ -73,7 +68,7 @@ public class AuthorizeModel(OidcProviderService oidcService) : PageModel
|
||||
ModelState.AddModelError("client_id", "Client not found");
|
||||
return NotFound("Client not found");
|
||||
}
|
||||
|
||||
|
||||
if (client.Status != CustomAppStatus.Developing)
|
||||
{
|
||||
// Validate redirect URI for non-Developing apps
|
||||
@ -93,14 +88,14 @@ public class AuthorizeModel(OidcProviderService oidcService) : PageModel
|
||||
public async Task<IActionResult> OnPostAsync(bool allow)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Sphere.Account.Account currentUser) return Unauthorized();
|
||||
|
||||
|
||||
// First, validate the client ID
|
||||
if (string.IsNullOrEmpty(ClientIdString) || !Guid.TryParse(ClientIdString, out var clientId))
|
||||
{
|
||||
ModelState.AddModelError("client_id", "Invalid client_id format");
|
||||
return BadRequest("Invalid client_id format");
|
||||
}
|
||||
|
||||
|
||||
ClientId = clientId;
|
||||
|
||||
// Check if a client exists
|
||||
@ -116,14 +111,14 @@ public class AuthorizeModel(OidcProviderService oidcService) : PageModel
|
||||
// User denied the authorization request
|
||||
if (string.IsNullOrEmpty(RedirectUri))
|
||||
return BadRequest("No redirect_uri provided");
|
||||
|
||||
|
||||
var deniedUriBuilder = new UriBuilder(RedirectUri);
|
||||
var deniedQuery = System.Web.HttpUtility.ParseQueryString(deniedUriBuilder.Query);
|
||||
deniedQuery["error"] = "access_denied";
|
||||
deniedQuery["error_description"] = "The user denied the authorization request";
|
||||
if (!string.IsNullOrEmpty(State)) deniedQuery["state"] = State;
|
||||
deniedUriBuilder.Query = deniedQuery.ToString();
|
||||
|
||||
|
||||
return Redirect(deniedUriBuilder.ToString());
|
||||
}
|
||||
|
||||
@ -147,20 +142,20 @@ public class AuthorizeModel(OidcProviderService oidcService) : PageModel
|
||||
// Build the redirect URI with the authorization code
|
||||
var redirectUri = new UriBuilder(RedirectUri);
|
||||
var query = System.Web.HttpUtility.ParseQueryString(redirectUri.Query);
|
||||
|
||||
|
||||
// Add the authorization code
|
||||
query["code"] = authCode;
|
||||
|
||||
|
||||
// Add state if provided (for CSRF protection)
|
||||
if (!string.IsNullOrEmpty(State))
|
||||
{
|
||||
query["state"] = State;
|
||||
}
|
||||
|
||||
|
||||
// Set the query string
|
||||
redirectUri.Query = query.ToString();
|
||||
|
||||
// Redirect back to the client with the authorization code
|
||||
return Redirect(redirectUri.ToString());
|
||||
}
|
||||
}
|
||||
}
|
@ -26,7 +26,7 @@
|
||||
{
|
||||
<div class="mb-4">
|
||||
<form method="post" asp-page-handler="SelectFactor" class="w-full" id="factor-@factor.Id">
|
||||
<input type="hidden" name="factorId" value="@factor.Id"/>
|
||||
<input type="hidden" name="SelectedFactorId" value="@factor.Id"/>
|
||||
|
||||
@if (factor.Type == AccountAuthFactorType.EmailCode)
|
||||
{
|
||||
|
@ -39,7 +39,7 @@ public class SelectFactorModel(
|
||||
var factor = await db.AccountAuthFactors.FindAsync(SelectedFactorId);
|
||||
if (factor?.EnabledAt == null || factor.Trustworthy <= 0)
|
||||
return BadRequest("Invalid authentication method.");
|
||||
|
||||
|
||||
// Store return URL in TempData to pass to the next step
|
||||
if (!string.IsNullOrEmpty(ReturnUrl))
|
||||
{
|
||||
@ -50,10 +50,14 @@ public class SelectFactorModel(
|
||||
try
|
||||
{
|
||||
// For OTP factors that require code delivery
|
||||
if (factor.Type == AccountAuthFactorType.EmailCode
|
||||
&& string.IsNullOrWhiteSpace(Hint))
|
||||
if (
|
||||
factor.Type == AccountAuthFactorType.EmailCode
|
||||
&& string.IsNullOrWhiteSpace(Hint)
|
||||
)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, $"Please provide a {factor.Type.ToString().ToLower().Replace("code", "")} to send the code to.");
|
||||
ModelState.AddModelError(string.Empty,
|
||||
$"Please provide a {factor.Type.ToString().ToLower().Replace("code", "")} to send the code to."
|
||||
);
|
||||
await LoadChallengeAndFactors();
|
||||
return Page();
|
||||
}
|
||||
@ -62,31 +66,30 @@ public class SelectFactorModel(
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, $"An error occurred while sending the verification code: {ex.Message}");
|
||||
ModelState.AddModelError(string.Empty,
|
||||
$"An error occurred while sending the verification code: {ex.Message}");
|
||||
await LoadChallengeAndFactors();
|
||||
return Page();
|
||||
}
|
||||
|
||||
// Redirect to verify page with return URL if available
|
||||
if (!string.IsNullOrEmpty(ReturnUrl))
|
||||
{
|
||||
return RedirectToPage("VerifyFactor", new { id = Id, factorId = factor.Id, returnUrl = ReturnUrl });
|
||||
}
|
||||
return RedirectToPage("VerifyFactor", new { id = Id, factorId = factor.Id });
|
||||
return !string.IsNullOrEmpty(ReturnUrl)
|
||||
? RedirectToPage("VerifyFactor", new { id = Id, factorId = factor.Id, returnUrl = ReturnUrl })
|
||||
: RedirectToPage("VerifyFactor", new { id = Id, factorId = factor.Id });
|
||||
}
|
||||
|
||||
private async Task LoadChallengeAndFactors()
|
||||
{
|
||||
AuthChallenge = await db.AuthChallenges
|
||||
.Include(e => e.Account)
|
||||
.ThenInclude(e => e.AuthFactors)
|
||||
.FirstOrDefaultAsync(e => e.Id == Id);
|
||||
|
||||
if (AuthChallenge != null)
|
||||
{
|
||||
AuthFactors = AuthChallenge.Account.AuthFactors
|
||||
.Where(e => e is { EnabledAt: not null, Trustworthy: >= 1 })
|
||||
.ToList();
|
||||
AuthFactors = await db.AccountAuthFactors
|
||||
.Where(e => e.AccountId == AuthChallenge.Account.Id)
|
||||
.Where(e => e.EnabledAt != null && e.Trustworthy >= 1)
|
||||
.ToListAsync();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,54 +8,35 @@ using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Pages.Auth
|
||||
{
|
||||
public class VerifyFactorModel : PageModel
|
||||
public class VerifyFactorModel(
|
||||
AppDatabase db,
|
||||
AccountService accounts,
|
||||
AuthService auth,
|
||||
ActionLogService als,
|
||||
IConfiguration configuration,
|
||||
IHttpClientFactory httpClientFactory
|
||||
)
|
||||
: PageModel
|
||||
{
|
||||
private readonly AppDatabase _db;
|
||||
private readonly AccountService _accounts;
|
||||
private readonly AuthService _auth;
|
||||
private readonly ActionLogService _als;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
[BindProperty(SupportsGet = true)] public Guid Id { get; set; }
|
||||
|
||||
[BindProperty(SupportsGet = true)]
|
||||
public Guid Id { get; set; }
|
||||
[BindProperty(SupportsGet = true)] public Guid FactorId { get; set; }
|
||||
|
||||
[BindProperty(SupportsGet = true)]
|
||||
public Guid FactorId { get; set; }
|
||||
|
||||
[BindProperty(SupportsGet = true)]
|
||||
public string? ReturnUrl { get; set; }
|
||||
[BindProperty(SupportsGet = true)] public string? ReturnUrl { get; set; }
|
||||
|
||||
[BindProperty, Required]
|
||||
public string Code { get; set; } = string.Empty;
|
||||
[BindProperty, Required] public string Code { get; set; } = string.Empty;
|
||||
|
||||
public Challenge? AuthChallenge { get; set; }
|
||||
public AccountAuthFactor? Factor { get; set; }
|
||||
public AccountAuthFactorType FactorType => Factor?.Type ?? AccountAuthFactorType.EmailCode;
|
||||
|
||||
public VerifyFactorModel(
|
||||
AppDatabase db,
|
||||
AccountService accounts,
|
||||
AuthService auth,
|
||||
ActionLogService als,
|
||||
IConfiguration configuration,
|
||||
IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
_db = db;
|
||||
_accounts = accounts;
|
||||
_auth = auth;
|
||||
_als = als;
|
||||
_configuration = configuration;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnGetAsync()
|
||||
{
|
||||
await LoadChallengeAndFactor();
|
||||
if (AuthChallenge == null) return NotFound("Challenge not found or expired.");
|
||||
if (Factor == null) return NotFound("Authentication method not found.");
|
||||
if (AuthChallenge.StepRemain == 0) return await ExchangeTokenAndRedirect();
|
||||
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
@ -73,25 +54,25 @@ namespace DysonNetwork.Sphere.Pages.Auth
|
||||
|
||||
try
|
||||
{
|
||||
if (await _accounts.VerifyFactorCode(Factor, Code))
|
||||
if (await accounts.VerifyFactorCode(Factor, Code))
|
||||
{
|
||||
AuthChallenge.StepRemain -= Factor.Trustworthy;
|
||||
AuthChallenge.StepRemain = Math.Max(0, AuthChallenge.StepRemain);
|
||||
AuthChallenge.BlacklistFactors.Add(Factor.Id);
|
||||
_db.Update(AuthChallenge);
|
||||
db.Update(AuthChallenge);
|
||||
|
||||
_als.CreateActionLogFromRequest(ActionLogType.ChallengeSuccess,
|
||||
als.CreateActionLogFromRequest(ActionLogType.ChallengeSuccess,
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
{ "challenge_id", AuthChallenge.Id },
|
||||
{ "factor_id", Factor?.Id.ToString() ?? string.Empty }
|
||||
}, Request, AuthChallenge.Account);
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
if (AuthChallenge.StepRemain == 0)
|
||||
{
|
||||
_als.CreateActionLogFromRequest(ActionLogType.NewLogin,
|
||||
als.CreateActionLogFromRequest(ActionLogType.NewLogin,
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
{ "challenge_id", AuthChallenge.Id },
|
||||
@ -104,7 +85,7 @@ namespace DysonNetwork.Sphere.Pages.Auth
|
||||
else
|
||||
{
|
||||
// If more steps are needed, redirect back to select factor
|
||||
return RedirectToPage("SelectFactor", new { id = Id });
|
||||
return RedirectToPage("SelectFactor", new { id = Id, returnUrl = ReturnUrl });
|
||||
}
|
||||
}
|
||||
else
|
||||
@ -117,10 +98,10 @@ namespace DysonNetwork.Sphere.Pages.Auth
|
||||
if (AuthChallenge != null)
|
||||
{
|
||||
AuthChallenge.FailedAttempts++;
|
||||
_db.Update(AuthChallenge);
|
||||
await _db.SaveChangesAsync();
|
||||
db.Update(AuthChallenge);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
_als.CreateActionLogFromRequest(ActionLogType.ChallengeFailure,
|
||||
als.CreateActionLogFromRequest(ActionLogType.ChallengeFailure,
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
{ "challenge_id", AuthChallenge.Id },
|
||||
@ -136,30 +117,30 @@ namespace DysonNetwork.Sphere.Pages.Auth
|
||||
|
||||
private async Task LoadChallengeAndFactor()
|
||||
{
|
||||
AuthChallenge = await _db.AuthChallenges
|
||||
AuthChallenge = await db.AuthChallenges
|
||||
.Include(e => e.Account)
|
||||
.FirstOrDefaultAsync(e => e.Id == Id);
|
||||
|
||||
if (AuthChallenge?.Account != null)
|
||||
{
|
||||
Factor = await _db.AccountAuthFactors
|
||||
.FirstOrDefaultAsync(e => e.Id == FactorId &&
|
||||
e.AccountId == AuthChallenge.Account.Id &&
|
||||
e.EnabledAt != null &&
|
||||
e.Trustworthy > 0);
|
||||
Factor = await db.AccountAuthFactors
|
||||
.FirstOrDefaultAsync(e => e.Id == FactorId &&
|
||||
e.AccountId == AuthChallenge.Account.Id &&
|
||||
e.EnabledAt != null &&
|
||||
e.Trustworthy > 0);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IActionResult> ExchangeTokenAndRedirect()
|
||||
{
|
||||
var challenge = await _db.AuthChallenges
|
||||
var challenge = await db.AuthChallenges
|
||||
.Include(e => e.Account)
|
||||
.FirstOrDefaultAsync(e => e.Id == Id);
|
||||
|
||||
if (challenge == null) return BadRequest("Authorization code not found or expired.");
|
||||
if (challenge.StepRemain != 0) return BadRequest("Challenge not yet completed.");
|
||||
|
||||
var session = await _db.AuthSessions
|
||||
var session = await db.AuthSessions
|
||||
.FirstOrDefaultAsync(e => e.ChallengeId == challenge.Id);
|
||||
|
||||
if (session == null)
|
||||
@ -171,15 +152,15 @@ namespace DysonNetwork.Sphere.Pages.Auth
|
||||
Account = challenge.Account,
|
||||
Challenge = challenge,
|
||||
};
|
||||
_db.AuthSessions.Add(session);
|
||||
await _db.SaveChangesAsync();
|
||||
db.AuthSessions.Add(session);
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
var token = _auth.CreateToken(session);
|
||||
Response.Cookies.Append("access_token", token, new CookieOptions
|
||||
var token = auth.CreateToken(session);
|
||||
Response.Cookies.Append(AuthConstants.CookieTokenName, token, new CookieOptions
|
||||
{
|
||||
HttpOnly = true,
|
||||
Secure = !_configuration.GetValue<bool>("Debug"),
|
||||
Secure = !configuration.GetValue<bool>("Debug"),
|
||||
SameSite = SameSiteMode.Strict,
|
||||
Path = "/"
|
||||
});
|
||||
@ -189,9 +170,10 @@ namespace DysonNetwork.Sphere.Pages.Auth
|
||||
{
|
||||
return Redirect(ReturnUrl);
|
||||
}
|
||||
|
||||
|
||||
// Check TempData for return URL (in case it was passed through multiple steps)
|
||||
if (TempData.TryGetValue("ReturnUrl", out var tempReturnUrl) && tempReturnUrl is string returnUrl && !string.IsNullOrEmpty(returnUrl) && Url.IsLocalUrl(returnUrl))
|
||||
if (TempData.TryGetValue("ReturnUrl", out var tempReturnUrl) && tempReturnUrl is string returnUrl &&
|
||||
!string.IsNullOrEmpty(returnUrl) && Url.IsLocalUrl(returnUrl))
|
||||
{
|
||||
return Redirect(returnUrl);
|
||||
}
|
||||
@ -199,4 +181,4 @@ namespace DysonNetwork.Sphere.Pages.Auth
|
||||
return RedirectToPage("/Index");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -17,7 +17,7 @@
|
||||
<div class="flex items-center ml-auto">
|
||||
@if (Context.Request.Cookies.TryGetValue(AuthConstants.CookieTokenName, out _))
|
||||
{
|
||||
<a href="/Account/Profile" class="text-gray-900 dark:text-white hover:text-gray-700 dark:hover:text-gray-300 px-3 py-2 rounded-md text-sm font-medium">Profile</a>
|
||||
<a href="/web/account/profile" class="text-gray-900 dark:text-white hover:text-gray-700 dark:hover:text-gray-300 px-3 py-2 rounded-md text-sm font-medium">Profile</a>
|
||||
<form method="post" asp-page="/Account/Profile" asp-page-handler="Logout" class="inline">
|
||||
<button type="submit" class="text-gray-900 dark:text-white hover:text-gray-700 dark:hover:text-gray-300 px-3 py-2 rounded-md text-sm font-medium">Logout</button>
|
||||
</form>
|
||||
@ -31,20 +31,11 @@
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
@* The header 64px + The footer 56px = 118px *@
|
||||
@* The header 64px *@
|
||||
<main class="h-full">
|
||||
@RenderBody()
|
||||
</main>
|
||||
|
||||
<footer class="bg-white dark:bg-gray-800 fixed bottom-0 left-0 right-0 shadow-[0_-1px_3px_0_rgba(0,0,0,0.1)]">
|
||||
<div class="container-default" style="padding: 1rem 0;">
|
||||
<p class="text-center text-gray-500 dark:text-gray-400">
|
||||
© @DateTime.Now.Year Solsynth LLC. All
|
||||
rights reserved.
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@await RenderSectionAsync("Scripts", required: false)
|
||||
</body>
|
||||
</html>
|
Reference in New Issue
Block a user