Skip to content

Commit 8a4c0f6

Browse files
authored
Merge pull request #411 from lowcoder-org/keycloak_auth
Keycloak auth
2 parents f7f37fc + bcde50c commit 8a4c0f6

File tree

13 files changed

+281
-49
lines changed

13 files changed

+281
-49
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package org.lowcoder.sdk.auth;
2+
3+
import static org.lowcoder.sdk.auth.constants.Oauth2Constants.INSTANCE_ID_PLACEHOLDER;
4+
import static org.lowcoder.sdk.auth.constants.Oauth2Constants.REALM_PLACEHOLDER;
5+
6+
import com.fasterxml.jackson.annotation.JsonCreator;
7+
import com.fasterxml.jackson.annotation.JsonProperty;
8+
9+
import lombok.Getter;
10+
11+
/**
12+
*
13+
* Keycloak OAuth configuration.
14+
*/
15+
@Getter
16+
public class Oauth2KeycloakAuthConfig extends Oauth2SimpleAuthConfig
17+
{
18+
protected String instanceId;
19+
protected String realm;
20+
21+
@JsonCreator
22+
public Oauth2KeycloakAuthConfig(
23+
@JsonProperty("id") String id,
24+
@JsonProperty("enable") Boolean enable,
25+
@JsonProperty("enableRegister") Boolean enableRegister,
26+
@JsonProperty("source") String source,
27+
@JsonProperty("sourceName") String sourceName,
28+
@JsonProperty("clientId") String clientId,
29+
@JsonProperty("clientSecret") String clientSecret,
30+
@JsonProperty("instanceId") String instanceId,
31+
@JsonProperty("realm") String realm,
32+
@JsonProperty("authType") String authType)
33+
{
34+
super(id, enable, enableRegister, source, sourceName, clientId, clientSecret, authType);
35+
this.instanceId = instanceId;
36+
this.realm = realm;
37+
}
38+
39+
40+
41+
@Override
42+
public String replaceAuthUrlClientIdPlaceholder(String url)
43+
{
44+
return super.replaceAuthUrlClientIdPlaceholder(url)
45+
.replace(INSTANCE_ID_PLACEHOLDER, instanceId)
46+
.replace(REALM_PLACEHOLDER, realm);
47+
}
48+
49+
50+
}

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

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,12 @@
11
package org.lowcoder.sdk.auth;
22

3-
import com.fasterxml.jackson.annotation.JsonCreator;
4-
import com.fasterxml.jackson.annotation.JsonView;
5-
import lombok.Getter;
6-
import org.apache.commons.lang3.StringUtils;
7-
import org.lowcoder.sdk.auth.constants.AuthTypeConstants;
8-
import org.lowcoder.sdk.auth.constants.Oauth2Constants;
9-
import org.lowcoder.sdk.config.SerializeConfig.JsonViews;
3+
import static org.lowcoder.sdk.auth.constants.Oauth2Constants.INSTANCE_ID_PLACEHOLDER;
104

115
import javax.annotation.Nullable;
12-
import java.util.function.Function;
136

14-
import static org.lowcoder.sdk.auth.constants.Oauth2Constants.CLIENT_ID_PLACEHOLDER;
15-
import static org.lowcoder.sdk.auth.constants.Oauth2Constants.INSTANCE_ID_PLACEHOLDER;
7+
import com.fasterxml.jackson.annotation.JsonCreator;
8+
9+
import lombok.Getter;
1610

