diff --git a/framework/gateway/src/main/java/io/metersphere/gateway/controller/SSOController.java b/framework/gateway/src/main/java/io/metersphere/gateway/controller/SSOController.java index 1ca0b61298..7639becedd 100644 --- a/framework/gateway/src/main/java/io/metersphere/gateway/controller/SSOController.java +++ b/framework/gateway/src/main/java/io/metersphere/gateway/controller/SSOController.java @@ -40,6 +40,14 @@ public class SSOController { .build(); } + @GetMapping("callback/oauth2") + @MsAuditLog(module = OperLogModule.AUTH_TITLE, type = OperLogConstants.LOGIN, title = "登录") + public Rendering callbackOauth(@RequestParam("code") String code, @RequestParam("state") String authId, WebSession session, Locale locale) throws Exception { + Optional sessionUser = ssoService.exchangeOauth2Token(code, authId, session, locale); + return Rendering.redirectTo("/#/?_token=" + CodingUtil.base64Encoding(session.getId()) + "&_csrf=" + sessionUser.get().getCsrfToken()) + .build(); + } + @GetMapping("/callback/cas/{authId}") @MsAuditLog(module = OperLogModule.AUTH_TITLE, type = OperLogConstants.LOGIN, title = "登录") public Rendering casCallback(@RequestParam("ticket") String ticket, @PathVariable("authId") String authId, WebSession session, Locale locale) throws Exception { diff --git a/framework/gateway/src/main/java/io/metersphere/gateway/service/SSOService.java b/framework/gateway/src/main/java/io/metersphere/gateway/service/SSOService.java index 987ab2fad2..b9f28cb17a 100644 --- a/framework/gateway/src/main/java/io/metersphere/gateway/service/SSOService.java +++ b/framework/gateway/src/main/java/io/metersphere/gateway/service/SSOService.java @@ -1,14 +1,12 @@ package io.metersphere.gateway.service; +import com.fasterxml.jackson.core.type.TypeReference; import io.metersphere.base.domain.AuthSource; import io.metersphere.base.domain.User; -import io.metersphere.commons.constants.SessionConstants; import io.metersphere.commons.exception.MSException; import io.metersphere.commons.user.SessionUser; -import io.metersphere.commons.utils.CodingUtil; -import io.metersphere.commons.utils.IOUtils; -import io.metersphere.commons.utils.JSON; -import io.metersphere.commons.utils.SessionUtils; +import io.metersphere.commons.utils.*; +import io.metersphere.i18n.Translator; import io.metersphere.request.LoginRequest; import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpResponse; @@ -43,9 +41,7 @@ import java.nio.charset.StandardCharsets; import java.security.KeyManagementException; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; -import java.util.Locale; -import java.util.Map; -import java.util.Optional; +import java.util.*; import java.util.concurrent.TimeUnit; @@ -229,4 +225,120 @@ public class SSOService { SessionUtils.kickOutUser(name); stringRedisTemplate.delete(ticket); } + + public Optional exchangeOauth2Token(String code, String authId, WebSession session, Locale locale) throws Exception { + AuthSource authSource = authSourceService.getAuthSource(authId); + Map config = JSON.parseObject(authSource.getConfiguration(), new TypeReference>() {}); + String url = config.get("tokenUrl") + + "?client_id=" + config.get("clientId") + + "&client_secret=" + config.get("secret") + + "&redirect_uri=" + config.get("redirectUrl") + + "&code=" + code + + "&grant_type=authorization_code"; + + Map resultObj = null; + try { + RestTemplate restTemplate = getRestTemplateIgnoreSSL(); + HttpHeaders headers = new HttpHeaders(); + headers.add(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE); + HttpEntity param = new HttpEntity<>(headers); + ResponseEntity response = restTemplate.postForEntity(url, param, String.class); + String content = response.getBody(); + resultObj = JSON.parseObject(content, new TypeReference>() {}); + } catch (Exception e) { + LogUtil.error("fail to get access_token", e); + MSException.throwException("fail to get access_token!"); + } + + String accessToken = resultObj.get("access_token"); + + if (StringUtils.isBlank(accessToken)) { + MSException.throwException("access_token is empty!"); + } + + return doOauth2Login(authSource, accessToken, session, locale); + } + + private Optional doOauth2Login(AuthSource authSource, String accessToken, WebSession session, Locale locale) throws Exception { + Map oauth2Config = null; + Map resultObj = null; + try { + oauth2Config = JSON.parseObject(authSource.getConfiguration(), new TypeReference>() {}); + String userInfoUrl = oauth2Config.get("userInfoUrl"); + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", "Bearer " + accessToken); + RestTemplate restTemplate = getRestTemplateIgnoreSSL(); + HttpEntity> httpEntity = new HttpEntity<>(headers); + ResponseEntity response = restTemplate.exchange(userInfoUrl, HttpMethod.GET, httpEntity, String.class); + resultObj = JSON.parseObject(response.getBody(), new TypeReference>() {}); + } catch (Exception e) { + LogUtil.error("fail to get user info", e); + MSException.throwException("fail to get user info!"); + } + + String attrMapping = oauth2Config.get("mapping"); + Map mapping = this.getOauth2AttrMapping(attrMapping); + + String userid = resultObj.get(mapping.get("userid")); + String username = resultObj.get(mapping.get("username")); + String email = resultObj.get(mapping.get("email")); + + if (StringUtils.isBlank(userid)) { + MSException.throwException("userid is empty!"); + } + if (StringUtils.isBlank(username)) { + username = userid; + } + if (!StringUtils.contains(email, "@")) { + email = null; + } + + User u = userLoginService.selectUser(userid, email); + if (u == null) { + // + User user = new User(); + user.setId(userid); + user.setName(username); + user.setEmail(email); + user.setSource(authSource.getType()); + userLoginService.createOssUser(user); + } else { + if (StringUtils.equals(u.getEmail(), email) && !StringUtils.equals(u.getId(), userid)) { + MSException.throwException("email already exists!"); + } + } + + LoginRequest loginRequest = new LoginRequest(); + loginRequest.setUsername(userid); + loginRequest.setPassword("nothing"); + loginRequest.setAuthenticate(authSource.getType()); + Optional userOptional = userLoginService.login(loginRequest, session, locale); + session.getAttributes().put("authenticate", authSource.getType()); + session.getAttributes().put("authId", authSource.getId()); + return userOptional; + } + + private Map getOauth2AttrMapping(String mappingStr) { + Map mapping = new HashMap<>(); + try { + mapping = JSON.parseObject(mappingStr, new TypeReference>() {}); + } catch (Exception e) { + LogUtil.error("get oauth2 mapping config error!", e); + MSException.throwException(Translator.get("oauth_mapping_config_error")); + } + String userid = mapping.get("userid"); + if (StringUtils.isBlank(userid)) { + MSException.throwException(Translator.get("oauth_mapping_value_null") + ": userid"); + } + String username = mapping.get("username"); + if (StringUtils.isBlank(username)) { + MSException.throwException(Translator.get("oauth_mapping_value_null") + ": username"); + } + String email = mapping.get("email"); + if (StringUtils.isBlank(email)) { + MSException.throwException(Translator.get("oauth_mapping_value_null") + ": email"); + } + return mapping; + } + } diff --git a/framework/gateway/src/main/java/io/metersphere/gateway/service/UserLoginService.java b/framework/gateway/src/main/java/io/metersphere/gateway/service/UserLoginService.java index 480485da8b..1dfdd985cc 100644 --- a/framework/gateway/src/main/java/io/metersphere/gateway/service/UserLoginService.java +++ b/framework/gateway/src/main/java/io/metersphere/gateway/service/UserLoginService.java @@ -47,6 +47,7 @@ public class UserLoginService { switch (request.getAuthenticate()) { case "OIDC": case "CAS": + case "OAuth2": userDTO = loginSsoMode(request.getUsername(), request.getAuthenticate()); break; case "LDAP": diff --git a/framework/sdk-parent/frontend/src/business/login/index.vue b/framework/sdk-parent/frontend/src/business/login/index.vue index 748fd7feb0..62732604f2 100644 --- a/framework/sdk-parent/frontend/src/business/login/index.vue +++ b/framework/sdk-parent/frontend/src/business/login/index.vue @@ -254,7 +254,7 @@ export default { sessionStorage.setItem('redirectUrl', redirectUrl); sessionStorage.setItem('lastUser', getCurrentUserId()); - this.$router.push({ name: "login_redirect", path: redirectUrl || '/', query: this.otherQuery}); + this.$router.push({name: "login_redirect", path: redirectUrl || '/', query: this.otherQuery}); }, doLogin() { const userStore = useUserStore() @@ -310,6 +310,14 @@ export default { url = config.authUrl + "?client_id=" + config.clientId + "&redirect_uri=" + redirectUrl + "&response_type=code&scope=openid+profile+email&state=" + authId; } + if (source.type === 'OAuth2') { + url = config.authUrl + + "?client_id=" + config.clientId + + "&scope=" + config.scope + + "&response_type=code" + + "&redirect_uri=" + redirectUrl + + "&state=" + authId; + } if (url) { window.location.href = url; } diff --git a/framework/sdk-parent/sdk/src/main/resources/i18n/commons_en_US.properties b/framework/sdk-parent/sdk/src/main/resources/i18n/commons_en_US.properties index dc29b87e1e..93bc480dec 100644 --- a/framework/sdk-parent/sdk/src/main/resources/i18n/commons_en_US.properties +++ b/framework/sdk-parent/sdk/src/main/resources/i18n/commons_en_US.properties @@ -181,6 +181,8 @@ login_fail_ou_error=Login failed, please check the user OU login_fail_filter_error=Login failed, please check the user filter check_ldap_mapping=Check LDAP attribute mapping ldap_mapping_value_null=LDAP user attribute mapping field is empty +oauth_mapping_config_error=OAuth2 attribute mapping misconfiguration +oauth_mapping_value_null=OAuth2 user attribute mapping field is empty #quota quota_project_excess_ws_api=The total number of interface tests for a project cannot exceed the workspace quota quota_project_excess_ws_performance=The total number of performance tests for a project cannot exceed the workspace quota diff --git a/framework/sdk-parent/sdk/src/main/resources/i18n/commons_zh_CN.properties b/framework/sdk-parent/sdk/src/main/resources/i18n/commons_zh_CN.properties index a6728b5436..27ba6e526b 100644 --- a/framework/sdk-parent/sdk/src/main/resources/i18n/commons_zh_CN.properties +++ b/framework/sdk-parent/sdk/src/main/resources/i18n/commons_zh_CN.properties @@ -181,6 +181,8 @@ login_fail_ou_error=登录失败,请检查用户OU login_fail_filter_error=登录失败,请检查用户过滤器 check_ldap_mapping=检查LDAP属性映射 ldap_mapping_value_null=LDAP用户属性映射字段为空值 +oauth_mapping_config_error=OAuth2属性映射配置错误 +oauth_mapping_value_null=OAuth2用户属性映射字段为空值 #quota quota_project_excess_ws_api=项目的接口测试数量总和不能超过工作空间的配额 quota_project_excess_ws_performance=项目的性能测试数量总和不能超过工作空间的配额 diff --git a/framework/sdk-parent/sdk/src/main/resources/i18n/commons_zh_TW.properties b/framework/sdk-parent/sdk/src/main/resources/i18n/commons_zh_TW.properties index 0f287992e9..e8a30ccdbd 100644 --- a/framework/sdk-parent/sdk/src/main/resources/i18n/commons_zh_TW.properties +++ b/framework/sdk-parent/sdk/src/main/resources/i18n/commons_zh_TW.properties @@ -181,6 +181,8 @@ login_fail_ou_error=登錄失敗,請檢查用戶OU login_fail_filter_error=登錄失敗,請檢查用戶過濾器 check_ldap_mapping=檢查LDAP屬性映射 ldap_mapping_value_null=LDAP用戶屬性映射字段為空值 +oauth_mapping_config_error=OAuth2屬性映射配置錯誤 +oauth_mapping_value_null=OAuth2用戶屬性映射字段為空值 #quota quota_project_excess_ws_api=項目的接口測試數量總和不能超過工作空間的配額 quota_project_excess_ws_performance=項目的性能測試數量總和不能超過工作空間的配額 diff --git a/system-setting/frontend/src/business/system/setting/MxAuth.vue b/system-setting/frontend/src/business/system/setting/MxAuth.vue index 30b9f0b309..0d86abc6ce 100644 --- a/system-setting/frontend/src/business/system/setting/MxAuth.vue +++ b/system-setting/frontend/src/business/system/setting/MxAuth.vue @@ -17,6 +17,7 @@ @@ -74,6 +75,7 @@ @change="changeAuthType(form.type)"> + @@ -176,6 +178,76 @@ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +