Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extract oauth error information and fail early if it exists #3266

Merged
20 changes: 19 additions & 1 deletion src/main/java/com/faforever/client/login/LoginController.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,17 @@
import java.net.URI;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.regex.Pattern;

@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Slf4j
@RequiredArgsConstructor
public class LoginController extends NodeController<Pane> {

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 OperatingSystem operatingSystem;
private final GameRunner gameRunner;
private final LoginService loginService;
Expand Down Expand Up @@ -279,6 +283,20 @@ private Mono<Void> loginWithCode(String code, URI redirectUri, String codeVerifi
return loginService.login(code, codeVerifier, redirectUri);
}

private String getLoginErrorSpecificCause(Throwable throwable) {
String cause = throwable.getMessage();
if (cause == null) {
cause = "";
}
if (ERROR_SCOPE_DENIED.matcher(cause).find()) {
return "login.scopeDenied";
}
if (ERROR_NO_CSRF.matcher(cause).find()) {
return "login.noCSRF";
}
return "login.failed";
}

private Void onLoginFailed(Throwable throwable) {
if (loginService.getOwnUser() != null && loginService.getOwnPlayer() != null) {
log.info("Previous login request failed but user is already logged in", throwable);
Expand All @@ -294,7 +312,7 @@ private Void onLoginFailed(Throwable throwable) {
List.of(new DismissAction(i18n))));
} else {
log.error("Could not log in", throwable);
notificationService.addImmediateErrorNotification(throwable, "login.failed");
notificationService.addImmediateErrorNotification(throwable, getLoginErrorSpecificCause(throwable));
}

showLoginForm();
Expand Down
18 changes: 18 additions & 0 deletions src/main/java/com/faforever/client/login/OAuthValuesReceiver.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -33,6 +34,8 @@ 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_DESCRIPTION_PATTERN = Pattern.compile("error_description=([^ &]+)");

private final PlatformService platformService;
private final LoginService loginService;
Expand Down Expand Up @@ -89,6 +92,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;
Expand Down Expand Up @@ -147,5 +151,19 @@ private String extractValue(String request, Pattern pattern) {
return matcher.group(1);
}

private void checkForError(String request) {
String decodedRequest = URLDecoder.decode(request, StandardCharsets.UTF_8);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of decoding twice I was thinking we can decode the request string once and change the regex patterns, thoughts @Sheikah45 ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I think that would be best

Matcher matcher = ERROR_PATTERN.matcher(request);
if (matcher.find()) {
Matcher errorDescriptionMatcher = ERROR_DESCRIPTION_PATTERN.matcher(request);
if (errorDescriptionMatcher.find()) {
String decodedDescription = URLDecoder.decode(errorDescriptionMatcher.group(1), StandardCharsets.UTF_8);
throw new IllegalStateException("Login failed with error '" + matcher.group(1) + "' and description: " + decodedDescription + ". The full request is: " + decodedRequest);
} else {
throw new IllegalStateException("Login failed with error '" + matcher.group(1) + "'. The full request is: " + decodedRequest);
}
}
}

public record Values(String code, String state, URI redirectUri) {}
}
2 changes: 2 additions & 0 deletions src/main/resources/i18n/messages.properties
Original file line number Diff line number Diff line change
Expand Up @@ -1088,6 +1088,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
Expand Down
26 changes: 26 additions & 0 deletions src/test/java/com/faforever/client/login/LoginControllerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,32 @@ public void testLoginFailsNoPorts() throws Exception {
assertTrue(instance.loginFormPane.isVisible());
}

@Test
public void testLoginFailsScopeDenied() throws Exception {
when(oAuthValuesReceiver.receiveValues(anyString(), anyString()))
.thenReturn(CompletableFuture.failedFuture(new IllegalStateException("scope_denied")));

instance.onLoginButtonClicked();
WaitForAsyncUtils.waitForFxEvents();

verify(notificationService).addImmediateErrorNotification(any(), eq("login.scopeDenied"));
assertFalse(instance.loginProgressPane.isVisible());
assertTrue(instance.loginFormPane.isVisible());
}

@Test
public void testLoginFailsNoCSRF() throws Exception {
when(oAuthValuesReceiver.receiveValues(anyString(), anyString()))
.thenReturn(CompletableFuture.failedFuture(new IllegalStateException("No CSRF value")));

instance.onLoginButtonClicked();
WaitForAsyncUtils.waitForFxEvents();

verify(notificationService).addImmediateErrorNotification(any(), eq("login.noCSRF"));
assertFalse(instance.loginProgressPane.isVisible());
assertTrue(instance.loginFormPane.isVisible());
}

@Test
public void testLoginFailsTimeout() throws Exception {
when(oAuthValuesReceiver.receiveValues(anyString(), anyString()))
Expand Down
Loading