diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/authentication/AuthenticationService.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/authentication/AuthenticationService.java index 19b337922..b416b3f44 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/authentication/AuthenticationService.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/authentication/AuthenticationService.java @@ -10,9 +10,9 @@ public interface AuthenticationService { EmailAuthConfig DEFAULT_AUTH_CONFIG = new EmailAuthConfig(AuthSourceConstants.EMAIL, true, true); - Mono findAuthConfigByAuthId(String authId); + Mono findAuthConfigByAuthId(String orgId, String authId); - Mono findAuthConfigBySource(String source); + Mono findAuthConfigBySource(String orgId, String source); - Flux findAllAuthConfigs(boolean enableOnly); + Flux findAllAuthConfigs(String orgId, boolean enableOnly); } diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/authentication/AuthenticationServiceImpl.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/authentication/AuthenticationServiceImpl.java index 6345558d4..cb3104ccb 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/authentication/AuthenticationServiceImpl.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/authentication/AuthenticationServiceImpl.java @@ -1,12 +1,6 @@ package org.lowcoder.domain.authentication; -import static org.lowcoder.sdk.exception.BizError.LOG_IN_SOURCE_NOT_SUPPORTED; -import static org.lowcoder.sdk.util.ExceptionUtils.ofError; - -import java.util.Objects; -import java.util.function.Function; -import java.util.stream.Collectors; - +import lombok.extern.slf4j.Slf4j; import org.lowcoder.domain.organization.service.OrganizationService; import org.lowcoder.sdk.auth.AbstractAuthConfig; import org.lowcoder.sdk.config.AuthProperties; @@ -14,11 +8,16 @@ import org.lowcoder.sdk.constants.WorkspaceMode; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; - -import lombok.extern.slf4j.Slf4j; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static org.lowcoder.sdk.exception.BizError.LOG_IN_SOURCE_NOT_SUPPORTED; +import static org.lowcoder.sdk.util.ExceptionUtils.ofError; + @Slf4j @Service public class AuthenticationServiceImpl implements AuthenticationService { @@ -31,35 +30,35 @@ public class AuthenticationServiceImpl implements AuthenticationService { private AuthProperties authProperties; @Override - public Mono findAuthConfigByAuthId(String authId) { - return findAuthConfig(abstractAuthConfig -> Objects.equals(authId, abstractAuthConfig.getId())); + public Mono findAuthConfigByAuthId(String orgId, String authId) { + return findAuthConfig(orgId, abstractAuthConfig -> Objects.equals(authId, abstractAuthConfig.getId())); } @Override @Deprecated - public Mono findAuthConfigBySource(String source) { - return findAuthConfig(abstractAuthConfig -> Objects.equals(source, abstractAuthConfig.getSource())); + public Mono findAuthConfigBySource(String orgId, String source) { + return findAuthConfig(orgId, abstractAuthConfig -> Objects.equals(source, abstractAuthConfig.getSource())); } - private Mono findAuthConfig(Function condition) { - return findAllAuthConfigs(true) + private Mono findAuthConfig(String orgId, Function condition) { + return findAllAuthConfigs(orgId,true) .filter(findAuthConfig -> condition.apply(findAuthConfig.authConfig())) .next() .switchIfEmpty(ofError(LOG_IN_SOURCE_NOT_SUPPORTED, "LOG_IN_SOURCE_NOT_SUPPORTED")); } @Override - public Flux findAllAuthConfigs(boolean enableOnly) { + public Flux findAllAuthConfigs(String orgId, boolean enableOnly) { return findAllAuthConfigsByDomain() .switchIfEmpty(findAllAuthConfigsForEnterpriseMode()) - .switchIfEmpty(findAllAuthConfigsForSaasMode()) + .switchIfEmpty(findAllAuthConfigsForSaasMode(orgId)) .filter(findAuthConfig -> { if (enableOnly) { return findAuthConfig.authConfig().isEnable(); } return true; }) - .defaultIfEmpty(new FindAuthConfig(DEFAULT_AUTH_CONFIG, null)); + .concatWithValues(new FindAuthConfig(DEFAULT_AUTH_CONFIG, null)); } private Flux findAllAuthConfigsByDomain() { @@ -85,10 +84,20 @@ protected Flux findAllAuthConfigsForEnterpriseMode() { ); } - private Flux findAllAuthConfigsForSaasMode() { + private Flux findAllAuthConfigsForSaasMode(String orgId) { if (commonConfig.getWorkspace().getMode() == WorkspaceMode.SAAS) { - return Flux.fromIterable(authProperties.getAuthConfigs()) - .map(abstractAuthConfig -> new FindAuthConfig(abstractAuthConfig, null)); + + // Get the auth configs for the current org + if(orgId != null) { + return organizationService.getById(orgId) + .flatMapIterable(organization -> + organization.getAuthConfigs() + .stream() + .map(abstractAuthConfig -> new FindAuthConfig(abstractAuthConfig, organization)) + .collect(Collectors.toList()) + ); + } + } return Flux.empty(); } diff --git a/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/auth/Oauth2SimpleAuthConfig.java b/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/auth/Oauth2SimpleAuthConfig.java index c1a56204f..ef152a8ce 100644 --- a/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/auth/Oauth2SimpleAuthConfig.java +++ b/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/auth/Oauth2SimpleAuthConfig.java @@ -14,6 +14,8 @@ import lombok.Getter; +import static org.lowcoder.sdk.auth.constants.Oauth2Constants.CLIENT_ID_PLACEHOLDER; + /** * simple oauth2 auth config. */ @@ -48,8 +50,8 @@ public Oauth2SimpleAuthConfig( @JsonView(JsonViews.Public.class) public String getAuthorizeUrl() { return switch (authType) { - case AuthTypeConstants.GOOGLE -> Oauth2Constants.GOOGLE_AUTHORIZE_URL; - case AuthTypeConstants.GITHUB -> Oauth2Constants.GITHUB_AUTHORIZE_URL; + case AuthTypeConstants.GOOGLE -> replaceAuthUrlClientIdPlaceholder(Oauth2Constants.GOOGLE_AUTHORIZE_URL); + case AuthTypeConstants.GITHUB -> replaceAuthUrlClientIdPlaceholder(Oauth2Constants.GITHUB_AUTHORIZE_URL); default -> null; }; } @@ -70,4 +72,8 @@ public void merge(AbstractAuthConfig oldConfig) { this.clientSecret = oldSimpleConfig.getClientSecret(); } } + + private String replaceAuthUrlClientIdPlaceholder(String url) { + return url.replace(CLIENT_ID_PLACEHOLDER, clientId); + } } diff --git a/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/exception/BizError.java b/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/exception/BizError.java index 494c72a01..d653407ea 100644 --- a/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/exception/BizError.java +++ b/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/exception/BizError.java @@ -101,6 +101,7 @@ public enum BizError { USER_NOT_EXIST(400, 5618), JWT_NOT_FIND(400, 5619), ID_NOT_EXIST(500, 5620), + DUPLICATE_AUTH_CONFIG_ADDITION(400, 5621), // asset related, code range 5700 - 5799 diff --git a/server/api-service/lowcoder-sdk/src/main/resources/locale_en.properties b/server/api-service/lowcoder-sdk/src/main/resources/locale_en.properties index 2c67433fc..d81ecbdf2 100644 --- a/server/api-service/lowcoder-sdk/src/main/resources/locale_en.properties +++ b/server/api-service/lowcoder-sdk/src/main/resources/locale_en.properties @@ -277,3 +277,4 @@ CERTIFICATE_EMPTY=Certificate is empty. ORG_DELETED_FOR_ENTERPRISE_MODE=Provided enterpriseOrgId workspace has been deleted, please contact Lowcoder team. DISABLE_AUTH_CONFIG_FORBIDDEN=Can not disable current administrator''s last identity provider. USER_NOT_EXIST=User not exist. +DUPLICATE_AUTH_CONFIG_ADDITION=Provider auth type already added to organization diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/AuthenticationController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/AuthenticationController.java index 8d53db29f..0d2829fff 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/AuthenticationController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/AuthenticationController.java @@ -1,7 +1,7 @@ package org.lowcoder.api.authentication; -import java.util.List; - +import com.fasterxml.jackson.annotation.JsonView; +import lombok.extern.slf4j.Slf4j; import org.lowcoder.api.authentication.dto.AuthConfigRequest; import org.lowcoder.api.authentication.service.AuthenticationApiService; import org.lowcoder.api.framework.view.ResponseView; @@ -17,21 +17,12 @@ import org.lowcoder.sdk.constants.AuthSourceConstants; import org.lowcoder.sdk.util.CookieHelper; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import org.springframework.web.server.ServerWebExchange; - -import com.fasterxml.jackson.annotation.JsonView; - -import lombok.extern.slf4j.Slf4j; import reactor.core.publisher.Mono; +import java.util.List; + @Slf4j @RestController @RequestMapping(value = {NewUrl.CUSTOM_AUTH}) @@ -72,9 +63,10 @@ public Mono> loginWithThirdParty( @RequestParam(required = false) String source, @RequestParam String code, @RequestParam(required = false) String invitationId, - @RequestParam(required = false) String redirectUrl, + @RequestParam String redirectUrl, + @RequestParam String orgId, ServerWebExchange exchange) { - return authenticationApiService.authenticateByOauth2(authId, source, code, redirectUrl) + return authenticationApiService.authenticateByOauth2(authId, source, code, redirectUrl, orgId) .flatMap(authUser -> authenticationApiService.loginOrRegister(authUser, exchange, invitationId)) .thenReturn(ResponseView.success(true)); } @@ -99,10 +91,10 @@ public Mono> disableAuthConfig(@PathVariable("id") String id) .thenReturn(ResponseView.success(null)); } - @JsonView(JsonViews.Public.class) + @JsonView(JsonViews.Internal.class) @GetMapping("/configs") public Mono>> getAllConfigs() { - return authenticationService.findAllAuthConfigs(false) + return authenticationApiService.findAuthConfigs(false) .map(FindAuthConfig::authConfig) .collectList() .map(ResponseView::success); diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/request/AuthRequest.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/request/AuthRequest.java index 614bcc621..20d9ef98f 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/request/AuthRequest.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/request/AuthRequest.java @@ -1,9 +1,7 @@ package org.lowcoder.api.authentication.request; import org.lowcoder.domain.authentication.context.AuthRequestContext; -import org.lowcoder.domain.user.model.AuthToken; import org.lowcoder.domain.user.model.AuthUser; - import reactor.core.publisher.Mono; /** @@ -13,7 +11,5 @@ public interface AuthRequest { Mono auth(AuthRequestContext authRequestContext); - default Mono refresh(String refreshToken) { - return Mono.error(new UnsupportedOperationException()); - } + Mono refresh(String refreshToken); } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/request/form/FormAuthRequest.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/request/form/FormAuthRequest.java index 7ba33b7e6..25ff34255 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/request/form/FormAuthRequest.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/request/form/FormAuthRequest.java @@ -1,7 +1,5 @@ package org.lowcoder.api.authentication.request.form; -import static org.lowcoder.sdk.util.ExceptionUtils.ofError; - import org.lowcoder.api.authentication.request.AuthRequest; import org.lowcoder.domain.authentication.context.AuthRequestContext; import org.lowcoder.domain.authentication.context.FormAuthRequestContext; @@ -15,9 +13,10 @@ import org.lowcoder.sdk.exception.BizException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; - import reactor.core.publisher.Mono; +import static org.lowcoder.sdk.util.ExceptionUtils.ofError; + @Component public class FormAuthRequest implements AuthRequest { @@ -58,4 +57,9 @@ public Mono auth(AuthRequestContext authRequestContext) { }) .thenReturn(AuthUser.builder().uid(context.getLoginId()).username(context.getLoginId()).build()); } + + @Override + public Mono refresh(String refreshToken) { + return Mono.error(new UnsupportedOperationException()); + } } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/request/oauth2/OAuth2RequestContext.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/request/oauth2/OAuth2RequestContext.java index c756a1780..ae5eae6c3 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/request/oauth2/OAuth2RequestContext.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/request/oauth2/OAuth2RequestContext.java @@ -9,7 +9,8 @@ public final class OAuth2RequestContext extends AuthRequestContext { private final String code; private final String redirectUrl; - public OAuth2RequestContext(String code, String redirectUrl) { + public OAuth2RequestContext(String orgId, String code, String redirectUrl) { + this.setOrgId(orgId); this.code = code; this.redirectUrl = redirectUrl; } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/request/oauth2/Oauth2DefaultSource.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/request/oauth2/Oauth2DefaultSource.java index fa081d6b5..7ac0a7b28 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/request/oauth2/Oauth2DefaultSource.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/request/oauth2/Oauth2DefaultSource.java @@ -13,6 +13,11 @@ public String userInfo() { return "https://api.github.com/user"; } + @Override + public String refresh() { + return "https://www.googleapis.com/oauth2/v4/token"; + } + }, GOOGLE { @Override @@ -25,5 +30,10 @@ public String userInfo() { return "https://www.googleapis.com/oauth2/v3/userinfo"; } + @Override + public String refresh() { + return "https://www.googleapis.com/oauth2/v4/token"; + } + } } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/request/oauth2/Oauth2Source.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/request/oauth2/Oauth2Source.java index 8fdf23dc5..47748510b 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/request/oauth2/Oauth2Source.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/request/oauth2/Oauth2Source.java @@ -6,9 +6,7 @@ public interface Oauth2Source { String userInfo(); - default String refresh() { - throw new UnsupportedOperationException(getName()); - } + String refresh(); default String getName() { if (this instanceof Enum) { diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/request/oauth2/request/AbstractOauth2Request.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/request/oauth2/request/AbstractOauth2Request.java index 639294e04..b394556fb 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/request/oauth2/request/AbstractOauth2Request.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/request/oauth2/request/AbstractOauth2Request.java @@ -37,7 +37,19 @@ public Mono auth(AuthRequestContext authRequestContext) { .subscribeOn(AUTH_REQUEST_THREAD_POOL); } + public Mono refresh(String refreshToken) { + return refreshAuthToken(refreshToken) + .flatMap(authToken -> getAuthUser(authToken).doOnNext(authUser -> authUser.setAuthToken(authToken))) + .onErrorResume(throwable -> { + log.error("failed to refresh token: ", throwable); + return deferredError(FAIL_TO_GET_OIDC_INFO, "FAIL_TO_GET_OIDC_INFO", throwable.getMessage()); + }) + .subscribeOn(AUTH_REQUEST_THREAD_POOL); + } + protected abstract Mono getAuthToken(OAuth2RequestContext context); + protected abstract Mono refreshAuthToken(String refreshToken); + protected abstract Mono getAuthUser(AuthToken authToken); } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/request/oauth2/request/GithubRequest.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/request/oauth2/request/GithubRequest.java index edd3370ac..6d5217678 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/request/oauth2/request/GithubRequest.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/request/oauth2/request/GithubRequest.java @@ -59,6 +59,11 @@ protected Mono getAuthToken(OAuth2RequestContext context) { }); } + @Override + protected Mono refreshAuthToken(String refreshToken) { + return Mono.empty(); + } + private Map parseStringToMap(String s) { if (StringUtils.isBlank(s)) { return new HashMap<>(); diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/request/oauth2/request/GoogleRequest.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/request/oauth2/request/GoogleRequest.java index f9f26f5c3..fa1e0be5d 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/request/oauth2/request/GoogleRequest.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/request/oauth2/request/GoogleRequest.java @@ -53,11 +53,47 @@ protected Mono getAuthToken(OAuth2RequestContext context) { AuthToken authToken = AuthToken.builder() .accessToken(MapUtils.getString(map, "access_token")) .expireIn(MapUtils.getIntValue(map, "expires_in")) + .refreshToken(MapUtils.getString(map, "refresh_token")) .build(); return Mono.just(authToken); }); } + @Override + protected Mono refreshAuthToken(String refreshToken) { + + URI uri; + try { + uri = new URIBuilder(source.refresh()) + .addParameter("refresh_token", refreshToken) + .addParameter("client_id", config.getClientId()) + .addParameter("client_secret", config.getClientSecret()) + .addParameter("grant_type", "refresh_token") + .build(); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + + return WebClientBuildHelper.builder() + .systemProxy() + .build() + .post() + .uri(uri) + .exchangeToMono(response -> response.bodyToMono(new ParameterizedTypeReference>() { + })) + .flatMap(map -> { + if (map.containsKey("error") || map.containsKey("error_description")) { + throw new AuthException(JsonUtils.toJson(map)); + } + AuthToken authToken = AuthToken.builder() + .accessToken(MapUtils.getString(map, "access_token")) + .expireIn(MapUtils.getIntValue(map, "expires_in")) + .build(); + return Mono.just(authToken); + }); + + } + @Override protected Mono getAuthUser(AuthToken authToken) { return WebClientBuildHelper.builder() diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/service/AuthenticationApiService.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/service/AuthenticationApiService.java index 70164aa5d..c3db123f0 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/service/AuthenticationApiService.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/service/AuthenticationApiService.java @@ -1,20 +1,23 @@ package org.lowcoder.api.authentication.service; import org.lowcoder.api.authentication.dto.AuthConfigRequest; +import org.lowcoder.domain.authentication.FindAuthConfig; import org.lowcoder.domain.user.model.AuthUser; import org.springframework.web.server.ServerWebExchange; - +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; public interface AuthenticationApiService { Mono authenticateByForm(String loginId, String password, String source, boolean register, String authId); - Mono authenticateByOauth2(String authId, String source, String code, String redirectUrl); + Mono authenticateByOauth2(String authId, String source, String code, String redirectUrl, String orgId); Mono loginOrRegister(AuthUser authUser, ServerWebExchange exchange, String invitationId); Mono enableAuthConfig(AuthConfigRequest authConfigRequest); Mono disableAuthConfig(String authId); + + Flux findAuthConfigs(boolean enableOnly); } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/service/AuthenticationApiServiceImpl.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/service/AuthenticationApiServiceImpl.java index 6f2c9a07b..da0d58aff 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/service/AuthenticationApiServiceImpl.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/service/AuthenticationApiServiceImpl.java @@ -1,22 +1,6 @@ package org.lowcoder.api.authentication.service; -import static org.lowcoder.sdk.exception.BizError.AUTH_ERROR; -import static org.lowcoder.sdk.exception.BizError.DISABLE_AUTH_CONFIG_FORBIDDEN; -import static org.lowcoder.sdk.exception.BizError.USER_NOT_EXIST; -import static org.lowcoder.sdk.util.ExceptionUtils.deferredError; -import static org.lowcoder.sdk.util.ExceptionUtils.ofError; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.function.Function; -import java.util.stream.Collectors; - -import javax.annotation.Nullable; - +import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.lowcoder.api.authentication.dto.AuthConfigRequest; @@ -51,10 +35,18 @@ import org.springframework.security.core.context.ReactiveSecurityContextHolder; import org.springframework.stereotype.Service; import org.springframework.web.server.ServerWebExchange; - -import lombok.extern.slf4j.Slf4j; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import javax.annotation.Nullable; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static org.lowcoder.sdk.exception.BizError.*; +import static org.lowcoder.sdk.util.ExceptionUtils.deferredError; +import static org.lowcoder.sdk.util.ExceptionUtils.ofError; + @Service @Slf4j public class AuthenticationApiServiceImpl implements AuthenticationApiService { @@ -94,17 +86,17 @@ public Mono authenticateByForm(String loginId, String password, String } @Override - public Mono authenticateByOauth2(String authId, String source, String code, String redirectUrl) { - return authenticate(authId, source, new OAuth2RequestContext(code, redirectUrl)); + public Mono authenticateByOauth2(String authId, String source, String code, String redirectUrl, String orgId) { + return authenticate(authId, source, new OAuth2RequestContext(orgId, code, redirectUrl)); } protected Mono authenticate(String authId, @Deprecated String source, AuthRequestContext context) { return Mono.defer(() -> { if (StringUtils.isNotBlank(authId)) { - return authenticationService.findAuthConfigByAuthId(authId); + return authenticationService.findAuthConfigByAuthId(context.getOrgId(), authId); } log.warn("source is deprecated and will be removed in the future, please use authId instead. {}", source); - return authenticationService.findAuthConfigBySource(source); + return authenticationService.findAuthConfigBySource(context.getOrgId(), source); }) .doOnNext(findAuthConfig -> { context.setAuthConfig(findAuthConfig.authConfig()); @@ -166,6 +158,19 @@ private Mono updateOrCreateUser(AuthUser authUser) { return userService.update(user.getId(), user); } + // if the user is logging/registering via OAuth provider for the first time, + // but is not anonymous, then just add a new connection + + userService.findById(authUser.getUid()) + .switchIfEmpty(Mono.empty()) + .filter(user -> { + // not logged in yet + return !user.isAnonymous(); + }).doOnNext(user -> { + userService.addNewConnection(user.getId(), authUser.toAuthConnection()); + }).subscribe(); + + if (authUser.getAuthContext().getAuthConfig().isEnableRegister()) { return userService.createNewUserByAuthUser(authUser); } @@ -182,7 +187,7 @@ protected Mono findByAuthUser(AuthUser authUser) { /** * Update the connection after re-authenticating */ - private void updateConnection(AuthUser authUser, User user) { + public void updateConnection(AuthUser authUser, User user) { String orgId = authUser.getOrgId(); Connection oldConnection = getAuthConnection(authUser, user); @@ -224,7 +229,12 @@ public Mono enableAuthConfig(AuthConfigRequest authConfigRequest) { return checkIfAdmin() .then(sessionUserService.getVisitorOrgMemberCache()) .flatMap(orgMember -> organizationService.getById(orgMember.getOrgId())) - .doOnNext(organization -> addOrUpdateNewAuthConfig(organization, authConfigFactory.build(authConfigRequest, true))) + .doOnNext(organization -> { + boolean duplicateAuthType = addOrUpdateNewAuthConfig(organization, authConfigFactory.build(authConfigRequest, true)); + if(duplicateAuthType) { + deferredError(DUPLICATE_AUTH_CONFIG_ADDITION, "DUPLICATE_AUTH_CONFIG_ADDITION"); + } + }) .flatMap(organization -> organizationService.update(organization.getId(), organization)); } @@ -244,6 +254,14 @@ public Mono disableAuthConfig(String authId) { }); } + @Override + public Flux findAuthConfigs(boolean enableOnly) { + return checkIfAdmin(). + then(sessionUserService.getVisitorOrgMemberCache()) + .flatMapMany(orgMember -> authenticationService.findAllAuthConfigs(orgMember.getOrgId(),false)); + } + + private Mono removeTokensByAuthId(String authId) { return sessionUserService.getVisitorOrgMemberCache() .flatMapMany(orgMember -> orgMemberService.getOrganizationMembers(orgMember.getOrgId())) @@ -273,7 +291,7 @@ private Mono checkIfOnlyEffectiveCurrentUserConnections(String authId) { .filter(connection -> StringUtils.isNotBlank(connection.getAuthId())) .map(Connection::getAuthId) .collectList(); - Mono> orgAuthIdListMono = authenticationService.findAllAuthConfigs(true) + Mono> orgAuthIdListMono = authenticationService.findAllAuthConfigs(null, true) .map(FindAuthConfig::authConfig) .map(AbstractAuthConfig::getId) .collectList(); @@ -303,7 +321,7 @@ private void disableAuthConfig(Organization organization, String authId) { /** * If the source of the newAuthConfig exists in the auth configs of the organization, update it. Otherwise, add it. */ - private void addOrUpdateNewAuthConfig(Organization organization, AbstractAuthConfig newAuthConfig) { + private boolean addOrUpdateNewAuthConfig(Organization organization, AbstractAuthConfig newAuthConfig) { OrganizationDomain organizationDomain = organization.getOrganizationDomain(); if (organizationDomain == null) { organizationDomain = new OrganizationDomain(); @@ -313,6 +331,13 @@ private void addOrUpdateNewAuthConfig(Organization organization, AbstractAuthCon Map authConfigMap = organizationDomain.getConfigs() .stream() .collect(Collectors.toMap(AbstractAuthConfig::getId, Function.identity())); + + boolean authTypeAlreadyExists = authConfigMap.values().stream() + .anyMatch(config -> !config.getId().equals(newAuthConfig.getId()) && config.getAuthType().equals(newAuthConfig.getAuthType())); + if(authTypeAlreadyExists) { + return false; + } + // Under the organization, the source can uniquely identify the whole auth config. AbstractAuthConfig old = authConfigMap.get(newAuthConfig.getId()); if (old != null) { @@ -320,6 +345,9 @@ private void addOrUpdateNewAuthConfig(Organization organization, AbstractAuthCon } authConfigMap.put(newAuthConfig.getId(), newAuthConfig); organizationDomain.setConfigs(new ArrayList<>(authConfigMap.values())); + + return true; + } // static inner class diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/config/ConfigController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/config/ConfigController.java index 1bf9064be..3e83a7e9e 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/config/ConfigController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/config/ConfigController.java @@ -13,12 +13,7 @@ import org.lowcoder.sdk.config.dynamic.Conf; import org.lowcoder.sdk.config.dynamic.ConfigCenter; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import org.springframework.web.server.ServerWebExchange; import com.fasterxml.jackson.annotation.JsonView; @@ -70,8 +65,8 @@ public Mono> updateServerConfig(@PathVariable String @JsonView(JsonViews.Public.class) @GetMapping - public Mono> getConfig(ServerWebExchange exchange) { - return orgApiService.getOrganizationConfigs() + public Mono> getConfig(ServerWebExchange exchange,@RequestParam(required = false) String orgId) { + return orgApiService.getOrganizationConfigs(orgId) .map(ResponseView::success); } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/UserSessionPersistenceFilter.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/UserSessionPersistenceFilter.java index 5f106604e..fb6c24253 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/UserSessionPersistenceFilter.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/UserSessionPersistenceFilter.java @@ -1,28 +1,49 @@ package org.lowcoder.api.framework.filter; -import static org.lowcoder.api.authentication.util.AuthenticationUtils.toAuthentication; -import static org.springframework.security.core.context.ReactiveSecurityContextHolder.withAuthentication; - -import javax.annotation.Nonnull; - +import lombok.extern.slf4j.Slf4j; +import org.lowcoder.api.authentication.request.AuthRequest; +import org.lowcoder.api.authentication.request.AuthRequestFactory; +import org.lowcoder.api.authentication.request.oauth2.OAuth2RequestContext; +import org.lowcoder.api.authentication.service.AuthenticationApiServiceImpl; import org.lowcoder.api.home.SessionUserService; +import org.lowcoder.domain.authentication.AuthenticationService; +import org.lowcoder.domain.authentication.FindAuthConfig; +import org.lowcoder.domain.authentication.context.AuthRequestContext; +import org.lowcoder.domain.user.model.AuthUser; import org.lowcoder.sdk.util.CookieHelper; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilterChain; - -import lombok.extern.slf4j.Slf4j; import reactor.core.publisher.Mono; +import javax.annotation.Nonnull; +import java.time.Instant; +import java.util.LinkedList; +import java.util.List; + +import static org.lowcoder.api.authentication.util.AuthenticationUtils.toAuthentication; +import static org.lowcoder.domain.authentication.AuthenticationService.DEFAULT_AUTH_CONFIG; +import static org.springframework.security.core.context.ReactiveSecurityContextHolder.withAuthentication; + @Slf4j public class UserSessionPersistenceFilter implements WebFilter { private final SessionUserService service; private final CookieHelper cookieHelper; - public UserSessionPersistenceFilter(SessionUserService service, CookieHelper cookieHelper) { + private final AuthenticationService authenticationService; + + private final AuthenticationApiServiceImpl authenticationApiService; + + private final AuthRequestFactory authRequestFactory; + + public UserSessionPersistenceFilter(SessionUserService service, CookieHelper cookieHelper, AuthenticationService authenticationService, + AuthenticationApiServiceImpl authenticationApiService, AuthRequestFactory authRequestFactory) { this.service = service; this.cookieHelper = cookieHelper; + this.authenticationService = authenticationService; + this.authenticationApiService = authenticationApiService; + this.authRequestFactory = authRequestFactory; } @Nonnull @@ -31,6 +52,42 @@ public Mono filter(@Nonnull ServerWebExchange exchange, WebFilterChain cha String cookieToken = cookieHelper.getCookieToken(exchange); return service.resolveSessionUserFromCookie(cookieToken) .switchIfEmpty(chain.filter(exchange).then(Mono.empty())) + .doOnNext(user -> { + + List tokensToRemove = new LinkedList<>(); + + user.getConnections().forEach(connection -> { + if(!connection.getAuthId().equals(DEFAULT_AUTH_CONFIG.getId())) { + Instant next5Minutes = Instant.now().plusSeconds( 300 ); + boolean isAccessTokenExpiryNear = connection.getAuthConnectionAuthToken().getExpireAt() <= next5Minutes.toEpochMilli(); + if(isAccessTokenExpiryNear) { + connection.getOrgIds().forEach(orgId -> { + FindAuthConfig findAuthConfig = authenticationService.findAuthConfigByAuthId(orgId, connection.getAuthId()).block(); + if(findAuthConfig == null) { + return; + } + OAuth2RequestContext oAuth2RequestContext = new OAuth2RequestContext(orgId, null, null); + oAuth2RequestContext.setAuthConfig(findAuthConfig.authConfig()); + AuthRequest authRequest = authRequestFactory.build(oAuth2RequestContext).block(); + try { + AuthUser authUser = authRequest.refresh(connection.getAuthConnectionAuthToken().getRefreshToken()).block(); + authUser.setAuthContext(oAuth2RequestContext); + authenticationApiService.updateConnection(authUser, user); + } catch (Exception e) { + log.error("Failed to refresh access token. Removing user sessions/tokens."); + tokensToRemove.addAll(connection.getTokens()); + } + }); + + } + } + }); + + tokensToRemove.forEach(token -> { + service.removeUserSession(token).block(); + }); + + }) .flatMap(user -> chain.filter(exchange).contextWrite(withAuthentication(toAuthentication(user))) .then(service.extendValidity(cookieToken)) ); diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/security/SecurityConfig.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/security/SecurityConfig.java index 38ed3d03a..f7221862a 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/security/SecurityConfig.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/security/SecurityConfig.java @@ -1,8 +1,12 @@ package org.lowcoder.api.framework.security; +import org.lowcoder.api.authentication.request.AuthRequestFactory; +import org.lowcoder.api.authentication.service.AuthenticationApiServiceImpl; import org.lowcoder.api.framework.filter.UserSessionPersistenceFilter; import org.lowcoder.api.home.SessionUserService; +import org.lowcoder.domain.authentication.AuthenticationService; +import org.lowcoder.domain.authentication.context.AuthRequestContext; import org.lowcoder.domain.user.model.User; import org.lowcoder.infra.constant.NewUrl; import org.lowcoder.sdk.config.CommonConfig; @@ -52,6 +56,15 @@ public class SecurityConfig { @Autowired private CookieHelper cookieHelper; + @Autowired + AuthenticationService authenticationService; + + @Autowired + AuthenticationApiServiceImpl authenticationApiService; + + @Autowired + AuthRequestFactory authRequestFactory; + @Bean public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { @@ -135,7 +148,7 @@ public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { .authenticationEntryPoint(serverAuthenticationEntryPoint) .accessDeniedHandler(accessDeniedHandler); - http.addFilterBefore(new UserSessionPersistenceFilter(sessionUserService, cookieHelper), SecurityWebFiltersOrder.AUTHENTICATION); + http.addFilterBefore(new UserSessionPersistenceFilter(sessionUserService, cookieHelper, authenticationService, authenticationApiService, authRequestFactory), SecurityWebFiltersOrder.AUTHENTICATION); return http.build(); } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrgApiService.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrgApiService.java index c748677fa..2990d8047 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrgApiService.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrgApiService.java @@ -44,6 +44,6 @@ public interface OrgApiService { Mono tryAddUserToOrgAndSwitchOrg(String orgId, String userId); - Mono getOrganizationConfigs(); + Mono getOrganizationConfigs(String orgId); } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrgApiServiceImpl.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrgApiServiceImpl.java index 6ff6af286..f50b0018b 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrgApiServiceImpl.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrgApiServiceImpl.java @@ -355,8 +355,8 @@ public Mono tryAddUserToOrgAndSwitchOrg(String orgId, String userId) { } @Override - public Mono getOrganizationConfigs() { - return authenticationService.findAllAuthConfigs(true) + public Mono getOrganizationConfigs(String orgId) { + return authenticationService.findAllAuthConfigs(orgId,true) .map(FindAuthConfig::authConfig) .collectList() .zipWith(organizationService.getByDomain().hasElement()) diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/view/InvitationVO.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/view/InvitationVO.java index e23d182cf..a10dc067d 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/view/InvitationVO.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/view/InvitationVO.java @@ -17,11 +17,14 @@ public class InvitationVO { private final String invitedOrganizationName; + private final String invitedOrganizationId; + public static InvitationVO from(Invitation invitation, User createUser, Organization invitedOrganization) { return InvitationVO.builder() .inviteCode(invitation.getId()) .createUserName(createUser.getName()) .invitedOrganizationName(invitedOrganization.getName()) + .invitedOrganizationId(invitedOrganization.getId()) .build(); } diff --git a/server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yml b/server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yml index 785311948..a6a3df204 100644 --- a/server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yml +++ b/server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yml @@ -1,7 +1,3 @@ -auth: - email: - enable: true - spring: data: mongodb: diff --git a/server/api-service/lowcoder-server/src/test/java/org/lowcoder/api/authentication/AuthenticationControllerTest.java b/server/api-service/lowcoder-server/src/test/java/org/lowcoder/api/authentication/AuthenticationControllerTest.java index 090e4280a..b4689f312 100644 --- a/server/api-service/lowcoder-server/src/test/java/org/lowcoder/api/authentication/AuthenticationControllerTest.java +++ b/server/api-service/lowcoder-server/src/test/java/org/lowcoder/api/authentication/AuthenticationControllerTest.java @@ -189,7 +189,7 @@ public void testLoginFailByLoginIdNotExist() { } private String getEmailAuthConfigId() { - return authenticationService.findAuthConfigBySource(AuthSourceConstants.EMAIL) + return authenticationService.findAuthConfigBySource(null, AuthSourceConstants.EMAIL) .map(FindAuthConfig::authConfig) .map(AbstractAuthConfig::getId) .block(); diff --git a/server/api-service/lowcoder-server/src/test/java/org/lowcoder/api/authentication/GoogleAuthenticateTest.java b/server/api-service/lowcoder-server/src/test/java/org/lowcoder/api/authentication/GoogleAuthenticateTest.java index 7148ef8a1..7ed312d66 100644 --- a/server/api-service/lowcoder-server/src/test/java/org/lowcoder/api/authentication/GoogleAuthenticateTest.java +++ b/server/api-service/lowcoder-server/src/test/java/org/lowcoder/api/authentication/GoogleAuthenticateTest.java @@ -91,7 +91,7 @@ public void testGoogleRegisterSuccess() { } private String getGoogleAuthConfigId() { - return authenticationService.findAuthConfigBySource(AuthSourceConstants.GOOGLE) + return authenticationService.findAuthConfigBySource(null, AuthSourceConstants.GOOGLE) .map(FindAuthConfig::authConfig) .map(AbstractAuthConfig::getId) .block();