1711
/**
1812
* OAuth2 ORY auth config.

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ public String getAuthorizeUrl() {
5353
case AuthTypeConstants.GOOGLE -> replaceAuthUrlClientIdPlaceholder(Oauth2Constants.GOOGLE_AUTHORIZE_URL);
5454
case AuthTypeConstants.GITHUB -> replaceAuthUrlClientIdPlaceholder(Oauth2Constants.GITHUB_AUTHORIZE_URL);
5555
case AuthTypeConstants.ORY -> replaceAuthUrlClientIdPlaceholder(Oauth2Constants.ORY_AUTHORIZE_URL);
56+
case AuthTypeConstants.KEYCLOAK -> replaceAuthUrlClientIdPlaceholder(Oauth2Constants.KEYCLOAK_AUTHORIZE_URL);
5657
default -> null;
5758
};
5859
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ public class AuthTypeConstants {
99
public static final String GOOGLE = "GOOGLE";
1010
public static final String GITHUB = "GITHUB";
1111
public static final String ORY = "ORY";
12+
public static final String KEYCLOAK = "KEYCLOAK";
1213
}

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ public class Oauth2Constants {
66
public static final String CLIENT_ID_PLACEHOLDER = "$CLIENT_ID";
77
public static final String REDIRECT_URL_PLACEHOLDER = "$REDIRECT_URL";
88
public static final String STATE_PLACEHOLDER = "$STATE";
9+
public static final String REALM_PLACEHOLDER = "$REALM";
910

10-
public static final String INSTANCE_ID_PLACEHOLDER = "INSTANCE_ID";
11+
public static final String INSTANCE_ID_PLACEHOLDER = "$INSTANCE_ID";
1112

1213
// authorize url
1314
public static final String GITHUB_AUTHORIZE_URL = "https://github.com/login/oauth/authorize"
@@ -32,4 +33,11 @@ public class Oauth2Constants {
3233
+ "&redirect_uri=" + REDIRECT_URL_PLACEHOLDER
3334
+ "&state=" + STATE_PLACEHOLDER
3435
+ "&scope=openid email profile offline_access";
36+
37+
public static final String KEYCLOAK_AUTHORIZE_URL = "https://" + INSTANCE_ID_PLACEHOLDER + "/realms/" + REALM_PLACEHOLDER + "/protocol/openid-connect/auth"
38+
+ "?response_type=code"
39+
+ "&client_id=" + CLIENT_ID_PLACEHOLDER
40+
+ "&redirect_uri=" + REDIRECT_URL_PLACEHOLDER
41+
+ "&state=" + STATE_PLACEHOLDER
42+
+ "&scope=openid email profile";
3543
}

server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/constants/AuthSourceConstants.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@ public class AuthSourceConstants {
1111
public static final String GOOGLE = "GOOGLE";
1212
public static final String GITHUB = "GITHUB";
1313
public static final String ORY = "ORY";
14+
public static final String KEYCLOAK = "KEYCLOAK";
1415

1516
// source name
1617
public static final String GOOGLE_NAME = "Google";
1718
public static final String GITHUB_NAME = "Github";
1819
public static final String ORY_NAME = "Ory";
20+
public static final String KEYCLOAK_NAME = "Keycloak";
1921

2022
// default source and source name for common protocol
2123
// oauth 2.0

server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/util/JsonUtils.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import javax.annotation.Nullable;
99

1010
import org.lowcoder.sdk.auth.EmailAuthConfig;
11+
import org.lowcoder.sdk.auth.Oauth2KeycloakAuthConfig;
1112
import org.lowcoder.sdk.auth.Oauth2OryAuthConfig;
1213
import org.lowcoder.sdk.auth.Oauth2SimpleAuthConfig;
1314

@@ -46,6 +47,7 @@ public final class JsonUtils {
4647
OBJECT_MAPPER.registerSubtypes(new NamedType(Oauth2SimpleAuthConfig.class, GITHUB));
4748
OBJECT_MAPPER.registerSubtypes(new NamedType(Oauth2SimpleAuthConfig.class, GOOGLE));
4849
OBJECT_MAPPER.registerSubtypes(new NamedType(Oauth2OryAuthConfig.class, ORY));
50+
OBJECT_MAPPER.registerSubtypes(new NamedType(Oauth2KeycloakAuthConfig.class, KEYCLOAK));
4951
}
5052

5153
public static final JsonNode EMPTY_JSON_NODE = createObjectNode();

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

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,35 @@
11
package org.lowcoder.api.authentication;
22

3-
import com.fasterxml.jackson.annotation.JsonView;
4-
import lombok.extern.slf4j.Slf4j;
3+
import java.util.List;
4+
55
import org.lowcoder.api.authentication.dto.AuthConfigRequest;
66
import org.lowcoder.api.authentication.service.AuthenticationApiService;
77
import org.lowcoder.api.framework.view.ResponseView;
88
import org.lowcoder.api.home.SessionUserService;
99
import org.lowcoder.api.usermanagement.UserController;
1010
import org.lowcoder.api.usermanagement.UserController.UpdatePasswordRequest;
1111
import org.lowcoder.api.util.BusinessEventPublisher;
12-
import org.lowcoder.domain.authentication.AuthenticationService;
1312
import org.lowcoder.domain.authentication.FindAuthConfig;
1413
import org.lowcoder.infra.constant.NewUrl;
1514
import org.lowcoder.sdk.auth.AbstractAuthConfig;
1615
import org.lowcoder.sdk.config.SerializeConfig.JsonViews;
1716
import org.lowcoder.sdk.constants.AuthSourceConstants;
1817
import org.lowcoder.sdk.util.CookieHelper;
1918
import org.springframework.beans.factory.annotation.Autowired;
20-
import org.springframework.web.bind.annotation.*;
19+
import org.springframework.web.bind.annotation.DeleteMapping;
20+
import org.springframework.web.bind.annotation.GetMapping;
21+
import org.springframework.web.bind.annotation.PathVariable;
22+
import org.springframework.web.bind.annotation.PostMapping;
23+
import org.springframework.web.bind.annotation.RequestBody;
24+
import org.springframework.web.bind.annotation.RequestMapping;
25+
import org.springframework.web.bind.annotation.RequestParam;
26+
import org.springframework.web.bind.annotation.RestController;
2127
import org.springframework.web.server.ServerWebExchange;
22-
import reactor.core.publisher.Mono;
2328

24-
import java.util.List;
29+
import com.fasterxml.jackson.annotation.JsonView;
30+
31+
import reactor.core.publisher.Mono;
2532

26-
@Slf4j
2733
@RestController
2834
@RequestMapping(value = {NewUrl.CUSTOM_AUTH})
2935
public class AuthenticationController {
@@ -36,8 +42,6 @@ public class AuthenticationController {
3642
private CookieHelper cookieHelper;
3743
@Autowired
3844
private BusinessEventPublisher businessEventPublisher;
39-
@Autowired
40-
private AuthenticationService authenticationService;
4145

4246
/**
4347
* login by email or phone with password; or register by email for now.

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
import org.lowcoder.api.authentication.request.oauth2.request.AbstractOauth2Request;
88
import org.lowcoder.api.authentication.request.oauth2.request.GithubRequest;
99
import org.lowcoder.api.authentication.request.oauth2.request.GoogleRequest;
10+
import org.lowcoder.api.authentication.request.oauth2.request.KeycloakRequest;
1011
import org.lowcoder.api.authentication.request.oauth2.request.OryRequest;
12+
import org.lowcoder.sdk.auth.Oauth2KeycloakAuthConfig;
1113
import org.lowcoder.sdk.auth.Oauth2OryAuthConfig;
1214
import org.lowcoder.sdk.auth.Oauth2SimpleAuthConfig;
1315
import org.springframework.stereotype.Component;
@@ -29,6 +31,7 @@ private AbstractOauth2Request<? extends Oauth2SimpleAuthConfig> buildRequest(OAu
2931
case GITHUB -> new GithubRequest((Oauth2SimpleAuthConfig) context.getAuthConfig());
3032
case GOOGLE -> new GoogleRequest((Oauth2SimpleAuthConfig) context.getAuthConfig());
3133
case ORY -> new OryRequest((Oauth2OryAuthConfig) context.getAuthConfig());
34+
case KEYCLOAK -> new KeycloakRequest((Oauth2KeycloakAuthConfig)context.getAuthConfig());
3235
default -> throw new UnsupportedOperationException(context.getAuthConfig().getAuthType());
3336
};
3437
}
@@ -38,6 +41,7 @@ public Set<String> supportedAuthTypes() {
3841
return Set.of(
3942
GITHUB,
4043
GOOGLE,
41-
ORY);
44+
ORY,
45+
KEYCLOAK);
4246
}
4347
}

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

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.lowcoder.api.authentication.request.oauth2;
22

33
import org.lowcoder.sdk.auth.constants.Oauth2Constants;
4+
import org.springframework.security.config.oauth2.client.CommonOAuth2Provider;
45

56
public enum Oauth2DefaultSource implements Oauth2Source {
67

@@ -17,7 +18,7 @@ public String userInfo() {
1718

1819
@Override
1920
public String refresh() {
20-
return "https://www.googleapis.com/oauth2/v4/token";
21+
return "https://github.com/login/oauth/access_token";
2122
}
2223

2324
},
@@ -55,5 +56,24 @@ public String refresh() {
5556
return "https://" + Oauth2Constants.INSTANCE_ID_PLACEHOLDER + "/oauth2/token";
5657
}
5758

59+
},
60+
61+
KEYCLOAK {
62+
63+
@Override
64+
public String accessToken() {
65+
return "http://" + Oauth2Constants.INSTANCE_ID_PLACEHOLDER + "/realms/" + Oauth2Constants.REALM_PLACEHOLDER + "/protocol/openid-connect/token";
66+
}
67+
68+
@Override
69+
public String userInfo() {
70+
return "http://" + Oauth2Constants.INSTANCE_ID_PLACEHOLDER + "/realms/" + Oauth2Constants.REALM_PLACEHOLDER + "/protocol/openid-connect/userinfo";
71+
}
72+
73+
@Override
74+
public String refresh() {
75+
return "http://" + Oauth2Constants.INSTANCE_ID_PLACEHOLDER + "/realms/" + Oauth2Constants.REALM_PLACEHOLDER + "/protocol/openid-connect/token";
76+
}
77+
5878
}
5979
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package org.lowcoder.api.authentication.request.oauth2.request;
2+
3+
import static org.springframework.web.reactive.function.BodyInserters.fromFormData;
4+
5+
import java.net.URI;
6+
import java.net.URISyntaxException;
7+
import java.util.Map;
8+
9+
import org.apache.commons.collections4.MapUtils;
10+
import org.apache.http.client.utils.URIBuilder;
11+
import org.lowcoder.api.authentication.request.AuthException;
12+
import org.lowcoder.api.authentication.request.oauth2.OAuth2RequestContext;
13+
import org.lowcoder.api.authentication.request.oauth2.Oauth2DefaultSource;
14+
import org.lowcoder.domain.user.model.AuthToken;
15+
import org.lowcoder.domain.user.model.AuthUser;
16+
import org.lowcoder.sdk.auth.Oauth2KeycloakAuthConfig;
17+
import org.lowcoder.sdk.util.JsonUtils;
18+
import org.lowcoder.sdk.webclient.WebClientBuildHelper;
19+
import org.springframework.core.ParameterizedTypeReference;
20+
import org.springframework.http.MediaType;
21+
22+
import reactor.core.publisher.Mono;
23+
24+
public class KeycloakRequest extends AbstractOauth2Request<Oauth2KeycloakAuthConfig>
25+
{
26+
27+
public KeycloakRequest(Oauth2KeycloakAuthConfig config)
28+
{
29+
super(config, Oauth2DefaultSource.KEYCLOAK);
30+
}
31+
32+
@Override
33+
protected Mono<AuthToken> getAuthToken(OAuth2RequestContext context) {
34+
URI uri;
35+
try {
36+
uri = new URIBuilder(config.replaceAuthUrlClientIdPlaceholder(source.accessToken())).build();
37+
} catch (URISyntaxException e) {
38+
throw new RuntimeException(e);
39+
}
40+
41+
return WebClientBuildHelper.builder()
42+
.systemProxy()
43+
.build()
44+
.post()
45+
.uri(uri)
46+
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
47+
.body(fromFormData("code", context.getCode())
48+
.with("client_id", config.getClientId())
49+
.with("client_secret", config.getClientSecret())
50+
.with("grant_type", "authorization_code")
51+
.with("redirect_uri", context.getRedirectUrl()))
52+
.exchangeToMono(response -> response.bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {
53+
}))
54+
.flatMap(map -> {
55+
if (map.containsKey("error") || map.containsKey("error_description")) {
56+
throw new AuthException(JsonUtils.toJson(map));
57+
}
58+
AuthToken authToken = AuthToken.builder()
59+
.accessToken(MapUtils.getString(map, "access_token"))
60+
.expireIn(MapUtils.getIntValue(map, "expires_in"))
61+
.refreshToken(MapUtils.getString(map, "refresh_token"))
62+
.build();
63+
return Mono.just(authToken);
64+
});
65+
}
66+
67+
@Override
68+
protected Mono<AuthToken> refreshAuthToken(String refreshToken) {
69+
70+
URI uri;
71+
try {
72+
uri = new URIBuilder(config.replaceAuthUrlClientIdPlaceholder(source.refresh())).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+
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
83+
.body(fromFormData("refresh_token", refreshToken)
84+
.with("client_id", config.getClientId())
85+
.with("client_secret", config.getClientSecret())
86+
.with("grant_type", "refresh_token"))
87+
.exchangeToMono(response -> response.bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {
88+
}))
89+
.flatMap(map -> {
90+
if (map.containsKey("error") || map.containsKey("error_description")) {
91+
throw new AuthException(JsonUtils.toJson(map));
92+
}
93+
AuthToken authToken = AuthToken.builder()
94+
.accessToken(MapUtils.getString(map, "access_token"))
95+
.expireIn(MapUtils.getIntValue(map, "expires_in"))
96+
.refreshToken(MapUtils.getString(map, "refresh_token"))
97+
.build();
98+
return Mono.just(authToken);
99+
});
100+
101+
}
102+
103+
@Override
104+
protected Mono<AuthUser> getAuthUser(AuthToken authToken) {
105+
return WebClientBuildHelper.builder()
106+
.systemProxy()
107+
.build()
108+
.post()
109+
.uri(config.replaceAuthUrlClientIdPlaceholder(source.userInfo()))
110+
.header("Authorization", "Bearer " + authToken.getAccessToken())
111+
.exchangeToMono(response -> response.bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {
112+
}))
113+
.flatMap(map -> {
114+
if (map.containsKey("error") || map.containsKey("error_description")) {
115+
throw new AuthException(JsonUtils.toJson(map));
116+
}
117+
AuthUser authUser = AuthUser.builder()
118+
.uid(MapUtils.getString(map, "sub"))
119+
.username(MapUtils.getString(map, "name"))
120+
.rawUserInfo(map)
121+
.build();
122+
return Mono.just(authUser);
123+
});
124+
}
125+
}

0 commit comments

Comments
 (0)