自主注册、登录、退出、当前用户与密码修改
兼容现有 PBKDF2 密码和 hz_session Cookie TOTP 绑定、二步登录、防重放、恢复码与 AES-GCM 密钥 与 Node 完全一致的 Redis 键格式,可跨运行时共享会话 生产环境开启原生认证时强制要求 Redis 默认保持兼容代理;设置 AUTH_NATIVE_ENABLED=true 即可切换
This commit is contained in:
@@ -9,6 +9,7 @@ $smokeArtifactRoot = [System.IO.Path]::GetFullPath((Join-Path $repositoryRoot 'a
|
||||
$testDirectory = Join-Path $smokeArtifactRoot ("eis-migration-smoke-{0}" -f [guid]::NewGuid().ToString('N'))
|
||||
$nodeProcess = $null
|
||||
$dotnetProcess = $null
|
||||
$nativeAuthProcess = $null
|
||||
|
||||
function Get-AvailableTcpPort {
|
||||
$listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, 0)
|
||||
@@ -88,8 +89,10 @@ try {
|
||||
|
||||
$legacyPort = Get-AvailableTcpPort
|
||||
$webPort = Get-AvailableTcpPort
|
||||
$nativeAuthPort = Get-AvailableTcpPort
|
||||
$legacyBaseUrl = "http://127.0.0.1:$legacyPort"
|
||||
$webBaseUrl = "http://127.0.0.1:$webPort"
|
||||
$nativeAuthBaseUrl = "http://127.0.0.1:$nativeAuthPort"
|
||||
|
||||
$nodeExecutable = (Get-Command node.exe -ErrorAction Stop).Source
|
||||
$dotnetExecutable = (Get-Command dotnet.exe -ErrorAction Stop).Source
|
||||
@@ -120,6 +123,20 @@ try {
|
||||
}
|
||||
$seedProcess.Dispose()
|
||||
|
||||
$registrationSwitchProcess = Start-TestProcess -FileName $nodeExecutable -ArgumentList @(
|
||||
'tests/helpers/enable-self-registration.mjs', $smokeDatabasePath
|
||||
) -Environment $nodeEnvironment
|
||||
if (-not $registrationSwitchProcess.WaitForExit(10000)) {
|
||||
$registrationSwitchProcess.Kill($true)
|
||||
throw 'Timed out while enabling self-registration in the smoke-test database'
|
||||
}
|
||||
$registrationSwitchOutput = $registrationSwitchProcess.StandardOutput.ReadToEnd()
|
||||
$registrationSwitchError = $registrationSwitchProcess.StandardError.ReadToEnd()
|
||||
if ($registrationSwitchProcess.ExitCode -ne 0) {
|
||||
throw "Could not enable self-registration in the smoke-test database`n$registrationSwitchOutput`n$registrationSwitchError"
|
||||
}
|
||||
$registrationSwitchProcess.Dispose()
|
||||
|
||||
$nodeProcess = Start-TestProcess -FileName $nodeExecutable -ArgumentList @('server.mjs') -Environment $nodeEnvironment
|
||||
|
||||
$projectPath = Join-Path $repositoryRoot 'src\Eis.Web\Eis.Web.csproj'
|
||||
@@ -280,6 +297,125 @@ try {
|
||||
throw 'Native document verification did not reject an invalid code'
|
||||
}
|
||||
|
||||
$nativeAuthProcess = Start-TestProcess -FileName $dotnetExecutable -ArgumentList @(
|
||||
'run',
|
||||
'--project', $projectPath,
|
||||
'--configuration', 'Release',
|
||||
'--no-build',
|
||||
'--no-launch-profile',
|
||||
'--',
|
||||
'--urls', $nativeAuthBaseUrl
|
||||
) -Environment @{
|
||||
ASPNETCORE_ENVIRONMENT = 'Development'
|
||||
AUTH_NATIVE_ENABLED = 'true'
|
||||
LegacyNode__Enabled = 'true'
|
||||
LegacyNode__BaseUrl = $legacyBaseUrl
|
||||
DATABASE_CLIENT = 'sqlite'
|
||||
SQLITE_PATH = $smokeDatabasePath
|
||||
TOTP_ENCRYPTION_KEY = 'migration-smoke-totp-key-32-characters-minimum'
|
||||
DOCUMENT_VERIFICATION_SECRET = 'migration-smoke-document-key-32-characters-minimum'
|
||||
REDIS_URL = ''
|
||||
REDIS_SESSION_URL = ''
|
||||
}
|
||||
Wait-ForUrl -Uri "$nativeAuthBaseUrl/health/live" -Processes @($nodeProcess, $nativeAuthProcess)
|
||||
|
||||
$registrationSchool = @($homePayload.schools | Select-Object -First 1)[0]
|
||||
$registrationClass = @($homePayload.classes | Where-Object schoolId -eq $registrationSchool.id | Select-Object -First 1)[0]
|
||||
if ($null -eq $registrationSchool -or $null -eq $registrationClass) {
|
||||
throw 'Seed data did not provide a source school and class for native registration'
|
||||
}
|
||||
$registrationBody = @{
|
||||
name = '原生迁移注册考生'
|
||||
gender = '女'
|
||||
password = 'Registration456!'
|
||||
schoolId = $registrationSchool.id
|
||||
classId = $registrationClass.id
|
||||
} | ConvertTo-Json -Compress
|
||||
$registrationResponse = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/auth/register" -Method Post -ContentType 'application/json' -Body $registrationBody
|
||||
if ($registrationResponse.StatusCode -ne 201 -or $registrationResponse.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
||||
throw 'Self-registration request did not use the native ASP.NET Core endpoint'
|
||||
}
|
||||
$registration = $registrationResponse.Content | ConvertFrom-Json
|
||||
$registeredLoginBody = @{ username = $registration.registrationNumber; password = 'Registration456!' } | ConvertTo-Json -Compress
|
||||
$registeredLogin = Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/auth/login" -Method Post -ContentType 'application/json' -Body $registeredLoginBody
|
||||
if ($registeredLogin.user.candidateNumber -ne $registration.registrationNumber) {
|
||||
throw 'Native self-registration did not create a usable candidate account'
|
||||
}
|
||||
|
||||
$nativeSession = [Microsoft.PowerShell.Commands.WebRequestSession]::new()
|
||||
$nativeLoginResponse = Invoke-WebRequest -Uri "$nativeAuthBaseUrl/api/auth/login" -Method Post -ContentType 'application/json' -Body $loginBody -WebSession $nativeSession
|
||||
if ($nativeLoginResponse.Headers['X-EIS-Implementation'] -ne 'aspnet-core') {
|
||||
throw 'Authentication login request did not use the native ASP.NET Core endpoint'
|
||||
}
|
||||
$nativeLogin = $nativeLoginResponse.Content | ConvertFrom-Json
|
||||
if ($nativeLogin.ok -ne $true -or $nativeLogin.user.username -ne 'admin') {
|
||||
throw 'Native authentication could not verify the existing Node PBKDF2 account'
|
||||
}
|
||||
|
||||
$nativeMe = Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/auth/me" -WebSession $nativeSession
|
||||
if ($nativeMe.user.username -ne 'admin' -or $nativeMe.permissions[0] -ne '*') {
|
||||
throw 'Native authentication did not preserve the session or administrator projection'
|
||||
}
|
||||
|
||||
$totpSetupBody = @{ currentPassword = '12345678' } | ConvertTo-Json -Compress
|
||||
$totpSetup = Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/auth/totp/setup" -Method Post -ContentType 'application/json' -Body $totpSetupBody -WebSession $nativeSession
|
||||
if ($totpSetup.qrCode -notmatch '^data:image/png;base64,' -or $totpSetup.uri -notmatch '^otpauth://totp/') {
|
||||
throw 'Native TOTP setup did not return a local PNG QR code and otpauth URI'
|
||||
}
|
||||
|
||||
$totpCodeProcess = Start-TestProcess -FileName $nodeExecutable -ArgumentList @(
|
||||
'tests/helpers/current-totp-code.mjs', $totpSetup.secret
|
||||
) -Environment $nodeEnvironment
|
||||
if (-not $totpCodeProcess.WaitForExit(10000)) {
|
||||
$totpCodeProcess.Kill($true)
|
||||
throw 'Timed out while generating the native TOTP smoke-test code'
|
||||
}
|
||||
$totpCode = $totpCodeProcess.StandardOutput.ReadToEnd().Trim()
|
||||
$totpCodeError = $totpCodeProcess.StandardError.ReadToEnd()
|
||||
if ($totpCodeProcess.ExitCode -ne 0 -or -not $totpCode) {
|
||||
throw "Could not generate the native TOTP smoke-test code`n$totpCodeError"
|
||||
}
|
||||
$totpCodeProcess.Dispose()
|
||||
|
||||
$totpEnableBody = @{ code = $totpCode } | ConvertTo-Json -Compress
|
||||
$totpEnable = Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/auth/totp/enable" -Method Post -ContentType 'application/json' -Body $totpEnableBody -WebSession $nativeSession
|
||||
if ($totpEnable.user.totpEnabled -ne $true -or $totpEnable.recoveryCodes.Count -ne 8) {
|
||||
throw 'Native TOTP enablement did not persist security state or issue eight recovery codes'
|
||||
}
|
||||
|
||||
Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/auth/logout" -Method Post -WebSession $nativeSession | Out-Null
|
||||
$totpPasswordLogin = Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/auth/login" -Method Post -ContentType 'application/json' -Body $loginBody
|
||||
if ($totpPasswordLogin.requiresTotp -ne $true -or -not $totpPasswordLogin.challenge) {
|
||||
throw 'Native password login bypassed enabled TOTP'
|
||||
}
|
||||
|
||||
$totpLoginSession = [Microsoft.PowerShell.Commands.WebRequestSession]::new()
|
||||
$recoveryLoginBody = @{ challenge = $totpPasswordLogin.challenge; code = $totpEnable.recoveryCodes[0] } | ConvertTo-Json -Compress
|
||||
$totpLogin = Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/auth/login/totp" -Method Post -ContentType 'application/json' -Body $recoveryLoginBody -WebSession $totpLoginSession
|
||||
if ($totpLogin.usedRecoveryCode -ne $true -or $totpLogin.user.username -ne 'admin') {
|
||||
throw 'Native TOTP recovery-code login did not create a session'
|
||||
}
|
||||
|
||||
$totpStatus = Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/auth/totp" -WebSession $totpLoginSession
|
||||
if ($totpStatus.enabled -ne $true -or $totpStatus.recoveryCodesRemaining -ne 7) {
|
||||
throw 'Native TOTP recovery code was not consumed exactly once'
|
||||
}
|
||||
|
||||
$disableBody = @{ currentPassword = '12345678'; code = $totpEnable.recoveryCodes[1] } | ConvertTo-Json -Compress
|
||||
$disabled = Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/auth/totp/disable" -Method Post -ContentType 'application/json' -Body $disableBody -WebSession $totpLoginSession
|
||||
if ($disabled.user.totpEnabled -ne $false) {
|
||||
throw 'Native TOTP disable endpoint did not clear the security state'
|
||||
}
|
||||
|
||||
$changePasswordBody = @{ currentPassword = '12345678'; newPassword = 'MigrationAuth456!' } | ConvertTo-Json -Compress
|
||||
Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/auth/change-password" -Method Post -ContentType 'application/json' -Body $changePasswordBody -WebSession $totpLoginSession | Out-Null
|
||||
Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/auth/logout" -Method Post -WebSession $totpLoginSession | Out-Null
|
||||
$changedLoginBody = @{ username = 'admin'; password = 'MigrationAuth456!' } | ConvertTo-Json -Compress
|
||||
$changedLogin = Invoke-RestMethod -Uri "$nativeAuthBaseUrl/api/auth/login" -Method Post -ContentType 'application/json' -Body $changedLoginBody
|
||||
if ($changedLogin.user.username -ne 'admin') {
|
||||
throw 'Native password change did not create a Node-compatible PBKDF2 hash'
|
||||
}
|
||||
|
||||
[pscustomobject]@{
|
||||
AspNetCoreHost = 'passed'
|
||||
StaticAssets = 'passed'
|
||||
@@ -288,10 +424,11 @@ try {
|
||||
NativePublicApi = 'passed'
|
||||
PublicParity = 'passed'
|
||||
DocumentCodes = 'passed'
|
||||
NativeAuthentication = 'passed'
|
||||
} | Format-List
|
||||
}
|
||||
finally {
|
||||
foreach ($process in @($dotnetProcess, $nodeProcess)) {
|
||||
foreach ($process in @($nativeAuthProcess, $dotnetProcess, $nodeProcess)) {
|
||||
if ($null -ne $process -and -not $process.HasExited) {
|
||||
$process.Kill($true)
|
||||
$process.WaitForExit()
|
||||
|
||||
Reference in New Issue
Block a user