Upgrading to Spring boot 2.2.2 for features in Spring security

Now, we have an authenticationSuccessHandler & authenticationFailureHandler for OAuth & Form sign ups. This makes the whole flow much easier to handle.
This commit is contained in:
Arpit Mohan 2019-12-16 10:53:17 +05:30
parent 274a686e51
commit 82a6d96b1a
11 changed files with 113 additions and 75 deletions

View File

@ -47,21 +47,22 @@
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId> <artifactId>spring-boot-starter-security</artifactId>
<version>2.2.2.RELEASE</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.springframework.security</groupId> <groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-client</artifactId> <artifactId>spring-security-oauth2-client</artifactId>
<version>5.1.4.RELEASE</version> <version>5.2.1.RELEASE</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.springframework.security</groupId> <groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId> <artifactId>spring-security-oauth2-jose</artifactId>
<version>5.1.4.RELEASE</version> <version>5.2.1.RELEASE</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.springframework.security</groupId> <groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId> <artifactId>spring-security-config</artifactId>
<version>5.1.4.RELEASE</version> <version>5.2.1.RELEASE</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>

View File

@ -57,13 +57,10 @@ public class ClientUserRepository implements ServerOAuth2AuthorizedClientReposit
UserService userService; UserService userService;
OrganizationService organizationService;
CommonConfig commonConfig; CommonConfig commonConfig;
public ClientUserRepository(UserService userService, OrganizationService organizationService, CommonConfig commonConfig) { public ClientUserRepository(UserService userService, CommonConfig commonConfig) {
this.userService = userService; this.userService = userService;
this.organizationService = organizationService;
this.commonConfig = commonConfig; this.commonConfig = commonConfig;
} }
@ -108,7 +105,6 @@ public class ClientUserRepository implements ServerOAuth2AuthorizedClientReposit
* 1. Clustered environment * 1. Clustered environment
* 2. Redis saved sessions * 2. Redis saved sessions
*/ */
.then(checkAndCreateUser((OidcUser) principal.getPrincipal()))
.then(Mono.empty()); .then(Mono.empty());
} }

View File

