-
Notifications
You must be signed in to change notification settings - Fork 296
feat(auth): add GitLab OAuth2 provider support #264
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,138 @@ | ||
| package com.iflytek.skillhub.auth.oauth; | ||
|
|
||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
| import org.springframework.http.HttpHeaders; | ||
| import org.springframework.http.MediaType; | ||
| import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; | ||
| import org.springframework.security.oauth2.core.user.OAuth2User; | ||
| import org.springframework.stereotype.Component; | ||
| import org.springframework.web.client.RestClient; | ||
|
|
||
| import java.util.List; | ||
| import java.util.Map; | ||
|
|
||
| /** | ||
| * Provider-specific claims extractor that enriches GitLab OAuth users with their | ||
| * verified email information. | ||
| * | ||
| * <p>GitLab OAuth2 user info endpoint returns user profile data. This extractor | ||
| * fetches additional email information from GitLab API when needed. | ||
| */ | ||
| @Component | ||
| public class GitLabClaimsExtractor implements OAuthClaimsExtractor { | ||
|
|
||
| private static final Logger log = LoggerFactory.getLogger(GitLabClaimsExtractor.class); | ||
|
|
||
| private final RestClient restClient; | ||
|
|
||
| public GitLabClaimsExtractor() { | ||
| this.restClient = RestClient.builder() | ||
| .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) | ||
| .build(); | ||
| } | ||
|
|
||
| @Override | ||
| public String getProvider() { | ||
| return "gitlab"; | ||
| } | ||
|
|
||
| @Override | ||
| public OAuthClaims extract(OAuth2UserRequest request, OAuth2User oAuth2User) { | ||
| Map<String, Object> attrs = oAuth2User.getAttributes(); | ||
| log.debug("Extracting GitLab OAuth claims for user attributes: {}", attrs.keySet()); | ||
|
|
||
| // GitLab returns email directly in user info | ||
| String email = (String) attrs.get("email"); | ||
|
|
||
| // GitLab provides email_verified field in the user info response | ||
| Boolean emailVerifiedObj = (Boolean) attrs.get("email_verified"); | ||
| boolean emailVerified = emailVerifiedObj != null && emailVerifiedObj; | ||
|
|
||
| log.debug("Initial email from GitLab: {}, verified: {}", email, emailVerified); | ||
|
|
||
| // If email is not verified or not present, try to fetch from emails API | ||
| if (email == null || !emailVerified) { | ||
| log.debug("Email not verified or missing, attempting to fetch from GitLab emails API"); | ||
| GitLabEmail primaryEmail = loadPrimaryEmail(request); | ||
| if (primaryEmail != null) { | ||
| email = primaryEmail.email(); | ||
| emailVerified = true; | ||
| log.debug("Found verified email from GitLab API: {}", email); | ||
| } else { | ||
| log.debug("No verified email found from GitLab emails API"); | ||
| } | ||
| } | ||
|
|
||
| // GitLab uses "username" for login name | ||
| String username = (String) attrs.get("username"); | ||
| if (username == null) { | ||
| username = (String) attrs.get("login"); | ||
| } | ||
|
|
||
| String subject = String.valueOf(attrs.get("id")); | ||
| log.info("GitLab OAuth claims extracted - subject: {}, username: {}, email: {}, emailVerified: {}", | ||
| subject, username, email, emailVerified); | ||
|
|
||
| return new OAuthClaims( | ||
| "gitlab", | ||
| subject, | ||
| email, | ||
| emailVerified, | ||
| username, | ||
| attrs | ||
| ); | ||
| } | ||
|
|
||
| private GitLabEmail loadPrimaryEmail(OAuth2UserRequest request) { | ||
| String baseUrl = getGitLabApiBaseUrl(request); | ||
| log.debug("Loading primary email from GitLab API base URL: {}", baseUrl); | ||
|
|
||
| try { | ||
| List<GitLabEmail> emails = restClient.get() | ||
| .uri(baseUrl + "/user/emails") | ||
| .header(HttpHeaders.AUTHORIZATION, "Bearer " + request.getAccessToken().getTokenValue()) | ||
| .retrieve() | ||
| .body(new org.springframework.core.ParameterizedTypeReference<List<GitLabEmail>>() {}); | ||
|
|
||
| if (emails == null || emails.isEmpty()) { | ||
| log.debug("No emails returned from GitLab emails API"); | ||
| return null; | ||
| } | ||
|
|
||
| log.debug("Retrieved {} emails from GitLab API", emails.size()); | ||
|
|
||
| // Return the primary verified email | ||
| return emails.stream() | ||
| .filter(GitLabEmail::confirmed) | ||
| .findFirst() | ||
| .orElse(null); | ||
| } catch (Exception e) { | ||
| log.warn("Failed to fetch emails from GitLab API: {}", e.getMessage()); | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Determines the GitLab API base URL from the provider configuration. | ||
| * The user-info-uri is configured as ${OAUTH2_GITLAB_BASE_URI}/api/v4/user, | ||
| * so we simply remove the /user suffix to get the API base URL. | ||
| */ | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
如果自建 GitLab 的域名或路径中包含 "user"(例如 建议改为: if (userInfoUri.endsWith("/user")) {
return userInfoUri.substring(0, userInfoUri.length() - "/user".length());
}
return userInfoUri; |
||
| private String getGitLabApiBaseUrl(OAuth2UserRequest request) { | ||
| String userInfoUri = request.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri(); | ||
| log.debug("GitLab user info URI: {}", userInfoUri); | ||
| // user-info-uri format: ${OAUTH2_GITLAB_BASE_URI}/api/v4/user | ||
| // Remove /user suffix to get API base URL | ||
| String baseUrl = userInfoUri.replace("/user", ""); | ||
| log.debug("GitLab API base URL: {}", baseUrl); | ||
| return baseUrl; | ||
| } | ||
|
|
||
| /** | ||
| * Represents a GitLab email object from the /user/emails API. | ||
| * | ||
| * @param email the email address | ||
| * @param confirmed whether the email has been confirmed | ||
| */ | ||
| private record GitLabEmail(String email, boolean confirmed) {} | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,33 @@ interface LoginButtonProps { | |
| returnTo?: string | ||
| } | ||
|
|
||
| /** | ||
| * Returns the appropriate icon for a given OAuth provider. | ||
| */ | ||
| function OAuthIcon({ provider }: { provider: string }) { | ||
| const normalizedProvider = provider.toLowerCase() | ||
|
|
||
| // GitLab icon | ||
| if (normalizedProvider === 'gitlab') { | ||
| return ( | ||
| <img | ||
| src="/gitlab-logo.svg" | ||
| alt="GitLab" | ||
| className="w-5 h-5 mr-3" | ||
| /> | ||
| ) | ||
| } | ||
|
|
||
| // GitHub icon (default) | ||
| return ( | ||
| <img | ||
| src="/github-logo.svg" | ||
| alt="GitHub" | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 未知 provider 默认返回 GitHub 图标 当前 fallback 是 GitHub logo,如果后续加入 Gitee 等其他 provider 会显示错误图标。建议 fallback 用一个通用登录图标,或者根据 provider name 动态匹配: // 通用 fallback
return <LogIn className="w-5 h-5 mr-3" />或者让后端在 provider 列表中返回 icon URL,前端直接渲染。 |
||
| className="w-5 h-5 mr-3" | ||
| /> | ||
| ) | ||
| } | ||
|
|
||
| /** | ||
| * Renders OAuth login buttons from the auth-method catalog returned by the backend. | ||
| */ | ||
|
|
@@ -37,12 +64,11 @@ export function LoginButton({ returnTo }: LoginButtonProps) { | |
| window.location.href = provider.actionUrl | ||
| }} | ||
| > | ||
| <svg className="w-5 h-5 mr-3" fill="currentColor" viewBox="0 0 24 24"> | ||
| <path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" /> | ||
| </svg> | ||
| <OAuthIcon provider={provider.provider} /> | ||
| {t('loginButton.loginWith', { name: provider.displayName })} | ||
| </Button> | ||
| ))} | ||
| </div> | ||
| ) | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
scope 格式不一致
GitHub 已改为 YAML list 格式,但 GitLab 仍用逗号分隔字符串。Spring Security 对逗号分隔 scope 的处理可能因版本而异,建议统一为 list 格式: