Skip to content

Commit 481568a

Browse files
aq-ikhwa-techludomikula
authored andcommitted
Add oAuth Providers Integration
- Add/Rework CRUD APIs for AuthConfigs to support org wide auth providers management. - Add auth token and refresh token handling if access token about to expire in next 5 minutes - Add support to allow same user to add multiple oAuth providers
1 parent 417c528 commit 481568a

File tree

24 files changed

+276
-110
lines changed

24 files changed

+276
-110
lines changed

server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/authentication/AuthenticationService.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ public interface AuthenticationService {
1010

1111
EmailAuthConfig DEFAULT_AUTH_CONFIG = new EmailAuthConfig(AuthSourceConstants.EMAIL, true, true);
1212

13-
Mono<FindAuthConfig> findAuthConfigByAuthId(String authId);
13+
Mono<FindAuthConfig> findAuthConfigByAuthId(String orgId, String authId);
1414

15-
Mono<FindAuthConfig> findAuthConfigBySource(String source);
15+
Mono<FindAuthConfig> findAuthConfigBySource(String orgId, String source);
1616

17-
Flux<FindAuthConfig> findAllAuthConfigs(boolean enableOnly);
17+
Flux<FindAuthConfig> findAllAuthConfigs(String orgId, boolean enableOnly);
1818
}

server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/authentication/AuthenticationServiceImpl.java

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,23 @@
11
package org.lowcoder.domain.authentication;
22

3-
import static org.lowcoder.sdk.exception.BizError.LOG_IN_SOURCE_NOT_SUPPORTED;
4-
import static org.lowcoder.sdk.util.ExceptionUtils.ofError;
5-
6-
import java.util.Objects;
7-
import java.util.function.Function;
8-
import java.util.stream.Collectors;
9-
3+
import lombok.extern.slf4j.Slf4j;
104
import org.lowcoder.domain.organization.service.OrganizationService;
115
import org.lowcoder.sdk.auth.AbstractAuthConfig;
126
import org.lowcoder.sdk.config.AuthProperties;
137
import org.lowcoder.sdk.config.CommonConfig;
148
import org.lowcoder.sdk.constants.WorkspaceMode;
159
import org.springframework.beans.factory.annotation.Autowired;
1610
import org.springframework.stereotype.Service;
17-
18-
import lombok.extern.slf4j.Slf4j;
1911
import reactor.core.publisher.Flux;
2012
import reactor.core.publisher.Mono;
2113

