Merge branch 'trisha-dev' into 'master'

Oauth2 implementation - #17

See merge request theappsmith/internal-tools-server!3
This commit is contained in:
Trisha Anand 2019-08-12 04:44:29 +00:00
commit 659f3eab54
11 changed files with 229 additions and 9 deletions

View File

@ -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 <T extends OAuth2AuthorizedClient> Mono<T> 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<Void> 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<String, OAuth2AuthorizedClient> 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<User> 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<Void> 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<String, OAuth2AuthorizedClient> 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<String, OAuth2AuthorizedClient> getAuthorizedClients(WebSession session) {
Map<String, OAuth2AuthorizedClient> authorizedClients = session == null ? null :
(Map<String, OAuth2AuthorizedClient>) session.getAttribute(this.sessionAttributeName);
if (authorizedClients == null) {
authorizedClients = new HashMap<>();
}
return authorizedClients;
}
}

View File

@ -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();
}
}

View File

@ -0,0 +1,5 @@
package com.mobtools.server.domains;
public enum LoginSource {
GOOGLE, FORM
}

View File

@ -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<Role> roles;
private String password;
private LoginSource source;
private UserState state;
private Boolean isEnabled;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if (roles == null || roles.isEmpty()) //No existing roles found.
return null;
Collection<SimpleGrantedAuthority> 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;
}
}

View File

@ -0,0 +1,5 @@
package com.mobtools.server.domains;
public enum UserState {
NEW, INVITED, ACTIVATED
}

View File

@ -8,4 +8,5 @@ import reactor.core.publisher.Mono;
public interface UserRepository extends BaseRepository<User, String> {
Mono<User> findByName(String name);
Mono<User> findByEmail(String email);
}

View File

@ -6,4 +6,6 @@ import reactor.core.publisher.Mono;
public interface UserService extends CrudService<User, String> {
Mono<User> findByUsername(String name);
Mono<User> findByEmail(String email);
Mono<User> save(User newUser);
}

View File

@ -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<UserRepository, User, String> implements UserService {
public class UserServiceImpl extends BaseService<UserRepository, User, String> implements UserService, UserDetailsService {
private UserRepository repository;
@ -25,4 +28,19 @@ public class UserServiceImpl extends BaseService<UserRepository, User, String> i
public Mono<User> findByUsername(String name) {
return repository.findByName(name);
}
@Override
public Mono<User> findByEmail(String email) {
return repository.findByEmail(email);
}
@Override
public Mono<User> save(User user) {
return repository.save(user);
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return repository.findByName(username).block();
}
}

View File

@ -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

View File

@ -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
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

View File

@ -13,3 +13,4 @@ spring.jpa.show-sql=true
# Jackson Properties
spring.jackson.default-property-inclusion=non_null