2 Commits
98 changed files with 20 additions and 8187 deletions

No files matched your search

+19 -46
View File
@@ -56,7 +56,6 @@ public sealed class SsoController(
return SsoProblem("统一身份认证尚未启用。", StatusCodes.Status404NotFound); return SsoProblem("统一身份认证尚未启用。", StatusCodes.Status404NotFound);
var safeReturnUrl = NormalizeReturnUrl(returnUrl); var safeReturnUrl = NormalizeReturnUrl(returnUrl);
var accountPage = FrontendPagePath(safeReturnUrl, "account", nativeApp);
if (string.IsNullOrWhiteSpace(bindingIntent) && if (string.IsNullOrWhiteSpace(bindingIntent) &&
(string.IsNullOrWhiteSpace(captchaId) || (string.IsNullOrWhiteSpace(captchaId) ||
string.IsNullOrWhiteSpace(captchaCode) || string.IsNullOrWhiteSpace(captchaCode) ||
@@ -78,7 +77,7 @@ public sealed class SsoController(
BindingIntentCacheKey(bindingIntent), BindingIntentCacheKey(bindingIntent),
cancellationToken); cancellationToken);
if (targetUserId is null) if (targetUserId is null)
return RedirectToFrontendError("binding_intent_expired", accountPage); return RedirectToFrontendError("binding_intent_expired", "/account");
properties.Items[BindingIntentProperty] = bindingIntent; properties.Items[BindingIntentProperty] = bindingIntent;
} }
var completeUrl = Url.Action( var completeUrl = Url.Action(
@@ -98,9 +97,7 @@ public sealed class SsoController(
EffectiveCallbackUrl()); EffectiveCallbackUrl());
return RedirectToFrontendError( return RedirectToFrontendError(
"configuration_error", "configuration_error",
string.IsNullOrWhiteSpace(bindingIntent) string.IsNullOrWhiteSpace(bindingIntent) ? "/login" : "/account");
? FrontendPagePath(safeReturnUrl, "login", nativeApp)
: accountPage);
} }
} }
@@ -114,23 +111,16 @@ public sealed class SsoController(
if (!_options.Enabled) if (!_options.Enabled)
return SsoProblem("统一身份认证尚未启用。", StatusCodes.Status404NotFound); return SsoProblem("统一身份认证尚未启用。", StatusCodes.Status404NotFound);
var safeReturnUrl = NormalizeReturnUrl(returnUrl);
var authentication = await HttpContext.AuthenticateAsync( var authentication = await HttpContext.AuthenticateAsync(
SsoAuthSchemes.ExternalCookie); SsoAuthSchemes.ExternalCookie);
var nativeApp = authentication.Properties is { } externalProperties &&
externalProperties.Items.TryGetValue(NativeAppProperty, out var nativeAppValue) &&
bool.TryParse(nativeAppValue, out var isNativeApp) &&
isNativeApp;
var loginPage = FrontendPagePath(safeReturnUrl, "login", nativeApp);
var accountPage = FrontendPagePath(safeReturnUrl, "account", nativeApp);
if (!authentication.Succeeded || authentication.Principal is null) if (!authentication.Succeeded || authentication.Principal is null)
return RedirectToFrontendError("authentication_failed", loginPage); return RedirectToFrontendError("authentication_failed");
var principal = authentication.Principal; var principal = authentication.Principal;
var subject = principal.FindFirstValue("sub") ?? var subject = principal.FindFirstValue("sub") ??
principal.FindFirstValue(ClaimTypes.NameIdentifier); principal.FindFirstValue(ClaimTypes.NameIdentifier);
if (string.IsNullOrWhiteSpace(subject)) if (string.IsNullOrWhiteSpace(subject))
return RedirectToFrontendError("missing_subject", loginPage); return RedirectToFrontendError("missing_subject");
ApplicationUser? user = null; ApplicationUser? user = null;
var bindingIntent = var bindingIntent =
@@ -140,6 +130,10 @@ public sealed class SsoController(
out var storedBindingIntent) out var storedBindingIntent)
? storedBindingIntent ? storedBindingIntent
: null; : null;
var nativeApp = authentication.Properties is { } externalProperties &&
externalProperties.Items.TryGetValue(NativeAppProperty, out var nativeAppValue) &&
bool.TryParse(nativeAppValue, out var isNativeApp) &&
isNativeApp;
var nativeState = authentication.Properties is { } nativeProperties && var nativeState = authentication.Properties is { } nativeProperties &&
nativeProperties.Items.TryGetValue(NativeAppStateProperty, out var storedNativeState) nativeProperties.Items.TryGetValue(NativeAppStateProperty, out var storedNativeState)
? storedNativeState ? storedNativeState
@@ -153,13 +147,13 @@ public sealed class SsoController(
? null ? null
: await userManager.FindByIdAsync(targetUserId); : await userManager.FindByIdAsync(targetUserId);
if (user is null) if (user is null)
return RedirectToFrontendError("binding_intent_expired", accountPage); return RedirectToFrontendError("binding_intent_expired", "/account");
if (!user.IsEnabled || await userManager.IsLockedOutAsync(user)) if (!user.IsEnabled || await userManager.IsLockedOutAsync(user))
return RedirectToFrontendError("account_disabled", accountPage); return RedirectToFrontendError("account_disabled", "/account");
var linkError = await LinkSsoIdentityAsync(user, subject); var linkError = await LinkSsoIdentityAsync(user, subject);
if (linkError is not null) if (linkError is not null)
return RedirectToFrontendError(linkError, accountPage); return RedirectToFrontendError(linkError, "/account");
await cache.RemoveAsync( await cache.RemoveAsync(
BindingIntentCacheKey(bindingIntent), BindingIntentCacheKey(bindingIntent),
cancellationToken); cancellationToken);
@@ -178,7 +172,7 @@ public sealed class SsoController(
{ {
var linkError = await LinkSsoIdentityAsync(user, subject); var linkError = await LinkSsoIdentityAsync(user, subject);
if (linkError is not null) if (linkError is not null)
return RedirectToFrontendError(linkError, loginPage); return RedirectToFrontendError(linkError);
} }
} }
} }
@@ -201,20 +195,18 @@ public sealed class SsoController(
cancellationToken); cancellationToken);
await HttpContext.SignOutAsync(SsoAuthSchemes.ExternalCookie); await HttpContext.SignOutAsync(SsoAuthSchemes.ExternalCookie);
var bindingPage = BuildFrontendUrl( var bindingPage = BuildFrontendUrl("/sso/bind", nativeApp) +
FrontendPagePath(safeReturnUrl, "sso/bind", nativeApp),
nativeApp) +
$"?code={Uri.EscapeDataString(bindingCode)}" + $"?code={Uri.EscapeDataString(bindingCode)}" +
$"&redirect={Uri.EscapeDataString(safeReturnUrl)}"; $"&redirect={Uri.EscapeDataString(NormalizeReturnUrl(returnUrl))}";
return Redirect(bindingPage); return Redirect(bindingPage);
} }
if (!user.IsEnabled || await userManager.IsLockedOutAsync(user)) if (!user.IsEnabled || await userManager.IsLockedOutAsync(user))
return RedirectToFrontendError("account_disabled", loginPage); return RedirectToFrontendError("account_disabled");
user.LastLoginAt = DateTime.UtcNow; user.LastLoginAt = DateTime.UtcNow;
var updateResult = await userManager.UpdateAsync(user); var updateResult = await userManager.UpdateAsync(user);
if (!updateResult.Succeeded) if (!updateResult.Succeeded)
return RedirectToFrontendError("account_update_failed", loginPage); return RedirectToFrontendError("account_update_failed");
var exchangeCode = WebEncoders.Base64UrlEncode( var exchangeCode = WebEncoders.Base64UrlEncode(
RandomNumberGenerator.GetBytes(32)); RandomNumberGenerator.GetBytes(32));
@@ -228,11 +220,9 @@ public sealed class SsoController(
cancellationToken); cancellationToken);
await HttpContext.SignOutAsync(SsoAuthSchemes.ExternalCookie); await HttpContext.SignOutAsync(SsoAuthSchemes.ExternalCookie);
var callback = BuildFrontendUrl( var callback = BuildFrontendUrl("/sso/callback", nativeApp) +
FrontendPagePath(safeReturnUrl, "sso/callback", nativeApp),
nativeApp) +
$"?code={Uri.EscapeDataString(exchangeCode)}" + $"?code={Uri.EscapeDataString(exchangeCode)}" +
$"&redirect={Uri.EscapeDataString(safeReturnUrl)}"; $"&redirect={Uri.EscapeDataString(NormalizeReturnUrl(returnUrl))}";
return Redirect(callback); return Redirect(callback);
} }
@@ -396,7 +386,6 @@ public sealed class SsoController(
[HttpPost("prepare-binding")] [HttpPost("prepare-binding")]
public async Task<ActionResult<SsoBindingStartResponse>> PrepareBinding( public async Task<ActionResult<SsoBindingStartResponse>> PrepareBinding(
CancellationToken cancellationToken, CancellationToken cancellationToken,
[FromQuery] string? returnUrl = null,
[FromQuery] bool nativeApp = false, [FromQuery] bool nativeApp = false,
[FromQuery] string? nativeState = null) [FromQuery] string? nativeState = null)
{ {
@@ -422,14 +411,11 @@ public sealed class SsoController(
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5) AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5)
}, },
cancellationToken); cancellationToken);
var safeReturnUrl = string.IsNullOrWhiteSpace(returnUrl)
? "/account"
: NormalizeReturnUrl(returnUrl);
var loginUrl = Url.Action( var loginUrl = Url.Action(
nameof(Login), nameof(Login),
values: new values: new
{ {
returnUrl = safeReturnUrl, returnUrl = "/account",
bindingIntent = intentCode, bindingIntent = intentCode,
nativeApp, nativeApp,
nativeState, nativeState,
@@ -478,19 +464,6 @@ public sealed class SsoController(
? returnUrl ? returnUrl
: "/dashboard"; : "/dashboard";
internal static string FrontendPagePath(
string? returnUrl,
string page,
bool nativeApp = false)
{
var normalizedPage = "/" + page.Trim('/');
if (nativeApp) return normalizedPage;
var normalizedReturnUrl = NormalizeReturnUrl(returnUrl);
return normalizedReturnUrl.StartsWith("/next/", StringComparison.OrdinalIgnoreCase)
? "/next" + normalizedPage
: normalizedPage;
}
private string BuildFrontendUrl(string path, bool nativeApp = false) private string BuildFrontendUrl(string path, bool nativeApp = false)
{ {
if (nativeApp) if (nativeApp)
@@ -119,23 +119,6 @@ public sealed class SsoControllerTests
Assert.Equal(expected, SsoController.NormalizeReturnUrl(value)); Assert.Equal(expected, SsoController.NormalizeReturnUrl(value));
} }
[Theory]
[InlineData("/dashboard", "sso/callback", false, "/sso/callback")]
[InlineData("/next/dashboard", "sso/callback", false, "/next/sso/callback")]
[InlineData("/next/account", "account", false, "/next/account")]
[InlineData("/next/dashboard", "sso/callback", true, "/sso/callback")]
[InlineData("https://evil.example/path", "login", false, "/login")]
public void FrontendPagePath_SelectsReactOnlyForNextWebRoutes(
string? returnUrl,
string page,
bool nativeApp,
string expected)
{
Assert.Equal(
expected,
SsoController.FrontendPagePath(returnUrl, page, nativeApp));
}
private sealed class BindingFixture : IAsyncDisposable private sealed class BindingFixture : IAsyncDisposable
{ {
private readonly SqliteConnection _connection; private readonly SqliteConnection _connection;
Loaded 3 of 98 files, more files were not shown because too many files have changed in this diff. Show more