14+
import java.util.Objects;
15+
import java.util.function.Function;
16+
import java.util.stream.Collectors;
17+
18+
import static org.lowcoder.sdk.exception.BizError.LOG_IN_SOURCE_NOT_SUPPORTED;
19+
import static org.lowcoder.sdk.util.ExceptionUtils.ofError;
20+
2221
@Slf4j
2322
@Service
2423
public class AuthenticationServiceImpl implements AuthenticationService {
@@ -31,35 +30,35 @@ public class AuthenticationServiceImpl implements AuthenticationService {
3130
private AuthProperties authProperties;
3231

3332
@Override
34-
public Mono<FindAuthConfig> findAuthConfigByAuthId(String authId) {
35-
return findAuthConfig(abstractAuthConfig -> Objects.equals(authId, abstractAuthConfig.getId()));
33+
public Mono<FindAuthConfig> findAuthConfigByAuthId(String orgId, String authId) {
34+
return findAuthConfig(orgId, abstractAuthConfig -> Objects.equals(authId, abstractAuthConfig.getId()));
3635
}
3736

3837
@Override
3938
@Deprecated
40-
public Mono<FindAuthConfig> findAuthConfigBySource(String source) {
41-
return findAuthConfig(abstractAuthConfig -> Objects.equals(source, abstractAuthConfig.getSource()));
39+
public Mono<FindAuthConfig> findAuthConfigBySource(String orgId, String source) {
40+
return findAuthConfig(orgId, abstractAuthConfig -> Objects.equals(source, abstractAuthConfig.getSource()));
4241
}
4342

44-
private Mono<FindAuthConfig> findAuthConfig(Function<AbstractAuthConfig, Boolean> condition) {
45-
return findAllAuthConfigs(true)
43+
private Mono<FindAuthConfig> findAuthConfig(String orgId, Function<AbstractAuthConfig, Boolean> condition) {
44+
return findAllAuthConfigs(orgId,true)
4645
.filter(findAuthConfig -> condition.apply(findAuthConfig.authConfig()))
4746
.next()
4847
.switchIfEmpty(ofError(LOG_IN_SOURCE_NOT_SUPPORTED, "LOG_IN_SOURCE_NOT_SUPPORTED"));
4948
}
5049

5150
@Override
52-
public Flux<FindAuthConfig> findAllAuthConfigs(boolean enableOnly) {
51+
public Flux<FindAuthConfig> findAllAuthConfigs(String orgId, boolean enableOnly) {
5352
return findAllAuthConfigsByDomain()
5453
.switchIfEmpty(findAllAuthConfigsForEnterpriseMode())
55-
.switchIfEmpty(findAllAuthConfigsForSaasMode())
54+
.switchIfEmpty(findAllAuthConfigsForSaasMode(orgId))
5655
.filter(findAuthConfig -> {
5756
if (enableOnly) {
5857
return findAuthConfig.authConfig().isEnable();
5958
}
6059
return true;
6160
})
62-
.defaultIfEmpty(new FindAuthConfig(DEFAULT_AUTH_CONFIG, null));
61+
.concatWithValues(new FindAuthConfig(DEFAULT_AUTH_CONFIG, null));
6362
}
6463

6564
private Flux<FindAuthConfig> findAllAuthConfigsByDomain() {
@@ -85,10 +84,20 @@ protected Flux<FindAuthConfig> findAllAuthConfigsForEnterpriseMode() {
8584
);
8685
}
8786

88-
private Flux<FindAuthConfig> findAllAuthConfigsForSaasMode() {
87+
private Flux<FindAuthConfig> findAllAuthConfigsForSaasMode(String orgId) {
8988
if (commonConfig.getWorkspace().getMode() == WorkspaceMode.SAAS) {
90-
return Flux.fromIterable(authProperties.getAuthConfigs())
91-
.map(abstractAuthConfig -> new FindAuthConfig(abstractAuthConfig, null));
89+
90+
// Get the auth configs for the current org
91+
if(orgId != null) {
92+
return organizationService.getById(orgId)
93+
.flatMapIterable(organization ->
94+
organization.getAuthConfigs()
95+
.stream()
96+
.map(abstractAuthConfig -> new FindAuthConfig(abstractAuthConfig, organization))
97+
.collect(Collectors.toList())
98+
);
99+
}
100+
92101
}
93102
return Flux.empty();
94103
}

server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/auth/Oauth2SimpleAuthConfig.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
import lombok.Getter;
1616

17+
import static org.lowcoder.sdk.auth.constants.Oauth2Constants.CLIENT_ID_PLACEHOLDER;
18+
1719
/**
1820
* simple oauth2 auth config.
1921
*/
@@ -48,8 +50,8 @@ public Oauth2SimpleAuthConfig(
4850
@JsonView(JsonViews.Public.class)
4951
public String getAuthorizeUrl() {
5052
return switch (authType) {
51-
case AuthTypeConstants.GOOGLE -> Oauth2Constants.GOOGLE_AUTHORIZE_URL;
52-
case AuthTypeConstants.GITHUB -> Oauth2Constants.GITHUB_AUTHORIZE_URL;
53+
case AuthTypeConstants.GOOGLE -> replaceAuthUrlClientIdPlaceholder(Oauth2Constants.GOOGLE_AUTHORIZE_URL);
54+
case AuthTypeConstants.GITHUB -> replaceAuthUrlClientIdPlaceholder(Oauth2Constants.GITHUB_AUTHORIZE_URL);
5355
default -> null;
5456
};
5557
}
@@ -70,4 +72,8 @@ public void merge(AbstractAuthConfig oldConfig) {
7072
this.clientSecret = oldSimpleConfig.getClientSecret();
7173
}
7274
}
75+
76+
private String replaceAuthUrlClientIdPlaceholder(String url) {
77+
return url.replace(CLIENT_ID_PLACEHOLDER, clientId);
78+
}
7379
}

server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/exception/BizError.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ public enum BizError {
101101
USER_NOT_EXIST(400, 5618),
102102
JWT_NOT_FIND(400, 5619),
103103
ID_NOT_EXIST(500, 5620),
104+
DUPLICATE_AUTH_CONFIG_ADDITION(400, 5621),
104105

105106

106107
// asset related, code range 5700 - 5799

server/api-service/lowcoder-sdk/src/main/resources/locale_en.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,3 +277,4 @@ CERTIFICATE_EMPTY=Certificate is empty.
277277
ORG_DELETED_FOR_ENTERPRISE_MODE=Provided enterpriseOrgId workspace has been deleted, please contact Lowcoder team.
278278
DISABLE_AUTH_CONFIG_FORBIDDEN=Can not disable current administrator''s last identity provider.
279279
USER_NOT_EXIST=User not exist.
280+
DUPLICATE_AUTH_CONFIG_ADDITION=Provider auth type already added to organization

server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/AuthenticationController.java

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package org.lowcoder.api.authentication;
22

3-
import java.util.List;
4-
3+
import com.fasterxml.jackson.annotation.JsonView;
4+
import lombok.extern.slf4j.Slf4j;
55
import org.lowcoder.api.authentication.dto.AuthConfigRequest;
66
import org.lowcoder.api.authentication.service.AuthenticationApiService;
77
import org.lowcoder.api.framework.view.ResponseView;
@@ -17,21 +17,12 @@
1717
import org.lowcoder.sdk.constants.AuthSourceConstants;
1818
import org.lowcoder.sdk.util.CookieHelper;
1919
import org.springframework.beans.factory.annotation.Autowired;
20-
import org.springframework.web.bind.annotation.DeleteMapping;
21-
import org.springframework.web.bind.annotation.GetMapping;
22-
import org.springframework.web.bind.annotation.PathVariable;
23-
import org.springframework.web.bind.annotation.PostMapping;
24-
import org.springframework.web.bind.annotation.RequestBody;
25-
import org.springframework.web.bind.annotation.RequestMapping;
26-
import org.springframework.web.bind.annotation.RequestParam;
27-
import org.springframework.web.bind.annotation.RestController;
20+
import org.springframework.web.bind.annotation.*;
2821
import org.springframework.web.server.ServerWebExchange;
29-
30-
import com.fasterxml.jackson.annotation.JsonView;
31-
32-
import lombok.extern.slf4j.Slf4j;
3322
import reactor.core.publisher.Mono;
3423

24+
import java.util.List;
25+
3526
@Slf4j
3627
@RestController
3728
@RequestMapping(value = {NewUrl.CUSTOM_AUTH})
@@ -72,9 +63,10 @@ public Mono<ResponseView<Boolean>> loginWithThirdParty(
7263
@RequestParam(required = false) String source,
7364
@RequestParam String code,
7465
@RequestParam(required = false) String invitationId,
75-
@RequestParam(required = false) String redirectUrl,
66+
@RequestParam String redirectUrl,
67+
@RequestParam String orgId,
7668
ServerWebExchange exchange) {
77-
return authenticationApiService.authenticateByOauth2(authId, source, code, redirectUrl)
69+
return authenticationApiService.authenticateByOauth2(authId, source, code, redirectUrl, orgId)
7870
.flatMap(authUser -> authenticationApiService.loginOrRegister(authUser, exchange, invitationId))
7971
.thenReturn(ResponseView.success(true));
8072
}
@@ -99,10 +91,10 @@ public Mono<ResponseView<Void>> disableAuthConfig(@PathVariable("id") String id)
9991
.thenReturn(ResponseView.success(null));
10092
}
10193

102-
@JsonView(JsonViews.Public.class)
94+
@JsonView(JsonViews.Internal.class)
10395
@GetMapping("/configs")
10496
public Mono<ResponseView<List<AbstractAuthConfig>>> getAllConfigs() {
105-
return authenticationService.findAllAuthConfigs(false)
97+
return authenticationApiService.findAuthConfigs(false)
10698
.map(FindAuthConfig::authConfig)
10799
.collectList()
108100
.map(ResponseView::success);
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
package org.lowcoder.api.authentication.request;
22

33
import org.lowcoder.domain.authentication.context.AuthRequestContext;
4-
import org.lowcoder.domain.user.model.AuthToken;
54
import org.lowcoder.domain.user.model.AuthUser;
6-
75
import reactor.core.publisher.Mono;
86

97
/**
@@ -13,7 +11,5 @@ public interface AuthRequest {
1311

1412
Mono<AuthUser> auth(AuthRequestContext authRequestContext);
1513

16-
default Mono<AuthToken> refresh(String refreshToken) {
17-
return Mono.error(new UnsupportedOperationException());
18-
}
14+
Mono<AuthUser> refresh(String refreshToken);
1915
}

server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/request/form/FormAuthRequest.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
package org.lowcoder.api.authentication.request.form;
22

3-
import static org.lowcoder.sdk.util.ExceptionUtils.ofError;
4-
53
import org.lowcoder.api.authentication.request.AuthRequest;
64
import org.lowcoder.domain.authentication.context.AuthRequestContext;
75
import org.lowcoder.domain.authentication.context.FormAuthRequestContext;
@@ -15,9 +13,10 @@
1513
import org.lowcoder.sdk.exception.BizException;
1614
import org.springframework.beans.factory.annotation.Autowired;
1715
import org.springframework.stereotype.Component;
18-
1916
import reactor.core.publisher.Mono;
2017

18+
import static org.lowcoder.sdk.util.ExceptionUtils.ofError;
19+
2120
@Component
2221
public class FormAuthRequest implements AuthRequest {
2322

@@ -58,4 +57,9 @@ public Mono<AuthUser> auth(AuthRequestContext authRequestContext) {
5857
})
5958
.thenReturn(AuthUser.builder().uid(context.getLoginId()).username(context.getLoginId()).build());
6059
}
60+
61+
@Override
62+
public Mono<AuthUser> refresh(String refreshToken) {
63+
return Mono.error(new UnsupportedOperationException());
64+
}
6165
}

server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/request/oauth2/OAuth2RequestContext.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ public final class OAuth2RequestContext extends AuthRequestContext {
99
private final String code;
1010
private final String redirectUrl;
1111

12-
public OAuth2RequestContext(String code, String redirectUrl) {
12+
public OAuth2RequestContext(String orgId, String code, String redirectUrl) {
13+
this.setOrgId(orgId);
1314
this.code = code;
1415
this.redirectUrl = redirectUrl;
1516
}

server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/request/oauth2/Oauth2DefaultSource.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ public String userInfo() {
1313
return "https://api.github.com/user";
1414
}
1515

16+
@Override
17+
public String refresh() {
18+
return "https://www.googleapis.com/oauth2/v4/token";
19+
}
20+
1621
},
1722
GOOGLE {
1823
@Override
@@ -25,5 +30,10 @@ public String userInfo() {
2530
return "https://www.googleapis.com/oauth2/v3/userinfo";
2631
}
2732

33+
@Override
34+
public String refresh() {
35+
return "https://www.googleapis.com/oauth2/v4/token";
36+
}
37+
2838
}
2939
}

server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/request/oauth2/Oauth2Source.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@ public interface Oauth2Source {
66

77
String userInfo();
88

9-
default String refresh() {
10-
throw new UnsupportedOperationException(getName());
11-
}
9+
String refresh();
1210

1311
default String getName() {
1412
if (this instanceof Enum) {

server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/request/oauth2/request/AbstractOauth2Request.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,19 @@ public Mono<AuthUser> auth(AuthRequestContext authRequestContext) {
3737
.subscribeOn(AUTH_REQUEST_THREAD_POOL);
3838
}
3939

40+
public Mono<AuthUser> refresh(String refreshToken) {
41+
return refreshAuthToken(refreshToken)
42+
.flatMap(authToken -> getAuthUser(authToken).doOnNext(authUser -> authUser.setAuthToken(authToken)))
43+
.onErrorResume(throwable -> {
44+
log.error("failed to refresh token: ", throwable);
45+
return deferredError(FAIL_TO_GET_OIDC_INFO, "FAIL_TO_GET_OIDC_INFO", throwable.getMessage());
46+
})
47+
.subscribeOn(AUTH_REQUEST_THREAD_POOL);
48+
}
49+
4050
protected abstract Mono<AuthToken> getAuthToken(OAuth2RequestContext context);
4151

52+
protected abstract Mono<AuthToken> refreshAuthToken(String refreshToken);
53+
4254
protected abstract Mono<AuthUser> getAuthUser(AuthToken authToken);
4355
}

server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/request/oauth2/request/GithubRequest.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ protected Mono<AuthToken> getAuthToken(OAuth2RequestContext context) {
5959
});
6060
}
6161

62+
@Override
63+
protected Mono<AuthToken> refreshAuthToken(String refreshToken) {
64+
return Mono.empty();
65+
}
66+
6267
private Map<String, String> parseStringToMap(String s) {
6368
if (StringUtils.isBlank(s)) {
6469
return new HashMap<>();

server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/request/oauth2/request/GoogleRequest.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,47 @@ protected Mono<AuthToken> getAuthToken(OAuth2RequestContext context) {
5353
AuthToken authToken = AuthToken.builder()
5454
.accessToken(MapUtils.getString(map, "access_token"))
5555
.expireIn(MapUtils.getIntValue(map, "expires_in"))
56+
.refreshToken(MapUtils.getString(map, "refresh_token"))
5657
.build();
5758
return Mono.just(authToken);
5859
});
5960
}
6061

62+
@Override
63+
protected Mono<AuthToken> refreshAuthToken(String refreshToken) {
64+
65+
URI uri;
66+
try {
67+
uri = new URIBuilder(source.refresh())
68+
.addParameter("refresh_token", refreshToken)
69+
.addParameter("client_id", config.getClientId())
70+
.addParameter("client_secret", config.getClientSecret())
71+
.addParameter("grant_type", "refresh_token")
72+
.build();
73+
} catch (URISyntaxException e) {
74+
throw new RuntimeException(e);
75+
}
76+
77+
return WebClientBuildHelper.builder()
78+
.systemProxy()
79+
.build()
80+
.post()
81+
.uri(uri)
82+
.exchangeToMono(response -> response.bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {
83+
}))
84+
.flatMap(map -> {
85+
if (map.containsKey("error") || map.containsKey("error_description")) {
86+
throw new AuthException(JsonUtils.toJson(map));
87+
}
88+
AuthToken authToken = AuthToken.builder()
89+
.accessToken(MapUtils.getString(map, "access_token"))
90+
.expireIn(MapUtils.getIntValue(map, "expires_in"))
91+
.build();
92+
return Mono.just(authToken);
93+
});
94+
95+
}
96+
6197
@Override
6298
protected Mono<AuthUser> getAuthUser(AuthToken authToken) {
6399
return WebClientBuildHelper.builder()

0 commit comments

Comments
 (0)