diff --git a/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/auth/Oauth2OryAuthConfig.java b/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/auth/Oauth2OryAuthConfig.java new file mode 100644 index 000000000..8082a62cb --- /dev/null +++ b/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/auth/Oauth2OryAuthConfig.java @@ -0,0 +1,44 @@ +package org.lowcoder.sdk.auth; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonView; +import lombok.Getter; +import org.apache.commons.lang3.StringUtils; +import org.lowcoder.sdk.auth.constants.AuthTypeConstants; +import org.lowcoder.sdk.auth.constants.Oauth2Constants; +import org.lowcoder.sdk.config.SerializeConfig.JsonViews; + +import javax.annotation.Nullable; +import java.util.function.Function; + +import static org.lowcoder.sdk.auth.constants.Oauth2Constants.CLIENT_ID_PLACEHOLDER; +import static org.lowcoder.sdk.auth.constants.Oauth2Constants.INSTANCE_ID_PLACEHOLDER; + +/** + * OAuth2 ORY auth config. + */ +@Getter +public class Oauth2OryAuthConfig extends Oauth2SimpleAuthConfig { + + protected String instanceId; + + @JsonCreator + public Oauth2OryAuthConfig( + @Nullable String id, + Boolean enable, + Boolean enableRegister, + String source, + String sourceName, + String clientId, + String clientSecret, + String instanceId, + String authType) { + super(id, enable, enableRegister, source, sourceName, clientId, clientSecret, authType); + this.instanceId = instanceId; + } + + @Override + public String replaceAuthUrlClientIdPlaceholder(String url) { + return super.replaceAuthUrlClientIdPlaceholder(url).replace(INSTANCE_ID_PLACEHOLDER, instanceId); + } +} 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 ef152a8ce..7ab980785 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 @@ -52,6 +52,7 @@ public String getAuthorizeUrl() { return switch (authType) { case AuthTypeConstants.GOOGLE -> replaceAuthUrlClientIdPlaceholder(Oauth2Constants.GOOGLE_AUTHORIZE_URL); case AuthTypeConstants.GITHUB -> replaceAuthUrlClientIdPlaceholder(Oauth2Constants.GITHUB_AUTHORIZE_URL); + case AuthTypeConstants.ORY -> replaceAuthUrlClientIdPlaceholder(Oauth2Constants.ORY_AUTHORIZE_URL); default -> null; }; } @@ -73,7 +74,7 @@ public void merge(AbstractAuthConfig oldConfig) { } } - private String replaceAuthUrlClientIdPlaceholder(String url) { + public 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/auth/constants/AuthTypeConstants.java b/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/auth/constants/AuthTypeConstants.java index 495e96aa7..bed41b4f8 100644 --- a/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/auth/constants/AuthTypeConstants.java +++ b/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/auth/constants/AuthTypeConstants.java @@ -8,4 +8,5 @@ public class AuthTypeConstants { public static final String FORM = "FORM"; public static final String GOOGLE = "GOOGLE"; public static final String GITHUB = "GITHUB"; + public static final String ORY = "ORY"; } diff --git a/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/auth/constants/Oauth2Constants.java b/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/auth/constants/Oauth2Constants.java index d9522c894..c6e686322 100644 --- a/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/auth/constants/Oauth2Constants.java +++ b/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/auth/constants/Oauth2Constants.java @@ -7,6 +7,8 @@ public class Oauth2Constants { public static final String REDIRECT_URL_PLACEHOLDER = "$REDIRECT_URL"; public static final String STATE_PLACEHOLDER = "$STATE"; + public static final String INSTANCE_ID_PLACEHOLDER = "INSTANCE_ID"; + // authorize url public static final String GITHUB_AUTHORIZE_URL = "https://github.com/login/oauth/authorize" + "?response_type=code" @@ -23,4 +25,11 @@ public class Oauth2Constants { + "&access_type=offline" + "&scope=openid email profile" + "&prompt=select_account"; + + public static final String ORY_AUTHORIZE_URL = "https://" + INSTANCE_ID_PLACEHOLDER + "/oauth2/auth" + + "?response_type=code" + + "&client_id=" + CLIENT_ID_PLACEHOLDER + + "&redirect_uri=" + REDIRECT_URL_PLACEHOLDER + + "&state=" + STATE_PLACEHOLDER + + "&scope=openid email profile offline_access"; } diff --git a/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/constants/AuthSourceConstants.java b/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/constants/AuthSourceConstants.java index 9373c197b..073d333d0 100644 --- a/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/constants/AuthSourceConstants.java +++ b/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/constants/AuthSourceConstants.java @@ -10,10 +10,12 @@ public class AuthSourceConstants { public static final String PHONE = "PHONE"; public static final String GOOGLE = "GOOGLE"; public static final String GITHUB = "GITHUB"; + public static final String ORY = "ORY"; // source name public static final String GOOGLE_NAME = "Google"; public static final String GITHUB_NAME = "Github"; + public static final String ORY_NAME = "Ory"; // default source and source name for common protocol // oauth 2.0 diff --git a/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/util/JsonUtils.java b/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/util/JsonUtils.java index f3c97ee0a..eedb3fe42 100644 --- a/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/util/JsonUtils.java +++ b/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/util/JsonUtils.java @@ -1,9 +1,5 @@ package org.lowcoder.sdk.util; -import static org.lowcoder.sdk.auth.constants.AuthTypeConstants.FORM; -import static org.lowcoder.sdk.auth.constants.AuthTypeConstants.GITHUB; -import static org.lowcoder.sdk.auth.constants.AuthTypeConstants.GOOGLE; - import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Map; @@ -12,6 +8,7 @@ import javax.annotation.Nullable; import org.lowcoder.sdk.auth.EmailAuthConfig; +import org.lowcoder.sdk.auth.Oauth2OryAuthConfig; import org.lowcoder.sdk.auth.Oauth2SimpleAuthConfig; import com.fasterxml.jackson.annotation.JsonCreator; @@ -32,6 +29,8 @@ import lombok.extern.slf4j.Slf4j; +import static org.lowcoder.sdk.auth.constants.AuthTypeConstants.*; + @Slf4j public final class JsonUtils { @@ -46,6 +45,7 @@ public final class JsonUtils { OBJECT_MAPPER.registerSubtypes(new NamedType(EmailAuthConfig.class, FORM)); OBJECT_MAPPER.registerSubtypes(new NamedType(Oauth2SimpleAuthConfig.class, GITHUB)); OBJECT_MAPPER.registerSubtypes(new NamedType(Oauth2SimpleAuthConfig.class, GOOGLE)); + OBJECT_MAPPER.registerSubtypes(new NamedType(Oauth2OryAuthConfig.class, ORY)); } public static final JsonNode EMPTY_JSON_NODE = createObjectNode(); diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/dto/AuthConfigRequest.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/dto/AuthConfigRequest.java index 14a424aea..77f134458 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/dto/AuthConfigRequest.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/dto/AuthConfigRequest.java @@ -28,6 +28,11 @@ public boolean isEnableRegister() { return MapUtils.getBoolean(this, "enableRegister", true); } + @Nullable + public String getInstanceId() { + return getString("instanceId"); + } + @Nullable public String getClientId() { return getString("clientId"); diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/request/oauth2/Oauth2AuthRequestFactory.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/request/oauth2/Oauth2AuthRequestFactory.java index 607a0fad8..84794e494 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/request/oauth2/Oauth2AuthRequestFactory.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/request/oauth2/Oauth2AuthRequestFactory.java @@ -1,8 +1,5 @@ package org.lowcoder.api.authentication.request.oauth2; -import static org.lowcoder.sdk.auth.constants.AuthTypeConstants.GITHUB; -import static org.lowcoder.sdk.auth.constants.AuthTypeConstants.GOOGLE; - import java.util.Set; import org.lowcoder.api.authentication.request.AuthRequest; @@ -10,11 +7,15 @@ import org.lowcoder.api.authentication.request.oauth2.request.AbstractOauth2Request; import org.lowcoder.api.authentication.request.oauth2.request.GithubRequest; import org.lowcoder.api.authentication.request.oauth2.request.GoogleRequest; +import org.lowcoder.api.authentication.request.oauth2.request.OryRequest; +import org.lowcoder.sdk.auth.Oauth2OryAuthConfig; import org.lowcoder.sdk.auth.Oauth2SimpleAuthConfig; import org.springframework.stereotype.Component; import reactor.core.publisher.Mono; +import static org.lowcoder.sdk.auth.constants.AuthTypeConstants.*; + @Component public class Oauth2AuthRequestFactory implements AuthRequestFactory { @@ -27,6 +28,7 @@ private AbstractOauth2Request buildRequest(OAu return switch (context.getAuthConfig().getAuthType()) { case GITHUB -> new GithubRequest((Oauth2SimpleAuthConfig) context.getAuthConfig()); case GOOGLE -> new GoogleRequest((Oauth2SimpleAuthConfig) context.getAuthConfig()); + case ORY -> new OryRequest((Oauth2OryAuthConfig) context.getAuthConfig()); default -> throw new UnsupportedOperationException(context.getAuthConfig().getAuthType()); }; } @@ -35,6 +37,7 @@ private AbstractOauth2Request buildRequest(OAu public Set supportedAuthTypes() { return Set.of( GITHUB, - GOOGLE); + GOOGLE, + ORY); } } 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 7ac0a7b28..c0242d153 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 @@ -1,5 +1,7 @@ package org.lowcoder.api.authentication.request.oauth2; +import org.lowcoder.sdk.auth.constants.Oauth2Constants; + public enum Oauth2DefaultSource implements Oauth2Source { GITHUB { @@ -35,5 +37,23 @@ public String refresh() { return "https://www.googleapis.com/oauth2/v4/token"; } + }, + + ORY { + @Override + public String accessToken() { + return "https://" + Oauth2Constants.INSTANCE_ID_PLACEHOLDER + "/oauth2/token"; + } + + @Override + public String userInfo() { + return "https://" + Oauth2Constants.INSTANCE_ID_PLACEHOLDER + "/userinfo"; + } + + @Override + public String refresh() { + return "https://" + Oauth2Constants.INSTANCE_ID_PLACEHOLDER + "/oauth2/token"; + } + } } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/request/oauth2/request/OryRequest.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/request/oauth2/request/OryRequest.java new file mode 100644 index 000000000..72e634a24 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/request/oauth2/request/OryRequest.java @@ -0,0 +1,123 @@ +package org.lowcoder.api.authentication.request.oauth2.request; + +import org.apache.commons.collections4.MapUtils; +import org.apache.http.client.utils.URIBuilder; +import org.lowcoder.api.authentication.request.AuthException; +import org.lowcoder.api.authentication.request.oauth2.OAuth2RequestContext; +import org.lowcoder.api.authentication.request.oauth2.Oauth2DefaultSource; +import org.lowcoder.domain.user.model.AuthToken; +import org.lowcoder.domain.user.model.AuthUser; +import org.lowcoder.sdk.auth.Oauth2OryAuthConfig; +import org.lowcoder.sdk.util.JsonUtils; +import org.lowcoder.sdk.webclient.WebClientBuildHelper; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.MediaType; +import reactor.core.publisher.Mono; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Map; + +import static org.springframework.web.reactive.function.BodyInserters.fromFormData; + +public class OryRequest extends AbstractOauth2Request { + + public OryRequest(Oauth2OryAuthConfig config) { + super(config, Oauth2DefaultSource.ORY); + } + + @Override + protected Mono getAuthToken(OAuth2RequestContext context) { + URI uri; + try { + uri = new URIBuilder(config.replaceAuthUrlClientIdPlaceholder(source.accessToken())).build(); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + + return WebClientBuildHelper.builder() + .systemProxy() + .build() + .post() + .uri(uri) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .body(fromFormData("code", context.getCode()) + .with("client_id", config.getClientId()) + .with("client_secret", config.getClientSecret()) + .with("grant_type", "authorization_code") + .with("redirect_uri", context.getRedirectUrl())) + .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")) + .refreshToken(MapUtils.getString(map, "refresh_token")) + .build(); + return Mono.just(authToken); + }); + } + + @Override + protected Mono refreshAuthToken(String refreshToken) { + + URI uri; + try { + uri = new URIBuilder(config.replaceAuthUrlClientIdPlaceholder(source.refresh())).build(); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + + return WebClientBuildHelper.builder() + .systemProxy() + .build() + .post() + .uri(uri) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .body(fromFormData("refresh_token", refreshToken) + .with("client_id", config.getClientId()) + .with("client_secret", config.getClientSecret()) + .with("grant_type", "refresh_token")) + .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")) + .refreshToken(MapUtils.getString(map, "refresh_token")) + .build(); + return Mono.just(authToken); + }); + + } + + @Override + protected Mono getAuthUser(AuthToken authToken) { + return WebClientBuildHelper.builder() + .systemProxy() + .build() + .post() + .uri(config.replaceAuthUrlClientIdPlaceholder(source.userInfo())) + .header("Authorization", "Bearer " + authToken.getAccessToken()) + .exchangeToMono(response -> response.bodyToMono(new ParameterizedTypeReference>() { + })) + .flatMap(map -> { + if (map.containsKey("error") || map.containsKey("error_description")) { + throw new AuthException(JsonUtils.toJson(map)); + } + AuthUser authUser = AuthUser.builder() + .uid(MapUtils.getString(map, "sub")) + .username(MapUtils.getString(map, "name")) + .avatar(MapUtils.getString(map, "picture")) + .rawUserInfo(map) + .build(); + return Mono.just(authUser); + }); + } +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/service/factory/AuthConfigFactoryImpl.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/service/factory/AuthConfigFactoryImpl.java index 15122109e..b5434c8e0 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/service/factory/AuthConfigFactoryImpl.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/service/factory/AuthConfigFactoryImpl.java @@ -1,21 +1,19 @@ package org.lowcoder.api.authentication.service.factory; -import static java.util.Objects.requireNonNull; -import static org.lowcoder.sdk.constants.AuthSourceConstants.GITHUB; -import static org.lowcoder.sdk.constants.AuthSourceConstants.GITHUB_NAME; -import static org.lowcoder.sdk.constants.AuthSourceConstants.GOOGLE; -import static org.lowcoder.sdk.constants.AuthSourceConstants.GOOGLE_NAME; - -import java.util.Set; - import org.apache.commons.collections4.MapUtils; import org.lowcoder.api.authentication.dto.AuthConfigRequest; import org.lowcoder.sdk.auth.AbstractAuthConfig; import org.lowcoder.sdk.auth.EmailAuthConfig; +import org.lowcoder.sdk.auth.Oauth2OryAuthConfig; import org.lowcoder.sdk.auth.Oauth2SimpleAuthConfig; import org.lowcoder.sdk.auth.constants.AuthTypeConstants; import org.springframework.stereotype.Component; +import java.util.Set; + +import static java.util.Objects.requireNonNull; +import static org.lowcoder.sdk.constants.AuthSourceConstants.*; + @Component public class AuthConfigFactoryImpl implements AuthConfigFactory { @@ -25,6 +23,7 @@ public AbstractAuthConfig build(AuthConfigRequest authConfigRequest, boolean ena case AuthTypeConstants.FORM -> buildEmailAuthConfig(authConfigRequest, enable); case AuthTypeConstants.GITHUB -> buildOauth2SimpleAuthConfig(GITHUB, GITHUB_NAME, authConfigRequest, enable); case AuthTypeConstants.GOOGLE -> buildOauth2SimpleAuthConfig(GOOGLE, GOOGLE_NAME, authConfigRequest, enable); + case AuthTypeConstants.ORY -> buildOauth2OryAuthConfig(authConfigRequest, enable); default -> throw new UnsupportedOperationException(authConfigRequest.getAuthType()); }; } @@ -34,7 +33,8 @@ public Set supportAuthTypes() { return Set.of( AuthTypeConstants.FORM, AuthTypeConstants.GITHUB, - AuthTypeConstants.GOOGLE + AuthTypeConstants.GOOGLE, + AuthTypeConstants.ORY ); } @@ -55,4 +55,17 @@ private Oauth2SimpleAuthConfig buildOauth2SimpleAuthConfig(String source, String authConfigRequest.getClientSecret(), authConfigRequest.getAuthType()); } + + private Oauth2SimpleAuthConfig buildOauth2OryAuthConfig(AuthConfigRequest authConfigRequest, boolean enable) { + return new Oauth2OryAuthConfig( + authConfigRequest.getId(), + enable, + authConfigRequest.isEnableRegister(), + AuthTypeConstants.ORY, + org.lowcoder.sdk.constants.AuthSourceConstants.ORY_NAME, + requireNonNull(authConfigRequest.getClientId(), "clientId can not be null."), + authConfigRequest.getClientSecret(), + authConfigRequest.getInstanceId(), + authConfigRequest.getAuthType()); + } } 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 fb6c24253..77d28486f 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 @@ -59,7 +59,7 @@ public Mono filter(@Nonnull ServerWebExchange exchange, WebFilterChain cha 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(); + boolean isAccessTokenExpiryNear = (connection.getAuthConnectionAuthToken().getExpireAt()*1000) <= next5Minutes.toEpochMilli(); if(isAccessTokenExpiryNear) { connection.getOrgIds().forEach(orgId -> { FindAuthConfig findAuthConfig = authenticationService.findAuthConfigByAuthId(orgId, connection.getAuthId()).block();