Skip to content

Add oAuth Providers Integration #305

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ public interface AuthenticationService {

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

Mono<FindAuthConfig> findAuthConfigByAuthId(String authId);
Mono<FindAuthConfig> findAuthConfigByAuthId(String orgId, String authId);

Mono<FindAuthConfig> findAuthConfigBySource(String source);
Mono<FindAuthConfig> findAuthConfigBySource(String orgId, String source);

Flux<FindAuthConfig> findAllAuthConfigs(boolean enableOnly);
Flux<FindAuthConfig> findAllAuthConfigs(String orgId, boolean enableOnly);
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
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;
import org.lowcoder.sdk.config.CommonConfig;
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 {
Expand All @@ -31,35 +30,35 @@ public class AuthenticationServiceImpl implements AuthenticationService {
private AuthProperties authProperties;

@Override
public Mono<FindAuthConfig> findAuthConfigByAuthId(String authId) {
return findAuthConfig(abstractAuthConfig -> Objects.equals(authId, abstractAuthConfig.getId()));
public Mono<FindAuthConfig> findAuthConfigByAuthId(String orgId, String authId) {
return findAuthConfig(orgId, abstractAuthConfig -> Objects.equals(authId, abstractAuthConfig.getId()));
}

@Override
@Deprecated
public Mono<FindAuthConfig> findAuthConfigBySource(String source) {
return findAuthConfig(abstractAuthConfig -> Objects.equals(source, abstractAuthConfig.getSource()));
public Mono<FindAuthConfig> findAuthConfigBySource(String orgId, String source) {
return findAuthConfig(orgId, abstractAuthConfig -> Objects.equals(source, abstractAuthConfig.getSource()));
}

private Mono<FindAuthConfig> findAuthConfig(Function<AbstractAuthConfig, Boolean> condition) {
return findAllAuthConfigs(true)
private Mono<FindAuthConfig> findAuthConfig(String orgId, Function<AbstractAuthConfig, Boolean> 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<FindAuthConfig> findAllAuthConfigs(boolean enableOnly) {
public Flux<FindAuthConfig> 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<FindAuthConfig> findAllAuthConfigsByDomain() {
Expand All @@ -85,10 +84,20 @@ protected Flux<FindAuthConfig> findAllAuthConfigsForEnterpriseMode() {
);
}

private Flux<FindAuthConfig> findAllAuthConfigsForSaasMode() {
private Flux<FindAuthConfig> 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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

import lombok.Getter;

import static org.lowcoder.sdk.auth.constants.Oauth2Constants.CLIENT_ID_PLACEHOLDER;

/**
* simple oauth2 auth config.
*/
Expand Down Expand Up @@ -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;
};
}
Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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})
Expand Down Expand Up @@ -72,9 +63,10 @@ public Mono<ResponseView<Boolean>> 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));
}
Expand All @@ -99,10 +91,10 @@ public Mono<ResponseView<Void>> disableAuthConfig(@PathVariable("id") String id)
.thenReturn(ResponseView.success(null));
}

@JsonView(JsonViews.Public.class)
@JsonView(JsonViews.Internal.class)
@GetMapping("/configs")
public Mono<ResponseView<List<AbstractAuthConfig>>> getAllConfigs() {
return authenticationService.findAllAuthConfigs(false)
return authenticationApiService.findAuthConfigs(false)
.map(FindAuthConfig::authConfig)
.collectList()
.map(ResponseView::success);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/**
Expand All @@ -13,7 +11,5 @@ public interface AuthRequest {

Mono<AuthUser> auth(AuthRequestContext authRequestContext);

default Mono<AuthToken> refresh(String refreshToken) {
return Mono.error(new UnsupportedOperationException());
}
Mono<AuthUser> refresh(String refreshToken);
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 {

Expand Down Expand Up @@ -58,4 +57,9 @@ public Mono<AuthUser> auth(AuthRequestContext authRequestContext) {
})
.thenReturn(AuthUser.builder().uid(context.getLoginId()).username(context.getLoginId()).build());
}

@Override
public Mono<AuthUser> refresh(String refreshToken) {
return Mono.error(new UnsupportedOperationException());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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";
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,19 @@ public Mono<AuthUser> auth(AuthRequestContext authRequestContext) {
.subscribeOn(AUTH_REQUEST_THREAD_POOL);
}

public Mono<AuthUser> 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<AuthToken> getAuthToken(OAuth2RequestContext context);

protected abstract Mono<AuthToken> refreshAuthToken(String refreshToken);

protected abstract Mono<AuthUser> getAuthUser(AuthToken authToken);
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ protected Mono<AuthToken> getAuthToken(OAuth2RequestContext context) {
});
}

@Override
protected Mono<AuthToken> refreshAuthToken(String refreshToken) {
return Mono.empty();
}

private Map<String, String> parseStringToMap(String s) {
if (StringUtils.isBlank(s)) {
return new HashMap<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,47 @@ protected Mono<AuthToken> 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<AuthToken> 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<Map<String, Object>>() {
}))
.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<AuthUser> getAuthUser(AuthToken authToken) {
return WebClientBuildHelper.builder()
Expand Down
Loading