This is an automated email from the ASF dual-hosted git repository.
chufenggao pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/dolphinscheduler.git
The following commit(s) were added to refs/heads/dev by this push:
new 70731a1617 [Feature][Authentication] support oauth2 login (#14743)
70731a1617 is described below
commit 70731a1617b9be6713042ee7d3adb60a33540de7
Author: yangyang zhong <[email protected]>
AuthorDate: Mon Aug 21 15:57:30 2023 +0800
[Feature][Authentication] support oauth2 login (#14743)
* support oauth2 login
---------
Co-authored-by: Eric Gao <[email protected]>
---
docs/docs/en/guide/security/authentication-type.md | 88 +++++++++++++++++-
docs/docs/zh/guide/security/authentication-type.md | 88 +++++++++++++++++-
.../authentication/create-client-credentials-1.png | Bin 0 -> 242907 bytes
.../authentication/create-client-credentials-2.png | Bin 0 -> 331135 bytes
.../security/authentication/login-with-oauth2.png | Bin 0 -> 234149 bytes
.../api/configuration/AppConfiguration.java | 3 +-
.../api/configuration/OAuth2Configuration.java | 53 +++++++++++
.../api/controller/LoginController.java | 100 +++++++++++++++++++++
.../src/main/resources/application.yaml | 23 +++++
.../api/controller/LoginControllerTest.java | 69 ++++++++++++++
.../src/test/resources/application.yaml | 49 +++++++++-
.../dolphinscheduler/common/utils/OkHttpUtils.java | 3 +-
.../src/main/resources/application.yaml | 26 ++++++
dolphinscheduler-ui/src/locales/en_US/login.ts | 1 +
dolphinscheduler-ui/src/locales/zh_CN/login.ts | 1 +
.../src/service/modules/login/index.ts | 14 +++
.../src/service/modules/login/types.ts | 10 ++-
.../src/views/login/index.module.scss | 7 +-
dolphinscheduler-ui/src/views/login/index.tsx | 24 ++++-
dolphinscheduler-ui/src/views/login/use-login.ts | 50 +++++++++--
20 files changed, 593 insertions(+), 16 deletions(-)
diff --git a/docs/docs/en/guide/security/authentication-type.md
b/docs/docs/en/guide/security/authentication-type.md
index b5bd66f065..d6431ffe8b 100644
--- a/docs/docs/en/guide/security/authentication-type.md
+++ b/docs/docs/en/guide/security/authentication-type.md
@@ -1,6 +1,6 @@
# Authentication Type
-* So far we support three authentication types, Apache DolphinScheduler
password, LDAP and Casdoor SSO.
+* So far we support four authentication types, Apache DolphinScheduler
password, LDAP, Casdoor SSO and OAuth2,the OAuth2 authorization login mode can
be used with other authentication modes.
## Change Authentication Type
@@ -30,6 +30,29 @@ security:
# jks file absolute path && password
trust-store: "/ldapkeystore.jks"
trust-store-password: "password"
+ oauth2:
+ enable: false
+ provider:
+ github:
+ authorizationUri: ""
+ redirectUri: ""
+ clientId: ""
+ clientSecret: ""
+ tokenUri: ""
+ userInfoUri: ""
+ callbackUrl: ""
+ iconUri: ""
+ provider: github
+ google:
+ authorizationUri: ""
+ redirectUri: ""
+ clientId: ""
+ clientSecret: ""
+ tokenUri: ""
+ userInfoUri: ""
+ callbackUrl: ""
+ iconUri: ""
+ provider: google
```
For detailed explanation of specific fields, please see: [Api-server related
configuration](../../architecture/configuration.md)
@@ -110,3 +133,66 @@ casdoor:
redirect-url: http://localhost:5173/login
```
+## OAuth2
+
+Dolphinscheduler can support multiple OAuth2 providers.
+
+### Step1. Create Client Credentials
+
+
+
+
+
+### Step2.Enable OAuth2 Login In The Api's Configuration File
+
+```yaml
+security:
+ authentication:
+ …… # omit
+ oauth2:
+ # Set enable to true to enable oauth2 login mode
+ enable: true
+ provider:
+ github:
+ # Set the provider authorization address, for
example:https://github.com/login/oauth/authorize
+ authorizationUri: ""
+ # dolphinscheduler backend redirection interface address, for
example :http://127.0.0.1:12345/dolphinscheduler/redirect/login/oauth2
+ redirectUri: ""
+ # clientId
+ clientId: ""
+ # client secret
+ clientSecret: ""
+ # Set the provider's request token address
+ tokenUri: ""
+ # Set the provider address for requesting user information
+ userInfoUri: ""
+ # Redirect address after successful login, http://{ip}:{port}/login
+ callbackUrl: ""
+ # The image url of the login page jump button, if not filled, a text
button will be displayed
+ iconUri: ""
+ provider: github
+ google:
+ authorizationUri: ""
+ redirectUri: ""
+ clientId: ""
+ clientSecret: ""
+ tokenUri: ""
+ userInfoUri: ""
+ callbackUrl: ""
+ iconUri: ""
+ provider: google
+ gitee:
+ authorizationUri: "https://gitee.com/oauth/authorize"
+ redirectUri:
"http://127.0.0.1:12345/dolphinscheduler/redirect/login/oauth2"
+ clientId: ""
+ clientSecret: ""
+ tokenUri:
"https://gitee.com/oauth/token?grant_type=authorization_code"
+ userInfoUri: "https://gitee.com/api/v5/user"
+ callbackUrl: "http://127.0.0.1:5173/login"
+ iconUri: ""
+ provider: gitee
+```
+
+### Step.3 Login With OAuth2
+
+
diff --git a/docs/docs/zh/guide/security/authentication-type.md
b/docs/docs/zh/guide/security/authentication-type.md
index 89f8420333..c87b411881 100644
--- a/docs/docs/zh/guide/security/authentication-type.md
+++ b/docs/docs/zh/guide/security/authentication-type.md
@@ -1,6 +1,6 @@
# 认证方式
-* 目前我们支持三种认证方式,Apache DolphinScheduler自身账号密码登录,LDAP和通过Casdoor实现的SSO登录。
+* 目前我们支持四种认证方式,Apache DolphinScheduler自身账号密码登录,LDAP,
通过Casdoor实现的SSO登录和通过Oauth2授权登录,并且oauth2授权登录方式可以和其他认证方式同时使用。
## 修改认证方式
@@ -30,6 +30,29 @@ security:
# jks file absolute path && password
trust-store: "/ldapkeystore.jks"
trust-store-password: "password"
+ oauth2:
+ enable: false
+ provider:
+ github:
+ authorizationUri: ""
+ redirectUri: ""
+ clientId: ""
+ clientSecret: ""
+ tokenUri: ""
+ userInfoUri: ""
+ callbackUrl: ""
+ iconUri: ""
+ provider: github
+ google:
+ authorizationUri: ""
+ redirectUri: ""
+ clientId: ""
+ clientSecret: ""
+ tokenUri: ""
+ userInfoUri: ""
+ callbackUrl: ""
+ iconUri: ""
+ provider: google
```
具体字段解释详见:[Api-server相关配置](../../architecture/configuration.md)
@@ -106,3 +129,66 @@ casdoor:
redirect-url: http://localhost:5173/login
```
+## 通过OAuth2授权认证登录
+
+dolphinscheduler可以同时支持多种OAuth2的provider,只需要在配置文件中打开Oauth2的开关并进行简单的配置即可。
+
+### 步骤1. 获取OAuth2客户端凭据
+
+
+
+
+
+### 步骤2. 在api的配置文件中开启oauth2登录
+
+```yaml
+security:
+ authentication:
+ …… # 省略
+ oauth2:
+ # 将enable设置为true 开启oauth2登录模式
+ enable: true
+ provider:
+ github:
+ # 设置provider的授权地址,例如https://github.com/login/oauth/authorize
+ authorizationUri: ""
+ #
dolphinscheduler的后端重定向接口地址,例如http://127.0.0.1:12345/dolphinscheduler/redirect/login/oauth2
+ redirectUri: ""
+ # oauth2的 clientId
+ clientId: ""
+ # oauth2的 clientSecret
+ clientSecret: ""
+ # 设置provider的请求token的地址
+ tokenUri: ""
+ # 设置provider的请求用户信息的地址
+ userInfoUri: ""
+ # 登录成功后的重定向地址, http://{ip}:{port}/login
+ callbackUrl: ""
+ # 登录页跳转按钮的图片url,不填写则会展示一个文字按钮
+ iconUri: ""
+ provider: github
+ google:
+ authorizationUri: ""
+ redirectUri: ""
+ clientId: ""
+ clientSecret: ""
+ tokenUri: ""
+ userInfoUri: ""
+ callbackUrl: ""
+ iconUri: ""
+ provider: google
+ gitee:
+ authorizationUri: "https://gitee.com/oauth/authorize"
+ redirectUri:
"http://127.0.0.1:12345/dolphinscheduler/redirect/login/oauth2"
+ clientId: ""
+ clientSecret: ""
+ tokenUri:
"https://gitee.com/oauth/token?grant_type=authorization_code"
+ userInfoUri: "https://gitee.com/api/v5/user"
+ callbackUrl: "http://127.0.0.1:5173/login"
+ iconUri: ""
+ provider: gitee
+```
+
+### 步骤3.使用oauth2登录
+
+
diff --git a/docs/img/security/authentication/create-client-credentials-1.png
b/docs/img/security/authentication/create-client-credentials-1.png
new file mode 100644
index 0000000000..467bd57b0d
Binary files /dev/null and
b/docs/img/security/authentication/create-client-credentials-1.png differ
diff --git a/docs/img/security/authentication/create-client-credentials-2.png
b/docs/img/security/authentication/create-client-credentials-2.png
new file mode 100644
index 0000000000..9e401b1dc1
Binary files /dev/null and
b/docs/img/security/authentication/create-client-credentials-2.png differ
diff --git a/docs/img/security/authentication/login-with-oauth2.png
b/docs/img/security/authentication/login-with-oauth2.png
new file mode 100644
index 0000000000..270d904bbf
Binary files /dev/null and
b/docs/img/security/authentication/login-with-oauth2.png differ
diff --git
a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/configuration/AppConfiguration.java
b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/configuration/AppConfiguration.java
index 9fde9eec17..50293fc6e7 100644
---
a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/configuration/AppConfiguration.java
+++
b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/configuration/AppConfiguration.java
@@ -105,7 +105,8 @@ public class AppConfiguration implements WebMvcConfigurer {
.addPathPatterns(LOGIN_INTERCEPTOR_PATH_PATTERN)
.excludePathPatterns(LOGIN_PATH_PATTERN, REGISTER_PATH_PATTERN,
"/swagger-resources/**", "/webjars/**",
"/v3/api-docs/**", "/api-docs/**", "/swagger-ui.html",
- "/doc.html", "/swagger-ui/**", "*.html", "/ui/**",
"/error");
+ "/doc.html", "/swagger-ui/**", "*.html", "/ui/**",
"/error", "/oauth2-provider",
+ "/redirect/login/oauth2", "/cookies");
}
@Override
diff --git
a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/configuration/OAuth2Configuration.java
b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/configuration/OAuth2Configuration.java
new file mode 100644
index 0000000000..37cce1b515
--- /dev/null
+++
b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/configuration/OAuth2Configuration.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dolphinscheduler.api.configuration;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+@Getter
+@Setter
+@Configuration
+@ConditionalOnProperty(prefix = "security.authentication.oauth2", name =
"enable", havingValue = "true")
+@ConfigurationProperties(prefix = "security.authentication.oauth2")
+public class OAuth2Configuration {
+
+ private Map<String, OAuth2ClientProperties> provider = new HashMap<>();
+
+ @Getter
+ @Setter
+ public static class OAuth2ClientProperties {
+
+ private String authorizationUri;
+ private String clientId;
+ private String redirectUri;
+ private String clientSecret;
+ private String tokenUri;
+ private String userInfoUri;
+ private String callbackUrl;
+ private String iconUri;
+ private String provider;
+
+ }
+}
diff --git
a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/controller/LoginController.java
b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/controller/LoginController.java
index 0531bd9a98..e03cafe0dc 100644
---
a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/controller/LoginController.java
+++
b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/controller/LoginController.java
@@ -23,27 +23,40 @@ import static
org.apache.dolphinscheduler.api.enums.Status.SIGN_OUT_ERROR;
import static org.apache.dolphinscheduler.api.enums.Status.USER_LOGIN_FAILURE;
import org.apache.dolphinscheduler.api.aspect.AccessLogAnnotation;
+import org.apache.dolphinscheduler.api.configuration.OAuth2Configuration;
import org.apache.dolphinscheduler.api.enums.Status;
import org.apache.dolphinscheduler.api.exceptions.ApiException;
import org.apache.dolphinscheduler.api.security.Authenticator;
import org.apache.dolphinscheduler.api.security.impl.AbstractSsoAuthenticator;
import org.apache.dolphinscheduler.api.service.SessionService;
+import org.apache.dolphinscheduler.api.service.UsersService;
import org.apache.dolphinscheduler.api.utils.Result;
import org.apache.dolphinscheduler.common.constants.Constants;
+import org.apache.dolphinscheduler.common.utils.JSONUtils;
+import org.apache.dolphinscheduler.common.utils.OkHttpUtils;
import org.apache.dolphinscheduler.dao.entity.User;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpStatus;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.UUID;
+import java.util.stream.Collectors;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+
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.PostMapping;
import org.springframework.web.bind.annotation.RequestAttribute;
@@ -63,6 +76,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
@Tag(name = "LOGIN_TAG")
@RestController
@RequestMapping("")
+@Slf4j
public class LoginController extends BaseController {
@Autowired
@@ -71,6 +85,12 @@ public class LoginController extends BaseController {
@Autowired
private Authenticator authenticator;
+ @Autowired(required = false)
+ private OAuth2Configuration oAuth2Configuration;
+
+ @Autowired
+ private UsersService usersService;
+
/**
* login
*
@@ -160,4 +180,84 @@ public class LoginController extends BaseController {
request.removeAttribute(Constants.SESSION_USER);
return success();
}
+
+ @DeleteMapping("cookies")
+ public void clearCookieSessionId(HttpServletRequest request,
HttpServletResponse response) {
+ Cookie[] cookies = request.getCookies();
+ for (Cookie cookie : cookies) {
+ cookie.setMaxAge(0);
+ cookie.setValue(null);
+ response.addCookie(cookie);
+ }
+ response.setStatus(HttpStatus.SC_OK);
+ }
+
+ @Operation(summary = "getOauth2Provider", description =
"GET_OAUTH2_PROVIDER")
+ @GetMapping("oauth2-provider")
+ public Result<List<OAuth2Configuration.OAuth2ClientProperties>>
oauth2Provider() {
+ if (oAuth2Configuration == null) {
+ return Result.success(new ArrayList<>());
+ }
+
+ Collection<OAuth2Configuration.OAuth2ClientProperties> values =
oAuth2Configuration.getProvider().values();
+ List<OAuth2Configuration.OAuth2ClientProperties> providers =
values.stream().map(e -> {
+ OAuth2Configuration.OAuth2ClientProperties oAuth2ClientProperties =
+ new OAuth2Configuration.OAuth2ClientProperties();
+
oAuth2ClientProperties.setAuthorizationUri(e.getAuthorizationUri());
+ oAuth2ClientProperties.setRedirectUri(e.getRedirectUri());
+ oAuth2ClientProperties.setClientId(e.getClientId());
+ oAuth2ClientProperties.setProvider(e.getProvider());
+ oAuth2ClientProperties.setIconUri(e.getIconUri());
+ return oAuth2ClientProperties;
+ }).collect(Collectors.toList());
+ return Result.success(providers);
+ }
+
+ @SneakyThrows
+ @Operation(summary = "redirectToOauth2", description =
"REDIRECT_TO_OAUTH2_LOGIN")
+ @GetMapping("redirect/login/oauth2")
+ public void loginByAuth2(@RequestParam String code, @RequestParam String
provider,
+ HttpServletRequest request, HttpServletResponse
response) {
+ OAuth2Configuration.OAuth2ClientProperties oAuth2ClientProperties =
+ oAuth2Configuration.getProvider().get(provider);
+ try {
+ Map<String, String> tokenRequestHeader = new HashMap<>();
+ tokenRequestHeader.put("Accept", "application/json");
+ Map<String, Object> requestBody = new HashMap<>(16);
+ requestBody.put("client_secret",
oAuth2ClientProperties.getClientSecret());
+ HashMap<String, Object> requestParamsMap = new HashMap<>();
+ requestParamsMap.put("client_id",
oAuth2ClientProperties.getClientId());
+ requestParamsMap.put("code", code);
+ requestParamsMap.put("grant_type", "authorization_code");
+ requestParamsMap.put("redirect_uri",
+ String.format("%s?provider=%s",
oAuth2ClientProperties.getRedirectUri(), provider));
+ String tokenJsonStr =
OkHttpUtils.post(oAuth2ClientProperties.getTokenUri(), tokenRequestHeader,
+ requestParamsMap, requestBody);
+ String accessToken = JSONUtils.getNodeString(tokenJsonStr,
"access_token");
+ Map<String, String> userInfoRequestHeaders = new HashMap<>();
+ userInfoRequestHeaders.put("Accept", "application/json");
+ Map<String, Object> userInfoQueryMap = new HashMap<>();
+ userInfoQueryMap.put("access_token", accessToken);
+ userInfoRequestHeaders.put("Authorization", "Bearer " +
accessToken);
+ String userInfoJsonStr =
+ OkHttpUtils.get(oAuth2ClientProperties.getUserInfoUri(),
userInfoRequestHeaders, userInfoQueryMap);
+ String username = JSONUtils.getNodeString(userInfoJsonStr,
"login");
+ User user = usersService.getUserByUserName(username);
+ if (user == null) {
+ user = usersService.createUser(username, null, null, 0, null,
null, 1);
+ }
+ String sessionId = sessionService.createSession(user, null);
+ if (sessionId == null) {
+ log.error("Failed to create session, userName:{}.",
user.getUserName());
+ }
+ response.setStatus(HttpStatus.SC_MOVED_TEMPORARILY);
+ response.sendRedirect(String.format("%s?sessionId=%s&authType=%s",
oAuth2ClientProperties.getCallbackUrl(),
+ sessionId, "oauth2"));
+ } catch (Exception ex) {
+ log.error(ex.getMessage(), ex);
+ response.setStatus(HttpStatus.SC_MOVED_TEMPORARILY);
+ response.sendRedirect(String.format("%s?authType=%s&error=%s",
oAuth2ClientProperties.getCallbackUrl(),
+ "oauth2", "oauth2 auth error"));
+ }
+ }
}
diff --git a/dolphinscheduler-api/src/main/resources/application.yaml
b/dolphinscheduler-api/src/main/resources/application.yaml
index 1165437972..911b8ac106 100644
--- a/dolphinscheduler-api/src/main/resources/application.yaml
+++ b/dolphinscheduler-api/src/main/resources/application.yaml
@@ -178,6 +178,29 @@ security:
# jks file absolute path && password
trust-store: "/ldapkeystore.jks"
trust-store-password: "password"
+ oauth2:
+ enable: false
+ provider:
+ github:
+ authorizationUri: ""
+ redirectUri: ""
+ clientId: ""
+ clientSecret: ""
+ tokenUri: ""
+ userInfoUri: ""
+ callbackUrl: ""
+ iconUri: ""
+ provider: github
+ google:
+ authorizationUri: ""
+ redirectUri: ""
+ clientId: ""
+ clientSecret: ""
+ tokenUri: ""
+ userInfoUri: ""
+ callbackUrl: ""
+ iconUri: ""
+ provider: google
# Override by profile
diff --git
a/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/controller/LoginControllerTest.java
b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/controller/LoginControllerTest.java
index 64913465f1..25a4518186 100644
---
a/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/controller/LoginControllerTest.java
+++
b/dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/controller/LoginControllerTest.java
@@ -17,6 +17,8 @@
package org.apache.dolphinscheduler.api.controller;
+import static
org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
+import static
org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static
org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static
org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static
org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@@ -25,14 +27,22 @@ import org.apache.dolphinscheduler.api.enums.Status;
import org.apache.dolphinscheduler.api.utils.Result;
import org.apache.dolphinscheduler.common.constants.Constants;
import org.apache.dolphinscheduler.common.utils.JSONUtils;
+import org.apache.dolphinscheduler.common.utils.OkHttpUtils;
+
+import org.apache.http.HttpStatus;
import java.util.Map;
+import javax.servlet.http.Cookie;
+
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
+import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
@@ -79,4 +89,63 @@ public class LoginControllerTest extends
AbstractControllerTest {
Assertions.assertEquals(Status.SUCCESS.getCode(),
result.getCode().intValue());
logger.info(mvcResult.getResponse().getContentAsString());
}
+
+ @Test
+ void testClearCookie() throws Exception {
+ MvcResult mvcResult = mockMvc.perform(delete("/cookies")
+ .header("sessionId", sessionId)
+ .cookie(new Cookie("sessionId", sessionId)))
+ .andExpect(status().isOk())
+ .andReturn();
+ MockHttpServletResponse response = mvcResult.getResponse();
+ Cookie[] cookies = response.getCookies();
+ for (Cookie cookie : cookies) {
+ Assertions.assertEquals(0, cookie.getMaxAge());
+ Assertions.assertNull(cookie.getValue());
+ }
+ }
+
+ @Test
+ void testGetOauth2Provider() throws Exception {
+ MvcResult mvcResult = mockMvc.perform(get("/oauth2-provider"))
+ .andExpect(status().isOk())
+ .andReturn();
+ Result result =
JSONUtils.parseObject(mvcResult.getResponse().getContentAsString(),
Result.class);
+ Assertions.assertEquals(Status.SUCCESS.getCode(),
result.getCode().intValue());
+ }
+
+ @Test
+ void testOauth2Redirect() throws Exception {
+ String tokenResult = "{\"access_token\":\"test-token\"}";
+ String userInfoResult = "{\"login\":\"username\"}";
+ MockedStatic<OkHttpUtils> okHttpUtilsMockedStatic =
Mockito.mockStatic(OkHttpUtils.class);
+ okHttpUtilsMockedStatic
+ .when(() -> OkHttpUtils.post(Mockito.notNull(), Mockito.any(),
Mockito.any(), Mockito.any()))
+ .thenReturn(tokenResult);
+ okHttpUtilsMockedStatic.when(() -> OkHttpUtils.get(Mockito.notNull(),
Mockito.any(), Mockito.any()))
+ .thenReturn(userInfoResult);
+ MvcResult mvcResult =
mockMvc.perform(get("/redirect/login/oauth2?code=test&provider=github"))
+ .andExpect(status().is3xxRedirection())
+ .andReturn();
+ MockHttpServletResponse response = mvcResult.getResponse();
+ Assertions.assertEquals(HttpStatus.SC_MOVED_TEMPORARILY,
response.getStatus());
+ String redirectedUrl = response.getRedirectedUrl();
+ Assertions.assertTrue(redirectedUrl != null &&
redirectedUrl.contains("sessionId"));
+ okHttpUtilsMockedStatic.close();
+ }
+
+ @Test
+ void testOauth2RedirectError() throws Exception {
+ MockedStatic<OkHttpUtils> okHttpUtilsMockedStatic =
Mockito.mockStatic(OkHttpUtils.class);
+ okHttpUtilsMockedStatic.when(() -> OkHttpUtils.post(Mockito.any(),
Mockito.any(), Mockito.any(), Mockito.any()))
+ .thenThrow(new RuntimeException("oauth error"));
+ MvcResult mvcResult =
mockMvc.perform(get("/redirect/login/oauth2?code=test&provider=github"))
+ .andExpect(status().is3xxRedirection())
+ .andReturn();
+ MockHttpServletResponse response = mvcResult.getResponse();
+ Assertions.assertEquals(HttpStatus.SC_MOVED_TEMPORARILY,
response.getStatus());
+ String redirectedUrl = response.getRedirectedUrl();
+ Assertions.assertTrue(redirectedUrl != null &&
redirectedUrl.contains("error"));
+ okHttpUtilsMockedStatic.close();
+ }
}
diff --git a/dolphinscheduler-api/src/test/resources/application.yaml
b/dolphinscheduler-api/src/test/resources/application.yaml
index fda37e4ea4..d6cd8ff0af 100644
--- a/dolphinscheduler-api/src/test/resources/application.yaml
+++ b/dolphinscheduler-api/src/test/resources/application.yaml
@@ -62,4 +62,51 @@ api:
connect-timeout: 0
# Close each active connection of socket server if python program not
active after x milliseconds. Define value is
# (0 = infinite), and socket server would never close even though no
requests accept
- read-timeout: 0
\ No newline at end of file
+ read-timeout: 0
+
+security:
+ authentication:
+ # Authentication types (supported types: PASSWORD,LDAP,CASDOOR_SSO)
+ type: PASSWORD
+ # IF you set type `LDAP`, below config will be effective
+ ldap:
+ # ldap server config
+ urls: ldap://ldap.forumsys.com:389/
+ base-dn: dc=example,dc=com
+ username: cn=read-only-admin,dc=example,dc=com
+ password: password
+ user:
+ # admin userId when you use LDAP login
+ admin: read-only-admin
+ identity-attribute: uid
+ email-attribute: mail
+ # action when ldap user is not exist (supported types: CREATE,DENY)
+ not-exist-action: CREATE
+ ssl:
+ enable: false
+ # jks file absolute path && password
+ trust-store: "/ldapkeystore.jks"
+ trust-store-password: "password"
+ oauth2:
+ enable: true
+ provider:
+ github:
+ authorizationUri: http://oauth2-test
+ redirectUri: http://oauth2-test
+ clientId: ""
+ clientSecret: ""
+ tokenUri: http://oauth2-token-url-test
+ userInfoUri: http://oauth2-user-info-url-test
+ callbackUrl: ""
+ iconUri: ""
+ provider: github
+ google:
+ authorizationUri: ""
+ redirectUri: ""
+ clientId: ""
+ clientSecret: ""
+ tokenUri: ""
+ userInfoUri: ""
+ callbackUrl: ""
+ iconUri: ""
+ provider: google
diff --git
a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/utils/OkHttpUtils.java
b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/utils/OkHttpUtils.java
index a7229a23e9..ecae881b48 100644
---
a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/utils/OkHttpUtils.java
+++
b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/utils/OkHttpUtils.java
@@ -38,7 +38,7 @@ public class OkHttpUtils {
private static final OkHttpClient CLIENT = new OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.MINUTES) // connect timeout
.writeTimeout(5, TimeUnit.MINUTES) // write timeout
- .readTimeout(5, TimeUnit.MINUTES) // read timeout
+ .readTimeout(5, TimeUnit.MINUTES)
.build();
public static @NonNull String get(@NonNull String url,
@@ -59,6 +59,7 @@ public class OkHttpUtils {
@Nullable Map<String, Object>
requestBodyMap) throws IOException {
String finalUrl = addUrlParams(requestParamsMap, url);
Request.Builder requestBuilder = new Request.Builder().url(finalUrl);
+ addHeader(httpHeaders, requestBuilder);
if (requestBodyMap != null) {
requestBuilder =
requestBuilder.post(RequestBody.create(MediaType.parse("application/json"),
JSONUtils.toJsonString(requestBodyMap)));
diff --git
a/dolphinscheduler-standalone-server/src/main/resources/application.yaml
b/dolphinscheduler-standalone-server/src/main/resources/application.yaml
index 75ff196f0d..a9250b7392 100644
--- a/dolphinscheduler-standalone-server/src/main/resources/application.yaml
+++ b/dolphinscheduler-standalone-server/src/main/resources/application.yaml
@@ -111,6 +111,32 @@ security:
# jks file absolute path && password
trust-store: "/ldapkeystore.jks"
trust-store-password: ""
+ oauth2:
+ enable: false
+ provider:
+ github:
+ authorizationUri: "https://github.com/login/oauth/authorize"
+ redirectUri:
"http://localhost:12345/dolphinscheduler/redirect/login/oauth2"
+ clientId: ""
+ clientSecret: ""
+ tokenUri: "https://github.com/login/oauth/access_token"
+ userInfoUri: "https://api.github.com/user"
+ callbackUrl: "http://localhost:5173/login"
+ iconUri: ""
+ provider: github
+ gitee:
+ authorizationUri: "https://gitee.com/oauth/authorize"
+ redirectUri:
"http://127.0.0.1:12345/dolphinscheduler/redirect/login/oauth2"
+ clientId: ""
+ clientSecret: ""
+ tokenUri:
"https://gitee.com/oauth/token?grant_type=authorization_code"
+ userInfoUri: "https://gitee.com/api/v5/user"
+ callbackUrl: "http://127.0.0.1:5173/login"
+ iconUri: ""
+ provider: gitee
+
+
+
master:
diff --git a/dolphinscheduler-ui/src/locales/en_US/login.ts
b/dolphinscheduler-ui/src/locales/en_US/login.ts
index 70ad9546a3..7d2cdcfbab 100644
--- a/dolphinscheduler-ui/src/locales/en_US/login.ts
+++ b/dolphinscheduler-ui/src/locales/en_US/login.ts
@@ -22,5 +22,6 @@ export default {
userPassword: 'Password',
userPassword_tips: 'Please enter your password',
login: 'Login',
+ loginWithOAuth2: 'login with OAuth2',
ssoLogin: 'SSO Login'
}
diff --git a/dolphinscheduler-ui/src/locales/zh_CN/login.ts
b/dolphinscheduler-ui/src/locales/zh_CN/login.ts
index 89bac0ea3f..fbb802af08 100644
--- a/dolphinscheduler-ui/src/locales/zh_CN/login.ts
+++ b/dolphinscheduler-ui/src/locales/zh_CN/login.ts
@@ -22,5 +22,6 @@ export default {
userPassword: '密码',
userPassword_tips: '请输入密码',
login: '登录',
+ loginWithOAuth2: '通过OAuth2登录',
ssoLogin: '单点登录'
}
diff --git a/dolphinscheduler-ui/src/service/modules/login/index.ts
b/dolphinscheduler-ui/src/service/modules/login/index.ts
index e426b8330d..e095f932ae 100644
--- a/dolphinscheduler-ui/src/service/modules/login/index.ts
+++ b/dolphinscheduler-ui/src/service/modules/login/index.ts
@@ -32,3 +32,17 @@ export function ssoLoginUrl(): any {
method: 'get'
})
}
+
+export function getOauth2Provider(): any {
+ return axios({
+ url: '/oauth2-provider',
+ method: 'get',
+ })
+}
+
+export function clearCookie(): any {
+ return axios({
+ url: '/cookies',
+ method: 'delete',
+ })
+}
\ No newline at end of file
diff --git a/dolphinscheduler-ui/src/service/modules/login/types.ts
b/dolphinscheduler-ui/src/service/modules/login/types.ts
index d0471de8da..7765288cec 100644
--- a/dolphinscheduler-ui/src/service/modules/login/types.ts
+++ b/dolphinscheduler-ui/src/service/modules/login/types.ts
@@ -25,4 +25,12 @@ interface LoginRes {
sessionId: string
}
-export { LoginReq, LoginRes }
+interface OAuth2Provider {
+ clientId: string,
+ redirectUri: string,
+ provider: string,
+ authorizationUri: string,
+ iconUri: string
+}
+
+export { LoginReq, LoginRes, OAuth2Provider }
diff --git a/dolphinscheduler-ui/src/views/login/index.module.scss
b/dolphinscheduler-ui/src/views/login/index.module.scss
index e4605586ad..250ea080fe 100644
--- a/dolphinscheduler-ui/src/views/login/index.module.scss
+++ b/dolphinscheduler-ui/src/views/login/index.module.scss
@@ -53,5 +53,10 @@
.form-model {
padding: 30px 20px;
}
+
+ .oauth2-provider {
+ margin-top: 10px;
+ margin-bottom: 30px;
+ }
}
-}
+}
\ No newline at end of file
diff --git a/dolphinscheduler-ui/src/views/login/index.tsx
b/dolphinscheduler-ui/src/views/login/index.tsx
index aa23755f3b..066fa3f4fa 100644
--- a/dolphinscheduler-ui/src/views/login/index.tsx
+++ b/dolphinscheduler-ui/src/views/login/index.tsx
@@ -29,7 +29,10 @@ import {
NSwitch,
NForm,
NFormItem,
- useMessage
+ useMessage,
+ NSpace,
+ NDivider,
+ NImage
} from 'naive-ui'
import { useForm } from './use-form'
import { useTranslate } from './use-translate'
@@ -38,15 +41,15 @@ import { useLocalesStore } from '@/store/locales/locales'
import { useThemeStore } from '@/store/theme/theme'
import cookies from 'js-cookie'
import { ssoLoginUrl } from '@/service/modules/login'
+import type { OAuth2Provider } from '@/service/modules/login/types'
const login = defineComponent({
name: 'login',
setup() {
window.$message = useMessage()
-
const { state, t, locale } = useForm()
const { handleChange } = useTranslate(locale)
- const { handleLogin } = useLogin(state)
+ const { handleLogin, handleGetOAuth2Provider, oauth2Providers,
gotoOAuth2Page, handleRedirect } = useLogin(state)
const localesStore = useLocalesStore()
const themeStore = useThemeStore()
@@ -73,15 +76,19 @@ const login = defineComponent({
} else {
state.loginForm.ssoLoginUrl = ''
}
+ handleRedirect()
})
+ handleGetOAuth2Provider()
return {
t,
handleChange,
handleLogin,
...toRefs(state),
localesStore,
- trim
+ trim,
+ oauth2Providers,
+ gotoOAuth2Page
}
},
render() {
@@ -170,6 +177,15 @@ const login = defineComponent({
</NButton>
</a>
</div>
+ {this.oauth2Providers.length > 0 && <NDivider >
+ {this.t('login.loginWithOAuth2')}
+ </NDivider>}
+
+ <NSpace class={styles['oauth2-provider']} justify="center">
+ {this.oauth2Providers?.map((e: OAuth2Provider) => {
+ return (e.iconUri ? <div onClick={() =>
this.gotoOAuth2Page(e)}><NImage preview-disabled width="30"
src={e.iconUri}></NImage> </div> : <NButton onClick={() =>
this.gotoOAuth2Page(e)}>{e.provider}</NButton>)
+ })}
+ </NSpace>
</div>
</div>
)
diff --git a/dolphinscheduler-ui/src/views/login/use-login.ts
b/dolphinscheduler-ui/src/views/login/use-login.ts
index b39037aa59..0699af07ed 100644
--- a/dolphinscheduler-ui/src/views/login/use-login.ts
+++ b/dolphinscheduler-ui/src/views/login/use-login.ts
@@ -15,24 +15,25 @@
* limitations under the License.
*/
-import { useRouter } from 'vue-router'
-import { login } from '@/service/modules/login'
+import { useRouter,useRoute } from 'vue-router'
+import { clearCookie, getOauth2Provider, login } from '@/service/modules/login'
import { getUserInfo } from '@/service/modules/users'
import { useUserStore } from '@/store/user/user'
import type { Router } from 'vue-router'
-import type { LoginRes } from '@/service/modules/login/types'
+import type { LoginRes, OAuth2Provider } from '@/service/modules/login/types'
import type { UserInfoRes } from '@/service/modules/users/types'
import { useRouteStore } from '@/store/route/route'
import { useTimezoneStore } from '@/store/timezone/timezone'
import cookies from 'js-cookie'
import { queryBaseDir } from '@/service/modules/resources'
+import { ref } from 'vue'
export function useLogin(state: any) {
const router: Router = useRouter()
const userStore = useUserStore()
const routeStore = useRouteStore()
const timezoneStore = useTimezoneStore()
-
+ const route = useRoute()
const handleLogin = () => {
state.loginFormRef.validate(async (valid: any) => {
if (!valid) {
@@ -63,7 +64,46 @@ export function useLogin(state: any) {
})
}
+
+
+ const handleGetOAuth2Provider = () => {
+ getOauth2Provider().then((res: Array<OAuth2Provider> | []) => {
+ oauth2Providers.value = res
+ })
+ }
+
+ const oauth2Providers = ref<Array<OAuth2Provider> | []>([])
+
+ const gotoOAuth2Page = async (oauth2Provider: OAuth2Provider) => {
+ await clearCookie()
+ window.location.href =
`${oauth2Provider.authorizationUri}?client_id=${oauth2Provider.clientId}` +
+
`&response_type=code&redirect_uri=${oauth2Provider.redirectUri}?provider=${oauth2Provider.provider}`
+ }
+
+ const handleRedirect = async () => {
+ const authType = route.query.authType
+ if (authType && authType === 'oauth2') {
+ const sessionId = route.query.sessionId
+ if (sessionId) {
+ cookies.set('sessionId', String(sessionId), { path: '/' })
+ const userInfoRes: UserInfoRes = await getUserInfo()
+ await userStore.setUserInfo(userInfoRes)
+ const timezone = userInfoRes.timeZone ? userInfoRes.timeZone : 'UTC'
+ await timezoneStore.setTimezone(timezone)
+ router.push('home')
+ }
+ const error = route.query.error
+ if (error) {
+ window.$message.error(error)
+ }
+ }
+ }
+
return {
- handleLogin
+ handleLogin,
+ handleGetOAuth2Provider,
+ gotoOAuth2Page,
+ oauth2Providers,
+ handleRedirect
}
}