diff options
| author | Leitwolf11 | 2026-10-16 16:13:45 +0300 |
|---|---|---|
| committer | Leitwolf11 | 2026-10-16 16:13:45 +0300 |
| commit | 031cbf7efc5d97b28259da5b387cb3a3dbd242d5 (patch) | |
| tree | 44077b255fdf019e82d6960ba30e70c3a59a954a /apps/api-tests | |
| parent | 8f58831373b72dc5b60bad649a961acd0bb94aa4 (diff) | |
| download | progmaweb-031cbf7efc5d97b28259da5b387cb3a3dbd242d5.tar.gz | |
Log why a Google callback could not finish
A failed Google callback has returned the visitor to /login?google=failed
since the previous release, but nothing recorded why: a declined consent,
an expired or missing state cookie and a forged request all looked the
same, and the log of the service held no entry at all.
GoogleSignInFailure now handles OnRemoteFailure. It writes one warning,
event GoogleSignInFailed (id 5), with the cause: the error code Google
reported, such as access_denied, or the reason the handler refused the
callback, such as "The oauth state was missing or invalid." An error code
is written only when it is a short run of letters, digits, '_', '-' and
'.'; anything else from the query string is written as an unrecognized
error. The authorization code, the state and tokens are never logged.
The redirect is unchanged.
Four new tests cover the entry, the redirect, an unusual error value and
a refused state. The account API document describes the log entry.
Diffstat (limited to 'apps/api-tests')
| -rw-r--r-- | apps/api-tests/GoogleSignInFailureTests.cs | 65 |
1 files changed, 65 insertions, 0 deletions
diff --git a/apps/api-tests/GoogleSignInFailureTests.cs b/apps/api-tests/GoogleSignInFailureTests.cs new file mode 100644 index 0000000..7c44d5c --- /dev/null +++ b/apps/api-tests/GoogleSignInFailureTests.cs @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> +// SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.1 + +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Google; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Progmasoft.Progmaweb.Api.Accounts; + +namespace Progmasoft.Progmaweb.Api.Tests; + +public sealed class GoogleSignInFailureTests +{ + private sealed class RecordingLogger : ILogger + { + public List<(LogLevel Level, EventId Event, string Message)> Entries { get; } = []; + + public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null; + + public bool IsEnabled(LogLevel logLevel) => true; + + public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, + Func<TState, Exception?, string> formatter) => Entries.Add((logLevel, eventId, formatter(state, exception))); + } + + private static RemoteFailureContext Failure(string query, Exception failure) + { + DefaultHttpContext http = new(); + http.Request.QueryString = new QueryString(query); + AuthenticationScheme scheme = new(GoogleDefaults.AuthenticationScheme, null, typeof(GoogleHandler)); + return new RemoteFailureContext(http, scheme, new GoogleOptions(), failure); + } + + [Fact] + public async Task AFailedCallbackIsLoggedAndReturnsToTheSignInPage() + { + RecordingLogger logger = new(); + RemoteFailureContext context = Failure( + "?state=secret-state&code=secret-code", + new AuthenticationFailureException( + "An error was encountered while handling the remote login.", + new AuthenticationFailureException("The oauth state was missing or invalid."))); + + await GoogleSignInFailure.HandleAsync(context, logger); + + Assert.Equal("/login?google=failed", context.Response.Headers.Location.ToString()); + Assert.True(context.Result?.Handled); + (LogLevel level, EventId eventId, string message) = Assert.Single(logger.Entries); + Assert.Equal(LogLevel.Warning, level); + Assert.Equal("GoogleSignInFailed", eventId.Name); + Assert.Equal("Google sign-in could not be completed: The oauth state was missing or invalid.", message); + Assert.DoesNotContain("secret", message, StringComparison.Ordinal); + } + + [Theory] + [InlineData("?error=access_denied&state=s", "Google reported access_denied")] + [InlineData("?error=%3Cscript%3E&state=s", "Google reported an unrecognized error")] + [InlineData("?state=s", "the denied consent")] + public void TheReasonNamesWhatGoogleReportedOrWhyTheCallbackWasRefused(string query, string expected) + { + RemoteFailureContext context = Failure(query, new AuthenticationFailureException("the denied consent")); + + Assert.Equal(expected, GoogleSignInFailure.Reason(context)); + } +} |