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
+
+![create-client-credentials-1](../../../../img/security/authentication/create-client-credentials-1.png)
+
+![create-client-credentials-2](../../../../img/security/authentication/create-client-credentials-2.png)
+
+### 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
+
+![login-with-oauth2](../../../../img/security/authentication/login-with-oauth2.png)
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客户端凭据
+
+![create-client-credentials-1](../../../../img/security/authentication/create-client-credentials-1.png)
+
+![create-client-credentials-2](../../../../img/security/authentication/create-client-credentials-2.png)
+
+### 步骤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登录
+
+![login-with-oauth2](../../../../img/security/authentication/login-with-oauth2.png)
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
   }
 }


Reply via email to