Skip to content

Keycloak auth #411

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 3 commits into from
Oct 18, 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
@@ -0,0 +1,50 @@
package org.lowcoder.sdk.auth;

import static org.lowcoder.sdk.auth.constants.Oauth2Constants.INSTANCE_ID_PLACEHOLDER;
import static org.lowcoder.sdk.auth.constants.Oauth2Constants.REALM_PLACEHOLDER;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;

import lombok.Getter;

/**
*
* Keycloak OAuth configuration.
*/
@Getter
public class Oauth2KeycloakAuthConfig extends Oauth2SimpleAuthConfig
{
protected String instanceId;
protected String realm;

@JsonCreator
public Oauth2KeycloakAuthConfig(
@JsonProperty("id") String id,
@JsonProperty("enable") Boolean enable,
@JsonProperty("enableRegister") Boolean enableRegister,
@JsonProperty("source") String source,
@JsonProperty("sourceName") String sourceName,
@JsonProperty("clientId") String clientId,
@JsonProperty("clientSecret") String clientSecret,
@JsonProperty("instanceId") String instanceId,
@JsonProperty("realm") String realm,
@JsonProperty("authType") String authType)
{
super(id, enable, enableRegister, source, sourceName, clientId, clientSecret, authType);
this.instanceId = instanceId;
this.realm = realm;
}



@Override
public String replaceAuthUrlClientIdPlaceholder(String url)
{
return super.replaceAuthUrlClientIdPlaceholder(url)
.replace(INSTANCE_ID_PLACEHOLDER, instanceId)
.replace(REALM_PLACEHOLDER, realm);
}


}
Original file line number Diff line number Diff line change
@@ -1,18 +1,12 @@
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 static org.lowcoder.sdk.auth.constants.Oauth2Constants.INSTANCE_ID_PLACEHOLDER;

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;
import com.fasterxml.jackson.annotation.JsonCreator;

import lombok.Getter;

