diff --git a/app/server/src/main/java/com/mobtools/server/configurations/ClientUserRepository.java b/app/server/src/main/java/com/mobtools/server/configurations/ClientUserRepository.java new file mode 100644 index 0000000000..81c0b478bc --- /dev/null +++ b/app/server/src/main/java/com/mobtools/server/configurations/ClientUserRepository.java @@ -0,0 +1,125 @@ +package com.mobtools.server.configurations; + +import com.mobtools.server.domains.LoginSource; +import com.mobtools.server.domains.User; +import com.mobtools.server.domains.UserState; +import com.mobtools.server.repositories.UserRepository; +import com.mobtools.server.services.UserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository; +import org.springframework.security.oauth2.client.web.server.WebSessionServerOAuth2AuthorizedClientRepository; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.util.Assert; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebSession; +import reactor.core.publisher.Mono; + +import java.util.HashMap; +import java.util.Map; + + +/** + * This code has been copied from WebSessionServerOAuth2AuthorizedClientRepository.java + * which also implements ServerOAuth2AuthorizedClientRepository. This was done to make changes + * to saveAuthorizedClient to also handle adding users to UserRepository. + * + * This was done because on authorization, the user needs to be stored in appsmith domain. + * To achieve this, saveAuthorizedClient function has been edited in the following manner. + * In the reactive flow, post doOnSuccess transformation, another Mono.then has been added. In this, + * Authentication object is passed to checkAndCreateUser function. This object is used to get OidcUser from which + * user attributes like name, email, etc are extracted. If the user doesnt exist in User + * Repository, a new user is created and saved. + * + * The ClientUserRepository is created during SecurityWebFilterChain Bean creation. By + * configuring to use Oauth2Login, this ServerOAuth2AuthorizedClientRepository implementation + * is injected. This hack is used to ensure that on successful authentication, we are able + * to record the user in our database. Since ServerOAuth2AuthorizedClientRepository's + * saveAuthorizedClient is called on every successful OAuth2 authentication, this solves the problem + * of plugging a handler for the same purpose. + */ +public class ClientUserRepository implements ServerOAuth2AuthorizedClientRepository { + + UserService userService; + + public ClientUserRepository(UserService userService) { + this.userService = userService; + } + + private static final String DEFAULT_AUTHORIZED_CLIENTS_ATTR_NAME = + WebSessionServerOAuth2AuthorizedClientRepository.class.getName() + ".AUTHORIZED_CLIENTS"; + private final String sessionAttributeName = DEFAULT_AUTHORIZED_CLIENTS_ATTR_NAME; + + @Override + @SuppressWarnings("unchecked") + public Mono loadAuthorizedClient(String clientRegistrationId, Authentication principal, + ServerWebExchange exchange) { + Assert.hasText(clientRegistrationId, "clientRegistrationId cannot be empty"); + Assert.notNull(exchange, "exchange cannot be null"); + return exchange.getSession() + .map(this::getAuthorizedClients) + .flatMap(clients -> Mono.justOrEmpty((T) clients.get(clientRegistrationId))); + } + + @Override + public Mono saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal, + ServerWebExchange exchange) { + Assert.notNull(authorizedClient, "authorizedClient cannot be null"); + Assert.notNull(exchange, "exchange cannot be null"); + return exchange.getSession() + .doOnSuccess(session -> { + Map authorizedClients = getAuthorizedClients(session); + authorizedClients.put(authorizedClient.getClientRegistration().getRegistrationId(), authorizedClient); + session.getAttributes().put(this.sessionAttributeName, authorizedClients); + }) + /* + * TODO + * Need to test how this behaves in the following : + * 1. Clustered environment + * 2. Redis saved sessions + */ + .then(checkAndCreateUser((OidcUser) principal.getPrincipal())) + .then(Mono.empty()); + } + + private Mono checkAndCreateUser(OidcUser user) { + User newUser = new User(); + newUser.setName(user.getFullName()); + newUser.setEmail(user.getEmail()); + newUser.setSource(LoginSource.GOOGLE); + newUser.setState(UserState.ACTIVATED); + newUser.setIsEnabled(true); + + return userService.findByEmail(user.getEmail()) + .switchIfEmpty(userService.save(newUser)); + } + + @Override + public Mono removeAuthorizedClient(String clientRegistrationId, Authentication principal, + ServerWebExchange exchange) { + Assert.hasText(clientRegistrationId, "clientRegistrationId cannot be empty"); + Assert.notNull(exchange, "exchange cannot be null"); + return exchange.getSession() + .doOnSuccess(session -> { + Map authorizedClients = getAuthorizedClients(session); + authorizedClients.remove(clientRegistrationId); + if (authorizedClients.isEmpty()) { + session.getAttributes().remove(this.sessionAttributeName); + } else { + session.getAttributes().put(this.sessionAttributeName, authorizedClients); + } + }) + .then(Mono.empty()); + } + + @SuppressWarnings("unchecked") + private Map getAuthorizedClients(WebSession session) { + Map authorizedClients = session == null ? null : + (Map) session.getAttribute(this.sessionAttributeName); + if (authorizedClients == null) { + authorizedClients = new HashMap<>(); + } + return authorizedClients; + } +} diff --git a/app/server/src/main/java/com/mobtools/server/configurations/SecurityConfig.java b/app/server/src/main/java/com/mobtools/server/configurations/SecurityConfig.java index 0489254750..f4435b78ac 100644 --- a/app/server/src/main/java/com/mobtools/server/configurations/SecurityConfig.java +++ b/app/server/src/main/java/com/mobtools/server/configurations/SecurityConfig.java @@ -2,9 +2,10 @@ package com.mobtools.server.configurations; import com.mobtools.server.constants.Security; +import com.mobtools.server.repositories.UserRepository; +import com.mobtools.server.services.UserService; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.core.userdetails.MapReactiveUserDetailsService; @@ -16,16 +17,15 @@ import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.reactive.CorsWebFilter; import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource; -import org.springframework.web.reactive.config.EnableWebFlux; import java.util.Arrays; -@Configuration -@EnableWebFlux @EnableWebFluxSecurity -@EnableReactiveMethodSecurity public class SecurityConfig { + @Autowired + private UserService userService; + /** * This configuration enables CORS requests for the most common HTTP Methods * @@ -68,6 +68,9 @@ public class SecurityConfig { .anyExchange() .authenticated() .and().httpBasic() + .and().oauth2Login() + .authorizedClientRepository(new ClientUserRepository(userService)) + .and().formLogin() .and().build(); } } diff --git a/app/server/src/main/java/com/mobtools/server/domains/LoginSource.java b/app/server/src/main/java/com/mobtools/server/domains/LoginSource.java new file mode 100644 index 0000000000..576f07bf53 --- /dev/null +++ b/app/server/src/main/java/com/mobtools/server/domains/LoginSource.java @@ -0,0 +1,5 @@ +package com.mobtools.server.domains; + +public enum LoginSource { + GOOGLE, FORM +} diff --git a/app/server/src/main/java/com/mobtools/server/domains/User.java b/app/server/src/main/java/com/mobtools/server/domains/User.java index 567e57fdea..7e06fce541 100644 --- a/app/server/src/main/java/com/mobtools/server/domains/User.java +++ b/app/server/src/main/java/com/mobtools/server/domains/User.java @@ -5,8 +5,14 @@ import lombok.NoArgsConstructor; import lombok.Setter; import lombok.ToString; import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import java.util.ArrayList; +import java.util.Collection; import java.util.Set; +import java.util.stream.Collectors; @Getter @@ -14,7 +20,7 @@ import java.util.Set; @ToString @NoArgsConstructor @Document -public class User extends BaseDomain { +public class User extends BaseDomain implements UserDetails { private String name; @@ -23,4 +29,49 @@ public class User extends BaseDomain { private Set roles; private String password; + + private LoginSource source; + + private UserState state; + + private Boolean isEnabled; + + @Override + public Collection getAuthorities() { + + if (roles == null || roles.isEmpty()) //No existing roles found. + return null; + + Collection authorities = roles.stream() + .map(role -> new SimpleGrantedAuthority(role.toString())) + .collect(Collectors.toList()); + + return authorities; + } + + @Override + public String getUsername() { + return this.name; + } + + @Override + public boolean isAccountNonExpired() { + return this.isEnabled; + } + + @Override + public boolean isAccountNonLocked() { + return this.isEnabled; + } + + @Override + public boolean isCredentialsNonExpired() { + return this.isEnabled; + } + + @Override + public boolean isEnabled() { + return this.isEnabled; + } + } diff --git a/app/server/src/main/java/com/mobtools/server/domains/UserState.java b/app/server/src/main/java/com/mobtools/server/domains/UserState.java new file mode 100644 index 0000000000..9043527e78 --- /dev/null +++ b/app/server/src/main/java/com/mobtools/server/domains/UserState.java @@ -0,0 +1,5 @@ +package com.mobtools.server.domains; + +public enum UserState { + NEW, INVITED, ACTIVATED +} diff --git a/app/server/src/main/java/com/mobtools/server/repositories/UserRepository.java b/app/server/src/main/java/com/mobtools/server/repositories/UserRepository.java index eb2f09b27f..7a53303b29 100644 --- a/app/server/src/main/java/com/mobtools/server/repositories/UserRepository.java +++ b/app/server/src/main/java/com/mobtools/server/repositories/UserRepository.java @@ -8,4 +8,5 @@ import reactor.core.publisher.Mono; public interface UserRepository extends BaseRepository { Mono findByName(String name); + Mono findByEmail(String email); } diff --git a/app/server/src/main/java/com/mobtools/server/services/UserService.java b/app/server/src/main/java/com/mobtools/server/services/UserService.java index a820e03e42..4db2270f43 100644 --- a/app/server/src/main/java/com/mobtools/server/services/UserService.java +++ b/app/server/src/main/java/com/mobtools/server/services/UserService.java @@ -6,4 +6,6 @@ import reactor.core.publisher.Mono; public interface UserService extends CrudService { Mono findByUsername(String name); + Mono findByEmail(String email); + Mono save(User newUser); } diff --git a/app/server/src/main/java/com/mobtools/server/services/UserServiceImpl.java b/app/server/src/main/java/com/mobtools/server/services/UserServiceImpl.java index 8a87de67a5..58e81b81c8 100644 --- a/app/server/src/main/java/com/mobtools/server/services/UserServiceImpl.java +++ b/app/server/src/main/java/com/mobtools/server/services/UserServiceImpl.java @@ -4,12 +4,15 @@ import com.mobtools.server.domains.User; import com.mobtools.server.repositories.UserRepository; import org.springframework.data.mongodb.core.ReactiveMongoTemplate; import org.springframework.data.mongodb.core.convert.MongoConverter; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import reactor.core.publisher.Mono; import reactor.core.scheduler.Scheduler; @Service -public class UserServiceImpl extends BaseService implements UserService { +public class UserServiceImpl extends BaseService implements UserService, UserDetailsService { private UserRepository repository; @@ -25,4 +28,19 @@ public class UserServiceImpl extends BaseService i public Mono findByUsername(String name) { return repository.findByName(name); } + + @Override + public Mono findByEmail(String email) { + return repository.findByEmail(email); + } + + @Override + public Mono save(User user) { + return repository.save(user); + } + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + return repository.findByName(username).block(); + } } diff --git a/app/server/src/main/resources/application-local.properties b/app/server/src/main/resources/application-local.properties index cf17cdabec..fcadf3f47b 100644 --- a/app/server/src/main/resources/application-local.properties +++ b/app/server/src/main/resources/application-local.properties @@ -12,3 +12,8 @@ jdbc.postgres.driver=org.postgresql.Driver jdbc.postgres.url=jdbc:postgresql://localhost/mobtools jdbc.postgres.username=postgres jdbc.postgres.password=root + +#Spring security +spring.security.oauth2.client.registration.google.client-id=869021686091-9b84bbf7ea683t1aaefqnmefcnmk6fq6.apps.googleusercontent.com +spring.security.oauth2.client.registration.google.client-secret=9dvITt4OayEY1HfeY8bHX74p + diff --git a/app/server/src/main/resources/application-staging.properties b/app/server/src/main/resources/application-staging.properties index c939582a25..bba56a1d52 100644 --- a/app/server/src/main/resources/application-staging.properties +++ b/app/server/src/main/resources/application-staging.properties @@ -9,4 +9,8 @@ logging.level.com.mobtools=debug jdbc.postgres.driver=org.postgresql.Driver jdbc.postgres.url=jdbc:postgresql://ec2-54-247-85-251.eu-west-1.compute.amazonaws.com/d266aalso50024 jdbc.postgres.username=pornlzmggggpgk -jdbc.postgres.password=09275163cd7e737baf4c210b5e8db8ed88ddb7a0ee9acc82416fd75346ea4bbb \ No newline at end of file +jdbc.postgres.password=09275163cd7e737baf4c210b5e8db8ed88ddb7a0ee9acc82416fd75346ea4bbb + +#Spring security +spring.security.oauth2.client.registration.google.client-id=869021686091-9b84bbf7ea683t1aaefqnmefcnmk6fq6.apps.googleusercontent.com +spring.security.oauth2.client.registration.google.client-secret=9dvITt4OayEY1HfeY8bHX74p diff --git a/app/server/src/main/resources/application.properties b/app/server/src/main/resources/application.properties index bd7f5f3ef4..d3250adf5a 100644 --- a/app/server/src/main/resources/application.properties +++ b/app/server/src/main/resources/application.properties @@ -13,3 +13,4 @@ spring.jpa.show-sql=true # Jackson Properties spring.jackson.default-property-inclusion=non_null +