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 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> oauth2Provider() { + if (oAuth2Configuration == null) { + return Result.success(new ArrayList<>()); + } + + Collection values = oAuth2Configuration.getProvider().values(); + List 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 tokenRequestHeader = new HashMap<>(); + tokenRequestHeader.put("Accept", "application/json"); + Map requestBody = new HashMap<>(16); + requestBody.put("client_secret", oAuth2ClientProperties.getClientSecret()); + HashMap 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 userInfoRequestHeaders = new HashMap<>(); + userInfoRequestHeaders.put("Accept", "application/json"); + Map 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 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 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 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({ + {this.oauth2Providers.length > 0 && + {this.t('login.loginWithOAuth2')} + } + + + {this.oauth2Providers?.map((e: OAuth2Provider) => { + return (e.iconUri ?
this.gotoOAuth2Page(e)}>
: this.gotoOAuth2Page(e)}>{e.provider}) + })} +
) 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 | []) => { + oauth2Providers.value = res + }) + } + + const oauth2Providers = ref | []>([]) + + 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 } }