@ -17,11 +17,6 @@ import org.springframework.session.data.redis.config.annotation.web.server.Enabl
@EnableRedisWebSession @EnableRedisWebSession
public class RedisConfig { public class RedisConfig {
@Bean
public ReactiveRedisTemplate<String, String> reactiveRedisTemplate(ReactiveRedisConnectionFactory factory) {
return new ReactiveRedisTemplate<>(factory, RedisSerializationContext.string());
}
/** /**
* This is the topic to which we will publish & subscribe to. We can have multiple topics based on the messages * This is the topic to which we will publish & subscribe to. We can have multiple topics based on the messages
* that we wish to broadcast. Starting with a single one for now. * that we wish to broadcast. Starting with a single one for now.

View File

@ -29,17 +29,14 @@ public class SecurityConfig {
@Autowired @Autowired
private UserService userService; private UserService userService;
@Autowired
private OrganizationService organizationService;
@Autowired @Autowired
private CommonConfig commonConfig; private CommonConfig commonConfig;
@Autowired @Autowired
private ServerAuthenticationSuccessHandler formAuthenticationSuccessHandler; private ServerAuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired @Autowired
private ServerAuthenticationFailureHandler formAuthenticationFailureHandler; private ServerAuthenticationFailureHandler authenticationFailureHandler;
/** /**
* This routerFunction is required to map /public/** endpoints to the src/main/resources/public folder * This routerFunction is required to map /public/** endpoints to the src/main/resources/public folder
@ -92,12 +89,14 @@ public class SecurityConfig {
.authenticated() .authenticated()
.and().httpBasic() .and().httpBasic()
.and().oauth2Login() .and().oauth2Login()
.authorizedClientRepository(new ClientUserRepository(userService, organizationService, commonConfig)) .authenticationSuccessHandler(authenticationSuccessHandler)
.authenticationFailureHandler(authenticationFailureHandler)
.authorizedClientRepository(new ClientUserRepository(userService, commonConfig))
.and().formLogin() .and().formLogin()
.authenticationEntryPoint(new RedirectServerAuthenticationEntryPoint("/login")) .authenticationEntryPoint(new RedirectServerAuthenticationEntryPoint("/login"))
.requiresAuthenticationMatcher(ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, "/login")) .requiresAuthenticationMatcher(ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, "/login"))
.authenticationSuccessHandler(formAuthenticationSuccessHandler) .authenticationSuccessHandler(authenticationSuccessHandler)
.authenticationFailureHandler(formAuthenticationFailureHandler) .authenticationFailureHandler(authenticationFailureHandler)
.and().build(); .and().build();
} }

View File

@ -37,7 +37,7 @@ public class User extends BaseDomain implements UserDetails {
@JsonIgnore @JsonIgnore
private Boolean passwordResetInitiated = false; private Boolean passwordResetInitiated = false;
private LoginSource source; private LoginSource source = LoginSource.FORM;
private UserState state; private UserState state;

View File

@ -16,7 +16,7 @@ import java.net.URI;
@Slf4j @Slf4j
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
public class FormAuthenticationFailureHandler implements ServerAuthenticationFailureHandler { public class AuthenticationFailureHandler implements ServerAuthenticationFailureHandler {
private ServerRedirectStrategy redirectStrategy = new DefaultServerRedirectStrategy(); private ServerRedirectStrategy redirectStrategy = new DefaultServerRedirectStrategy();

View File

@ -0,0 +1,91 @@
package com.appsmith.server.filters;
import com.appsmith.server.constants.AclConstants;
import com.appsmith.server.domains.LoginSource;
import com.appsmith.server.domains.User;
import com.appsmith.server.domains.UserState;
import com.appsmith.server.services.UserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
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.ServerAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.net.URI;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
@Slf4j
@Component
@RequiredArgsConstructor
public class AuthenticationSuccessHandler implements ServerAuthenticationSuccessHandler {
@Autowired
UserService userService;
private ServerRedirectStrategy redirectStrategy = new DefaultServerRedirectStrategy();
/**
* On authentication success, we send a redirect to the endpoint that serve's the user's profile.
* The client browser will follow this redirect and fetch the user's profile JSON from the server.
* In the process, the client browser will also set the session ID in the cookie against the server's API domain.
*
* @param webFilterExchange
* @param authentication
* @return
*/
@Override
public Mono<Void> onAuthenticationSuccess(WebFilterExchange webFilterExchange,
Authentication authentication) {
log.debug("Login succeeded for user: {}", authentication.getPrincipal());
if(authentication instanceof OAuth2AuthenticationToken) {
OAuth2AuthenticationToken oauthAuthentication = (OAuth2AuthenticationToken) authentication;
return checkAndCreateUser(oauthAuthentication)
.then(handleRedirect(webFilterExchange));
}
return handleRedirect(webFilterExchange);
}
private Mono<Void> handleRedirect(WebFilterExchange webFilterExchange) {
ServerWebExchange exchange = webFilterExchange.getExchange();
// On authentication success, we send a redirect to the client's home page. This ensures that the session
// is set in the cookie on the browser.
String originHeader = exchange.getRequest().getHeaders().getOrigin();
if(originHeader == null || originHeader.isEmpty()) {
originHeader = "/";
}
URI defaultRedirectLocation = URI.create(originHeader);
return this.redirectStrategy.sendRedirect(exchange, defaultRedirectLocation);
}
private Mono<User> checkAndCreateUser(OAuth2AuthenticationToken authToken) {
Map<String, Object> userAttributes = authToken.getPrincipal().getAttributes();
User newUser = new User();
newUser.setName((String) userAttributes.get("name"));
newUser.setEmail((String) userAttributes.get("email"));
newUser.setSource(LoginSource.GOOGLE);
newUser.setState(UserState.ACTIVATED);
newUser.setIsEnabled(true);
// TODO: Check if this is a valid permission available in the DB
// TODO: Check to see if this user was invited or is it a new sign up
Set<String> permissions = new HashSet<>();
// Adding the create organization permission because this is a new user and we will have to create an organization
// after this for the user.
permissions.addAll(AclConstants.PERMISSIONS_CRUD_ORG);
newUser.setPermissions(permissions);
return userService.findByEmail((String) userAttributes.get("email"))
.switchIfEmpty(Mono.defer(() -> userService.create(newUser))); //In case the user doesn't exist, create and save the user.
}
}

View File

@ -1,49 +0,0 @@
package com.appsmith.server.filters;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
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.ServerAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.net.URI;
@Slf4j
@Component
@RequiredArgsConstructor
public class FormAuthenticationSuccessHandler implements ServerAuthenticationSuccessHandler {
private ServerRedirectStrategy redirectStrategy = new DefaultServerRedirectStrategy();
/**
* On authentication success, we send a redirect to the endpoint that serve's the user's profile.
* The client browser will follow this redirect and fetch the user's profile JSON from the server.
* In the process, the client browser will also set the session ID in the cookie against the server's API domain.
*
* @param webFilterExchange
* @param authentication
* @return
*/
@Override
public Mono<Void> onAuthenticationSuccess(WebFilterExchange webFilterExchange,
Authentication authentication) {
log.debug("Login succeeded for user: {}", authentication.getPrincipal());
ServerWebExchange exchange = webFilterExchange.getExchange();
// On authentication success, we send a redirect to the client's home page. This ensures that the session
// is set in the cookie on the browser.
String originHeader = exchange.getRequest().getHeaders().getOrigin();
if(originHeader == null || originHeader.isEmpty()) {
originHeader = "/";
}
URI defaultRedirectLocation = URI.create(originHeader);
return this.redirectStrategy.sendRedirect(exchange, defaultRedirectLocation);
}
}

View File

@ -441,7 +441,7 @@ public class ActionServiceImpl extends BaseService<ActionRepository, Action, Str
@Override @Override
public Flux<Action> get(MultiValueMap<String, String> params) { public Flux<Action> get(MultiValueMap<String, String> params) {
Action actionExample = new Action(); Action actionExample = new Action();
Sort sort = new Sort(Direction.ASC, FieldName.NAME ); Sort sort = Sort.by(FieldName.NAME );
if (params.getFirst(FieldName.NAME) != null) { if (params.getFirst(FieldName.NAME) != null) {
actionExample.setName(params.getFirst(FieldName.NAME)); actionExample.setName(params.getFirst(FieldName.NAME));

View File

@ -1,6 +1,7 @@
package com.appsmith.server.services; package com.appsmith.server.services;
import com.appsmith.server.domains.Group; import com.appsmith.server.domains.Group;
import com.appsmith.server.domains.LoginSource;
import com.appsmith.server.domains.Organization; import com.appsmith.server.domains.Organization;
import com.appsmith.server.domains.PasswordResetToken; import com.appsmith.server.domains.PasswordResetToken;
import com.appsmith.server.domains.User; import com.appsmith.server.domains.User;
@ -253,7 +254,11 @@ public class UserServiceImpl extends BaseService<UserRepository, User, String> i
@Override @Override
public Mono<User> create(User user) { public Mono<User> create(User user) {
user.setPassword(this.passwordEncoder.encode(user.getPassword()));
// Only encode the password if it's a form signup. For OAuth signups, we don't need password
if(LoginSource.FORM.equals(user.getSource())) {
user.setPassword(this.passwordEncoder.encode(user.getPassword()));
}
Organization personalOrg = new Organization(); Organization personalOrg = new Organization();
String name; String name;

View File

@ -4,7 +4,7 @@
<parent> <parent>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId> <artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.8.RELEASE</version> <version>2.2.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository --> <relativePath/> <!-- lookup parent from repository -->
</parent> </parent>