diff --git a/NuGet.config b/NuGet.config index 653bce8caccf..80454d301e83 100644 --- a/NuGet.config +++ b/NuGet.config @@ -4,12 +4,10 @@ - - - + - + @@ -28,12 +26,10 @@ - - - + - + diff --git a/eng/Baseline.Designer.props b/eng/Baseline.Designer.props index 979475fa9fef..543b6cca8d1f 100644 --- a/eng/Baseline.Designer.props +++ b/eng/Baseline.Designer.props @@ -2,28 +2,28 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 7.0.5 + 7.0.7 - 7.0.5 + 7.0.7 - 7.0.5 + 7.0.7 - 7.0.5 + 7.0.7 - 7.0.5 + 7.0.7 - - - + + + @@ -35,105 +35,105 @@ - 7.0.5 + 7.0.7 - 7.0.5 + 7.0.7 - 7.0.5 + 7.0.7 - 7.0.5 + 7.0.7 - 7.0.5 + 7.0.7 - 7.0.5 + 7.0.7 - 7.0.5 + 7.0.7 - 7.0.5 + 7.0.7 - 7.0.5 + 7.0.7 - 7.0.5 + 7.0.7 - 7.0.5 + 7.0.7 - 7.0.5 + 7.0.7 - 7.0.5 + 7.0.7 - 7.0.5 + 7.0.7 - 7.0.5 + 7.0.7 - 7.0.5 + 7.0.7 - 7.0.5 + 7.0.7 - 7.0.5 + 7.0.7 - 7.0.5 + 7.0.7 - + - + - 7.0.5 + 7.0.7 - 7.0.5 + 7.0.7 - 7.0.5 + 7.0.7 @@ -141,121 +141,121 @@ - 7.0.5 + 7.0.7 - - + + - - + + - - + + - 7.0.5 + 7.0.7 - + - 7.0.5 + 7.0.7 - 7.0.5 + 7.0.7 - + - 7.0.5 + 7.0.7 - - + + - 7.0.5 + 7.0.7 - 7.0.5 + 7.0.7 - - + + - 7.0.5 + 7.0.7 - + - 7.0.5 + 7.0.7 - + - 7.0.5 + 7.0.7 - - - + + + - 7.0.5 + 7.0.7 - - + + - 7.0.5 + 7.0.7 - - + + - 7.0.5 + 7.0.7 - 7.0.5 + 7.0.7 - 7.0.5 + 7.0.7 - - + + @@ -263,82 +263,82 @@ - 7.0.5 + 7.0.7 - + - 7.0.5 + 7.0.7 - + - + - + - + - 7.0.5 + 7.0.7 - 7.0.5 + 7.0.7 - + - + - + - 7.0.5 + 7.0.7 - - + + - + - - + + - + - - + + - + @@ -346,58 +346,58 @@ - 7.0.5 + 7.0.7 - 7.0.5 + 7.0.7 - - + + - 7.0.5 + 7.0.7 - + - + - + - 7.0.5 + 7.0.7 - + - + - + - 7.0.5 + 7.0.7 - + - 7.0.5 + 7.0.7 @@ -414,7 +414,7 @@ - 7.0.5 + 7.0.7 @@ -422,71 +422,71 @@ - 7.0.5 + 7.0.7 - + - 7.0.5 + 7.0.7 - - + + - - + + - - + + - - + + - 7.0.5 + 7.0.7 - - + + - + - - + + - 7.0.5 + 7.0.7 - - + + - 7.0.5 + 7.0.7 - - + + - 7.0.5 + 7.0.7 @@ -502,27 +502,27 @@ - 7.0.5 + 7.0.7 - 7.0.5 + 7.0.7 - 7.0.5 + 7.0.7 - + - 7.0.5 + 7.0.7 @@ -531,151 +531,151 @@ - 7.0.5 + 7.0.7 - + - 7.0.5 + 7.0.7 - 7.0.5 + 7.0.7 - 7.0.5 + 7.0.7 - - + + - - + + - - + + - 7.0.5 + 7.0.7 - - + + - - + + - - + + - - + + - 7.0.5 + 7.0.7 - + - + - + - + - + - 7.0.5 + 7.0.7 - + - + - + - 7.0.5 + 7.0.7 - + - + - + - 7.0.5 + 7.0.7 - + - + - + - 7.0.5 + 7.0.7 - - - - + + + + - 7.0.5 + 7.0.7 @@ -684,60 +684,60 @@ - 7.0.5 + 7.0.7 - 7.0.5 + 7.0.7 - 7.0.5 + 7.0.7 - 7.0.5 + 7.0.7 - + - 7.0.5 + 7.0.7 - 7.0.5 + 7.0.7 - 7.0.5 + 7.0.7 - 7.0.5 + 7.0.7 - 7.0.5 + 7.0.7 - 7.0.5 + 7.0.7 - 7.0.5 + 7.0.7 - 7.0.5 + 7.0.7 @@ -756,29 +756,29 @@ - 7.0.5 + 7.0.7 - + - + - + - 7.0.5 + 7.0.7 @@ -794,46 +794,46 @@ - 7.0.5 + 7.0.7 - + - + - + - + - + - + - 7.0.5 + 7.0.7 - 7.0.5 + 7.0.7 - - - + + + - 7.0.5 + 7.0.7 - 7.0.5 + 7.0.7 @@ -843,7 +843,7 @@ - 7.0.5 + 7.0.7 @@ -852,79 +852,79 @@ - 7.0.5 + 7.0.7 - + - + - + - 7.0.5 + 7.0.7 - + - + - + - 7.0.5 + 7.0.7 - + - + - + - + - + - + - 7.0.5 + 7.0.7 - 7.0.5 + 7.0.7 - + @@ -933,7 +933,7 @@ - + @@ -941,17 +941,17 @@ - + - 7.0.5 + 7.0.7 - 7.0.5 + 7.0.7 @@ -969,13 +969,13 @@ - 7.0.5 + 7.0.7 - 7.0.5 + 7.0.7 - + \ No newline at end of file diff --git a/eng/Baseline.xml b/eng/Baseline.xml index d69619e2d461..b428ac509c8f 100644 --- a/eng/Baseline.xml +++ b/eng/Baseline.xml @@ -4,109 +4,109 @@ This file contains a list of all the packages and their versions which were rele Update this list when preparing for a new patch. --> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index fe08af6e15bd..08c476cea384 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -9,37 +9,37 @@ --> - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 81a09d5f0ca45f744ec8a4b8938f38780bb15a4c + e00be013488a9a56a0e29230be0fe10026dbb209 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 81a09d5f0ca45f744ec8a4b8938f38780bb15a4c + e00be013488a9a56a0e29230be0fe10026dbb209 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 81a09d5f0ca45f744ec8a4b8938f38780bb15a4c + e00be013488a9a56a0e29230be0fe10026dbb209 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 81a09d5f0ca45f744ec8a4b8938f38780bb15a4c + e00be013488a9a56a0e29230be0fe10026dbb209 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 81a09d5f0ca45f744ec8a4b8938f38780bb15a4c + e00be013488a9a56a0e29230be0fe10026dbb209 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 81a09d5f0ca45f744ec8a4b8938f38780bb15a4c + e00be013488a9a56a0e29230be0fe10026dbb209 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 81a09d5f0ca45f744ec8a4b8938f38780bb15a4c + e00be013488a9a56a0e29230be0fe10026dbb209 - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 81a09d5f0ca45f744ec8a4b8938f38780bb15a4c + e00be013488a9a56a0e29230be0fe10026dbb209 https://dev.azure.com/dnceng/internal/_git/dotnet-runtime @@ -177,9 +177,9 @@ https://dev.azure.com/dnceng/internal/_git/dotnet-runtime d099f075e45d2aa6007a22b71b45a08758559f80 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 5b20af47d99620150c53eaf5db8636fdf730b126 + 8e9a17b2216f51a5788f8b1c467a4cf3b769e7d7 https://github.com/dotnet/source-build-externals @@ -262,33 +262,33 @@ https://dev.azure.com/dnceng/internal/_git/dotnet-runtime d099f075e45d2aa6007a22b71b45a08758559f80 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 5b20af47d99620150c53eaf5db8636fdf730b126 + 8e9a17b2216f51a5788f8b1c467a4cf3b769e7d7 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 5b20af47d99620150c53eaf5db8636fdf730b126 + 8e9a17b2216f51a5788f8b1c467a4cf3b769e7d7 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 5b20af47d99620150c53eaf5db8636fdf730b126 + 8e9a17b2216f51a5788f8b1c467a4cf3b769e7d7 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 5b20af47d99620150c53eaf5db8636fdf730b126 + 8e9a17b2216f51a5788f8b1c467a4cf3b769e7d7 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 5b20af47d99620150c53eaf5db8636fdf730b126 + 8e9a17b2216f51a5788f8b1c467a4cf3b769e7d7 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 5b20af47d99620150c53eaf5db8636fdf730b126 + 8e9a17b2216f51a5788f8b1c467a4cf3b769e7d7 https://github.com/dotnet/xdt @@ -298,26 +298,26 @@ - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 5b20af47d99620150c53eaf5db8636fdf730b126 + 8e9a17b2216f51a5788f8b1c467a4cf3b769e7d7 - + https://github.com/dotnet/arcade - 7c5e5a782c67460b123c8e41d484ebcca8002c93 + 59ac824080b9807fd91dbc8a6d2b4447e74874ab - + https://github.com/dotnet/arcade - 7c5e5a782c67460b123c8e41d484ebcca8002c93 + 59ac824080b9807fd91dbc8a6d2b4447e74874ab - + https://github.com/dotnet/arcade - 7c5e5a782c67460b123c8e41d484ebcca8002c93 + 59ac824080b9807fd91dbc8a6d2b4447e74874ab - + https://github.com/dotnet/arcade - 7c5e5a782c67460b123c8e41d484ebcca8002c93 + 59ac824080b9807fd91dbc8a6d2b4447e74874ab diff --git a/eng/Versions.props b/eng/Versions.props index f1c326621af3..2f5daccfadee 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -8,7 +8,7 @@ 7 0 - 7 + 9 false 7.0.0 - 7.0.7 - 7.0.7 - 7.0.7 - 7.0.7 - 7.0.7 - 7.0.7-servicing.23274.4 + 7.0.9 + 7.0.9 + 7.0.9 + 7.0.9 + 7.0.9 + 7.0.9-servicing.23320.18 7.0.0 7.0.0 7.0.0 @@ -103,7 +103,7 @@ 7.0.0 7.0.1 7.0.0 - 7.0.7-servicing.23274.4 + 7.0.9-servicing.23320.18 7.0.0 7.0.2 7.0.0 @@ -123,19 +123,19 @@ 7.0.0 7.0.0 - 7.0.3 + 7.0.4 - 7.0.7 - 7.0.7 - 7.0.7 - 7.0.7 - 7.0.7 - 7.0.7 - 7.0.7 - 7.0.7 + 7.0.9 + 7.0.9 + 7.0.9 + 7.0.9 + 7.0.9 + 7.0.9 + 7.0.9 + 7.0.9 - 7.0.0-beta.23211.2 - 7.0.0-beta.23211.2 + 7.0.0-beta.23313.4 + 7.0.0-beta.23313.4 7.0.0-alpha.1.22505.1 @@ -223,9 +223,9 @@ 3.0.1 11.1.0 6.21.0 - 6.2.0 - 6.2.0 - 6.2.0 + 6.2.4 + 6.2.4 + 6.2.4 5.0.0 5.0.0-alpha.20560.6 5.0.0 @@ -238,7 +238,7 @@ 5.0.17-servicing-22215-7 $(MicrosoftAspNetCoreAzureAppServicesSiteExtension50Version) $(MicrosoftAspNetCoreAzureAppServicesSiteExtension50Version) - 6.0.16-servicing-23174-6 + 6.0.18-servicing-23269-9 $(MicrosoftAspNetCoreAzureAppServicesSiteExtension60Version) $(MicrosoftAspNetCoreAzureAppServicesSiteExtension60Version) diff --git a/eng/common/templates/job/job.yml b/eng/common/templates/job/job.yml index e3ba9398016b..ef337eac55ec 100644 --- a/eng/common/templates/job/job.yml +++ b/eng/common/templates/job/job.yml @@ -24,7 +24,7 @@ parameters: enablePublishBuildAssets: false enablePublishTestResults: false enablePublishUsingPipelines: false - disableComponentGovernance: false + disableComponentGovernance: '' mergeTestResults: false testRunTitle: '' testResultsFormat: '' @@ -73,6 +73,10 @@ jobs: - ${{ if eq(parameters.enableRichCodeNavigation, 'true') }}: - name: EnableRichCodeNavigation value: 'true' + # Retry signature validation up to three times, waiting 2 seconds between attempts. + # See https://learn.microsoft.com/en-us/nuget/reference/errors-and-warnings/nu3028#retry-untrusted-root-failures + - name: NUGET_EXPERIMENTAL_CHAIN_BUILD_RETRY_POLICY + value: 3,2000 - ${{ each variable in parameters.variables }}: # handle name-value variable syntax # example: @@ -81,7 +85,7 @@ jobs: - ${{ if ne(variable.name, '') }}: - name: ${{ variable.name }} value: ${{ variable.value }} - + # handle variable groups - ${{ if ne(variable.group, '') }}: - group: ${{ variable.group }} @@ -142,14 +146,20 @@ jobs: richNavLogOutputDirectory: $(Build.SourcesDirectory)/artifacts/bin continueOnError: true - - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest'), ne(parameters.disableComponentGovernance, 'true')) }}: - - task: ComponentGovernanceComponentDetection@0 - continueOnError: true + - template: /eng/common/templates/steps/component-governance.yml + parameters: + ${{ if eq(parameters.disableComponentGovernance, '') }}: + ${{ if and(ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest'), eq(parameters.runAsPublic, 'false'), or(startsWith(variables['Build.SourceBranch'], 'refs/heads/release/'), startsWith(variables['Build.SourceBranch'], 'refs/heads/dotnet/'), startsWith(variables['Build.SourceBranch'], 'refs/heads/microsoft/'), eq(variables['Build.SourceBranch'], 'refs/heads/main'))) }}: + disableComponentGovernance: false + ${{ else }}: + disableComponentGovernance: true + ${{ else }}: + disableComponentGovernance: ${{ parameters.disableComponentGovernance }} - ${{ if eq(parameters.enableMicrobuild, 'true') }}: - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: - task: MicroBuildCleanup@1 - displayName: Execute Microbuild cleanup tasks + displayName: Execute Microbuild cleanup tasks condition: and(always(), in(variables['_SignType'], 'real', 'test'), eq(variables['Agent.Os'], 'Windows_NT')) continueOnError: ${{ parameters.continueOnError }} env: @@ -217,7 +227,7 @@ jobs: displayName: Publish XUnit Test Results inputs: testResultsFormat: 'xUnit' - testResultsFiles: '*.xml' + testResultsFiles: '*.xml' searchFolder: '$(Build.SourcesDirectory)/artifacts/TestResults/$(_BuildConfig)' testRunTitle: ${{ coalesce(parameters.testRunTitle, parameters.name, '$(System.JobName)') }}-xunit mergeTestResults: ${{ parameters.mergeTestResults }} @@ -228,7 +238,7 @@ jobs: displayName: Publish TRX Test Results inputs: testResultsFormat: 'VSTest' - testResultsFiles: '*.trx' + testResultsFiles: '*.trx' searchFolder: '$(Build.SourcesDirectory)/artifacts/TestResults/$(_BuildConfig)' testRunTitle: ${{ coalesce(parameters.testRunTitle, parameters.name, '$(System.JobName)') }}-trx mergeTestResults: ${{ parameters.mergeTestResults }} diff --git a/eng/common/templates/steps/component-governance.yml b/eng/common/templates/steps/component-governance.yml new file mode 100644 index 000000000000..babc2757d8d1 --- /dev/null +++ b/eng/common/templates/steps/component-governance.yml @@ -0,0 +1,10 @@ +parameters: + disableComponentGovernance: false + +steps: +- ${{ if eq(parameters.disableComponentGovernance, 'true') }}: + - script: "echo ##vso[task.setvariable variable=skipComponentGovernanceDetection]true" + displayName: Set skipComponentGovernanceDetection variable +- ${{ if ne(parameters.disableComponentGovernance, 'true') }}: + - task: ComponentGovernanceComponentDetection@0 + continueOnError: true \ No newline at end of file diff --git a/global.json b/global.json index 9962c52e84f1..afc8fc984bd9 100644 --- a/global.json +++ b/global.json @@ -1,9 +1,9 @@ { "sdk": { - "version": "7.0.105" + "version": "7.0.107" }, "tools": { - "dotnet": "7.0.105", + "dotnet": "7.0.107", "runtimes": { "dotnet/x86": [ "$(MicrosoftNETCoreBrowserDebugHostTransportVersion)" @@ -27,7 +27,7 @@ }, "msbuild-sdks": { "Yarn.MSBuild": "1.22.10", - "Microsoft.DotNet.Arcade.Sdk": "7.0.0-beta.23211.2", - "Microsoft.DotNet.Helix.Sdk": "7.0.0-beta.23211.2" + "Microsoft.DotNet.Arcade.Sdk": "7.0.0-beta.23313.4", + "Microsoft.DotNet.Helix.Sdk": "7.0.0-beta.23313.4" } } diff --git a/src/Identity/Core/src/SignInManager.cs b/src/Identity/Core/src/SignInManager.cs index 6a4fe1c09b9a..269230364cb8 100644 --- a/src/Identity/Core/src/SignInManager.cs +++ b/src/Identity/Core/src/SignInManager.cs @@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Security.Claims; +using System.Text; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; @@ -388,7 +389,14 @@ public virtual async Task CheckPasswordSignInAsync(TUser user, str // Only reset the lockout when not in quirks mode if either TFA is not enabled or the client is remembered for TFA. if (alwaysLockout || !await IsTfaEnabled(user) || await IsTwoFactorClientRememberedAsync(user)) { - await ResetLockout(user); + var resetLockoutResult = await ResetLockoutWithResult(user); + if (!resetLockoutResult.Succeeded) + { + // ResetLockout got an unsuccessful result that could be caused by concurrency failures indicating an + // attacker could be trying to bypass the MaxFailedAccessAttempts limit. Return the same failure we do + // when failing to increment the lockout to avoid giving an attacker extra guesses at the password. + return SignInResult.Failed; + } } return SignInResult.Success; @@ -398,7 +406,13 @@ public virtual async Task CheckPasswordSignInAsync(TUser user, str if (UserManager.SupportsUserLockout && lockoutOnFailure) { // If lockout is requested, increment access failed count which might lock out the user - await UserManager.AccessFailedAsync(user); + var incrementLockoutResult = await UserManager.AccessFailedAsync(user) ?? IdentityResult.Success; + if (!incrementLockoutResult.Succeeded) + { + // Return the same failure we do when resetting the lockout fails after a correct password. + return SignInResult.Failed; + } + if (await UserManager.IsLockedOutAsync(user)) { return await LockedOut(user); @@ -467,18 +481,23 @@ public virtual async Task TwoFactorRecoveryCodeSignInAsync(string var result = await UserManager.RedeemTwoFactorRecoveryCodeAsync(user, recoveryCode); if (result.Succeeded) { - await DoTwoFactorSignInAsync(user, twoFactorInfo, isPersistent: false, rememberClient: false); - return SignInResult.Success; + return await DoTwoFactorSignInAsync(user, twoFactorInfo, isPersistent: false, rememberClient: false); } // We don't protect against brute force attacks since codes are expected to be random. return SignInResult.Failed; } - private async Task DoTwoFactorSignInAsync(TUser user, TwoFactorAuthenticationInfo twoFactorInfo, bool isPersistent, bool rememberClient) + private async Task DoTwoFactorSignInAsync(TUser user, TwoFactorAuthenticationInfo twoFactorInfo, bool isPersistent, bool rememberClient) { - // When token is verified correctly, clear the access failed count used for lockout - await ResetLockout(user); + var resetLockoutResult = await ResetLockoutWithResult(user); + if (!resetLockoutResult.Succeeded) + { + // ResetLockout got an unsuccessful result that could be caused by concurrency failures indicating an + // attacker could be trying to bypass the MaxFailedAccessAttempts limit. Return the same failure we do + // when failing to increment the lockout to avoid giving an attacker extra guesses at the two factor code. + return SignInResult.Failed; + } var claims = new List(); claims.Add(new Claim("amr", "mfa")); @@ -496,6 +515,7 @@ private async Task DoTwoFactorSignInAsync(TUser user, TwoFactorAuthenticationInf await RememberTwoFactorClientAsync(user); } await SignInWithClaimsAsync(user, isPersistent, claims); + return SignInResult.Success; } /// @@ -528,13 +548,18 @@ public virtual async Task TwoFactorAuthenticatorSignInAsync(string if (await UserManager.VerifyTwoFactorTokenAsync(user, Options.Tokens.AuthenticatorTokenProvider, code)) { - await DoTwoFactorSignInAsync(user, twoFactorInfo, isPersistent, rememberClient); - return SignInResult.Success; + return await DoTwoFactorSignInAsync(user, twoFactorInfo, isPersistent, rememberClient); } // If the token is incorrect, record the failure which also may cause the user to be locked out if (UserManager.SupportsUserLockout) { - await UserManager.AccessFailedAsync(user); + var incrementLockoutResult = await UserManager.AccessFailedAsync(user) ?? IdentityResult.Success; + if (!incrementLockoutResult.Succeeded) + { + // Return the same failure we do when resetting the lockout fails after a correct two factor code. + // This is currently redundant, but it's here in case the code gets copied elsewhere. + return SignInResult.Failed; + } } return SignInResult.Failed; } @@ -569,13 +594,18 @@ public virtual async Task TwoFactorSignInAsync(string provider, st } if (await UserManager.VerifyTwoFactorTokenAsync(user, provider, code)) { - await DoTwoFactorSignInAsync(user, twoFactorInfo, isPersistent, rememberClient); - return SignInResult.Success; + return await DoTwoFactorSignInAsync(user, twoFactorInfo, isPersistent, rememberClient); } // If the token is incorrect, record the failure which also may cause the user to be locked out if (UserManager.SupportsUserLockout) { - await UserManager.AccessFailedAsync(user); + var incrementLockoutResult = await UserManager.AccessFailedAsync(user) ?? IdentityResult.Success; + if (!incrementLockoutResult.Succeeded) + { + // Return the same failure we do when resetting the lockout fails after a correct two factor code. + // This is currently redundant, but it's here in case the code gets copied elsewhere. + return SignInResult.Failed; + } } return SignInResult.Failed; } @@ -867,13 +897,77 @@ protected virtual Task LockedOut(TUser user) /// /// The user /// The that represents the asynchronous operation, containing the of the operation. - protected virtual Task ResetLockout(TUser user) + protected virtual async Task ResetLockout(TUser user) { if (UserManager.SupportsUserLockout) { - return UserManager.ResetAccessFailedCountAsync(user); + // The IdentityResult should not be null according to the annotations, but our own tests return null and I'm trying to limit breakages. + var result = await UserManager.ResetAccessFailedCountAsync(user) ?? IdentityResult.Success; + + if (!result.Succeeded) + { + throw new IdentityResultException(result); + } + } + } + + private async Task ResetLockoutWithResult(TUser user) + { + // Avoid relying on throwing an exception if we're not in a derived class. + if (GetType() == typeof(SignInManager)) + { + if (!UserManager.SupportsUserLockout) + { + return IdentityResult.Success; + } + + return await UserManager.ResetAccessFailedCountAsync(user) ?? IdentityResult.Success; + } + + try + { + var resetLockoutTask = ResetLockout(user); + + if (resetLockoutTask is Task resultTask) + { + return await resultTask ?? IdentityResult.Success; + } + + await resetLockoutTask; + return IdentityResult.Success; + } + catch (IdentityResultException ex) + { + return ex.IdentityResult; + } + } + + private sealed class IdentityResultException : Exception + { + internal IdentityResultException(IdentityResult result) : base() + { + IdentityResult = result; + } + + internal IdentityResult IdentityResult { get; set; } + + public override string Message + { + get + { + var sb = new StringBuilder("ResetLockout failed."); + + foreach (var error in IdentityResult.Errors) + { + sb.AppendLine(); + sb.Append(error.Code); + sb.Append(": "); + sb.Append(error.Description); + } + + return sb.ToString(); + } } - return Task.CompletedTask; } internal sealed class TwoFactorAuthenticationInfo diff --git a/src/Identity/test/Identity.Test/SignInManagerTest.cs b/src/Identity/test/Identity.Test/SignInManagerTest.cs index 147c16e31de1..81cd53ce5b4d 100644 --- a/src/Identity/test/Identity.Test/SignInManagerTest.cs +++ b/src/Identity/test/Identity.Test/SignInManagerTest.cs @@ -975,4 +975,234 @@ public async Task ExternalLoginInfoAsyncReturnsAuthenticationPropertiesWithCusto Assert.NotNull(externalProperties); Assert.Equal("fizzbuzz", customValue); } + + public static object[][] SignInManagerTypeNames => new object[][] + { + new[] { nameof(SignInManager) }, + new[] { nameof(NoOverridesSignInManager) }, + new[] { nameof(OverrideAndAwaitBaseResetSignInManager) }, + new[] { nameof(OverrideAndPassThroughUserManagerResetSignInManager) }, + }; + + [Theory] + [MemberData(nameof(SignInManagerTypeNames))] + public async Task CheckPasswordSignInFailsWhenResetLockoutFails(string signInManagerTypeName) + { + // Setup + var user = new PocoUser { UserName = "Foo" }; + var manager = SetupUserManager(user); + manager.Setup(m => m.SupportsUserLockout).Returns(true).Verifiable(); + manager.Setup(m => m.IsLockedOutAsync(user)).ReturnsAsync(false).Verifiable(); + manager.Setup(m => m.CheckPasswordAsync(user, "[PLACEHOLDER]-1a")).ReturnsAsync(true).Verifiable(); + manager.Setup(m => m.ResetAccessFailedCountAsync(user)).ReturnsAsync(IdentityResult.Failed()).Verifiable(); + + var context = new DefaultHttpContext(); + var helper = SetupSignInManagerType(manager.Object, context, signInManagerTypeName); + + // Act + var result = await helper.CheckPasswordSignInAsync(user, "[PLACEHOLDER]-1a", false); + + // Assert + Assert.Same(SignInResult.Failed, result); + manager.Verify(); + } + + [Theory] + [MemberData(nameof(SignInManagerTypeNames))] + public async Task PasswordSignInWorksWhenResetLockoutReturnsNullIdentityResult(string signInManagerTypeName) + { + // Setup + var user = new PocoUser { UserName = "Foo" }; + var manager = SetupUserManager(user); + manager.Setup(m => m.SupportsUserLockout).Returns(true).Verifiable(); + manager.Setup(m => m.IsLockedOutAsync(user)).ReturnsAsync(false).Verifiable(); + manager.Setup(m => m.CheckPasswordAsync(user, "[PLACEHOLDER]-1a")).ReturnsAsync(true).Verifiable(); + manager.Setup(m => m.ResetAccessFailedCountAsync(user)).ReturnsAsync((IdentityResult)null).Verifiable(); + + var context = new DefaultHttpContext(); + var auth = MockAuth(context); + SetupSignIn(context, auth); + var helper = SetupSignInManagerType(manager.Object, context, signInManagerTypeName); + + // Act + var result = await helper.PasswordSignInAsync(user.UserName, "[PLACEHOLDER]-1a", false, false); + + // Assert + Assert.True(result.Succeeded); + manager.Verify(); + auth.Verify(); + } + + [Fact] + public async Task TwoFactorSignFailsWhenResetLockoutFails() + { + // Setup + var user = new PocoUser { UserName = "Foo" }; + var manager = SetupUserManager(user); + var provider = "twofactorprovider"; + var code = "123456"; + manager.Setup(m => m.SupportsUserLockout).Returns(true).Verifiable(); + manager.Setup(m => m.IsLockedOutAsync(user)).ReturnsAsync(false).Verifiable(); + manager.Setup(m => m.VerifyTwoFactorTokenAsync(user, provider, code)).ReturnsAsync(true).Verifiable(); + + manager.Setup(m => m.ResetAccessFailedCountAsync(user)).ReturnsAsync(IdentityResult.Failed()).Verifiable(); + + var context = new DefaultHttpContext(); + var auth = MockAuth(context); + var helper = SetupSignInManager(manager.Object, context); + var id = SignInManager.StoreTwoFactorInfo(user.Id, null); + auth.Setup(a => a.AuthenticateAsync(context, IdentityConstants.TwoFactorUserIdScheme)) + .ReturnsAsync(AuthenticateResult.Success(new AuthenticationTicket(id, null, IdentityConstants.TwoFactorUserIdScheme))).Verifiable(); + + // Act + var result = await helper.TwoFactorSignInAsync(provider, code, false, false); + + // Assert + Assert.Same(SignInResult.Failed, result); + manager.Verify(); + auth.Verify(); + } + + public static object[][] ExpectedLockedOutSignInResultsGivenAccessFailedResults => new object[][] + { + new object[] { IdentityResult.Success, SignInResult.LockedOut }, + new object[] { null, SignInResult.LockedOut }, + new object[] { IdentityResult.Failed(), SignInResult.Failed }, + }; + + [Theory] + [MemberData(nameof(ExpectedLockedOutSignInResultsGivenAccessFailedResults))] + public async Task CheckPasswordSignInLockedOutResultIsDependentOnTheAccessFailedAsyncResult(IdentityResult accessFailedResult, SignInResult expectedSignInResult) + { + // Setup + var isLockedOutCallCount = 0; + var user = new PocoUser { UserName = "Foo" }; + var manager = SetupUserManager(user); + manager.Setup(m => m.SupportsUserLockout).Returns(true).Verifiable(); + // Return false initially to allow the password to be checked Only return true the second time after the bogus password is checked. + manager.Setup(m => m.IsLockedOutAsync(user)).ReturnsAsync(() => isLockedOutCallCount++ > 0).Verifiable(); + manager.Setup(m => m.CheckPasswordAsync(user, "[PLACEHOLDER]-bogus1")).ReturnsAsync(false).Verifiable(); + manager.Setup(m => m.AccessFailedAsync(user)).ReturnsAsync(accessFailedResult).Verifiable(); + + var context = new DefaultHttpContext(); + // Since the PasswordSignInAsync calls the UserManager directly rather than a virtual SignInManager method like ResetLockout, we don't need to test derived SignInManagers. + var helper = SetupSignInManager(manager.Object, context); + + // Act + var result = await helper.CheckPasswordSignInAsync(user, "[PLACEHOLDER]-bogus1", lockoutOnFailure: true); + + // Assert + Assert.Same(expectedSignInResult, result); + manager.Verify(); + } + + public static object[][] AccessFailedResults => new object[][] + { + new object[] { IdentityResult.Success }, + new object[] { null }, + new object[] { IdentityResult.Failed() }, + }; + + [Theory] + [MemberData(nameof(AccessFailedResults))] + public async Task TwoFactorSignInLockedOutResultIsAlwaysGenericFailureRegardlessOfTheAccessFailedAsyncResult(IdentityResult accessFailedResult) + { + // Setup + var isLockedOutCallCount = 0; + var user = new PocoUser { UserName = "Foo" }; + var manager = SetupUserManager(user); + var provider = "twofactorprovider"; + var code = "123456"; + manager.Setup(m => m.SupportsUserLockout).Returns(true).Verifiable(); + // Return false initially to allow the 2fa code to be checked. Only return true if ever in the future it is called again after failure. + manager.Setup(m => m.IsLockedOutAsync(user)).ReturnsAsync(() => isLockedOutCallCount++ > 0).Verifiable(); + manager.Setup(m => m.VerifyTwoFactorTokenAsync(user, provider, code)).ReturnsAsync(false).Verifiable(); + + manager.Setup(m => m.AccessFailedAsync(user)).ReturnsAsync(accessFailedResult).Verifiable(); + + var context = new DefaultHttpContext(); + var auth = MockAuth(context); + var helper = SetupSignInManager(manager.Object, context); + var id = SignInManager.StoreTwoFactorInfo(user.Id, null); + auth.Setup(a => a.AuthenticateAsync(context, IdentityConstants.TwoFactorUserIdScheme)) + .ReturnsAsync(AuthenticateResult.Success(new AuthenticationTicket(id, null, IdentityConstants.TwoFactorUserIdScheme))).Verifiable(); + + // Act + var result = await helper.TwoFactorSignInAsync(provider, code, false, false); + + // Assert + // Unlike password sign in, 2fa always returns SignInResult.Failed rather than LockedOut. + Assert.Same(SignInResult.Failed, result); + manager.Verify(); + auth.Verify(); + } + + private static SignInManager SetupSignInManagerType(UserManager manager, HttpContext context, string typeName) + { + var contextAccessor = new Mock(); + contextAccessor.Setup(a => a.HttpContext).Returns(context); + var roleManager = MockHelpers.MockRoleManager(); + var options = Options.Create(new IdentityOptions()); + var claimsFactory = new UserClaimsPrincipalFactory(manager, roleManager.Object, options); + + return typeName switch + { + nameof(SignInManager) => new SignInManager(manager, contextAccessor.Object, claimsFactory, options, NullLogger>.Instance, Mock.Of(), new DefaultUserConfirmation()), + nameof(NoOverridesSignInManager) => new NoOverridesSignInManager(manager, contextAccessor.Object, claimsFactory, options), + nameof(OverrideAndAwaitBaseResetSignInManager) => new OverrideAndAwaitBaseResetSignInManager(manager, contextAccessor.Object, claimsFactory, options), + nameof(OverrideAndPassThroughUserManagerResetSignInManager) => new OverrideAndPassThroughUserManagerResetSignInManager(manager, contextAccessor.Object, claimsFactory, options), + _ => throw new NotImplementedException(), + }; + } + + private class NoOverridesSignInManager : SignInManager where TUser : class + { + public NoOverridesSignInManager( + UserManager userManager, + IHttpContextAccessor contextAccessor, + IUserClaimsPrincipalFactory claimsFactory, + IOptions optionsAccessor) + : base(userManager, contextAccessor, claimsFactory, optionsAccessor, NullLogger>.Instance, Mock.Of(), new DefaultUserConfirmation()) + { + } + } + + private class OverrideAndAwaitBaseResetSignInManager : SignInManager where TUser : class + { + public OverrideAndAwaitBaseResetSignInManager( + UserManager userManager, + IHttpContextAccessor contextAccessor, + IUserClaimsPrincipalFactory claimsFactory, + IOptions optionsAccessor) + : base(userManager, contextAccessor, claimsFactory, optionsAccessor, NullLogger>.Instance, Mock.Of(), new DefaultUserConfirmation()) + { + } + + protected override async Task ResetLockout(TUser user) + { + await base.ResetLockout(user); + } + } + + private class OverrideAndPassThroughUserManagerResetSignInManager : SignInManager where TUser : class + { + public OverrideAndPassThroughUserManagerResetSignInManager( + UserManager userManager, + IHttpContextAccessor contextAccessor, + IUserClaimsPrincipalFactory claimsFactory, + IOptions optionsAccessor) + : base(userManager, contextAccessor, claimsFactory, optionsAccessor, NullLogger>.Instance, Mock.Of(), new DefaultUserConfirmation()) + { + } + + protected override Task ResetLockout(TUser user) + { + if (UserManager.SupportsUserLockout) + { + return UserManager.ResetAccessFailedCountAsync(user); + } + + return Task.CompletedTask; + } + } } diff --git a/src/submodules/googletest b/src/submodules/googletest index 797b0ad2a3a4..04cf2989168a 160000 --- a/src/submodules/googletest +++ b/src/submodules/googletest @@ -1 +1 @@ -Subproject commit 797b0ad2a3a45608ecf5c67e6e289d377a3521ca +Subproject commit 04cf2989168a3f9218d463bea6f15f8ade2032fd diff --git a/src/submodules/spa-templates b/src/submodules/spa-templates index d5cae287824e..68cd408c62c6 160000 --- a/src/submodules/spa-templates +++ b/src/submodules/spa-templates @@ -1 +1 @@ -Subproject commit d5cae287824ec8bb94f1cf5514af3e89364864c9 +Subproject commit 68cd408c62c6a881a58f41a6f6f5f8b73b6ae515