From 234e7a55ff86980b7493a49da68d5c8617cc29a1 Mon Sep 17 00:00:00 2001 From: MaysWind Date: Thu, 23 Oct 2025 00:16:28 +0800 Subject: [PATCH] support Gitea OAuth 2.0 authentication --- conf/ezbookkeeping.ini | 7 +- pkg/auth/oauth2/gitea_oauth2_datasource.go | 87 +++++++++++++++++++ .../oauth2/gitea_oauth2_datasource_test.go | 60 +++++++++++++ pkg/auth/oauth2/oauth2_authentication.go | 3 + pkg/core/user_external_auth_type.go | 2 + pkg/settings/setting.go | 5 ++ src/consts/oauth2.ts | 1 + 7 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 pkg/auth/oauth2/gitea_oauth2_datasource.go create mode 100644 pkg/auth/oauth2/gitea_oauth2_datasource_test.go diff --git a/conf/ezbookkeeping.ini b/conf/ezbookkeeping.ini index 91db2136..b4765f69 100644 --- a/conf/ezbookkeeping.ini +++ b/conf/ezbookkeeping.ini @@ -297,7 +297,7 @@ oauth2_user_identifier = email # For "oauth2" authentication only, if the user returned by OAuth 2.0 is not registered, automatically create a new user (requires "enable_register" to be set to true) oauth2_auto_register = true -# For "oauth2" authentication only, OAuth 2.0 provider, supports "nextcloud" and "github" currently +# For "oauth2" authentication only, OAuth 2.0 provider, supports "nextcloud", "gitea" and "github" currently oauth2_provider = # For "oauth2" authentication only, OAuth 2.0 state expired seconds (60 - 4294967295), default is 300 (5 minutes) @@ -313,9 +313,12 @@ oauth2_proxy = system # For "oauth2" authentication only, set to true to skip tls verification when request OAuth 2.0 api oauth2_skip_tls_verify = false -# For "oauth2" authentication and "nextcloud" OAuth 2.0 provider only, nextcloud base url, e.g. "https://cloud.example.org/" +# For "oauth2" authentication and "nextcloud" OAuth 2.0 provider only, Nextcloud base url, e.g. "https://cloud.example.org/" nextcloud_base_url = +# For "oauth2" authentication and "gitea" OAuth 2.0 provider only, Gitea base url, e.g. "https://git.example.com/" +gitea_base_url = + [user] # Set to true to allow users to register account by themselves enable_register = true diff --git a/pkg/auth/oauth2/gitea_oauth2_datasource.go b/pkg/auth/oauth2/gitea_oauth2_datasource.go new file mode 100644 index 00000000..256eb708 --- /dev/null +++ b/pkg/auth/oauth2/gitea_oauth2_datasource.go @@ -0,0 +1,87 @@ +package oauth2 + +import ( + "encoding/json" + "net/http" + + "github.com/mayswind/ezbookkeeping/pkg/core" + "github.com/mayswind/ezbookkeeping/pkg/errs" + "github.com/mayswind/ezbookkeeping/pkg/log" +) + +type giteaUserInfoResponse struct { + Login string `json:"login"` + FullName string `json:"full_name"` + Email string `json:"email"` +} + +// GiteaOAuth2DataSource represents Gitea OAuth 2.0 data source +type GiteaOAuth2DataSource struct { + CommonOAuth2DataSource + baseUrl string +} + +// GetAuthUrl returns the authentication url of the Gitea data source +func (s *GiteaOAuth2DataSource) GetAuthUrl() string { + // Reference: https://docs.gitea.com/development/oauth2-provider + return s.baseUrl + "login/oauth/authorize" +} + +// GetTokenUrl returns the token url of the Gitea data source +func (s *GiteaOAuth2DataSource) GetTokenUrl() string { + // Reference: https://docs.gitea.com/development/oauth2-provider + return s.baseUrl + "login/oauth/access_token" +} + +// GetUserInfoRequest returns the user info request of the Gitea data source +func (s *GiteaOAuth2DataSource) GetUserInfoRequest() (*http.Request, error) { + // Reference: https://gitea.com/api/swagger#/user/userGetCurrent + req, err := http.NewRequest("GET", s.baseUrl+"api/v1/user", nil) + + if err != nil { + return nil, err + } + + req.Header.Set("Accept", "application/json") + return req, nil +} + +// GetScopes returns the scopes required by the Gitea provider +func (s *GiteaOAuth2DataSource) GetScopes() []string { + return []string{"read:user"} +} + +// ParseUserInfo returns the user info by parsing the response body +func (s *GiteaOAuth2DataSource) ParseUserInfo(c core.Context, body []byte) (*OAuth2UserInfo, error) { + userInfoResp := &giteaUserInfoResponse{} + err := json.Unmarshal(body, &userInfoResp) + + if err != nil { + log.Warnf(c, "[gitea_oauth2_datasource.ParseUserInfo] failed to parse user profile response body, because %s", err.Error()) + return nil, errs.ErrCannotRetrieveUserInfo + } + + if userInfoResp.Login == "" { + log.Warnf(c, "[gitea_oauth2_datasource.ParseUserInfo] invalid user profile response body") + return nil, errs.ErrCannotRetrieveUserInfo + } + + return &OAuth2UserInfo{ + UserName: userInfoResp.Login, + Email: userInfoResp.Email, + NickName: userInfoResp.FullName, + }, nil +} + +// NewGiteaOAuth2Provider creates a new Gitea OAuth 2.0 provider instance +func NewGiteaOAuth2Provider(baseUrl string) OAuth2Provider { + if baseUrl[len(baseUrl)-1] != '/' { + baseUrl += "/" + } + + return &CommonOAuth2Provider{ + dataSource: &GiteaOAuth2DataSource{ + baseUrl: baseUrl, + }, + } +} diff --git a/pkg/auth/oauth2/gitea_oauth2_datasource_test.go b/pkg/auth/oauth2/gitea_oauth2_datasource_test.go new file mode 100644 index 00000000..b45fc3f0 --- /dev/null +++ b/pkg/auth/oauth2/gitea_oauth2_datasource_test.go @@ -0,0 +1,60 @@ +package oauth2 + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/mayswind/ezbookkeeping/pkg/core" + "github.com/mayswind/ezbookkeeping/pkg/errs" +) + +func TestNewGiteaOAuth2Provider(t *testing.T) { + datasource := NewGiteaOAuth2Provider("https://example.com/") + assert.Equal(t, "https://example.com/login/oauth/authorize", datasource.GetAuthUrl()) + assert.Equal(t, "https://example.com/login/oauth/access_token", datasource.GetTokenUrl()) + + datasource = NewGiteaOAuth2Provider("https://example.com") + assert.Equal(t, "https://example.com/login/oauth/authorize", datasource.GetAuthUrl()) + assert.Equal(t, "https://example.com/login/oauth/access_token", datasource.GetTokenUrl()) +} + +func TestGiteaOAuth2Datasource_GetUserInfoRequest(t *testing.T) { + datasource := &GiteaOAuth2DataSource{baseUrl: "https://example.com/"} + req, err := datasource.GetUserInfoRequest() + + assert.Nil(t, err) + assert.Equal(t, "GET", req.Method) + assert.Equal(t, "https://example.com/api/v1/user", req.URL.String()) + assert.Equal(t, "application/json", req.Header.Get("Accept")) +} + +func TestGiteaOAuth2Datasource_ParseUserInfo_Success(t *testing.T) { + datasource := &GiteaOAuth2DataSource{} + responseContent := `{ + "login": "user1", + "full_name": "User", + "email": "user1@example.com" + }` + info, err := datasource.ParseUserInfo(core.NewNullContext(), []byte(responseContent)) + + assert.Nil(t, err) + assert.Equal(t, "user1", info.UserName) + assert.Equal(t, "user1@example.com", info.Email) + assert.Equal(t, "User", info.NickName) +} + +func TestGiteaOAuth2Datasource_ParseUserInfo_InvalidJson(t *testing.T) { + datasource := &GiteaOAuth2DataSource{} + _, err := datasource.ParseUserInfo(core.NewNullContext(), []byte("invalid")) + + assert.Equal(t, errs.ErrCannotRetrieveUserInfo, err) +} + +func TestGiteaOAuth2Datasource_ParseUserInfo_EmptyLogin(t *testing.T) { + datasource := &GiteaOAuth2DataSource{} + responseContent := `{"login": ""}` + _, err := datasource.ParseUserInfo(core.NewNullContext(), []byte(responseContent)) + + assert.Equal(t, errs.ErrCannotRetrieveUserInfo, err) +} diff --git a/pkg/auth/oauth2/oauth2_authentication.go b/pkg/auth/oauth2/oauth2_authentication.go index b68f1c8c..94457b75 100644 --- a/pkg/auth/oauth2/oauth2_authentication.go +++ b/pkg/auth/oauth2/oauth2_authentication.go @@ -40,6 +40,9 @@ func InitializeOAuth2Provider(config *settings.Config) error { if config.OAuth2Provider == settings.OAuth2ProviderNextcloud { oauth2Provider = NewNextcloudOAuth2Provider(config.OAuth2NextcloudBaseUrl) externalUserAuthType = core.USER_EXTERNAL_AUTH_TYPE_OAUTH2_NEXTCLOUD + } else if config.OAuth2Provider == settings.OAuth2ProviderGitea { + oauth2Provider = NewGiteaOAuth2Provider(config.OAuth2GiteaBaseUrl) + externalUserAuthType = core.USER_EXTERNAL_AUTH_TYPE_OAUTH2_GITEA } else if config.OAuth2Provider == settings.OAuth2ProviderGithub { oauth2Provider = NewGithubOAuth2Provider() externalUserAuthType = core.USER_EXTERNAL_AUTH_TYPE_OAUTH2_GITHUB diff --git a/pkg/core/user_external_auth_type.go b/pkg/core/user_external_auth_type.go index 35ce0200..76500f49 100644 --- a/pkg/core/user_external_auth_type.go +++ b/pkg/core/user_external_auth_type.go @@ -6,6 +6,7 @@ type UserExternalAuthType string // User External Auth Type const ( USER_EXTERNAL_AUTH_TYPE_OAUTH2_NEXTCLOUD UserExternalAuthType = "nextcloud" + USER_EXTERNAL_AUTH_TYPE_OAUTH2_GITEA UserExternalAuthType = "gitea" USER_EXTERNAL_AUTH_TYPE_OAUTH2_GITHUB UserExternalAuthType = "github" ) @@ -13,6 +14,7 @@ const ( func (t UserExternalAuthType) IsValid() bool { switch t { case USER_EXTERNAL_AUTH_TYPE_OAUTH2_NEXTCLOUD, + USER_EXTERNAL_AUTH_TYPE_OAUTH2_GITEA, USER_EXTERNAL_AUTH_TYPE_OAUTH2_GITHUB: return true } diff --git a/pkg/settings/setting.go b/pkg/settings/setting.go index cdfcd0a7..fc42fe70 100644 --- a/pkg/settings/setting.go +++ b/pkg/settings/setting.go @@ -94,6 +94,7 @@ const ( // OAuth 2.0 provider types const ( OAuth2ProviderNextcloud string = "nextcloud" + OAuth2ProviderGitea string = "gitea" OAuth2ProviderGithub string = "github" ) @@ -375,6 +376,7 @@ type Config struct { OAuth2Proxy string OAuth2SkipTLSVerify bool OAuth2NextcloudBaseUrl string + OAuth2GiteaBaseUrl string // User EnableUserRegister bool @@ -1003,6 +1005,8 @@ func loadAuthConfiguration(config *Config, configFile *ini.File, sectionName str config.OAuth2Provider = "" } else if oauth2Provider == OAuth2ProviderNextcloud { config.OAuth2Provider = OAuth2ProviderNextcloud + } else if oauth2Provider == OAuth2ProviderGitea { + config.OAuth2Provider = OAuth2ProviderGitea } else if oauth2Provider == OAuth2ProviderGithub { config.OAuth2Provider = OAuth2ProviderGithub } else { @@ -1022,6 +1026,7 @@ func loadAuthConfiguration(config *Config, configFile *ini.File, sectionName str config.OAuth2SkipTLSVerify = getConfigItemBoolValue(configFile, sectionName, "oauth2_skip_tls_verify", false) config.OAuth2NextcloudBaseUrl = getConfigItemStringValue(configFile, sectionName, "nextcloud_base_url") + config.OAuth2GiteaBaseUrl = getConfigItemStringValue(configFile, sectionName, "gitea_base_url") return nil } diff --git a/src/consts/oauth2.ts b/src/consts/oauth2.ts index 87f5454d..aad6287e 100644 --- a/src/consts/oauth2.ts +++ b/src/consts/oauth2.ts @@ -1,4 +1,5 @@ export const OAUTH2_PROVIDER_DISPLAY_NAME: Record = { 'nextcloud': 'Nextcloud', + 'gitea': 'Gitea', 'github': 'GitHub', };