/**
* OAuth2 ORY auth config.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ public String getAuthorizeUrl() {
case AuthTypeConstants.GOOGLE -> replaceAuthUrlClientIdPlaceholder(Oauth2Constants.GOOGLE_AUTHORIZE_URL);
case AuthTypeConstants.GITHUB -> replaceAuthUrlClientIdPlaceholder(Oauth2Constants.GITHUB_AUTHORIZE_URL);
case AuthTypeConstants.ORY -> replaceAuthUrlClientIdPlaceholder(Oauth2Constants.ORY_AUTHORIZE_URL);
case AuthTypeConstants.KEYCLOAK -> replaceAuthUrlClientIdPlaceholder(Oauth2Constants.KEYCLOAK_AUTHORIZE_URL);
default -> null;
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ public class AuthTypeConstants {
public static final String GOOGLE = "GOOGLE";
public static final String GITHUB = "GITHUB";
public static final String ORY = "ORY";
public static final String KEYCLOAK = "KEYCLOAK";
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ public class Oauth2Constants {
public static final String CLIENT_ID_PLACEHOLDER = "$CLIENT_ID";
public static final String REDIRECT_URL_PLACEHOLDER = "$REDIRECT_URL";
public static final String STATE_PLACEHOLDER = "$STATE";
public static final String REALM_PLACEHOLDER = "$REALM";

public static final String INSTANCE_ID_PLACEHOLDER = "INSTANCE_ID";
public static final String INSTANCE_ID_PLACEHOLDER = "$INSTANCE_ID";

// authorize url
public static final String GITHUB_AUTHORIZE_URL = "https://github.com/login/oauth/authorize"
Expand All @@ -32,4 +33,11 @@ public class Oauth2Constants {
+ "&redirect_uri=" + REDIRECT_URL_PLACEHOLDER
+ "&state=" + STATE_PLACEHOLDER
+ "&scope=openid email profile offline_access";

public static final String KEYCLOAK_AUTHORIZE_URL = "https://" + INSTANCE_ID_PLACEHOLDER + "/realms/" + REALM_PLACEHOLDER + "/protocol/openid-connect/auth"
+ "?response_type=code"
+ "&client_id=" + CLIENT_ID_PLACEHOLDER
+ "&redirect_uri=" + REDIRECT_URL_PLACEHOLDER
+ "&state=" + STATE_PLACEHOLDER
+ "&scope=openid email profile";
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ public class AuthSourceConstants {
public static final String GOOGLE = "GOOGLE";
public static final String GITHUB = "GITHUB";
public static final String ORY = "ORY";
public static final String KEYCLOAK = "KEYCLOAK";

// source name
public static final String GOOGLE_NAME = "Google";
public static final String GITHUB_NAME = "Github";
public static final String ORY_NAME = "Ory";
public static final String KEYCLOAK_NAME = "Keycloak";

// default source and source name for common protocol
// oauth 2.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import javax.annotation.Nullable;

import org.lowcoder.sdk.auth.EmailAuthConfig;
import org.lowcoder.sdk.auth.Oauth2KeycloakAuthConfig;
import org.lowcoder.sdk.auth.Oauth2OryAuthConfig;
import org.lowcoder.sdk.auth.Oauth2SimpleAuthConfig;

Expand Down Expand Up @@ -46,6 +47,7 @@ public final class JsonUtils {
OBJECT_MAPPER.registerSubtypes(new NamedType(Oauth2SimpleAuthConfig.class, GITHUB));
OBJECT_MAPPER.registerSubtypes(new NamedType(Oauth2SimpleAuthConfig.class, GOOGLE));
OBJECT_MAPPER.registerSubtypes(new NamedType(Oauth2OryAuthConfig.class, ORY));
OBJECT_MAPPER.registerSubtypes(new NamedType(Oauth2KeycloakAuthConfig.class, KEYCLOAK));
}

public static final JsonNode EMPTY_JSON_NODE = createObjectNode();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,35 @@
package org.lowcoder.api.authentication;

import com.fasterxml.jackson.annotation.JsonView;
import lombok.extern.slf4j.Slf4j;
import java.util.List;

import org.lowcoder.api.authentication.dto.AuthConfigRequest;
import org.lowcoder.api.authentication.service.AuthenticationApiService;
import org.lowcoder.api.framework.view.ResponseView;
import org.lowcoder.api.home.SessionUserService;
import org.lowcoder.api.usermanagement.UserController;
import org.lowcoder.api.usermanagement.UserController.UpdatePasswordRequest;
import org.lowcoder.api.util.BusinessEventPublisher;
import org.lowcoder.domain.authentication.AuthenticationService;
import org.lowcoder.domain.authentication.FindAuthConfig;
import org.lowcoder.infra.constant.NewUrl;
import org.lowcoder.sdk.auth.AbstractAuthConfig;
import org.lowcoder.sdk.config.SerializeConfig.JsonViews;
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.*;
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.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.List;
import com.fasterxml.jackson.annotation.JsonView;

import reactor.core.publisher.Mono;

@Slf4j
@RestController
@RequestMapping(value = {NewUrl.CUSTOM_AUTH})
public class AuthenticationController {
Expand All @@ -36,8 +42,6 @@ public class AuthenticationController {
private CookieHelper cookieHelper;
@Autowired
private BusinessEventPublisher businessEventPublisher;
@Autowired
private AuthenticationService authenticationService;

/**
* login by email or phone with password; or register by email for now.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
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.KeycloakRequest;
import org.lowcoder.api.authentication.request.oauth2.request.OryRequest;
import org.lowcoder.sdk.auth.Oauth2KeycloakAuthConfig;
import org.lowcoder.sdk.auth.Oauth2OryAuthConfig;
import org.lowcoder.sdk.auth.Oauth2SimpleAuthConfig;
import org.springframework.stereotype.Component;
Expand All @@ -29,6 +31,7 @@ private AbstractOauth2Request<? extends Oauth2SimpleAuthConfig> buildRequest(OAu
case GITHUB -> new GithubRequest((Oauth2SimpleAuthConfig) context.getAuthConfig());
case GOOGLE -> new GoogleRequest((Oauth2SimpleAuthConfig) context.getAuthConfig());
case ORY -> new OryRequest((Oauth2OryAuthConfig) context.getAuthConfig());
case KEYCLOAK -> new KeycloakRequest((Oauth2KeycloakAuthConfig)context.getAuthConfig());
default -> throw new UnsupportedOperationException(context.getAuthConfig().getAuthType());
};
}
Expand All @@ -38,6 +41,7 @@ public Set<String> supportedAuthTypes() {
return Set.of(
GITHUB,
GOOGLE,
ORY);
ORY,
KEYCLOAK);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.lowcoder.api.authentication.request.oauth2;

import org.lowcoder.sdk.auth.constants.Oauth2Constants;
import org.springframework.security.config.oauth2.client.CommonOAuth2Provider;

public enum Oauth2DefaultSource implements Oauth2Source {

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

@Override
public String refresh() {
return "https://www.googleapis.com/oauth2/v4/token";
return "https://github.com/login/oauth/access_token";
}

},
Expand Down Expand Up @@ -55,5 +56,24 @@ public String refresh() {
return "https://" + Oauth2Constants.INSTANCE_ID_PLACEHOLDER + "/oauth2/token";
}

},

KEYCLOAK {

@Override
public String accessToken() {
return "http://" + Oauth2Constants.INSTANCE_ID_PLACEHOLDER + "/realms/" + Oauth2Constants.REALM_PLACEHOLDER + "/protocol/openid-connect/token";
}

@Override
public String userInfo() {
return "http://" + Oauth2Constants.INSTANCE_ID_PLACEHOLDER + "/realms/" + Oauth2Constants.REALM_PLACEHOLDER + "/protocol/openid-connect/userinfo";
}

@Override
public String refresh() {
return "http://" + Oauth2Constants.INSTANCE_ID_PLACEHOLDER + "/realms/" + Oauth2Constants.REALM_PLACEHOLDER + "/protocol/openid-connect/token";
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package org.lowcoder.api.authentication.request.oauth2.request;

import static org.springframework.web.reactive.function.BodyInserters.fromFormData;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.Map;

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.Oauth2KeycloakAuthConfig;
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;

public class KeycloakRequest extends AbstractOauth2Request<Oauth2KeycloakAuthConfig>
{

public KeycloakRequest(Oauth2KeycloakAuthConfig config)
{
super(config, Oauth2DefaultSource.KEYCLOAK);
}

@Override
protected Mono<AuthToken> 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<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"))
.refreshToken(MapUtils.getString(map, "refresh_token"))
.build();
return Mono.just(authToken);
});
}

@Override
protected Mono<AuthToken> 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<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"))
.refreshToken(MapUtils.getString(map, "refresh_token"))
.build();
return Mono.just(authToken);
});

}

@Override
protected Mono<AuthUser> 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<Map<String, Object>>() {
}))
.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"))
.rawUserInfo(map)
.build();
return Mono.just(authUser);
});
}
}
Loading