Merge branch 'trisha-dev' into 'master'
Oauth2 implementation - #17 See merge request theappsmith/internal-tools-server!3
This commit is contained in:
commit
659f3eab54
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,9 +2,10 @@ package com.mobtools.server.configurations;
|
||||||
|
|
||||||
|
|
||||||
import com.mobtools.server.constants.Security;
|
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.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.annotation.web.reactive.EnableWebFluxSecurity;
|
||||||
import org.springframework.security.config.web.server.ServerHttpSecurity;
|
import org.springframework.security.config.web.server.ServerHttpSecurity;
|
||||||
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
|
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.CorsConfiguration;
|
||||||
import org.springframework.web.cors.reactive.CorsWebFilter;
|
import org.springframework.web.cors.reactive.CorsWebFilter;
|
||||||
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
|
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
|
||||||
import org.springframework.web.reactive.config.EnableWebFlux;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
@Configuration
|
|
||||||
@EnableWebFlux
|
|
||||||
@EnableWebFluxSecurity
|
@EnableWebFluxSecurity
|
||||||
@EnableReactiveMethodSecurity
|
|
||||||
public class SecurityConfig {
|
public class SecurityConfig {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserService userService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This configuration enables CORS requests for the most common HTTP Methods
|
* This configuration enables CORS requests for the most common HTTP Methods
|
||||||
*
|
*
|
||||||
|
|
@ -68,6 +68,9 @@ public class SecurityConfig {
|
||||||
.anyExchange()
|
.anyExchange()
|
||||||
.authenticated()
|
.authenticated()
|
||||||
.and().httpBasic()
|
.and().httpBasic()
|
||||||
|
.and().oauth2Login()
|
||||||
|
.authorizedClientRepository(new ClientUserRepository(userService))
|
||||||
|
.and().formLogin()
|
||||||
.and().build();
|
.and().build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
package com.mobtools.server.domains;
|
||||||
|
|
||||||
|
public enum LoginSource {
|
||||||
|
GOOGLE, FORM
|
||||||
|
}
|
||||||
|
|
@ -5,8 +5,14 @@ import lombok.NoArgsConstructor;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
import org.springframework.data.mongodb.core.mapping.Document;
|
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.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
|
|
@ -14,7 +20,7 @@ import java.util.Set;
|
||||||
@ToString
|
@ToString
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@Document
|
@Document
|
||||||
public class User extends BaseDomain {
|
public class User extends BaseDomain implements UserDetails {
|
||||||
|
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
|
|
@ -23,4 +29,49 @@ public class User extends BaseDomain {
|
||||||
private Set<Role> roles;
|
private Set<Role> roles;
|
||||||
|
|
||||||
private String password;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
package com.mobtools.server.domains;
|
||||||
|
|
||||||
|
public enum UserState {
|
||||||
|
NEW, INVITED, ACTIVATED
|
||||||
|
}
|
||||||
|
|
@ -8,4 +8,5 @@ import reactor.core.publisher.Mono;
|
||||||
public interface UserRepository extends BaseRepository<User, String> {
|
public interface UserRepository extends BaseRepository<User, String> {
|
||||||
|
|
||||||
Mono<User> findByName(String name);
|
Mono<User> findByName(String name);
|
||||||
|
Mono<User> findByEmail(String email);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,4 +6,6 @@ import reactor.core.publisher.Mono;
|
||||||
public interface UserService extends CrudService<User, String> {
|
public interface UserService extends CrudService<User, String> {
|
||||||
|
|
||||||
Mono<User> findByUsername(String name);
|
Mono<User> findByUsername(String name);
|
||||||
|
Mono<User> findByEmail(String email);
|
||||||
|
Mono<User> save(User newUser);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,15 @@ import com.mobtools.server.domains.User;
|
||||||
import com.mobtools.server.repositories.UserRepository;
|
import com.mobtools.server.repositories.UserRepository;
|
||||||
import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
|
import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
|
||||||
import org.springframework.data.mongodb.core.convert.MongoConverter;
|
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 org.springframework.stereotype.Service;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import reactor.core.scheduler.Scheduler;
|
import reactor.core.scheduler.Scheduler;
|
||||||
|
|
||||||
@Service
|
@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;
|
private UserRepository repository;
|
||||||
|
|
||||||
|
|
@ -25,4 +28,19 @@ public class UserServiceImpl extends BaseService<UserRepository, User, String> i
|
||||||
public Mono<User> findByUsername(String name) {
|
public Mono<User> findByUsername(String name) {
|
||||||
return repository.findByName(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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,3 +12,8 @@ jdbc.postgres.driver=org.postgresql.Driver
|
||||||
jdbc.postgres.url=jdbc:postgresql://localhost/mobtools
|
jdbc.postgres.url=jdbc:postgresql://localhost/mobtools
|
||||||
jdbc.postgres.username=postgres
|
jdbc.postgres.username=postgres
|
||||||
jdbc.postgres.password=root
|
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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,3 +10,7 @@ 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.url=jdbc:postgresql://ec2-54-247-85-251.eu-west-1.compute.amazonaws.com/d266aalso50024
|
||||||
jdbc.postgres.username=pornlzmggggpgk
|
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
|
||||||
|
|
|
||||||
|
|
@ -13,3 +13,4 @@ spring.jpa.show-sql=true
|
||||||
|
|
||||||
# Jackson Properties
|
# Jackson Properties
|
||||||
spring.jackson.default-property-inclusion=non_null
|
spring.jackson.default-property-inclusion=non_null
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user