diff --git a/src/main/java/com/faforever/client/login/KnownLoginErrorException.java b/src/main/java/com/faforever/client/login/KnownLoginErrorException.java new file mode 100644 index 0000000000..b27944b678 --- /dev/null +++ b/src/main/java/com/faforever/client/login/KnownLoginErrorException.java @@ -0,0 +1,13 @@ +package com.faforever.client.login; + +import lombok.Getter; + +@Getter +public class KnownLoginErrorException extends RuntimeException { + private final String i18nKey; + + public KnownLoginErrorException(String message, String i18nKey) { + super(message); + this.i18nKey = i18nKey; + } +} diff --git a/src/main/java/com/faforever/client/login/LoginController.java b/src/main/java/com/faforever/client/login/LoginController.java index 704ec509cc..6c916226e4 100644 --- a/src/main/java/com/faforever/client/login/LoginController.java +++ b/src/main/java/com/faforever/client/login/LoginController.java @@ -55,7 +55,6 @@ @Slf4j @RequiredArgsConstructor public class LoginController extends NodeController { - private final OperatingSystem operatingSystem; private final GameRunner gameRunner; private final LoginService loginService; @@ -292,6 +291,8 @@ private Void onLoginFailed(Throwable throwable) { notificationService.addNotification( new ServerNotification(i18n.get("login.failed"), loginException.getMessage(), Severity.ERROR, List.of(new DismissAction(i18n)))); + } else if (throwable instanceof KnownLoginErrorException loginException) { + notificationService.addImmediateErrorNotification(throwable, loginException.getI18nKey()); } else { log.error("Could not log in", throwable); notificationService.addImmediateErrorNotification(throwable, "login.failed"); diff --git a/src/main/java/com/faforever/client/login/OAuthValuesReceiver.java b/src/main/java/com/faforever/client/login/OAuthValuesReceiver.java index 97655c7b20..599bc5e7f0 100644 --- a/src/main/java/com/faforever/client/login/OAuthValuesReceiver.java +++ b/src/main/java/com/faforever/client/login/OAuthValuesReceiver.java @@ -19,6 +19,7 @@ import java.net.ServerSocket; import java.net.Socket; import java.net.URI; +import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; @@ -33,6 +34,9 @@ public class OAuthValuesReceiver { private static final Pattern CODE_PATTERN = Pattern.compile("code=([^ &]+)"); private static final Pattern STATE_PATTERN = Pattern.compile("state=([^ &]+)"); + private static final Pattern ERROR_PATTERN = Pattern.compile("error=([^ &]+)"); + private static final Pattern ERROR_SCOPE_DENIED = Pattern.compile("scope_denied"); + private static final Pattern ERROR_NO_CSRF = Pattern.compile("No\\+CSRF\\+value"); private final PlatformService platformService; private final LoginService loginService; @@ -89,6 +93,7 @@ private Values readValues(String state, String codeVerifier) { // Do not try with resources as the socket needs to stay open. try { + checkForError(request); Values values = readValues(request, redirectUri); success = true; return values; @@ -139,13 +144,32 @@ private Values readValues(String request, URI redirectUri) { return new Values(code, state, redirectUri); } + private String formatRequest(String request) { + return URLDecoder.decode(request, StandardCharsets.UTF_8); + } + private String extractValue(String request, Pattern pattern) { Matcher matcher = pattern.matcher(request); if (!matcher.find()) { - throw new IllegalStateException("Could not extract value with pattern '" + pattern + "' from: " + request); + throw new IllegalStateException("Could not extract value with pattern '" + pattern + "' from: " + formatRequest(request)); } return matcher.group(1); } + private void checkForError(String request) { + Matcher matcher = ERROR_PATTERN.matcher(request); + if (matcher.find()) { + String errorMessage = "Login failed with error '" + matcher.group(1) + "'. The full request is: " + formatRequest(request); + if (ERROR_SCOPE_DENIED.matcher(request).find()) { + throw new KnownLoginErrorException(errorMessage, "login.scopeDenied"); + } + + if (ERROR_NO_CSRF.matcher(request).find()) { + throw new KnownLoginErrorException(errorMessage, "login.noCSRF"); + } + throw new IllegalStateException(errorMessage); + } + } + public record Values(String code, String state, URI redirectUri) {} } diff --git a/src/main/resources/i18n/messages.properties b/src/main/resources/i18n/messages.properties index 58470cc31b..55f1bc6227 100644 --- a/src/main/resources/i18n/messages.properties +++ b/src/main/resources/i18n/messages.properties @@ -1089,6 +1089,8 @@ map.all = All Versions map.current = Current Version login.remember = Remember Me login.failed = Error occurred during login +login.scopeDenied = Login failed. You did not accept the scopes on the login page. +login.noCSRF = Login failed. Likely your login timed out, please try again. login.badState = State returned by user service does not match initial state if this continues to occur please reach out to technical help on the forum or discord channel login.oauthBaseUrl = OAuth base URL session.expired.title = Session Expired diff --git a/src/test/java/com/faforever/client/login/LoginControllerTest.java b/src/test/java/com/faforever/client/login/LoginControllerTest.java index 029cb6d7f0..ba18fabf19 100644 --- a/src/test/java/com/faforever/client/login/LoginControllerTest.java +++ b/src/test/java/com/faforever/client/login/LoginControllerTest.java @@ -160,7 +160,7 @@ public void testLoginFails() throws Exception { @Test public void testLoginFailsNoPorts() throws Exception { when(oAuthValuesReceiver.receiveValues(anyString(), anyString())) - .thenReturn(CompletableFuture.failedFuture(new IllegalStateException())); + .thenReturn(CompletableFuture.failedFuture(new IllegalStateException(""))); instance.onLoginButtonClicked(); WaitForAsyncUtils.waitForFxEvents(); @@ -170,6 +170,19 @@ public void testLoginFailsNoPorts() throws Exception { assertTrue(instance.loginFormPane.isVisible()); } + @Test + public void testLoginFailsKnownError() throws Exception { + when(oAuthValuesReceiver.receiveValues(anyString(), anyString())) + .thenReturn(CompletableFuture.failedFuture(new KnownLoginErrorException("", "login.known"))); + + instance.onLoginButtonClicked(); + WaitForAsyncUtils.waitForFxEvents(); + + verify(notificationService).addImmediateErrorNotification(any(), eq("login.known")); + assertFalse(instance.loginProgressPane.isVisible()); + assertTrue(instance.loginFormPane.isVisible()); + } + @Test public void testLoginFailsTimeout() throws Exception { when(oAuthValuesReceiver.receiveValues(anyString(), anyString())) @@ -236,7 +249,7 @@ public void testLoginRefreshFails() { .thenReturn(CompletableFuture.completedFuture(ClientConfigurationBuilder.create().defaultValues().get())); loginPrefs.setRememberMe(true); loginPrefs.setRefreshToken("abc"); - when(loginService.loginWithRefreshToken()).thenReturn(Mono.error(new Exception())); + when(loginService.loginWithRefreshToken()).thenReturn(Mono.error(new Exception(""))); runOnFxThreadAndWait(() -> reinitialize(instance)); verify(loginService).loginWithRefreshToken(); assertFalse(instance.loginProgressPane.isVisible());