Skip to content
23 changes: 16 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# plugin-oauth2

Halo 2.0 的 OAuth2 第三方登录插件。
Halo 2.0 的 OAuth2/OIDC 第三方登录插件。

## 使用方法

Expand All @@ -14,11 +14,14 @@ Halo 2.0 的 OAuth2 第三方登录插件。

目前支持的认证方式:

| 服务商 | 文档 | Halo 所需配置 | Scope | 回调地址 |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------- | ------------ | ------------------------------------- |
| GitHub | [https://docs.github.com](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app) | `Client ID` `Client Secret` | 无需手动设置 | `<SITE_URL>/login/oauth2/code/github` |
| GitLab | [https://docs.gitlab.com](https://docs.gitlab.com/ee/integration/oauth_provider.html#configure-gitlab-as-an-oauth-20-authentication-identity-provider) | `Client ID` `Client Secret` | `read_user` | `<SITE_URL>/login/oauth2/code/gitlab` |
| Gitee | <https://gitee.com/oauth/applications> | `Client ID` `Client Secret` | `user_info` | `<SITE_URL>/login/oauth2/code/gitee` |
| 服务商 | 文档 | Halo 所需配置 | Scope | 回调地址 |
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------- | ---------------- | -------------------------------------- |
| GitHub | [https://docs.github.com](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app) | `Client ID` `Client Secret` | 无需手动设置 | `<SITE_URL>/login/oauth2/code/github` |
| Google | [https://developers.google.com](https://developers.google.com/identity/openid-connect/openid-connect) | `Client ID` `Client Secret` | 无需手动设置 | `<SITE_URL>/login/oauth2/code/google` |
| GitLab | [https://docs.gitlab.com](https://docs.gitlab.com/ee/integration/oauth_provider.html#configure-gitlab-as-an-oauth-20-authentication-identity-provider) | `Client ID` `Client Secret` | `read_user` | `<SITE_URL>/login/oauth2/code/gitlab` |
| Gitee | [https://gitee.com](https://gitee.com/api/v5/oauth_doc#/) | `Client ID` `Client Secret` | `user_info` | `<SITE_URL>/login/oauth2/code/gitee` |
| LINUX DO | [https://wiki.linux.do](https://wiki.linux.do/Community/LinuxDoConnect) | `Client ID` `Client Secret` | 无需手动设置 | `<SITE_URL>/login/oauth2/code/linuxdo` |
| SSO(任意 OAuth2 / OpenID Connect 提供商) | 通用 OAuth2 / OpenID Connect(参见 [RFC 6749](https://www.rfc-editor.org/rfc/rfc6749) / OIDC 文档) | `Client ID` `Client Secret` `Authorization URI` `Token URI` `UserInfo Url` `Scopes` `UserName Attribute` | 按提供商要求设置 | `<SITE_URL>/login/oauth2/code/sso` |

注意事项:

Expand All @@ -28,7 +31,13 @@ Halo 2.0 的 OAuth2 第三方登录插件。

## 代理配置(可选)

如果你部署的 Halo 服务器无法直接访问 GitHub、GitLab 或 Gitee 的 API,你可以配置代理。
如果你部署的 Halo 服务器无法直接访问 GitHub、GitLab、Gitee 或 LINUX DO 的 API,你可以配置代理。

代理配置将应用于所有 OAuth2/OIDC 请求,包括:
- Token 交换请求
- 用户信息请求
- OIDC JWKS (JSON Web Key Set) 获取
- OIDC Issuer 发现
Comment thread
HowieHz marked this conversation as resolved.

配置路径示例:`${Halo 工作目录}/plugins/configs/plugin-oauth2.yaml`。配置示例如下所示:

Expand Down
119 changes: 96 additions & 23 deletions src/main/java/run/halo/oauth/HaloOAuth2AuthenticationWebFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,17 @@
import org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeReactiveAuthenticationManager;
import org.springframework.security.oauth2.client.oidc.authentication.ReactiveOidcIdTokenDecoderFactory;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcReactiveOAuth2UserService;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.userinfo.DefaultReactiveOAuth2UserService;
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizationCodeAuthenticationTokenConverter;
import org.springframework.security.oauth2.client.web.server.authentication.OAuth2LoginAuthenticationWebFilter;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.MappedJwtClaimSetConverter;
import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder;
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoderFactory;
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationFailureHandler;
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationSuccessHandler;
Expand Down Expand Up @@ -74,37 +81,26 @@ public HaloOAuth2AuthenticationWebFilter(Oauth2LoginConfiguration configuration,
var accessTokenResponseClient = new WebClientReactiveAuthorizationCodeTokenResponseClient();
accessTokenResponseClient.setWebClient(webClient);

var oauth2UserService = new DefaultReactiveOAuth2UserService();
oauth2UserService.setWebClient(webClient);

var oauth2AuthManager = new OAuth2LoginReactiveAuthenticationManager(
accessTokenResponseClient,
new DefaultReactiveOAuth2UserService()
oauth2UserService
);

var oidcUserService = new OidcReactiveOAuth2UserService();
oidcUserService.setOauth2UserService(oauth2UserService);

var oidcAuthManager = new OidcAuthorizationCodeReactiveAuthenticationManager(
accessTokenResponseClient,
new OidcReactiveOAuth2UserService()
oidcUserService
);
var oidcIdTokenDecodeFactory = new ReactiveOidcIdTokenDecoderFactory();
oidcIdTokenDecodeFactory.setJwsAlgorithmResolver(clientRegistration -> {
var configurationMetadata = clientRegistration.getProviderDetails()
.getConfigurationMetadata();
try {
var supportedJwsAlgorithms = JSONObjectUtils.getStringList(
new JSONObject(configurationMetadata),
"id_token_signing_alg_values_supported"
);
// we choose the first one as JWS algorithm
if (!supportedJwsAlgorithms.isEmpty()) {
var jwsAlgorithm = supportedJwsAlgorithms.get(0);
return SignatureAlgorithm.from(jwsAlgorithm);
}
} catch (ParseException e) {
// ignore the error.
}
// default algorithm
return SignatureAlgorithm.RS256;
});
// Create custom OIDC ID token decoder factory with proxy-enabled WebClient
var oidcIdTokenDecodeFactory = createOidcIdTokenDecoderFactory(webClient);
oidcAuthManager.setJwtDecoderFactory(oidcIdTokenDecodeFactory);
var authManager =
new DelegatingReactiveAuthenticationManager(oauth2AuthManager, oidcAuthManager);
new DelegatingReactiveAuthenticationManager(oidcAuthManager, oauth2AuthManager);
var filter = new OAuth2LoginAuthenticationWebFilter(
authManager, configuration.getAuthorizedClientRepository()
);
Expand Down Expand Up @@ -136,4 +132,81 @@ public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return delegate.filter(exchange, chain);
}

/**
* Creates a custom OIDC ID token decoder factory that uses the provided WebClient
* for JWKS retrieval and issuer discovery, ensuring proxy configuration is applied.
*
* When jwkSetUri is provided, it directly retrieves the JWKS from that URI.
* When only issuerUri is provided, it performs issuer-based discovery by fetching
* the OpenID Connect configuration from the issuer's .well-known endpoint.
*/
private ReactiveJwtDecoderFactory<ClientRegistration> createOidcIdTokenDecoderFactory(
WebClient webClient) {
return new ReactiveJwtDecoderFactory<ClientRegistration>() {
@Override
public ReactiveJwtDecoder createDecoder(ClientRegistration clientRegistration) {
// Determine the JWS algorithm from provider metadata
SignatureAlgorithm jwsAlgorithm = resolveJwsAlgorithm(clientRegistration);

String jwkSetUri = clientRegistration.getProviderDetails().getJwkSetUri();
NimbusReactiveJwtDecoder decoder;
if (StringUtils.hasText(jwkSetUri)) {
// Build decoder with custom WebClient for JWKS retrieval using explicit JWK Set URI
decoder = NimbusReactiveJwtDecoder
.withJwkSetUri(jwkSetUri)
.jwsAlgorithm(jwsAlgorithm)
.webClient(webClient)
.build();
}
else {
// Fall back to issuer-based discovery when JWK Set URI is not configured
String issuerUri = clientRegistration.getProviderDetails().getIssuerUri();
if (!StringUtils.hasText(issuerUri)) {
OAuth2Error oauth2Error = new OAuth2Error(
"missing_signature_verifier",
"Failed to find a Signature Verifier for Client Registration: '"
+ clientRegistration.getRegistrationId()
+ "'. Configure either the JWK Set URI or the Issuer URI.",
null
);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
decoder = NimbusReactiveJwtDecoder
.withIssuerLocation(issuerUri)
.jwsAlgorithm(jwsAlgorithm)
.webClient(webClient)
.build();
}

// Apply default OIDC claim type converters
decoder.setClaimSetConverter(
MappedJwtClaimSetConverter.withDefaults(
ReactiveOidcIdTokenDecoderFactory.createDefaultClaimTypeConverters()
)
);
return decoder;
}

private SignatureAlgorithm resolveJwsAlgorithm(ClientRegistration clientRegistration) {
var configurationMetadata = clientRegistration.getProviderDetails()
.getConfigurationMetadata();
try {
var supportedJwsAlgorithms = JSONObjectUtils.getStringList(
new JSONObject(configurationMetadata),
"id_token_signing_alg_values_supported"
);
// we choose the first one as JWS algorithm
if (!supportedJwsAlgorithms.isEmpty()) {
var jwsAlgorithm = supportedJwsAlgorithms.get(0);
return SignatureAlgorithm.from(jwsAlgorithm);
}
} catch (ParseException e) {
// Ignore the error if metadata is missing or malformed and fall back to default RS256 algorithm
}
// default algorithm
return SignatureAlgorithm.RS256;
}
};
}

}
46 changes: 45 additions & 1 deletion src/main/resources/extensions/auth-provider.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,28 @@ spec:
---
apiVersion: auth.halo.run/v1alpha1
kind: AuthProvider
metadata:
name: google
labels:
auth.halo.run/auth-binding: "true"
spec:
displayName: Google
description: Google LLC is an American multinational technology company that provides Google Sign-In via OAuth 2.0 / OpenID Connect.
logo: /plugins/plugin-oauth2/assets/static/google.svg
website: https://accounts.google.com
helpPage: https://developers.google.com/identity/protocols/oauth2/openid-connect
authenticationUrl: /oauth2/authorization/google
bindingUrl: /oauth2/authorization/google
unbindUrl: /apis/uc.api.auth.halo.run/v1alpha1/user-connections/google/disconnect
authType: oauth2
settingRef:
name: generic-oauth2-setting
group: genericOauth
configMapRef:
name: oauth2-google-config
---
apiVersion: auth.halo.run/v1alpha1
kind: AuthProvider
metadata:
name: gitee
labels:
Expand Down Expand Up @@ -64,6 +86,28 @@ spec:
---
apiVersion: auth.halo.run/v1alpha1
kind: AuthProvider
metadata:
name: linuxdo
labels:
auth.halo.run/auth-binding: "true"
spec:
displayName: LINUX DO
description: LINUX DO is a community forum that provides LINUX DO Connect for sharing forum user information via OAuth 2.0 / OpenID Connect.
logo: /plugins/plugin-oauth2/assets/static/linuxdo.svg
website: https://linux.do
helpPage: https://wiki.linux.do/Community/LinuxDoConnect
authenticationUrl: /oauth2/authorization/linuxdo
bindingUrl: /oauth2/authorization/linuxdo
unbindUrl: /apis/uc.api.auth.halo.run/v1alpha1/user-connections/linuxdo/disconnect
authType: oauth2
settingRef:
name: generic-oauth2-setting
group: genericOauth
configMapRef:
name: oauth2-linuxdo-config
---
apiVersion: auth.halo.run/v1alpha1
kind: AuthProvider
metadata:
name: sso
labels:
Expand All @@ -80,4 +124,4 @@ spec:
name: sso-oauth2-setting
group: ssoOauth
configMapRef:
name: oauth2-sso-config
name: oauth2-sso-config
40 changes: 40 additions & 0 deletions src/main/resources/extensions/client-registrations.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,26 @@ spec:
---
apiVersion: oauth.halo.run/v1alpha1
kind: Oauth2ClientRegistration
metadata:
name: google
spec:
clientAuthenticationMethod: "client_secret_basic"
authorizationGrantType: "authorization_code"
redirectUri: "{baseUrl}/login/oauth2/code/google"
scopes:
- "openid"
- "profile"
authorizationUri: "https://accounts.google.com/o/oauth2/v2/auth"
tokenUri: "https://oauth2.googleapis.com/token"
userInfoUri: "https://openidconnect.googleapis.com/v1/userinfo"
userInfoAuthenticationMethod: "header"
userNameAttributeName: "sub"
issuerUri: "https://accounts.google.com"
jwkSetUri: "https://www.googleapis.com/oauth2/v3/certs"
clientName: "Google"
---
apiVersion: oauth.halo.run/v1alpha1
kind: Oauth2ClientRegistration
metadata:
name: gitee
spec:
Expand Down Expand Up @@ -51,6 +71,26 @@ spec:
---
apiVersion: oauth.halo.run/v1alpha1
kind: Oauth2ClientRegistration
metadata:
name: linuxdo
spec:
clientAuthenticationMethod: "client_secret_basic"
authorizationGrantType: "authorization_code"
redirectUri: "{baseUrl}/login/oauth2/code/linuxdo"
scopes:
- "openid"
- "profile"
authorizationUri: "https://connect.linux.do/oauth2/authorize"
tokenUri: "https://connect.linux.do/oauth2/token"
userInfoUri: "https://connect.linux.do/api/user"
userInfoAuthenticationMethod: "header"
userNameAttributeName: "sub"
issuerUri: "https://connect.linux.do/"
jwkSetUri: "https://connect.linux.do/.well-known/jwks.json"
clientName: "LINUX DO"
---
apiVersion: oauth.halo.run/v1alpha1
kind: Oauth2ClientRegistration
metadata:
name: sso
spec:
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/static/google.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/main/resources/static/linuxdo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.