Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package com.appsmith.server.authentication.handlers;

import com.appsmith.server.authentication.handlers.ce.AuthenticationFailureHandlerCE;
import lombok.RequiredArgsConstructor;
import com.appsmith.server.authentication.helpers.AuthenticationFailureRetryHandler;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class AuthenticationFailureHandler extends AuthenticationFailureHandlerCE {}
public class AuthenticationFailureHandler extends AuthenticationFailureHandlerCE {

public AuthenticationFailureHandler(AuthenticationFailureRetryHandler authenticationFailureRetryHandler) {
super(authenticationFailureRetryHandler);
}
}
Original file line number Diff line number Diff line change
@@ -1,110 +1,21 @@
package com.appsmith.server.authentication.handlers.ce;

import com.appsmith.server.constants.Security;
import com.appsmith.server.exceptions.AppsmithError;
import com.appsmith.server.authentication.helpers.AuthenticationFailureRetryHandler;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.web.server.DefaultServerRedirectStrategy;
import org.springframework.security.web.server.ServerRedirectStrategy;
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;

import static com.appsmith.server.helpers.RedirectHelper.REDIRECT_URL_QUERY_PARAM;

@Slf4j
@RequiredArgsConstructor
public class AuthenticationFailureHandlerCE implements ServerAuthenticationFailureHandler {

private final ServerRedirectStrategy redirectStrategy = new DefaultServerRedirectStrategy();
private final AuthenticationFailureRetryHandler authenticationFailureRetryHandler;

@Override
public Mono<Void> onAuthenticationFailure(WebFilterExchange webFilterExchange, AuthenticationException exception) {
log.error("In the login failure handler. Cause: {}", exception.getMessage(), exception);
ServerWebExchange exchange = webFilterExchange.getExchange();
// On authentication failure, we send a redirect to the client's login error page. The browser will re-load the
// login page again with an error message shown to the user.
MultiValueMap<String, String> queryParams = exchange.getRequest().getQueryParams();
String state = queryParams.getFirst(Security.QUERY_PARAMETER_STATE);
String originHeader = "/";
String redirectUrl = queryParams.getFirst(REDIRECT_URL_QUERY_PARAM);

if (state != null && !state.isEmpty()) {
originHeader = getOriginFromState(state);
} else {
originHeader =
getOriginFromReferer(exchange.getRequest().getHeaders().getOrigin());
}

// Construct the redirect URL based on the exception type
String url = constructRedirectUrl(exception, originHeader, redirectUrl);

return redirectWithUrl(exchange, url);
}

private String getOriginFromState(String state) {
String[] stateArray = state.split(",");
for (int i = 0; i < stateArray.length; i++) {
String stateVar = stateArray[i];
if (stateVar != null && stateVar.startsWith(Security.STATE_PARAMETER_ORIGIN) && stateVar.contains("=")) {
return stateVar.split("=")[1];
}
}
return "/";
}

// this method extracts the origin from the referer header
private String getOriginFromReferer(String refererHeader) {
if (refererHeader != null && !refererHeader.isBlank()) {
try {
URI uri = new URI(refererHeader);
String authority = uri.getAuthority();
String scheme = uri.getScheme();
return scheme + "://" + authority;
} catch (URISyntaxException e) {
return "/";
}
}
return "/";
}

// this method constructs the redirect URL based on the exception type
private String constructRedirectUrl(AuthenticationException exception, String originHeader, String redirectUrl) {
String url = "";
if (exception instanceof OAuth2AuthenticationException
&& AppsmithError.SIGNUP_DISABLED
.getAppErrorCode()
.toString()
.equals(((OAuth2AuthenticationException) exception)
.getError()
.getErrorCode())) {
url = "/user/signup?error=" + URLEncoder.encode(exception.getMessage(), StandardCharsets.UTF_8);
} else {
if (exception instanceof InternalAuthenticationServiceException) {
url = originHeader + "/user/login?error=true&message="
+ URLEncoder.encode(exception.getMessage(), StandardCharsets.UTF_8);
} else {
url = originHeader + "/user/login?error=true";
}
}
if (redirectUrl != null && !redirectUrl.trim().isEmpty()) {
url = url + "&" + REDIRECT_URL_QUERY_PARAM + "=" + redirectUrl;
}
return url;
}

private Mono<Void> redirectWithUrl(ServerWebExchange exchange, String url) {
URI defaultRedirectLocation = URI.create(url);
return this.redirectStrategy.sendRedirect(exchange, defaultRedirectLocation);
return authenticationFailureRetryHandler.retryAndRedirectOnAuthenticationFailure(webFilterExchange, exception);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.appsmith.server.authentication.helpers;

public interface AuthenticationFailureRetryHandler extends AuthenticationFailureRetryHandlerCE {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.appsmith.server.authentication.helpers;

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.server.WebFilterExchange;
import reactor.core.publisher.Mono;

public interface AuthenticationFailureRetryHandlerCE {
Mono<Void> retryAndRedirectOnAuthenticationFailure(
WebFilterExchange webFilterExchange, AuthenticationException exception);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package com.appsmith.server.authentication.helpers;

import com.appsmith.server.constants.Security;
import com.appsmith.server.exceptions.AppsmithError;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.web.server.DefaultServerRedirectStrategy;
import org.springframework.security.web.server.ServerRedirectStrategy;
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;

import static com.appsmith.server.helpers.RedirectHelper.REDIRECT_URL_QUERY_PARAM;

@Slf4j
@Component
public class AuthenticationFailureRetryHandlerCEImpl implements AuthenticationFailureRetryHandlerCE {

private final ServerRedirectStrategy redirectStrategy = new DefaultServerRedirectStrategy();
protected final String LOGIN_ERROR_URL = "/user/login?error=true";
protected final String LOGIN_ERROR_MESSAGE_URL = LOGIN_ERROR_URL + "&message=";
protected final String SIGNUP_ERROR_URL = "/user/signup?error=";

@Override
public Mono<Void> retryAndRedirectOnAuthenticationFailure(
WebFilterExchange webFilterExchange, AuthenticationException exception) {
log.error("In the login failure handler. Cause: {}", exception.getMessage(), exception);
ServerWebExchange exchange = webFilterExchange.getExchange();
// On authentication failure, we send a redirect to the client's login error page. The browser will re-load the
// login page again with an error message shown to the user.
MultiValueMap<String, String> queryParams = exchange.getRequest().getQueryParams();
String state = queryParams.getFirst(Security.QUERY_PARAMETER_STATE);
String originHeader = "/";
String redirectUrl = queryParams.getFirst(REDIRECT_URL_QUERY_PARAM);

if (state != null && !state.isEmpty()) {
originHeader = getOriginFromState(state);
} else {
originHeader =
getOriginFromReferer(exchange.getRequest().getHeaders().getOrigin());
}

// Construct the redirect URL based on the exception type
String url = constructRedirectUrl(exception, originHeader, redirectUrl);

return redirectWithUrl(exchange, url);
}

private String getOriginFromState(String state) {
String[] stateArray = state.split(",");
for (int i = 0; i < stateArray.length; i++) {
String stateVar = stateArray[i];
if (stateVar != null && stateVar.startsWith(Security.STATE_PARAMETER_ORIGIN) && stateVar.contains("=")) {
return stateVar.split("=")[1];
}
}
return "/";
}

// this method extracts the origin from the referer header
private String getOriginFromReferer(String refererHeader) {
if (refererHeader != null && !refererHeader.isBlank()) {
try {
URI uri = new URI(refererHeader);
String authority = uri.getAuthority();
String scheme = uri.getScheme();
return scheme + "://" + authority;
} catch (URISyntaxException e) {
return "/";
}
}
return "/";
}

// this method constructs the redirect URL based on the exception type
private String constructRedirectUrl(AuthenticationException exception, String originHeader, String redirectUrl) {
String url = "";
if (exception instanceof OAuth2AuthenticationException
&& AppsmithError.SIGNUP_DISABLED
.getAppErrorCode()
.toString()
.equals(((OAuth2AuthenticationException) exception)
.getError()
.getErrorCode())) {
url = SIGNUP_ERROR_URL + URLEncoder.encode(exception.getMessage(), StandardCharsets.UTF_8);
} else {
if (exception instanceof InternalAuthenticationServiceException) {
url = originHeader
+ LOGIN_ERROR_MESSAGE_URL
+ URLEncoder.encode(exception.getMessage(), StandardCharsets.UTF_8);
} else {
url = originHeader + LOGIN_ERROR_URL;
}
}
if (redirectUrl != null && !redirectUrl.trim().isEmpty()) {
url = url + "&" + REDIRECT_URL_QUERY_PARAM + "=" + redirectUrl;
}
return url;
}

private Mono<Void> redirectWithUrl(ServerWebExchange exchange, String url) {
URI defaultRedirectLocation = URI.create(url);
return this.redirectStrategy.sendRedirect(exchange, defaultRedirectLocation);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.appsmith.server.authentication.helpers;

import org.springframework.stereotype.Component;

@Component
public class AuthenticationFailureRetryHandlerImpl extends AuthenticationFailureRetryHandlerCEImpl
implements AuthenticationFailureRetryHandler {}
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package com.appsmith.server.exceptions;

import com.appsmith.external.constants.AnalyticsEvents;
import com.appsmith.external.constants.MDCConstants;
import com.appsmith.external.exceptions.AppsmithErrorAction;
import com.appsmith.external.exceptions.BaseException;
import com.appsmith.external.exceptions.ErrorDTO;
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException;
import com.appsmith.server.constants.FieldName;
Expand All @@ -14,9 +11,6 @@
import com.appsmith.server.services.AnalyticsService;
import com.appsmith.server.services.SessionUserService;
import io.micrometer.core.instrument.util.StringUtils;
import io.sentry.Sentry;
import io.sentry.SentryLevel;
import io.sentry.protocol.User;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.jgit.api.errors.JGitInternalException;
Expand All @@ -35,12 +29,12 @@
import reactor.core.publisher.Mono;

import java.io.File;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;

import static com.appsmith.server.exceptions.util.SentryLogger.doLog;

/**
* This class catches all the Exceptions and formats them into a proper ResponseDTO<ErrorDTO> object before
* sending it to the client.
Expand All @@ -58,46 +52,6 @@ public class GlobalExceptionHandler {

private final SessionUserService sessionUserService;

private void doLog(Throwable error) {
if (error instanceof BaseException baseException && baseException.isHideStackTraceInLogs()) {
log.error(baseException.getClass().getSimpleName() + ": " + baseException.getMessage());
} else {
log.error("", error);
}

StringWriter stringWriter = new StringWriter();
PrintWriter printWriter = new PrintWriter(stringWriter);
error.printStackTrace(printWriter);
String stringStackTrace = stringWriter.toString();

Sentry.configureScope(scope -> {
/**
* Send stack trace as a string message. This is a work around till it is figured out why raw
* stack trace is not visible on Sentry dashboard.
* */
scope.setExtra("Stack Trace", stringStackTrace);
scope.setLevel(SentryLevel.ERROR);
scope.setTag("source", "appsmith-internal-server");
});

if (error instanceof BaseException) {
BaseException baseError = (BaseException) error;
if (baseError.getErrorAction() == AppsmithErrorAction.LOG_EXTERNALLY) {
Sentry.configureScope(scope -> {
baseError.getContextMap().forEach(scope::setTag);
scope.setExtra("downstreamErrorMessage", baseError.getDownstreamErrorMessage());
scope.setExtra("downstreamErrorCode", baseError.getDownstreamErrorCode());
});
final User user = new User();
user.setEmail(baseError.getContextMap().getOrDefault(MDCConstants.USER_EMAIL, "unknownUser"));
Sentry.setUser(user);
Sentry.captureException(error);
}
} else {
Sentry.captureException(error);
}
}

/**
* This function only catches the AppsmithException type and formats it into ResponseEntity<ErrorDTO> object
* Ideally, we should only be throwing AppsmithException from our code. This ensures that we can standardize
Expand Down
Loading