mirror of
https://github.com/go-sigma/sigma.git
synced 2026-10-28 14:34:34 +08:00
refactor(oauth2): stop persisting OAuth tokens on login
The callback stored the access and refresh token on user_3rdparty and re-issued the code repository sync on every login, which only worked because the login refreshed an expiring token. Credentials now live in the clone credential table, so the tokens have no reader left. - drop the token update on repeated login and the token fields from both binding-creation paths; Oauth2UserInfo no longer carries them - stop publishing a sync task when a binding is created: there is no credential at that point, so the task could only report a failure - derive the GitLab and Gitea OAuth endpoints from scm.providers[].baseUrl instead of hardcoding gitlab.com, and point Gitea at its own /login/oauth endpoints and redirect callback Verification: - CI_DATABASE_TYPE=turso go test -tags "netgo,timetzdata,exclude_graphdriver_btrfs,containers_image_openpgp" ./pkg/service/oauth2/...
This commit is contained in:
1 parent
a80519d86f
commit
5ca747ddd7
2 files changed
+46
-59
No files matched your search
+4
-6
@@ -35,12 +35,10 @@ type Oauth2RedirectCallbackRequest struct {
|
||||
|
||||
// Oauth2UserInfo represents user information returned by an OAuth2 provider.
|
||||
type Oauth2UserInfo struct {
|
||||
Provider enums.Provider `json:"provider"`
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Token string `json:"token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
Provider enums.Provider `json:"provider"`
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
// Oauth2CallbackResponse is the response returned after a successful OAuth2 callback.
|
||||
|
||||
@@ -37,7 +37,6 @@ import (
|
||||
"github.com/go-sigma/sigma/pkg/dal/models"
|
||||
"github.com/go-sigma/sigma/pkg/dal/query"
|
||||
repouser "github.com/go-sigma/sigma/pkg/dal/repository/user"
|
||||
"github.com/go-sigma/sigma/pkg/infra/workq"
|
||||
"github.com/go-sigma/sigma/pkg/server/errcode"
|
||||
"github.com/go-sigma/sigma/pkg/service/password"
|
||||
"github.com/go-sigma/sigma/pkg/service/token"
|
||||
@@ -63,7 +62,6 @@ type service struct {
|
||||
RepoUser repouser.UserRepository
|
||||
SvcToken token.Service
|
||||
SvcPassword password.Service
|
||||
Producer workq.Producer
|
||||
}
|
||||
|
||||
func NewService(params service) Service {
|
||||
@@ -105,26 +103,29 @@ func (s *service) Callback(ctx context.Context, provider enums.Provider, code st
|
||||
},
|
||||
}
|
||||
case enums.ProviderGitlab:
|
||||
baseURL := s.scmBaseURL(provider)
|
||||
conf = &oauth2.Config{
|
||||
ClientID: s.Config.Auth.Oauth2.Gitlab.ClientID,
|
||||
ClientSecret: s.Config.Auth.Oauth2.Gitlab.ClientSecret,
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: "https://gitlab.com/oauth/authorize", // #nosec G101 -- public OAuth endpoint, not a credential
|
||||
TokenURL: "https://gitlab.com/oauth/token", // #nosec G101 -- public OAuth endpoint, not a credential
|
||||
AuthURL: baseURL + "/oauth/authorize",
|
||||
TokenURL: baseURL + "/oauth/token",
|
||||
},
|
||||
RedirectURL: fmt.Sprintf("%s/api/v1/oauth2/%s/redirect_callback?endpoint=%s",
|
||||
s.Config.HTTP.Endpoint, enums.ProviderGitlab.String(), url.QueryEscape(endpoint)),
|
||||
Scopes: []string{"api", "read_api", "read_user", "read_repository"},
|
||||
}
|
||||
case enums.ProviderGitea:
|
||||
baseURL := s.scmBaseURL(provider)
|
||||
conf = &oauth2.Config{
|
||||
ClientID: s.Config.Auth.Oauth2.Gitea.ClientID,
|
||||
ClientSecret: s.Config.Auth.Oauth2.Gitea.ClientSecret,
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: "https://gitlab.com/oauth/authorize", // #nosec G101 -- public OAuth endpoint, not a credential
|
||||
TokenURL: "https://gitlab.com/oauth/token", // #nosec G101 -- public OAuth endpoint, not a credential
|
||||
AuthURL: baseURL + "/login/oauth/authorize",
|
||||
TokenURL: baseURL + "/login/oauth/access_token",
|
||||
},
|
||||
RedirectURL: "http://localhost:3000/api/v1/oauth2/github/redirect_callback",
|
||||
RedirectURL: fmt.Sprintf("%s/api/v1/oauth2/%s/redirect_callback?endpoint=%s",
|
||||
s.Config.HTTP.Endpoint, enums.ProviderGitea.String(), url.QueryEscape(endpoint)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,12 +151,10 @@ func (s *service) Callback(ctx context.Context, provider enums.Provider, code st
|
||||
return "", "", "", "", "", errcode.HTTPErrCodeInternalError.Detail(err.Error())
|
||||
}
|
||||
userInfo = api.Oauth2UserInfo{
|
||||
Provider: provider,
|
||||
ID: strconv.FormatInt(user.GetID(), 10),
|
||||
Username: user.GetLogin(),
|
||||
Email: user.GetEmail(),
|
||||
Token: oauth2Token.AccessToken,
|
||||
RefreshToken: oauth2Token.RefreshToken,
|
||||
Provider: provider,
|
||||
ID: strconv.FormatInt(user.GetID(), 10),
|
||||
Username: user.GetLogin(),
|
||||
Email: user.GetEmail(),
|
||||
}
|
||||
case enums.ProviderGitlab:
|
||||
glClient, err := gitlab.NewOAuthClient(oauth2Token.AccessToken) // nolint: staticcheck
|
||||
@@ -169,12 +168,10 @@ func (s *service) Callback(ctx context.Context, provider enums.Provider, code st
|
||||
return "", "", "", "", "", errcode.HTTPErrCodeInternalError.Detail(fmt.Sprintf("Get user info failed: %v", err))
|
||||
}
|
||||
userInfo = api.Oauth2UserInfo{
|
||||
Provider: provider,
|
||||
ID: strconv.FormatInt(user.ID, 10),
|
||||
Username: user.Name,
|
||||
Email: user.Email,
|
||||
Token: oauth2Token.AccessToken,
|
||||
RefreshToken: oauth2Token.RefreshToken,
|
||||
Provider: provider,
|
||||
ID: strconv.FormatInt(user.ID, 10),
|
||||
Username: user.Name,
|
||||
Email: user.Email,
|
||||
}
|
||||
case enums.ProviderGitea:
|
||||
// gitea.NewClient("", gitea.SetHTTPClient(client))
|
||||
@@ -197,16 +194,9 @@ func (s *service) Callback(ctx context.Context, provider enums.Provider, code st
|
||||
return "", "", "", "", "", errcode.HTTPErrCodeConflict.Detail("User already bound to another account")
|
||||
}
|
||||
|
||||
if userExist {
|
||||
err = s.RepoUser.UpdateUser3rdParty(ctx, user3rdPartyObj.ID, map[string]any{
|
||||
query.User3rdParty.Token.ColumnName().String(): oauth2Token.AccessToken,
|
||||
query.User3rdParty.RefreshToken.ColumnName().String(): oauth2Token.RefreshToken,
|
||||
})
|
||||
if err != nil {
|
||||
slog.Error("update user 3rdparty failed", "err", err)
|
||||
return "", "", "", "", "", errcode.HTTPErrCodeInternalError.Detail(err.Error())
|
||||
}
|
||||
}
|
||||
// OAuth tokens are no longer persisted: SCM credentials are configured
|
||||
// explicitly per provider (see pkg/service/coderepos) because OAuth access
|
||||
// tokens expire and refresh tokens were never used.
|
||||
|
||||
if !userExist {
|
||||
if userSignedObj == nil {
|
||||
@@ -236,24 +226,16 @@ func (s *service) Callback(ctx context.Context, provider enums.Provider, code st
|
||||
return errcode.HTTPErrCodeInternalError.Detail(fmt.Sprintf("Create user failed: %v", err))
|
||||
}
|
||||
user3rdPartyObj = &models.User3rdParty{
|
||||
ID: uuid.NewV7String(),
|
||||
Provider: provider,
|
||||
AccountID: new(userInfo.ID),
|
||||
Token: new(userInfo.Token),
|
||||
RefreshToken: new(userInfo.RefreshToken),
|
||||
UserID: userSignedObj.ID,
|
||||
ID: uuid.NewV7String(),
|
||||
Provider: provider,
|
||||
AccountID: new(userInfo.ID),
|
||||
UserID: userSignedObj.ID,
|
||||
}
|
||||
err = txUserRepository.CreateUser3rdParty(ctx, user3rdPartyObj)
|
||||
if err != nil {
|
||||
slog.Error("create user failed", "err", err)
|
||||
return errcode.HTTPErrCodeInternalError.Detail(fmt.Sprintf("Create user failed: %v", err))
|
||||
}
|
||||
err = s.Producer.Produce(ctx, enums.DaemonCodeRepository,
|
||||
api.DaemonCodeRepositoryPayload{User3rdPartyID: user3rdPartyObj.ID})
|
||||
if err != nil {
|
||||
slog.Error("publish sync code repository failed", "err", err, "user_id", user3rdPartyObj.UserID)
|
||||
return errcode.HTTPErrCodeInternalError.Detail("Publish sync code repository failed")
|
||||
}
|
||||
user3rdPartyObj.User = ptr.To(userSignedObj)
|
||||
return nil
|
||||
})
|
||||
@@ -264,24 +246,16 @@ func (s *service) Callback(ctx context.Context, provider enums.Provider, code st
|
||||
err = query.Q.Transaction(func(tx *query.Query) error {
|
||||
txUserRepository := repouser.NewUserRepository(tx)
|
||||
user3rdPartyObj = &models.User3rdParty{
|
||||
ID: uuid.NewV7String(),
|
||||
Provider: provider,
|
||||
AccountID: new(userInfo.ID),
|
||||
Token: new(userInfo.Token),
|
||||
RefreshToken: new(userInfo.RefreshToken),
|
||||
UserID: userSignedObj.ID,
|
||||
ID: uuid.NewV7String(),
|
||||
Provider: provider,
|
||||
AccountID: new(userInfo.ID),
|
||||
UserID: userSignedObj.ID,
|
||||
}
|
||||
err = txUserRepository.CreateUser3rdParty(ctx, user3rdPartyObj)
|
||||
if err != nil {
|
||||
slog.Error("create user failed", "err", err)
|
||||
return errcode.HTTPErrCodeInternalError.Detail(fmt.Sprintf("Create user failed: %v", err))
|
||||
}
|
||||
err = s.Producer.Produce(ctx, enums.DaemonCodeRepository,
|
||||
api.DaemonCodeRepositoryPayload{User3rdPartyID: user3rdPartyObj.ID})
|
||||
if err != nil {
|
||||
slog.Error("publish sync code repository failed", "err", err, "user_id", user3rdPartyObj.UserID)
|
||||
return errcode.HTTPErrCodeInternalError.Detail("Publish sync code repository failed")
|
||||
}
|
||||
user3rdPartyObj.User = ptr.To(userSignedObj)
|
||||
return nil
|
||||
})
|
||||
@@ -306,6 +280,21 @@ func (s *service) Callback(ctx context.Context, provider enums.Provider, code st
|
||||
return user3rdPartyObj.User.ID, user3rdPartyObj.User.Username, ptr.To(user3rdPartyObj.User.Email), accessToken, refreshToken, nil
|
||||
}
|
||||
|
||||
// scmBaseURL returns the base URL of a provider from the scm configuration, falling back to
|
||||
// the hosted endpoint when the provider is not configured.
|
||||
func (s *service) scmBaseURL(provider enums.Provider) string {
|
||||
if scmProvider, ok := s.Config.ScmProvider(provider); ok && scmProvider.BaseURL != "" {
|
||||
return scmProvider.BaseURL
|
||||
}
|
||||
switch provider {
|
||||
case enums.ProviderGithub:
|
||||
return "https://github.com"
|
||||
case enums.ProviderGitlab:
|
||||
return "https://gitlab.com"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// tryGetUser resolves the user from the Authorization header.
|
||||
// For Basic auth: lookup by username + verify password.
|
||||
// For Bearer auth: validate the JWT token.
|
||||
|
||||
Reference in new issue
Block a user