From 56f709571184b1949b9bbf584eda259e3c4e04b5 Mon Sep 17 00:00:00 2001 From: Anbraten <6918444+anbraten@users.noreply.github.com> Date: Mon, 22 Apr 2024 15:47:57 +0200 Subject: [PATCH 01/42] login with selected forge --- server/api/login.go | 44 +++++++++++++++++------ server/services/manager.go | 21 +++++------ web/src/assets/locales/en.json | 2 +- web/src/compositions/useAuthentication.ts | 4 +-- web/src/views/Login.vue | 38 ++++++++++++++++++-- 5 files changed, 82 insertions(+), 27 deletions(-) diff --git a/server/api/login.go b/server/api/login.go index 349861ce11..fc3c228b25 100644 --- a/server/api/login.go +++ b/server/api/login.go @@ -17,7 +17,9 @@ package api import ( "encoding/base32" "errors" + "fmt" "net/http" + "strconv" "time" "github.com/gin-gonic/gin" @@ -33,22 +35,42 @@ import ( "go.woodpecker-ci.org/woodpecker/v2/shared/token" ) +func getForgeID(c *gin.Context) int64 { + _forgeID := c.Query("forge_id") + if _forgeID == "" { + _forgeID, _ = c.Cookie("forge_id") + } + if _forgeID == "" { + return 1 + } + forgeID, err := strconv.ParseInt(_forgeID, 10, 64) + if err != nil { + return 1 + } + return forgeID +} + func HandleLogin(c *gin.Context) { + forgeID := getForgeID(c) + if err := c.Request.FormValue("error"); err != "" { - c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/login/error?code="+err) + c.Redirect(http.StatusSeeOther, fmt.Sprintf("%s/login/error?code=%s&forge_id=%d", server.Config.Server.RootPath, err, forgeID)) } else { - c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/authorize") + c.Redirect(http.StatusSeeOther, fmt.Sprintf("%s/authorize?forge_id=%d", server.Config.Server.RootPath, forgeID)) } } func HandleAuth(c *gin.Context) { _store := store.FromContext(c) - _forge, err := server.Config.Services.Manager.ForgeMain() + forgeID := getForgeID(c) + + log.Debug().Msgf("trying to login with forge_id: %d", forgeID) + + _forge, err := server.Config.Services.Manager.ForgeByID(forgeID) if err != nil { _ = c.AbortWithError(http.StatusInternalServerError, err) return } - forgeID := int64(1) // TODO: replace with forge id when multiple forges are supported // when dealing with redirects, we may need to adjust the content type. I // cannot, however, remember why, so need to revisit this line. @@ -138,7 +160,7 @@ func HandleAuth(c *gin.Context) { ForgeID: u.ForgeID, } if err := _store.OrgCreate(org); err != nil { - log.Error().Err(err).Msgf("on user creation, could create org for user") + log.Error().Err(err).Msgf("on user creation, could not create org for user") } u.OrgID = org.ID } @@ -235,17 +257,16 @@ func GetLogout(c *gin.Context) { func GetLoginToken(c *gin.Context) { _store := store.FromContext(c) - _forge, err := server.Config.Services.Manager.ForgeMain() // TODO: get selected forge from auth request + in := &tokenPayload{} + err := c.Bind(in) if err != nil { - log.Error().Err(err).Msg("Cannot get main forge") - c.AbortWithStatus(http.StatusInternalServerError) + _ = c.AbortWithError(http.StatusBadRequest, err) return } - in := &tokenPayload{} - err = c.Bind(in) + _forge, err := server.Config.Services.Manager.ForgeByID(in.ForgeID) if err != nil { - _ = c.AbortWithError(http.StatusBadRequest, err) + _ = c.AbortWithError(http.StatusInternalServerError, err) return } @@ -279,4 +300,5 @@ type tokenPayload struct { Access string `json:"access_token,omitempty"` Refresh string `json:"refresh_token,omitempty"` Expires int64 `json:"expires_in,omitempty"` + ForgeID int64 `json:"forge_id,omitempty"` } diff --git a/server/services/manager.go b/server/services/manager.go index ff281c40a2..26859e58ac 100644 --- a/server/services/manager.go +++ b/server/services/manager.go @@ -46,6 +46,7 @@ type Manager interface { EnvironmentService() environment.Service ForgeFromRepo(repo *model.Repo) (forge.Forge, error) ForgeFromUser(user *model.User) (forge.Forge, error) + ForgeByID(forgeID int64) (forge.Forge, error) ForgeMain() (forge.Forge, error) } @@ -115,24 +116,20 @@ func (m *manager) EnvironmentService() environment.Service { } func (m *manager) ForgeFromRepo(repo *model.Repo) (forge.Forge, error) { - return m.getForgeByID(repo.ForgeID) + return m.ForgeByID(repo.ForgeID) } func (m *manager) ForgeFromUser(user *model.User) (forge.Forge, error) { - return m.getForgeByID(user.ForgeID) + return m.ForgeByID(user.ForgeID) } -func (m *manager) ForgeMain() (forge.Forge, error) { - return m.getForgeByID(1) // main forge is always 1 and is configured via environment variables -} - -func (m *manager) getForgeByID(id int64) (forge.Forge, error) { - item := m.forgeCache.Get(id) +func (m *manager) ForgeByID(forgeID int64) (forge.Forge, error) { + item := m.forgeCache.Get(forgeID) if item != nil && !item.IsExpired() { return item.Value(), nil } - forgeModel, err := m.store.ForgeGet(id) + forgeModel, err := m.store.ForgeGet(forgeID) if err != nil { return nil, err } @@ -142,7 +139,11 @@ func (m *manager) getForgeByID(id int64) (forge.Forge, error) { return nil, err } - m.forgeCache.Set(id, forge, forgeCacheTTL) + m.forgeCache.Set(forgeID, forge, forgeCacheTTL) return forge, nil } + +func (m *manager) ForgeMain() (forge.Forge, error) { + return m.ForgeByID(1) // main forge is always 1 and is configured via environment variables +} diff --git a/web/src/assets/locales/en.json b/web/src/assets/locales/en.json index 83e98c2d81..008b6b9e4a 100644 --- a/web/src/assets/locales/en.json +++ b/web/src/assets/locales/en.json @@ -1,6 +1,6 @@ { "cancel": "Cancel", - "login": "Login", + "login_with": "Login with {forge}", "welcome": "Welcome to Woodpecker", "repos": "Repos", "repositories": "Repositories", diff --git a/web/src/compositions/useAuthentication.ts b/web/src/compositions/useAuthentication.ts index 287942eed5..8ed98f42da 100644 --- a/web/src/compositions/useAuthentication.ts +++ b/web/src/compositions/useAuthentication.ts @@ -7,11 +7,11 @@ export default () => user: useConfig().user, - authenticate(url?: string) { + authenticate(url?: string, forgeId?: number) { if (url) { const config = useUserConfig(); config.setUserConfig('redirectUrl', url); } - window.location.href = `${useConfig().rootPath}/login`; + window.location.href = `${useConfig().rootPath}/login?forge_id=${forgeId}`; }, }) as const; diff --git a/web/src/views/Login.vue b/web/src/views/Login.vue index d6fcec0b98..dbb8c8e572 100644 --- a/web/src/views/Login.vue +++ b/web/src/views/Login.vue @@ -11,7 +11,11 @@

{{ $t('welcome') }}

- +
+ +
@@ -32,9 +36,37 @@ const authentication = useAuthentication(); const errorMessage = ref(); const i18n = useI18n(); -function doLogin() { +type Forge = { + id: number; + url: string; + type: string; +}; + +const forges = ref([ + { + id: 1, + url: 'http://localhost:3000/', + type: 'gitea', + }, + { + id: 2, + url: '', + type: 'github', + }, +]); + +function getHostFromUrl(forge: Forge) { + if (!forge.url) { + return forge.type.charAt(0).toUpperCase() + forge.type.slice(1); + } + + const url = new URL(forge.url); + return url.hostname; +} + +function doLogin(forgeId?: number) { const url = typeof route.query.url === 'string' ? route.query.url : ''; - authentication.authenticate(url); + authentication.authenticate(url, forgeId); } const authErrorMessages = { From 29f8a31730aa716e5fb6c122b3865017ca52faee Mon Sep 17 00:00:00 2001 From: Anbraten <6918444+anbraten@users.noreply.github.com> Date: Tue, 23 Apr 2024 10:41:37 +0200 Subject: [PATCH 02/42] allow to select forge on login --- server/api/login.go | 24 ++++++++++++++----- server/api/repo.go | 8 +++---- server/api/user.go | 4 ++-- server/api/users.go | 20 +++++++++++++--- server/forge/bitbucket/bitbucket.go | 2 +- .../bitbucketdatacenter.go | 2 +- server/forge/gitea/gitea.go | 2 +- server/forge/github/github.go | 2 +- server/forge/gitlab/gitlab.go | 2 +- server/forge/types/oauth.go | 1 + server/model/user.go | 4 ++-- server/router/middleware/session/user.go | 6 ++++- server/store/datastore/user.go | 18 ++++---------- server/store/store.go | 6 ++--- server/web/config.go | 3 ++- web/src/router.ts | 1 + 16 files changed, 65 insertions(+), 40 deletions(-) diff --git a/server/api/login.go b/server/api/login.go index fc3c228b25..03ae89b8e5 100644 --- a/server/api/login.go +++ b/server/api/login.go @@ -62,6 +62,8 @@ func HandleLogin(c *gin.Context) { func HandleAuth(c *gin.Context) { _store := store.FromContext(c) + + // TODO: get forge-id in state forgeID := getForgeID(c) log.Debug().Msgf("trying to login with forge_id: %d", forgeID) @@ -76,7 +78,11 @@ func HandleAuth(c *gin.Context) { // cannot, however, remember why, so need to revisit this line. c.Writer.Header().Del("Content-Type") + stateID := "woodpecker-state-123" // TODO: generate a random id + // TODO: add selected forgeID to content of the state + tmpuser, redirectURL, err := _forge.Login(c, &forge_types.OAuthRequest{ + State: stateID, Error: c.Request.FormValue("error"), ErrorURI: c.Request.FormValue("error_uri"), ErrorDescription: c.Request.FormValue("error_description"), @@ -87,14 +93,19 @@ func HandleAuth(c *gin.Context) { c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/login?error=oauth_error") return } + // The user is not authorized yet -> redirect if tmpuser == nil { + // TODO: set forge-id in state + httputil.SetCookie(c.Writer, c.Request, "forge_id", fmt.Sprintf("%d", forgeID)) http.Redirect(c.Writer, c.Request, redirectURL, http.StatusSeeOther) return } + // TODO: set forge-id in state + httputil.DelCookie(c.Writer, c.Request, "forge_id") // get the user from the database - u, err := _store.GetUserRemoteID(tmpuser.ForgeRemoteID, tmpuser.Login) + u, err := _store.GetUserRemoteID(forgeID, tmpuser.ForgeRemoteID) if err != nil && !errors.Is(err, types.RecordNotExist) { _ = c.AbortWithError(http.StatusInternalServerError, err) return @@ -207,7 +218,7 @@ func HandleAuth(c *gin.Context) { } exp := time.Now().Add(server.Config.Server.SessionExpires).Unix() - tokenString, err := token.New(token.SessToken, u.Login).SignExpires(u.Hash, exp) + tokenString, err := token.New(token.SessToken, strconv.FormatInt(u.ID, 10)).SignExpires(u.Hash, exp) if err != nil { log.Error().Msgf("cannot create token for %s", u.Login) c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/login?error=internal_error") @@ -264,7 +275,9 @@ func GetLoginToken(c *gin.Context) { return } - _forge, err := server.Config.Services.Manager.ForgeByID(in.ForgeID) + forgeID := int64(1) // TODO: use correct forge + + _forge, err := server.Config.Services.Manager.ForgeByID(forgeID) if err != nil { _ = c.AbortWithError(http.StatusInternalServerError, err) return @@ -276,14 +289,14 @@ func GetLoginToken(c *gin.Context) { return } - user, err := _store.GetUserLogin(login) + user, err := _store.GetUserLogin(forgeID, login) if err != nil { handleDBError(c, err) return } exp := time.Now().Add(server.Config.Server.SessionExpires).Unix() - newToken := token.New(token.SessToken, user.Login) + newToken := token.New(token.SessToken, strconv.FormatInt(user.ID, 10)) tokenStr, err := newToken.SignExpires(user.Hash, exp) if err != nil { _ = c.AbortWithError(http.StatusInternalServerError, err) @@ -300,5 +313,4 @@ type tokenPayload struct { Access string `json:"access_token,omitempty"` Refresh string `json:"refresh_token,omitempty"` Expires int64 `json:"expires_in,omitempty"` - ForgeID int64 `json:"forge_id,omitempty"` } diff --git a/server/api/repo.go b/server/api/repo.go index 6d03dcede3..77bb890cc2 100644 --- a/server/api/repo.go +++ b/server/api/repo.go @@ -118,7 +118,7 @@ func PostRepo(c *gin.Context) { } // creates the jwt token used to verify the repository - t := token.New(token.HookToken, repo.FullName) + t := token.New(token.HookToken, strconv.FormatInt(repo.ID, 10)) sig, err := t.Sign(repo.Hash) if err != nil { msg := "could not generate new jwt token." @@ -520,7 +520,7 @@ func MoveRepo(c *gin.Context) { } // creates the jwt token used to verify the repository - t := token.New(token.HookToken, repo.FullName) + t := token.New(token.HookToken, strconv.FormatInt(repo.ID, 10)) sig, err := t.Sign(repo.Hash) if err != nil { c.String(http.StatusInternalServerError, err.Error()) @@ -536,7 +536,7 @@ func MoveRepo(c *gin.Context) { ) if err := _forge.Deactivate(c, user, repo, host); err != nil { - log.Trace().Err(err).Msgf("deactivate repo '%s' for move to activate later, got an error", repo.FullName) + log.Trace().Err(err).Msgf("deactivate repo '%s' for move to activate later, got an error", strconv.FormatInt(repo.ID, 10)) } if err := _forge.Activate(c, user, repo, hookURL); err != nil { c.String(http.StatusInternalServerError, err.Error()) @@ -620,7 +620,7 @@ func repairRepo(c *gin.Context, repo *model.Repo, withPerms, skipOnErr bool) { } // creates the jwt token used to verify the repository - t := token.New(token.HookToken, repo.FullName) + t := token.New(token.HookToken, strconv.FormatInt(repo.ID, 10)) sig, err := t.Sign(repo.Hash) if err != nil { c.String(http.StatusInternalServerError, err.Error()) diff --git a/server/api/user.go b/server/api/user.go index d24849bf39..8cab2bbf1a 100644 --- a/server/api/user.go +++ b/server/api/user.go @@ -153,7 +153,7 @@ func GetRepos(c *gin.Context) { // @Param Authorization header string true "Insert your personal access token" default(Bearer ) func PostToken(c *gin.Context) { user := session.User(c) - tokenString, err := token.New(token.UserToken, user.Login).Sign(user.Hash) + tokenString, err := token.New(token.UserToken, strconv.FormatInt(user.ID, 10)).Sign(user.Hash) if err != nil { _ = c.AbortWithError(http.StatusInternalServerError, err) return @@ -182,7 +182,7 @@ func DeleteToken(c *gin.Context) { return } - tokenString, err := token.New(token.UserToken, user.Login).Sign(user.Hash) + tokenString, err := token.New(token.UserToken, strconv.FormatInt(user.ID, 10)).Sign(user.Hash) if err != nil { _ = c.AbortWithError(http.StatusInternalServerError, err) return diff --git a/server/api/users.go b/server/api/users.go index c35339eefa..59faf04df1 100644 --- a/server/api/users.go +++ b/server/api/users.go @@ -17,6 +17,7 @@ package api import ( "encoding/base32" "net/http" + "strconv" "github.com/gin-gonic/gin" "github.com/gorilla/securecookie" @@ -57,7 +58,8 @@ func GetUsers(c *gin.Context) { // @Param Authorization header string true "Insert your personal access token" default(Bearer ) // @Param login path string true "the user's login name" func GetUser(c *gin.Context) { - user, err := store.FromContext(c).GetUserLogin(c.Param("login")) + forgeID := int64(1) // TODO: use correct forge + user, err := getUserFromLogin(c, forgeID) if err != nil { handleDBError(c, err) return @@ -87,7 +89,8 @@ func PatchUser(c *gin.Context) { return } - user, err := _store.GetUserLogin(c.Param("login")) + forgeID := int64(1) // TODO: use correct forge + user, err := getUserFromLogin(c, forgeID) if err != nil { handleDBError(c, err) return @@ -159,7 +162,8 @@ func PostUser(c *gin.Context) { func DeleteUser(c *gin.Context) { _store := store.FromContext(c) - user, err := _store.GetUserLogin(c.Param("login")) + forgeID := int64(1) // TODO: use correct forge + user, err := getUserFromLogin(c, forgeID) if err != nil { handleDBError(c, err) return @@ -170,3 +174,13 @@ func DeleteUser(c *gin.Context) { } c.Status(http.StatusNoContent) } + +func getUserFromLogin(c *gin.Context, forgeID int64) (*model.User, error) { + _store := store.FromContext(c) + + if userID, err := strconv.ParseInt(c.Param("login"), 10, 64); err == nil { + return _store.GetUser(userID) + } + + return _store.GetUserLogin(forgeID, c.Param("login")) +} diff --git a/server/forge/bitbucket/bitbucket.go b/server/forge/bitbucket/bitbucket.go index d3c96920e6..05d5c7dae6 100644 --- a/server/forge/bitbucket/bitbucket.go +++ b/server/forge/bitbucket/bitbucket.go @@ -81,7 +81,7 @@ func (c *config) URL() string { // Bitbucket account details are returned when the user is successfully authenticated. func (c *config) Login(ctx context.Context, req *forge_types.OAuthRequest) (*model.User, string, error) { config := c.newOAuth2Config() - redirectURL := config.AuthCodeURL("woodpecker") + redirectURL := config.AuthCodeURL(req.State) // get the OAuth errors if req.Error != "" { diff --git a/server/forge/bitbucketdatacenter/bitbucketdatacenter.go b/server/forge/bitbucketdatacenter/bitbucketdatacenter.go index b881a701db..e9c2234a5a 100644 --- a/server/forge/bitbucketdatacenter/bitbucketdatacenter.go +++ b/server/forge/bitbucketdatacenter/bitbucketdatacenter.go @@ -94,7 +94,7 @@ func (c *client) Login(ctx context.Context, req *forge_types.OAuthRequest) (*mod config := c.newOAuth2Config() // TODO: Add proper state and pkce... - redirectURL := config.AuthCodeURL("woodpecker") + redirectURL := config.AuthCodeURL(req.State) if req.Error != "" { return nil, redirectURL, &forge_types.AuthError{ diff --git a/server/forge/gitea/gitea.go b/server/forge/gitea/gitea.go index 964acb1cb9..4bfbdfc65f 100644 --- a/server/forge/gitea/gitea.go +++ b/server/forge/gitea/gitea.go @@ -115,7 +115,7 @@ func (c *Gitea) oauth2Config(ctx context.Context) (*oauth2.Config, context.Conte // Gitea account details are returned when the user is successfully authenticated. func (c *Gitea) Login(ctx context.Context, req *forge_types.OAuthRequest) (*model.User, string, error) { config, oauth2Ctx := c.oauth2Config(ctx) - redirectURL := config.AuthCodeURL("woodpecker") + redirectURL := config.AuthCodeURL(req.State) // check the OAuth errors if req.Error != "" { diff --git a/server/forge/github/github.go b/server/forge/github/github.go index 3e0ae25dc5..7102bcb57e 100644 --- a/server/forge/github/github.go +++ b/server/forge/github/github.go @@ -97,7 +97,7 @@ func (c *client) URL() string { // Login authenticates the session and returns the forge user details. func (c *client) Login(ctx context.Context, req *forge_types.OAuthRequest) (*model.User, string, error) { config := c.newConfig() - redirectURL := config.AuthCodeURL("woodpecker") + redirectURL := config.AuthCodeURL(req.State) // check the OAuth errors if req.Error != "" { diff --git a/server/forge/gitlab/gitlab.go b/server/forge/gitlab/gitlab.go index 4f05659a59..a80bee7d22 100644 --- a/server/forge/gitlab/gitlab.go +++ b/server/forge/gitlab/gitlab.go @@ -107,7 +107,7 @@ func (g *GitLab) oauth2Config(ctx context.Context) (*oauth2.Config, context.Cont // forge user details. func (g *GitLab) Login(ctx context.Context, req *forge_types.OAuthRequest) (*model.User, string, error) { config, oauth2Ctx := g.oauth2Config(ctx) - redirectURL := config.AuthCodeURL("woodpecker") + redirectURL := config.AuthCodeURL(req.State) // check the OAuth errors if req.Error != "" { diff --git a/server/forge/types/oauth.go b/server/forge/types/oauth.go index fba870569c..0897d7aa9b 100644 --- a/server/forge/types/oauth.go +++ b/server/forge/types/oauth.go @@ -15,6 +15,7 @@ package types type OAuthRequest struct { + State string Error string ErrorURI string ErrorDescription string diff --git a/server/model/user.go b/server/model/user.go index 578d6caf28..9323d024dc 100644 --- a/server/model/user.go +++ b/server/model/user.go @@ -34,14 +34,14 @@ type User struct { // required: true ID int64 `json:"id" xorm:"pk autoincr 'user_id'"` - ForgeID int64 `json:"forge_id,omitempty" xorm:"forge_id"` + ForgeID int64 `json:"forge_id,omitempty" xorm:"UNIQUE(s) 'forge_id'"` ForgeRemoteID ForgeRemoteID `json:"-" xorm:"forge_remote_id"` // Login is the username for this user. // // required: true - Login string `json:"login" xorm:"UNIQUE 'user_login'"` + Login string `json:"login" xorm:"UNIQUE(s) 'user_login'"` // Token is the oauth2 token. Token string `json:"-" xorm:"TEXT 'user_token'"` diff --git a/server/router/middleware/session/user.go b/server/router/middleware/session/user.go index d1b1507ef7..1b96b6864e 100644 --- a/server/router/middleware/session/user.go +++ b/server/router/middleware/session/user.go @@ -45,7 +45,11 @@ func SetUser() gin.HandlerFunc { t, err := token.ParseRequest(c.Request, func(t *token.Token) (string, error) { var err error - user, err = store.FromContext(c).GetUserLogin(t.Text) + userID, err := strconv.ParseInt(t.Text, 10, 64) + if err != nil { + return "", err + } + user, err = store.FromContext(c).GetUser(userID) return user.Hash, err }) if err == nil { diff --git a/server/store/datastore/user.go b/server/store/datastore/user.go index 39f09af75e..81b461623f 100644 --- a/server/store/datastore/user.go +++ b/server/store/datastore/user.go @@ -15,8 +15,6 @@ package datastore import ( - "xorm.io/xorm" - "go.woodpecker-ci.org/woodpecker/v2/server/model" ) @@ -25,23 +23,17 @@ func (s storage) GetUser(id int64) (*model.User, error) { return user, wrapGet(s.engine.ID(id).Get(user)) } -func (s storage) GetUserRemoteID(remoteID model.ForgeRemoteID, login string) (*model.User, error) { +func (s storage) GetUserRemoteID(forgeID int64, remoteID model.ForgeRemoteID) (*model.User, error) { sess := s.engine.NewSession() user := new(model.User) - err := wrapGet(sess.Where("forge_remote_id = ?", remoteID).Get(user)) - if err != nil { - user, err = s.getUserLogin(sess, login) - } + err := wrapGet(sess.Where("forge_id=? AND forge_remote_id = ?", forgeID, remoteID).Get(user)) return user, err } -func (s storage) GetUserLogin(login string) (*model.User, error) { - return s.getUserLogin(s.engine.NewSession(), login) -} - -func (s storage) getUserLogin(sess *xorm.Session, login string) (*model.User, error) { +func (s storage) GetUserLogin(forgeID int64, login string) (*model.User, error) { + sess := s.engine.NewSession() user := new(model.User) - return user, wrapGet(sess.Where("user_login=?", login).Get(user)) + return user, wrapGet(sess.Where("forge_id=? AND user_login=?", forgeID, login).Get(user)) } func (s storage) GetUserList(p *model.ListOptions) ([]*model.User, error) { diff --git a/server/store/store.go b/server/store/store.go index 6ba76573c4..7772e7c9c6 100644 --- a/server/store/store.go +++ b/server/store/store.go @@ -26,10 +26,10 @@ type Store interface { // Users // GetUser gets a user by unique ID. GetUser(int64) (*model.User, error) - // GetUserRemoteID gets a user by remote ID with fallback to login name. - GetUserRemoteID(model.ForgeRemoteID, string) (*model.User, error) + // GetUserRemoteID gets a user by remote ID. + GetUserRemoteID(forgeID int64, remoteID model.ForgeRemoteID) (*model.User, error) // GetUserLogin gets a user by unique Login name. - GetUserLogin(string) (*model.User, error) + GetUserLogin(forgeID int64, login string) (*model.User, error) // GetUserList gets a list of all users in the system. GetUserList(p *model.ListOptions) ([]*model.User, error) // GetUserCount gets a count of all users in the system. diff --git a/server/web/config.go b/server/web/config.go index 99e78461e0..987ef128d8 100644 --- a/server/web/config.go +++ b/server/web/config.go @@ -17,6 +17,7 @@ package web import ( "encoding/json" "net/http" + "strconv" "text/template" "github.com/gin-gonic/gin" @@ -35,7 +36,7 @@ func Config(c *gin.Context) { if user != nil { csrf, _ = token.New( token.CsrfToken, - user.Login, + strconv.FormatInt(user.ID, 10), ).Sign(user.Hash) } diff --git a/web/src/router.ts b/web/src/router.ts index e3540479c6..7053006ccc 100644 --- a/web/src/router.ts +++ b/web/src/router.ts @@ -202,6 +202,7 @@ router.beforeEach(async (to, _, next) => { if (redirectUrl !== '') { config.setUserConfig('redirectUrl', ''); next(redirectUrl); + return; } const authentication = useAuthentication(); From e1a1c246f4e11a460683f2ed09b219a245e21956 Mon Sep 17 00:00:00 2001 From: Anbraten <6918444+anbraten@users.noreply.github.com> Date: Sun, 12 May 2024 08:19:53 +0200 Subject: [PATCH 03/42] undo --- server/api/users.go | 20 ++-------- server/forge/bitbucket/bitbucket.go | 2 +- .../bitbucketdatacenter.go | 2 +- server/forge/gitea/gitea.go | 2 +- server/forge/github/github.go | 2 +- server/forge/gitlab/gitlab.go | 2 +- server/forge/types/oauth.go | 1 - server/model/user.go | 4 +- server/services/manager.go | 21 +++++----- server/store/datastore/user.go | 18 ++++++--- server/store/store.go | 6 +-- web/src/assets/locales/en.json | 2 +- web/src/compositions/useAuthentication.ts | 4 +- web/src/router.ts | 1 - web/src/views/Login.vue | 38 ++----------------- 15 files changed, 42 insertions(+), 83 deletions(-) diff --git a/server/api/users.go b/server/api/users.go index bd9829922b..1a591d1271 100644 --- a/server/api/users.go +++ b/server/api/users.go @@ -17,7 +17,6 @@ package api import ( "encoding/base32" "net/http" - "strconv" "github.com/gin-gonic/gin" "github.com/gorilla/securecookie" @@ -58,8 +57,7 @@ func GetUsers(c *gin.Context) { // @Param Authorization header string true "Insert your personal access token" default(Bearer ) // @Param login path string true "the user's login name" func GetUser(c *gin.Context) { - forgeID := int64(1) // TODO: use correct forge - user, err := getUserFromLogin(c, forgeID) + user, err := store.FromContext(c).GetUserLogin(c.Param("login")) if err != nil { handleDBError(c, err) return @@ -89,8 +87,7 @@ func PatchUser(c *gin.Context) { return } - forgeID := int64(1) // TODO: use correct forge - user, err := getUserFromLogin(c, forgeID) + user, err := _store.GetUserLogin(c.Param("login")) if err != nil { handleDBError(c, err) return @@ -162,8 +159,7 @@ func PostUser(c *gin.Context) { func DeleteUser(c *gin.Context) { _store := store.FromContext(c) - forgeID := int64(1) // TODO: use correct forge - user, err := getUserFromLogin(c, forgeID) + user, err := _store.GetUserLogin(c.Param("login")) if err != nil { handleDBError(c, err) return @@ -174,13 +170,3 @@ func DeleteUser(c *gin.Context) { } c.Status(http.StatusNoContent) } - -func getUserFromLogin(c *gin.Context, forgeID int64) (*model.User, error) { - _store := store.FromContext(c) - - if userID, err := strconv.ParseInt(c.Param("login"), 10, 64); err == nil { - return _store.GetUser(userID) - } - - return _store.GetUserLogin(forgeID, c.Param("login")) -} diff --git a/server/forge/bitbucket/bitbucket.go b/server/forge/bitbucket/bitbucket.go index 05d5c7dae6..d3c96920e6 100644 --- a/server/forge/bitbucket/bitbucket.go +++ b/server/forge/bitbucket/bitbucket.go @@ -81,7 +81,7 @@ func (c *config) URL() string { // Bitbucket account details are returned when the user is successfully authenticated. func (c *config) Login(ctx context.Context, req *forge_types.OAuthRequest) (*model.User, string, error) { config := c.newOAuth2Config() - redirectURL := config.AuthCodeURL(req.State) + redirectURL := config.AuthCodeURL("woodpecker") // get the OAuth errors if req.Error != "" { diff --git a/server/forge/bitbucketdatacenter/bitbucketdatacenter.go b/server/forge/bitbucketdatacenter/bitbucketdatacenter.go index 312b90d923..58b0c6d372 100644 --- a/server/forge/bitbucketdatacenter/bitbucketdatacenter.go +++ b/server/forge/bitbucketdatacenter/bitbucketdatacenter.go @@ -94,7 +94,7 @@ func (c *client) Login(ctx context.Context, req *forge_types.OAuthRequest) (*mod config := c.newOAuth2Config() // TODO: Add proper state and pkce... - redirectURL := config.AuthCodeURL(req.State) + redirectURL := config.AuthCodeURL("woodpecker") if req.Error != "" { return nil, redirectURL, &forge_types.AuthError{ diff --git a/server/forge/gitea/gitea.go b/server/forge/gitea/gitea.go index 4bfbdfc65f..964acb1cb9 100644 --- a/server/forge/gitea/gitea.go +++ b/server/forge/gitea/gitea.go @@ -115,7 +115,7 @@ func (c *Gitea) oauth2Config(ctx context.Context) (*oauth2.Config, context.Conte // Gitea account details are returned when the user is successfully authenticated. func (c *Gitea) Login(ctx context.Context, req *forge_types.OAuthRequest) (*model.User, string, error) { config, oauth2Ctx := c.oauth2Config(ctx) - redirectURL := config.AuthCodeURL(req.State) + redirectURL := config.AuthCodeURL("woodpecker") // check the OAuth errors if req.Error != "" { diff --git a/server/forge/github/github.go b/server/forge/github/github.go index 7102bcb57e..3e0ae25dc5 100644 --- a/server/forge/github/github.go +++ b/server/forge/github/github.go @@ -97,7 +97,7 @@ func (c *client) URL() string { // Login authenticates the session and returns the forge user details. func (c *client) Login(ctx context.Context, req *forge_types.OAuthRequest) (*model.User, string, error) { config := c.newConfig() - redirectURL := config.AuthCodeURL(req.State) + redirectURL := config.AuthCodeURL("woodpecker") // check the OAuth errors if req.Error != "" { diff --git a/server/forge/gitlab/gitlab.go b/server/forge/gitlab/gitlab.go index a80bee7d22..4f05659a59 100644 --- a/server/forge/gitlab/gitlab.go +++ b/server/forge/gitlab/gitlab.go @@ -107,7 +107,7 @@ func (g *GitLab) oauth2Config(ctx context.Context) (*oauth2.Config, context.Cont // forge user details. func (g *GitLab) Login(ctx context.Context, req *forge_types.OAuthRequest) (*model.User, string, error) { config, oauth2Ctx := g.oauth2Config(ctx) - redirectURL := config.AuthCodeURL(req.State) + redirectURL := config.AuthCodeURL("woodpecker") // check the OAuth errors if req.Error != "" { diff --git a/server/forge/types/oauth.go b/server/forge/types/oauth.go index 0897d7aa9b..fba870569c 100644 --- a/server/forge/types/oauth.go +++ b/server/forge/types/oauth.go @@ -15,7 +15,6 @@ package types type OAuthRequest struct { - State string Error string ErrorURI string ErrorDescription string diff --git a/server/model/user.go b/server/model/user.go index 9323d024dc..578d6caf28 100644 --- a/server/model/user.go +++ b/server/model/user.go @@ -34,14 +34,14 @@ type User struct { // required: true ID int64 `json:"id" xorm:"pk autoincr 'user_id'"` - ForgeID int64 `json:"forge_id,omitempty" xorm:"UNIQUE(s) 'forge_id'"` + ForgeID int64 `json:"forge_id,omitempty" xorm:"forge_id"` ForgeRemoteID ForgeRemoteID `json:"-" xorm:"forge_remote_id"` // Login is the username for this user. // // required: true - Login string `json:"login" xorm:"UNIQUE(s) 'user_login'"` + Login string `json:"login" xorm:"UNIQUE 'user_login'"` // Token is the oauth2 token. Token string `json:"-" xorm:"TEXT 'user_token'"` diff --git a/server/services/manager.go b/server/services/manager.go index 26859e58ac..ff281c40a2 100644 --- a/server/services/manager.go +++ b/server/services/manager.go @@ -46,7 +46,6 @@ type Manager interface { EnvironmentService() environment.Service ForgeFromRepo(repo *model.Repo) (forge.Forge, error) ForgeFromUser(user *model.User) (forge.Forge, error) - ForgeByID(forgeID int64) (forge.Forge, error) ForgeMain() (forge.Forge, error) } @@ -116,20 +115,24 @@ func (m *manager) EnvironmentService() environment.Service { } func (m *manager) ForgeFromRepo(repo *model.Repo) (forge.Forge, error) { - return m.ForgeByID(repo.ForgeID) + return m.getForgeByID(repo.ForgeID) } func (m *manager) ForgeFromUser(user *model.User) (forge.Forge, error) { - return m.ForgeByID(user.ForgeID) + return m.getForgeByID(user.ForgeID) } -func (m *manager) ForgeByID(forgeID int64) (forge.Forge, error) { - item := m.forgeCache.Get(forgeID) +func (m *manager) ForgeMain() (forge.Forge, error) { + return m.getForgeByID(1) // main forge is always 1 and is configured via environment variables +} + +func (m *manager) getForgeByID(id int64) (forge.Forge, error) { + item := m.forgeCache.Get(id) if item != nil && !item.IsExpired() { return item.Value(), nil } - forgeModel, err := m.store.ForgeGet(forgeID) + forgeModel, err := m.store.ForgeGet(id) if err != nil { return nil, err } @@ -139,11 +142,7 @@ func (m *manager) ForgeByID(forgeID int64) (forge.Forge, error) { return nil, err } - m.forgeCache.Set(forgeID, forge, forgeCacheTTL) + m.forgeCache.Set(id, forge, forgeCacheTTL) return forge, nil } - -func (m *manager) ForgeMain() (forge.Forge, error) { - return m.ForgeByID(1) // main forge is always 1 and is configured via environment variables -} diff --git a/server/store/datastore/user.go b/server/store/datastore/user.go index 81b461623f..39f09af75e 100644 --- a/server/store/datastore/user.go +++ b/server/store/datastore/user.go @@ -15,6 +15,8 @@ package datastore import ( + "xorm.io/xorm" + "go.woodpecker-ci.org/woodpecker/v2/server/model" ) @@ -23,17 +25,23 @@ func (s storage) GetUser(id int64) (*model.User, error) { return user, wrapGet(s.engine.ID(id).Get(user)) } -func (s storage) GetUserRemoteID(forgeID int64, remoteID model.ForgeRemoteID) (*model.User, error) { +func (s storage) GetUserRemoteID(remoteID model.ForgeRemoteID, login string) (*model.User, error) { sess := s.engine.NewSession() user := new(model.User) - err := wrapGet(sess.Where("forge_id=? AND forge_remote_id = ?", forgeID, remoteID).Get(user)) + err := wrapGet(sess.Where("forge_remote_id = ?", remoteID).Get(user)) + if err != nil { + user, err = s.getUserLogin(sess, login) + } return user, err } -func (s storage) GetUserLogin(forgeID int64, login string) (*model.User, error) { - sess := s.engine.NewSession() +func (s storage) GetUserLogin(login string) (*model.User, error) { + return s.getUserLogin(s.engine.NewSession(), login) +} + +func (s storage) getUserLogin(sess *xorm.Session, login string) (*model.User, error) { user := new(model.User) - return user, wrapGet(sess.Where("forge_id=? AND user_login=?", forgeID, login).Get(user)) + return user, wrapGet(sess.Where("user_login=?", login).Get(user)) } func (s storage) GetUserList(p *model.ListOptions) ([]*model.User, error) { diff --git a/server/store/store.go b/server/store/store.go index acc7bd71ef..2a007e487c 100644 --- a/server/store/store.go +++ b/server/store/store.go @@ -26,10 +26,10 @@ type Store interface { // Users // GetUser gets a user by unique ID. GetUser(int64) (*model.User, error) - // GetUserRemoteID gets a user by remote ID. - GetUserRemoteID(forgeID int64, remoteID model.ForgeRemoteID) (*model.User, error) + // GetUserRemoteID gets a user by remote ID with fallback to login name. + GetUserRemoteID(model.ForgeRemoteID, string) (*model.User, error) // GetUserLogin gets a user by unique Login name. - GetUserLogin(forgeID int64, login string) (*model.User, error) + GetUserLogin(string) (*model.User, error) // GetUserList gets a list of all users in the system. GetUserList(p *model.ListOptions) ([]*model.User, error) // GetUserCount gets a count of all users in the system. diff --git a/web/src/assets/locales/en.json b/web/src/assets/locales/en.json index 94f5380195..ed881f88fc 100644 --- a/web/src/assets/locales/en.json +++ b/web/src/assets/locales/en.json @@ -1,6 +1,6 @@ { "cancel": "Cancel", - "login_with": "Login with {forge}", + "login": "Login", "welcome": "Welcome to Woodpecker", "repos": "Repos", "repositories": "Repositories", diff --git a/web/src/compositions/useAuthentication.ts b/web/src/compositions/useAuthentication.ts index 8ed98f42da..287942eed5 100644 --- a/web/src/compositions/useAuthentication.ts +++ b/web/src/compositions/useAuthentication.ts @@ -7,11 +7,11 @@ export default () => user: useConfig().user, - authenticate(url?: string, forgeId?: number) { + authenticate(url?: string) { if (url) { const config = useUserConfig(); config.setUserConfig('redirectUrl', url); } - window.location.href = `${useConfig().rootPath}/login?forge_id=${forgeId}`; + window.location.href = `${useConfig().rootPath}/login`; }, }) as const; diff --git a/web/src/router.ts b/web/src/router.ts index 7053006ccc..e3540479c6 100644 --- a/web/src/router.ts +++ b/web/src/router.ts @@ -202,7 +202,6 @@ router.beforeEach(async (to, _, next) => { if (redirectUrl !== '') { config.setUserConfig('redirectUrl', ''); next(redirectUrl); - return; } const authentication = useAuthentication(); diff --git a/web/src/views/Login.vue b/web/src/views/Login.vue index dbb8c8e572..d6fcec0b98 100644 --- a/web/src/views/Login.vue +++ b/web/src/views/Login.vue @@ -11,11 +11,7 @@

{{ $t('welcome') }}

-
- -
+
@@ -36,37 +32,9 @@ const authentication = useAuthentication(); const errorMessage = ref(); const i18n = useI18n(); -type Forge = { - id: number; - url: string; - type: string; -}; - -const forges = ref([ - { - id: 1, - url: 'http://localhost:3000/', - type: 'gitea', - }, - { - id: 2, - url: '', - type: 'github', - }, -]); - -function getHostFromUrl(forge: Forge) { - if (!forge.url) { - return forge.type.charAt(0).toUpperCase() + forge.type.slice(1); - } - - const url = new URL(forge.url); - return url.hostname; -} - -function doLogin(forgeId?: number) { +function doLogin() { const url = typeof route.query.url === 'string' ? route.query.url : ''; - authentication.authenticate(url, forgeId); + authentication.authenticate(url); } const authErrorMessages = { From 5645696bb9db05ddfb5b2fbdf16035208684bb27 Mon Sep 17 00:00:00 2001 From: Anbraten <6918444+anbraten@users.noreply.github.com> Date: Sun, 12 May 2024 08:25:45 +0200 Subject: [PATCH 04/42] undo --- server/api/login.go | 57 ++++++++++----------------------------------- 1 file changed, 12 insertions(+), 45 deletions(-) diff --git a/server/api/login.go b/server/api/login.go index 03ae89b8e5..5cafc85bfd 100644 --- a/server/api/login.go +++ b/server/api/login.go @@ -17,7 +17,6 @@ package api import ( "encoding/base32" "errors" - "fmt" "net/http" "strconv" "time" @@ -35,54 +34,28 @@ import ( "go.woodpecker-ci.org/woodpecker/v2/shared/token" ) -func getForgeID(c *gin.Context) int64 { - _forgeID := c.Query("forge_id") - if _forgeID == "" { - _forgeID, _ = c.Cookie("forge_id") - } - if _forgeID == "" { - return 1 - } - forgeID, err := strconv.ParseInt(_forgeID, 10, 64) - if err != nil { - return 1 - } - return forgeID -} - func HandleLogin(c *gin.Context) { - forgeID := getForgeID(c) - if err := c.Request.FormValue("error"); err != "" { - c.Redirect(http.StatusSeeOther, fmt.Sprintf("%s/login/error?code=%s&forge_id=%d", server.Config.Server.RootPath, err, forgeID)) + c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/login/error?code="+err) } else { - c.Redirect(http.StatusSeeOther, fmt.Sprintf("%s/authorize?forge_id=%d", server.Config.Server.RootPath, forgeID)) + c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/authorize") } } func HandleAuth(c *gin.Context) { _store := store.FromContext(c) - - // TODO: get forge-id in state - forgeID := getForgeID(c) - - log.Debug().Msgf("trying to login with forge_id: %d", forgeID) - - _forge, err := server.Config.Services.Manager.ForgeByID(forgeID) + _forge, err := server.Config.Services.Manager.ForgeMain() if err != nil { _ = c.AbortWithError(http.StatusInternalServerError, err) return } + forgeID := int64(1) // TODO: replace with forge id when multiple forges are supported // when dealing with redirects, we may need to adjust the content type. I // cannot, however, remember why, so need to revisit this line. c.Writer.Header().Del("Content-Type") - stateID := "woodpecker-state-123" // TODO: generate a random id - // TODO: add selected forgeID to content of the state - tmpuser, redirectURL, err := _forge.Login(c, &forge_types.OAuthRequest{ - State: stateID, Error: c.Request.FormValue("error"), ErrorURI: c.Request.FormValue("error_uri"), ErrorDescription: c.Request.FormValue("error_description"), @@ -93,19 +66,14 @@ func HandleAuth(c *gin.Context) { c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/login?error=oauth_error") return } - // The user is not authorized yet -> redirect if tmpuser == nil { - // TODO: set forge-id in state - httputil.SetCookie(c.Writer, c.Request, "forge_id", fmt.Sprintf("%d", forgeID)) http.Redirect(c.Writer, c.Request, redirectURL, http.StatusSeeOther) return } - // TODO: set forge-id in state - httputil.DelCookie(c.Writer, c.Request, "forge_id") // get the user from the database - u, err := _store.GetUserRemoteID(forgeID, tmpuser.ForgeRemoteID) + u, err := _store.GetUserRemoteID(tmpuser.ForgeRemoteID, tmpuser.Login) if err != nil && !errors.Is(err, types.RecordNotExist) { _ = c.AbortWithError(http.StatusInternalServerError, err) return @@ -268,18 +236,17 @@ func GetLogout(c *gin.Context) { func GetLoginToken(c *gin.Context) { _store := store.FromContext(c) - in := &tokenPayload{} - err := c.Bind(in) + _forge, err := server.Config.Services.Manager.ForgeMain() // TODO: get selected forge from auth request if err != nil { - _ = c.AbortWithError(http.StatusBadRequest, err) + log.Error().Err(err).Msg("Cannot get main forge") + c.AbortWithStatus(http.StatusInternalServerError) return } - forgeID := int64(1) // TODO: use correct forge - - _forge, err := server.Config.Services.Manager.ForgeByID(forgeID) + in := &tokenPayload{} + err = c.Bind(in) if err != nil { - _ = c.AbortWithError(http.StatusInternalServerError, err) + _ = c.AbortWithError(http.StatusBadRequest, err) return } @@ -289,7 +256,7 @@ func GetLoginToken(c *gin.Context) { return } - user, err := _store.GetUserLogin(forgeID, login) + user, err := _store.GetUserLogin(login) if err != nil { handleDBError(c, err) return From ccb22709abdb564acaab4857a6a57f44c15c6c79 Mon Sep 17 00:00:00 2001 From: Anbraten <6918444+anbraten@users.noreply.github.com> Date: Mon, 13 May 2024 08:23:21 +0200 Subject: [PATCH 05/42] update comment --- server/forge/bitbucketdatacenter/bitbucketdatacenter.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/forge/bitbucketdatacenter/bitbucketdatacenter.go b/server/forge/bitbucketdatacenter/bitbucketdatacenter.go index 312b90d923..96b0254963 100644 --- a/server/forge/bitbucketdatacenter/bitbucketdatacenter.go +++ b/server/forge/bitbucketdatacenter/bitbucketdatacenter.go @@ -93,7 +93,7 @@ func (c *client) URL() string { func (c *client) Login(ctx context.Context, req *forge_types.OAuthRequest) (*model.User, string, error) { config := c.newOAuth2Config() - // TODO: Add proper state and pkce... + // TODO: Add pkce flow redirectURL := config.AuthCodeURL(req.State) if req.Error != "" { From 2fdbdd9c57cf30f13edbbdcf4af3c98026da0e59 Mon Sep 17 00:00:00 2001 From: Anbraten <6918444+anbraten@users.noreply.github.com> Date: Mon, 13 May 2024 08:54:44 +0200 Subject: [PATCH 06/42] update token --- server/api/login.go | 23 +++++++++++++--------- shared/token/token.go | 45 ++++++++++++++++++++++++++++++++++--------- 2 files changed, 50 insertions(+), 18 deletions(-) diff --git a/server/api/login.go b/server/api/login.go index 03ae89b8e5..a9fb48f6b5 100644 --- a/server/api/login.go +++ b/server/api/login.go @@ -38,7 +38,10 @@ import ( func getForgeID(c *gin.Context) int64 { _forgeID := c.Query("forge_id") if _forgeID == "" { - _forgeID, _ = c.Cookie("forge_id") + state, _ := c.Query("state") + if state != "" { + _forgeID = token.Parse(state, nil).Text + } } if _forgeID == "" { return 1 @@ -78,11 +81,17 @@ func HandleAuth(c *gin.Context) { // cannot, however, remember why, so need to revisit this line. c.Writer.Header().Del("Content-Type") - stateID := "woodpecker-state-123" // TODO: generate a random id - // TODO: add selected forgeID to content of the state + jwtSecret := "" // TODO set some secret + exp := time.Now().Add(time.Minute * 15).Unix() + state, err := token.New(token.OAuthStateToken, strconv.FormatInt(forgeID, 10)).SignExpires(jwtSecret, exp) + if err != nil { + log.Error().Err(err).Msg("cannot create state token") + c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/login?error=internal_error") + return + } tmpuser, redirectURL, err := _forge.Login(c, &forge_types.OAuthRequest{ - State: stateID, + State: state, Error: c.Request.FormValue("error"), ErrorURI: c.Request.FormValue("error_uri"), ErrorDescription: c.Request.FormValue("error_description"), @@ -96,13 +105,9 @@ func HandleAuth(c *gin.Context) { // The user is not authorized yet -> redirect if tmpuser == nil { - // TODO: set forge-id in state - httputil.SetCookie(c.Writer, c.Request, "forge_id", fmt.Sprintf("%d", forgeID)) http.Redirect(c.Writer, c.Request, redirectURL, http.StatusSeeOther) return } - // TODO: set forge-id in state - httputil.DelCookie(c.Writer, c.Request, "forge_id") // get the user from the database u, err := _store.GetUserRemoteID(forgeID, tmpuser.ForgeRemoteID) @@ -217,7 +222,7 @@ func HandleAuth(c *gin.Context) { return } - exp := time.Now().Add(server.Config.Server.SessionExpires).Unix() + exp = time.Now().Add(server.Config.Server.SessionExpires).Unix() tokenString, err := token.New(token.SessToken, strconv.FormatInt(u.ID, 10)).SignExpires(u.Hash, exp) if err != nil { log.Error().Msgf("cannot create token for %s", u.Login) diff --git a/shared/token/token.go b/shared/token/token.go index d5c48aafd9..c99c471589 100644 --- a/shared/token/token.go +++ b/shared/token/token.go @@ -25,11 +25,12 @@ import ( type SecretFunc func(*Token) (string, error) const ( - UserToken = "user" - SessToken = "sess" - HookToken = "hook" - CsrfToken = "csrf" - AgentToken = "agent" + UserToken = "user" + SessToken = "sess" + HookToken = "hook" + CsrfToken = "csrf" + AgentToken = "agent" + OAuthStateToken = "oauth-state" ) // SignerAlgo id default algorithm used to sign JWT tokens. @@ -37,7 +38,8 @@ const SignerAlgo = "HS256" type Token struct { Kind string - Text string + // Text string + Data map[string]string } func parse(raw string, fn SecretFunc) (*Token, error) { @@ -45,7 +47,8 @@ func parse(raw string, fn SecretFunc) (*Token, error) { parsed, err := jwt.Parse(raw, keyFunc(token, fn)) if err != nil { return nil, err - } else if !parsed.Valid { + } + if !parsed.Valid { return nil, jwt.ErrTokenUnverifiable } return token, nil @@ -100,7 +103,7 @@ func CheckCsrf(r *http.Request, fn SecretFunc) error { } func New(kind, text string) *Token { - return &Token{Kind: kind, Text: text} + return &Token{Kind: kind, Text: text, Data: map[string]string{}} } // Sign signs the token using the given secret hash @@ -118,13 +121,24 @@ func (t *Token) SignExpires(secret string, exp int64) (string, error) { return "", fmt.Errorf("token claim is not a MapClaims") } claims["type"] = t.Kind - claims["text"] = t.Text + // claims["text"] = t.Text if exp > 0 { claims["exp"] = float64(exp) } + for key, value := range t.Data { + claims[key] = value + } return token.SignedString([]byte(secret)) } +func (t *Token) Get(key string) string { + return t.Data[key] +} + +func (t *Token) Set(key, value string) { + t.Data[key] = value +} + func keyFunc(token *Token, fn SecretFunc) jwt.Keyfunc { return func(t *jwt.Token) (any, error) { claims, ok := t.Claims.(jwt.MapClaims) @@ -153,6 +167,19 @@ func keyFunc(token *Token, fn SecretFunc) jwt.Keyfunc { } token.Text, _ = text.(string) + // extract the token data and cast to + // expected type. + for key, value := range claims { + if key == "type" || key == "text" || key == "exp" { + continue + } + data, ok := value.(string) + if !ok { + return nil, jwt.ErrInvalidType + } + token.Data[key] = data + } + // invoke the callback function to retrieve // the secret key used to verify secret, err := fn(token) From 7106c456d146af2f0bee96ca3ec0a7f406848f50 Mon Sep 17 00:00:00 2001 From: Anbraten <6918444+anbraten@users.noreply.github.com> Date: Tue, 14 May 2024 12:24:56 +0200 Subject: [PATCH 07/42] use explicit token claims --- server/api/hook.go | 11 ++++--- server/api/login.go | 7 +++-- server/api/repo.go | 9 ++++-- server/api/user.go | 8 +++-- server/router/middleware/session/user.go | 2 +- server/web/config.go | 7 ++--- shared/token/token.go | 39 +++++++++++++++++------- 7 files changed, 55 insertions(+), 28 deletions(-) diff --git a/server/api/hook.go b/server/api/hook.go index 8046d289f1..207cf714bf 100644 --- a/server/api/hook.go +++ b/server/api/hook.go @@ -22,6 +22,7 @@ import ( "errors" "fmt" "net/http" + "strconv" "github.com/gin-gonic/gin" "github.com/rs/zerolog/log" @@ -159,7 +160,7 @@ func PostHook(c *gin.Context) { c.Status(http.StatusNoContent) return } - oldFullName := repo.FullName + currentRepoFullName := repo.FullName if repo.UserID == 0 { log.Warn().Msgf("ignoring hook. repo %s has no owner.", repo.FullName) @@ -179,7 +180,7 @@ func PostHook(c *gin.Context) { // // get the token and verify the hook is authorized - parsed, err := token.ParseRequest(c.Request, func(_ *token.Token) (string, error) { + parsedToken, err := token.ParseRequest(c.Request, func(_ *token.Token) (string, error) { return repo.Hash, nil }) if err != nil { @@ -188,7 +189,7 @@ func PostHook(c *gin.Context) { c.String(http.StatusBadRequest, msg) return } - verifiedKey := parsed.Text == oldFullName + verifiedKey := parsedToken.Get("repo-id") == strconv.FormatInt(repo.ID, 10) || parsedToken.Get("text") == currentRepoFullName if !verifiedKey { verifiedKey, err = _store.HasRedirectionForRepo(repo.ID, repo.FullName) if err != nil { @@ -200,7 +201,7 @@ func PostHook(c *gin.Context) { } if !verifiedKey { - msg := fmt.Sprintf("failure to verify token from hook. Expected %s, got %s", repo.FullName, parsed.Text) + msg := fmt.Sprintf("failure to verify token from hook. Expected %s, got %s", repo.FullName, parsedToken.Get("text")) log.Debug().Msg(msg) c.String(http.StatusForbidden, msg) return @@ -210,7 +211,7 @@ func PostHook(c *gin.Context) { // 4. Update repo // - if oldFullName != tmpRepo.FullName { + if currentRepoFullName != tmpRepo.FullName { // create a redirection err = _store.CreateRedirection(&model.Redirection{RepoID: repo.ID, FullName: repo.FullName}) if err != nil { diff --git a/server/api/login.go b/server/api/login.go index 5cafc85bfd..bdd21dd730 100644 --- a/server/api/login.go +++ b/server/api/login.go @@ -186,7 +186,9 @@ func HandleAuth(c *gin.Context) { } exp := time.Now().Add(server.Config.Server.SessionExpires).Unix() - tokenString, err := token.New(token.SessToken, strconv.FormatInt(u.ID, 10)).SignExpires(u.Hash, exp) + _token := token.New(token.SessToken) + _token.Set("user-id", strconv.FormatInt(u.ID, 10)) + tokenString, err := _token.SignExpires(u.Hash, exp) if err != nil { log.Error().Msgf("cannot create token for %s", u.Login) c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/login?error=internal_error") @@ -263,7 +265,8 @@ func GetLoginToken(c *gin.Context) { } exp := time.Now().Add(server.Config.Server.SessionExpires).Unix() - newToken := token.New(token.SessToken, strconv.FormatInt(user.ID, 10)) + newToken := token.New(token.SessToken) + newToken.Set("user-id", strconv.FormatInt(user.ID, 10)) tokenStr, err := newToken.SignExpires(user.Hash, exp) if err != nil { _ = c.AbortWithError(http.StatusInternalServerError, err) diff --git a/server/api/repo.go b/server/api/repo.go index 4d80b8d45f..faca0444b7 100644 --- a/server/api/repo.go +++ b/server/api/repo.go @@ -118,7 +118,8 @@ func PostRepo(c *gin.Context) { } // creates the jwt token used to verify the repository - t := token.New(token.HookToken, strconv.FormatInt(repo.ID, 10)) + t := token.New(token.HookToken) + t.Set("repo-id", strconv.FormatInt(repo.ID, 10)) sig, err := t.Sign(repo.Hash) if err != nil { msg := "could not generate new jwt token." @@ -520,7 +521,8 @@ func MoveRepo(c *gin.Context) { } // creates the jwt token used to verify the repository - t := token.New(token.HookToken, strconv.FormatInt(repo.ID, 10)) + t := token.New(token.HookToken) + t.Set("repo-id", strconv.FormatInt(repo.ID, 10)) sig, err := t.Sign(repo.Hash) if err != nil { c.String(http.StatusInternalServerError, err.Error()) @@ -622,7 +624,8 @@ func repairRepo(c *gin.Context, repo *model.Repo, withPerms, skipOnErr bool) { } // creates the jwt token used to verify the repository - t := token.New(token.HookToken, strconv.FormatInt(repo.ID, 10)) + t := token.New(token.HookToken) + t.Set("repo-id", strconv.FormatInt(repo.ID, 10)) sig, err := t.Sign(repo.Hash) if err != nil { c.String(http.StatusInternalServerError, err.Error()) diff --git a/server/api/user.go b/server/api/user.go index 857368c3e7..1785f1995a 100644 --- a/server/api/user.go +++ b/server/api/user.go @@ -153,7 +153,9 @@ func GetRepos(c *gin.Context) { // @Param Authorization header string true "Insert your personal access token" default(Bearer ) func PostToken(c *gin.Context) { user := session.User(c) - tokenString, err := token.New(token.UserToken, strconv.FormatInt(user.ID, 10)).Sign(user.Hash) + t := token.New(token.UserToken) + t.Set("user-id", strconv.FormatInt(user.ID, 10)) + tokenString, err := t.Sign(user.Hash) if err != nil { _ = c.AbortWithError(http.StatusInternalServerError, err) return @@ -182,7 +184,9 @@ func DeleteToken(c *gin.Context) { return } - tokenString, err := token.New(token.UserToken, strconv.FormatInt(user.ID, 10)).Sign(user.Hash) + t := token.New(token.UserToken) + t.Set("user-id", strconv.FormatInt(user.ID, 10)) + tokenString, err := t.Sign(user.Hash) if err != nil { _ = c.AbortWithError(http.StatusInternalServerError, err) return diff --git a/server/router/middleware/session/user.go b/server/router/middleware/session/user.go index 1b96b6864e..b413cf5281 100644 --- a/server/router/middleware/session/user.go +++ b/server/router/middleware/session/user.go @@ -45,7 +45,7 @@ func SetUser() gin.HandlerFunc { t, err := token.ParseRequest(c.Request, func(t *token.Token) (string, error) { var err error - userID, err := strconv.ParseInt(t.Text, 10, 64) + userID, err := strconv.ParseInt(t.Get("user-id"), 10, 64) if err != nil { return "", err } diff --git a/server/web/config.go b/server/web/config.go index 987ef128d8..daa3a2fc55 100644 --- a/server/web/config.go +++ b/server/web/config.go @@ -34,10 +34,9 @@ func Config(c *gin.Context) { var csrf string if user != nil { - csrf, _ = token.New( - token.CsrfToken, - strconv.FormatInt(user.ID, 10), - ).Sign(user.Hash) + t := token.New(token.CsrfToken) + t.Set("user-id", strconv.FormatInt(user.ID, 10)) + csrf, _ = t.Sign(user.Hash) } // TODO: remove this and use the forge type from the corresponding repo diff --git a/shared/token/token.go b/shared/token/token.go index d5c48aafd9..83c34a9167 100644 --- a/shared/token/token.go +++ b/shared/token/token.go @@ -37,11 +37,13 @@ const SignerAlgo = "HS256" type Token struct { Kind string - Text string + data map[string]string } func parse(raw string, fn SecretFunc) (*Token, error) { - token := &Token{} + token := &Token{ + data: map[string]string{}, + } parsed, err := jwt.Parse(raw, keyFunc(token, fn)) if err != nil { return nil, err @@ -99,8 +101,8 @@ func CheckCsrf(r *http.Request, fn SecretFunc) error { return err } -func New(kind, text string) *Token { - return &Token{Kind: kind, Text: text} +func New(kind string) *Token { + return &Token{Kind: kind, data: map[string]string{}} } // Sign signs the token using the given secret hash @@ -118,13 +120,25 @@ func (t *Token) SignExpires(secret string, exp int64) (string, error) { return "", fmt.Errorf("token claim is not a MapClaims") } claims["type"] = t.Kind - claims["text"] = t.Text if exp > 0 { claims["exp"] = float64(exp) } + + for k, v := range t.data { + claims[k] = v + } + return token.SignedString([]byte(secret)) } +func (t *Token) Set(key, value string) { + t.data[key] = value +} + +func (t *Token) Get(key string) string { + return t.data[key] +} + func keyFunc(token *Token, fn SecretFunc) jwt.Keyfunc { return func(t *jwt.Token) (any, error) { claims, ok := t.Claims.(jwt.MapClaims) @@ -145,13 +159,16 @@ func keyFunc(token *Token, fn SecretFunc) jwt.Keyfunc { } token.Kind, _ = kind.(string) - // extract the token value and cast to - // expected type. - text, ok := claims["text"] - if !ok { - return nil, jwt.ErrInvalidType + // extract the data claims + for k, v := range claims { + if k == "type" || k == "exp" { + continue + } + + if str, ok := v.(string); ok { + token.data[k] = str + } } - token.Text, _ = text.(string) // invoke the callback function to retrieve // the secret key used to verify From c49bfbe6512e035a130f235177c5b1318639bea2 Mon Sep 17 00:00:00 2001 From: Anbraten <6918444+anbraten@users.noreply.github.com> Date: Tue, 14 May 2024 12:33:50 +0200 Subject: [PATCH 08/42] skip internal claims --- shared/token/token.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/token/token.go b/shared/token/token.go index 83c34a9167..b2b91693cf 100644 --- a/shared/token/token.go +++ b/shared/token/token.go @@ -161,7 +161,7 @@ func keyFunc(token *Token, fn SecretFunc) jwt.Keyfunc { // extract the data claims for k, v := range claims { - if k == "type" || k == "exp" { + if k == "type" || k == "exp" || k == "nbf" || k == "iat" || k == "aud" || k == "iss" || k == "sub" || k == "jti" { continue } From a4320f1efd47ed61267edf0f70ffdaef2585d8d1 Mon Sep 17 00:00:00 2001 From: Anbraten <6918444+anbraten@users.noreply.github.com> Date: Tue, 14 May 2024 13:02:23 +0200 Subject: [PATCH 09/42] add todo --- docs/docs/91-migrations.md | 7 +++++++ server/api/hook.go | 2 ++ 2 files changed, 9 insertions(+) diff --git a/docs/docs/91-migrations.md b/docs/docs/91-migrations.md index 6ad8f7b13f..c420e08719 100644 --- a/docs/docs/91-migrations.md +++ b/docs/docs/91-migrations.md @@ -2,6 +2,13 @@ Some versions need some changes to the server configuration or the pipeline configuration files. + + ## `next` - Deprecated `steps.[name].group` in favor of `steps.[name].depends_on` (see [workflow syntax](./20-usage/20-workflow-syntax.md#depends_on) to learn how to set dependencies) diff --git a/server/api/hook.go b/server/api/hook.go index 207cf714bf..f69f2d2585 100644 --- a/server/api/hook.go +++ b/server/api/hook.go @@ -189,6 +189,8 @@ func PostHook(c *gin.Context) { c.String(http.StatusBadRequest, msg) return } + + // TODO: remove fallback for text full name in next major release verifiedKey := parsedToken.Get("repo-id") == strconv.FormatInt(repo.ID, 10) || parsedToken.Get("text") == currentRepoFullName if !verifiedKey { verifiedKey, err = _store.HasRedirectionForRepo(repo.ID, repo.FullName) From 4e4a8f71d46058fcbd58f2940bec13f5f1d8362b Mon Sep 17 00:00:00 2001 From: Anbraten <6918444+anbraten@users.noreply.github.com> Date: Mon, 20 May 2024 14:42:30 +0200 Subject: [PATCH 10/42] add state --- server/api/login.go | 4 +++- server/forge/bitbucket/bitbucket.go | 2 +- server/forge/gitea/gitea.go | 2 +- server/forge/github/github.go | 2 +- server/forge/gitlab/gitlab.go | 2 +- server/forge/types/oauth.go | 1 + shared/token/token.go | 13 ------------- 7 files changed, 8 insertions(+), 18 deletions(-) diff --git a/server/api/login.go b/server/api/login.go index 5ddec8e540..93b201b483 100644 --- a/server/api/login.go +++ b/server/api/login.go @@ -75,7 +75,9 @@ func HandleAuth(c *gin.Context) { jwtSecret := "" // TODO set some secret exp := time.Now().Add(time.Minute * 15).Unix() - state, err := token.New(token.OAuthStateToken, strconv.FormatInt(forgeID, 10)).SignExpires(jwtSecret, exp) + _token := token.New(token.OAuthStateToken) + _token.Set("forge-id", strconv.FormatInt(forgeID, 10)) + state, err := _token.SignExpires(jwtSecret, exp) if err != nil { log.Error().Err(err).Msg("cannot create state token") c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/login?error=internal_error") diff --git a/server/forge/bitbucket/bitbucket.go b/server/forge/bitbucket/bitbucket.go index 02e2f1c86b..67196f8137 100644 --- a/server/forge/bitbucket/bitbucket.go +++ b/server/forge/bitbucket/bitbucket.go @@ -81,7 +81,7 @@ func (c *config) URL() string { // Bitbucket account details are returned when the user is successfully authenticated. func (c *config) Login(ctx context.Context, req *forge_types.OAuthRequest) (*model.User, string, error) { config := c.newOAuth2Config() - redirectURL := config.AuthCodeURL("woodpecker") + redirectURL := config.AuthCodeURL(req.State) // get the OAuth errors if req.Error != "" { diff --git a/server/forge/gitea/gitea.go b/server/forge/gitea/gitea.go index e0d82c5062..09fd21b633 100644 --- a/server/forge/gitea/gitea.go +++ b/server/forge/gitea/gitea.go @@ -113,7 +113,7 @@ func (c *Gitea) oauth2Config(ctx context.Context) (*oauth2.Config, context.Conte // Gitea account details are returned when the user is successfully authenticated. func (c *Gitea) Login(ctx context.Context, req *forge_types.OAuthRequest) (*model.User, string, error) { config, oauth2Ctx := c.oauth2Config(ctx) - redirectURL := config.AuthCodeURL("woodpecker") + redirectURL := config.AuthCodeURL(req.State) // check the OAuth errors if req.Error != "" { diff --git a/server/forge/github/github.go b/server/forge/github/github.go index 6b3646ebd0..5edc806e03 100644 --- a/server/forge/github/github.go +++ b/server/forge/github/github.go @@ -100,7 +100,7 @@ func (c *client) URL() string { // Login authenticates the session and returns the forge user details. func (c *client) Login(ctx context.Context, req *forge_types.OAuthRequest) (*model.User, string, error) { config := c.newConfig() - redirectURL := config.AuthCodeURL("woodpecker") + redirectURL := config.AuthCodeURL(req.State) // check the OAuth errors if req.Error != "" { diff --git a/server/forge/gitlab/gitlab.go b/server/forge/gitlab/gitlab.go index 27cd50cb7f..d092fd77a5 100644 --- a/server/forge/gitlab/gitlab.go +++ b/server/forge/gitlab/gitlab.go @@ -115,7 +115,7 @@ func (g *GitLab) oauth2Config(ctx context.Context) (*oauth2.Config, context.Cont // forge user details. func (g *GitLab) Login(ctx context.Context, req *forge_types.OAuthRequest) (*model.User, string, error) { config, oauth2Ctx := g.oauth2Config(ctx) - redirectURL := config.AuthCodeURL("woodpecker") + redirectURL := config.AuthCodeURL(req.State) // check the OAuth errors if req.Error != "" { diff --git a/server/forge/types/oauth.go b/server/forge/types/oauth.go index fba870569c..c2ea8f4c51 100644 --- a/server/forge/types/oauth.go +++ b/server/forge/types/oauth.go @@ -19,4 +19,5 @@ type OAuthRequest struct { ErrorURI string ErrorDescription string Code string + State string } diff --git a/shared/token/token.go b/shared/token/token.go index 16ff7d788a..d06217792d 100644 --- a/shared/token/token.go +++ b/shared/token/token.go @@ -172,19 +172,6 @@ func keyFunc(token *Token, fn SecretFunc) jwt.Keyfunc { } } - // extract the token data and cast to - // expected type. - for key, value := range claims { - if key == "type" || key == "text" || key == "exp" { - continue - } - data, ok := value.(string) - if !ok { - return nil, jwt.ErrInvalidType - } - token.Data[key] = data - } - // invoke the callback function to retrieve // the secret key used to verify secret, err := fn(token) From 85fb52c0af7d05ed41c3b0a3acda62d3fb216bc9 Mon Sep 17 00:00:00 2001 From: Anbraten <6918444+anbraten@users.noreply.github.com> Date: Mon, 20 May 2024 15:12:34 +0200 Subject: [PATCH 11/42] adjust claim copy and add source for the registered claims --- shared/token/token.go | 40 ++++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/shared/token/token.go b/shared/token/token.go index b2b91693cf..9b6504a002 100644 --- a/shared/token/token.go +++ b/shared/token/token.go @@ -36,13 +36,13 @@ const ( const SignerAlgo = "HS256" type Token struct { - Kind string - data map[string]string + Kind string + claims jwt.MapClaims } func parse(raw string, fn SecretFunc) (*Token, error) { token := &Token{ - data: map[string]string{}, + claims: jwt.MapClaims{}, } parsed, err := jwt.Parse(raw, keyFunc(token, fn)) if err != nil { @@ -102,7 +102,7 @@ func CheckCsrf(r *http.Request, fn SecretFunc) error { } func New(kind string) *Token { - return &Token{Kind: kind, data: map[string]string{}} + return &Token{Kind: kind, claims: jwt.MapClaims{}} } // Sign signs the token using the given secret hash @@ -119,24 +119,30 @@ func (t *Token) SignExpires(secret string, exp int64) (string, error) { if !ok { return "", fmt.Errorf("token claim is not a MapClaims") } + + for k, v := range t.claims { + claims[k] = v + } + claims["type"] = t.Kind if exp > 0 { claims["exp"] = float64(exp) } - for k, v := range t.data { - claims[k] = v - } - return token.SignedString([]byte(secret)) } func (t *Token) Set(key, value string) { - t.data[key] = value + t.claims[key] = value } func (t *Token) Get(key string) string { - return t.data[key] + claim, ok := t.claims[key].(string) + if !ok { + return "" + } + + return claim } func keyFunc(token *Token, fn SecretFunc) jwt.Keyfunc { @@ -151,23 +157,25 @@ func keyFunc(token *Token, fn SecretFunc) jwt.Keyfunc { return nil, jwt.ErrSignatureInvalid } - // extract the token kind and cast to - // the expected type. + // extract the token kind and cast to the expected type kind, ok := claims["type"] if !ok { return nil, jwt.ErrInvalidType } token.Kind, _ = kind.(string) - // extract the data claims + // copy custom claims for k, v := range claims { - if k == "type" || k == "exp" || k == "nbf" || k == "iat" || k == "aud" || k == "iss" || k == "sub" || k == "jti" { + // skip the reserved claims https://datatracker.ietf.org/doc/html/rfc7519#section-4.1 + if k == "iss" || k == "sub" || k == "aud" || k == "exp" || k == "nbf" || k == "iat" || k == "jti" { continue } - if str, ok := v.(string); ok { - token.data[k] = str + if k == "type" { + continue } + + token.claims[k] = v } // invoke the callback function to retrieve From e18e6fe6b9825191f33df8426259e557962587a3 Mon Sep 17 00:00:00 2001 From: Anbraten <6918444+anbraten@users.noreply.github.com> Date: Mon, 27 May 2024 10:34:06 +0200 Subject: [PATCH 12/42] enhance --- server/api/hook.go | 2 +- server/api/login.go | 57 ++++++++++++++++------- server/services/manager.go | 12 ++--- server/web/config.go | 2 +- shared/token/token.go | 12 ++--- web/src/assets/locales/en.json | 1 + web/src/compositions/useAuthentication.ts | 4 +- web/src/views/Login.vue | 38 +++++++++++++-- 8 files changed, 91 insertions(+), 37 deletions(-) diff --git a/server/api/hook.go b/server/api/hook.go index 40a93ef356..e5d0a63ba2 100644 --- a/server/api/hook.go +++ b/server/api/hook.go @@ -104,7 +104,7 @@ func BlockTilQueueHasRunningItem(c *gin.Context) { func PostHook(c *gin.Context) { _store := store.FromContext(c) - _forge, err := server.Config.Services.Manager.ForgeMain() // TODO: get the forge for the specific repo somehow + _forge, err := server.Config.Services.Manager.ForgeByID(1) // TODO: get the forge for the specific repo somehow if err != nil { log.Error().Err(err).Msg("Cannot get main forge") c.AbortWithStatus(http.StatusInternalServerError) diff --git a/server/api/login.go b/server/api/login.go index 93b201b483..d6ee7cfeff 100644 --- a/server/api/login.go +++ b/server/api/login.go @@ -17,6 +17,7 @@ package api import ( "encoding/base32" "errors" + "fmt" "net/http" "strconv" "time" @@ -34,50 +35,74 @@ import ( "go.woodpecker-ci.org/woodpecker/v2/shared/token" ) -func getForgeID(c *gin.Context) int64 { +func getForgeID(c *gin.Context) (int64, error) { _forgeID := c.Query("forge_id") + + // check if forge id is in the state token if _forgeID == "" { - state, _ := c.Query("state") + state := c.Query("state") if state != "" { - _forgeID = token.Parse(state, nil).Text + stateToken, err := token.Parse(state, func(t *token.Token) (string, error) { + return "1234567890", nil // TODO: set some secret + }) + if err != nil { + return 0, err + } + + _forgeID = stateToken.Get("forge-id") } } + + // fallback to default forge if _forgeID == "" { - return 1 + return 1, nil } + forgeID, err := strconv.ParseInt(_forgeID, 10, 64) if err != nil { - return 1 + return 0, err } - return forgeID + return forgeID, nil } func HandleLogin(c *gin.Context) { + forgeID, err := getForgeID(c) + if err != nil { + log.Error().Err(err).Msg("cannot get forge id") + c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/login?error=internal_error") + return + } + if err := c.Request.FormValue("error"); err != "" { - c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/login/error?code="+err) + c.Redirect(http.StatusSeeOther, fmt.Sprintf("%s/login/error?code=%s&forge_id=%d", server.Config.Server.RootPath, err, forgeID)) } else { - c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/authorize") + c.Redirect(http.StatusSeeOther, fmt.Sprintf("%s/authorize?forge_id=%d", server.Config.Server.RootPath, forgeID)) } } func HandleAuth(c *gin.Context) { _store := store.FromContext(c) - _forge, err := server.Config.Services.Manager.ForgeMain() + + forgeID, err := getForgeID(c) + if err != nil { + _ = c.AbortWithError(http.StatusInternalServerError, err) + } + + _forge, err := server.Config.Services.Manager.ForgeByID(forgeID) if err != nil { _ = c.AbortWithError(http.StatusInternalServerError, err) return } - forgeID := int64(1) // TODO: replace with forge id when multiple forges are supported // when dealing with redirects, we may need to adjust the content type. I // cannot, however, remember why, so need to revisit this line. c.Writer.Header().Del("Content-Type") - jwtSecret := "" // TODO set some secret + jwtSecret := "1234567890" // TODO set some secret exp := time.Now().Add(time.Minute * 15).Unix() - _token := token.New(token.OAuthStateToken) - _token.Set("forge-id", strconv.FormatInt(forgeID, 10)) - state, err := _token.SignExpires(jwtSecret, exp) + stateToken := token.New(token.OAuthStateToken) + stateToken.Set("forge-id", strconv.FormatInt(forgeID, 10)) + state, err := stateToken.SignExpires(jwtSecret, exp) if err != nil { log.Error().Err(err).Msg("cannot create state token") c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/login?error=internal_error") @@ -215,7 +240,7 @@ func HandleAuth(c *gin.Context) { return } - exp := time.Now().Add(server.Config.Server.SessionExpires).Unix() + exp = time.Now().Add(server.Config.Server.SessionExpires).Unix() _token := token.New(token.SessToken) _token.Set("user-id", strconv.FormatInt(u.ID, 10)) tokenString, err := _token.SignExpires(u.Hash, exp) @@ -268,7 +293,7 @@ func GetLogout(c *gin.Context) { func GetLoginToken(c *gin.Context) { _store := store.FromContext(c) - _forge, err := server.Config.Services.Manager.ForgeMain() // TODO: get selected forge from auth request + _forge, err := server.Config.Services.Manager.ForgeByID(1) // TODO: get selected forge from auth request if err != nil { log.Error().Err(err).Msg("Cannot get main forge") c.AbortWithStatus(http.StatusInternalServerError) diff --git a/server/services/manager.go b/server/services/manager.go index 079222a02d..2fe13486e2 100644 --- a/server/services/manager.go +++ b/server/services/manager.go @@ -46,7 +46,7 @@ type Manager interface { EnvironmentService() environment.Service ForgeFromRepo(repo *model.Repo) (forge.Forge, error) ForgeFromUser(user *model.User) (forge.Forge, error) - ForgeMain() (forge.Forge, error) + ForgeByID(forgeID int64) (forge.Forge, error) } type manager struct { @@ -120,18 +120,14 @@ func (m *manager) EnvironmentService() environment.Service { } func (m *manager) ForgeFromRepo(repo *model.Repo) (forge.Forge, error) { - return m.getForgeByID(repo.ForgeID) + return m.ForgeByID(repo.ForgeID) } func (m *manager) ForgeFromUser(user *model.User) (forge.Forge, error) { - return m.getForgeByID(user.ForgeID) + return m.ForgeByID(user.ForgeID) } -func (m *manager) ForgeMain() (forge.Forge, error) { - return m.getForgeByID(1) // main forge is always 1 and is configured via environment variables -} - -func (m *manager) getForgeByID(id int64) (forge.Forge, error) { +func (m *manager) ForgeByID(id int64) (forge.Forge, error) { item := m.forgeCache.Get(id) if item != nil && !item.IsExpired() { return item.Value(), nil diff --git a/server/web/config.go b/server/web/config.go index daa3a2fc55..7c3bb0df9f 100644 --- a/server/web/config.go +++ b/server/web/config.go @@ -40,7 +40,7 @@ func Config(c *gin.Context) { } // TODO: remove this and use the forge type from the corresponding repo - mainForge, err := server.Config.Services.Manager.ForgeMain() + mainForge, err := server.Config.Services.Manager.ForgeByID(1) if err != nil { log.Error().Err(err).Msg("could not get main forge") c.AbortWithStatus(http.StatusInternalServerError) diff --git a/shared/token/token.go b/shared/token/token.go index f485e4b0fd..2b10326392 100644 --- a/shared/token/token.go +++ b/shared/token/token.go @@ -41,7 +41,7 @@ type Token struct { claims jwt.MapClaims } -func parse(raw string, fn SecretFunc) (*Token, error) { +func Parse(raw string, fn SecretFunc) (*Token, error) { token := &Token{ claims: jwt.MapClaims{}, } @@ -65,19 +65,19 @@ func ParseRequest(r *http.Request, fn SecretFunc) (*Token, error) { if _, err := fmt.Sscanf(token, "Bearer %s", &bearer); err != nil { return nil, err } - return parse(bearer, fn) + return Parse(bearer, fn) } token = r.Header.Get("X-Gitlab-Token") if len(token) != 0 { - return parse(token, fn) + return Parse(token, fn) } // then we attempt to get the token from the // access_token url query parameter token = r.FormValue("access_token") if len(token) != 0 { - return parse(token, fn) + return Parse(token, fn) } // and finally we attempt to get the token from @@ -86,7 +86,7 @@ func ParseRequest(r *http.Request, fn SecretFunc) (*Token, error) { if err != nil { return nil, err } - return parse(cookie.Value, fn) + return Parse(cookie.Value, fn) } func CheckCsrf(r *http.Request, fn SecretFunc) error { @@ -99,7 +99,7 @@ func CheckCsrf(r *http.Request, fn SecretFunc) error { // parse the raw CSRF token value and validate raw := r.Header.Get("X-CSRF-TOKEN") - _, err := parse(raw, fn) + _, err := Parse(raw, fn) return err } diff --git a/web/src/assets/locales/en.json b/web/src/assets/locales/en.json index 1233341275..cfbea2fdd3 100644 --- a/web/src/assets/locales/en.json +++ b/web/src/assets/locales/en.json @@ -1,5 +1,6 @@ { "cancel": "Cancel", + "login_with": "Login with {forge}", "login": "Login", "welcome": "Welcome to Woodpecker", "repos": "Repos", diff --git a/web/src/compositions/useAuthentication.ts b/web/src/compositions/useAuthentication.ts index 287942eed5..8ed98f42da 100644 --- a/web/src/compositions/useAuthentication.ts +++ b/web/src/compositions/useAuthentication.ts @@ -7,11 +7,11 @@ export default () => user: useConfig().user, - authenticate(url?: string) { + authenticate(url?: string, forgeId?: number) { if (url) { const config = useUserConfig(); config.setUserConfig('redirectUrl', url); } - window.location.href = `${useConfig().rootPath}/login`; + window.location.href = `${useConfig().rootPath}/login?forge_id=${forgeId}`; }, }) as const; diff --git a/web/src/views/Login.vue b/web/src/views/Login.vue index d6fcec0b98..dbb8c8e572 100644 --- a/web/src/views/Login.vue +++ b/web/src/views/Login.vue @@ -11,7 +11,11 @@

{{ $t('welcome') }}

- +
+ +
@@ -32,9 +36,37 @@ const authentication = useAuthentication(); const errorMessage = ref(); const i18n = useI18n(); -function doLogin() { +type Forge = { + id: number; + url: string; + type: string; +}; + +const forges = ref([ + { + id: 1, + url: 'http://localhost:3000/', + type: 'gitea', + }, + { + id: 2, + url: '', + type: 'github', + }, +]); + +function getHostFromUrl(forge: Forge) { + if (!forge.url) { + return forge.type.charAt(0).toUpperCase() + forge.type.slice(1); + } + + const url = new URL(forge.url); + return url.hostname; +} + +function doLogin(forgeId?: number) { const url = typeof route.query.url === 'string' ? route.query.url : ''; - authentication.authenticate(url); + authentication.authenticate(url, forgeId); } const authErrorMessages = { From c211ee5b74d1732320474bea70b791830c68475d Mon Sep 17 00:00:00 2001 From: Anbraten <6918444+anbraten@users.noreply.github.com> Date: Mon, 27 May 2024 10:57:24 +0200 Subject: [PATCH 13/42] rename improve --- server/api/login.go | 89 +++++++++++++++++----------------- server/store/datastore/user.go | 7 ++- 2 files changed, 50 insertions(+), 46 deletions(-) diff --git a/server/api/login.go b/server/api/login.go index d6ee7cfeff..8865fccc1d 100644 --- a/server/api/login.go +++ b/server/api/login.go @@ -109,7 +109,7 @@ func HandleAuth(c *gin.Context) { return } - tmpuser, redirectURL, err := _forge.Login(c, &forge_types.OAuthRequest{ + userFromForge, redirectURL, err := _forge.Login(c, &forge_types.OAuthRequest{ State: state, Error: c.Request.FormValue("error"), ErrorURI: c.Request.FormValue("error_uri"), @@ -122,13 +122,13 @@ func HandleAuth(c *gin.Context) { return } // The user is not authorized yet -> redirect - if tmpuser == nil { + if userFromForge == nil { http.Redirect(c.Writer, c.Request, redirectURL, http.StatusSeeOther) return } // get the user from the database - u, err := _store.GetUserRemoteID(tmpuser.ForgeRemoteID, tmpuser.Login) + user, err := _store.GetUserRemoteID(userFromForge.ForgeRemoteID, userFromForge.Login) if err != nil && !errors.Is(err, types.RecordNotExist) { _ = c.AbortWithError(http.StatusInternalServerError, err) return @@ -136,8 +136,8 @@ func HandleAuth(c *gin.Context) { if errors.Is(err, types.RecordNotExist) { // if self-registration is disabled we should return a not authorized error - if !server.Config.Permissions.Open && !server.Config.Permissions.Admins.IsAdmin(tmpuser) { - log.Error().Msgf("cannot register %s. registration closed", tmpuser.Login) + if !server.Config.Permissions.Open && !server.Config.Permissions.Admins.IsAdmin(userFromForge) { + log.Error().Msgf("cannot register %s. registration closed", userFromForge.Login) c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/login?error=access_denied") return } @@ -145,22 +145,22 @@ func HandleAuth(c *gin.Context) { // if self-registration is enabled for allowed organizations we need to // check the user's organization membership. if server.Config.Permissions.Orgs.IsConfigured { - teams, terr := _forge.Teams(c, tmpuser) + teams, terr := _forge.Teams(c, userFromForge) if terr != nil || !server.Config.Permissions.Orgs.IsMember(teams) { - log.Error().Err(terr).Msgf("cannot verify team membership for %s.", u.Login) + log.Error().Err(terr).Msgf("cannot verify team membership for %s.", user.Login) c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/login?error=access_denied") return } } // create the user account - u = &model.User{ - Login: tmpuser.Login, - ForgeRemoteID: tmpuser.ForgeRemoteID, - Token: tmpuser.Token, - Secret: tmpuser.Secret, - Email: tmpuser.Email, - Avatar: tmpuser.Avatar, + user = &model.User{ + Login: userFromForge.Login, + ForgeRemoteID: userFromForge.ForgeRemoteID, + Token: userFromForge.Token, + Secret: userFromForge.Secret, + Email: userFromForge.Email, + Avatar: userFromForge.Avatar, ForgeID: forgeID, Hash: base32.StdEncoding.EncodeToString( securecookie.GenerateRandomKey(32), @@ -168,17 +168,17 @@ func HandleAuth(c *gin.Context) { } // insert the user into the database - if err := _store.CreateUser(u); err != nil { - log.Error().Err(err).Msgf("cannot insert %s", u.Login) + if err := _store.CreateUser(user); err != nil { + log.Error().Err(err).Msgf("cannot insert %s", user.Login) c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/login?error=internal_error") return } // if another user already have activated repos on behave of that user, // the user was stored as org. now we adopt it to the user. - if org, err := _store.OrgFindByName(u.Login); err == nil && org != nil { + if org, err := _store.OrgFindByName(user.Login); err == nil && org != nil { org.IsUser = true - u.OrgID = org.ID + user.OrgID = org.ID if err := _store.OrgUpdate(org); err != nil { log.Error().Err(err).Msgf("on user creation, could not mark org as user") } @@ -188,76 +188,77 @@ func HandleAuth(c *gin.Context) { return } org = &model.Org{ - Name: u.Login, + Name: user.Login, IsUser: true, Private: false, - ForgeID: u.ForgeID, + ForgeID: user.ForgeID, } if err := _store.OrgCreate(org); err != nil { log.Error().Err(err).Msgf("on user creation, could not create org for user") } - u.OrgID = org.ID + user.OrgID = org.ID } } // update org name - if u.Login != tmpuser.Login { - org, err := _store.OrgGet(u.OrgID) + if user.Login != userFromForge.Login { + org, err := _store.OrgGet(user.OrgID) if err != nil { - log.Error().Err(err).Msgf("cannot get org %s", u.Login) + log.Error().Err(err).Msgf("cannot get org %s", user.Login) c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/login?error=internal_error") return } - org.Name = u.Login + org.Name = user.Login if err := _store.OrgUpdate(org); err != nil { log.Error().Err(err).Msgf("on user creation, could not mark org as user") } } // update the user meta data and authorization data. - u.Token = tmpuser.Token - u.Secret = tmpuser.Secret - u.Email = tmpuser.Email - u.Avatar = tmpuser.Avatar - u.ForgeRemoteID = tmpuser.ForgeRemoteID - u.Login = tmpuser.Login - u.Admin = u.Admin || server.Config.Permissions.Admins.IsAdmin(tmpuser) + user.Token = userFromForge.Token + user.Secret = userFromForge.Secret + user.Email = userFromForge.Email + user.Avatar = userFromForge.Avatar + user.ForgeID = forgeID + user.ForgeRemoteID = userFromForge.ForgeRemoteID + user.Login = userFromForge.Login + user.Admin = user.Admin || server.Config.Permissions.Admins.IsAdmin(userFromForge) // if self-registration is enabled for allowed organizations we need to // check the user's organization membership. if server.Config.Permissions.Orgs.IsConfigured { - teams, terr := _forge.Teams(c, u) + teams, terr := _forge.Teams(c, user) if terr != nil || !server.Config.Permissions.Orgs.IsMember(teams) { - log.Error().Err(terr).Msgf("cannot verify team membership for %s", u.Login) + log.Error().Err(terr).Msgf("cannot verify team membership for %s", user.Login) c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/login?error=access_denied") return } } - if err := _store.UpdateUser(u); err != nil { - log.Error().Err(err).Msgf("cannot update %s", u.Login) + if err := _store.UpdateUser(user); err != nil { + log.Error().Err(err).Msgf("cannot update %s", user.Login) c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/login?error=internal_error") return } exp = time.Now().Add(server.Config.Server.SessionExpires).Unix() _token := token.New(token.SessToken) - _token.Set("user-id", strconv.FormatInt(u.ID, 10)) - tokenString, err := _token.SignExpires(u.Hash, exp) + _token.Set("user-id", strconv.FormatInt(user.ID, 10)) + tokenString, err := _token.SignExpires(user.Hash, exp) if err != nil { - log.Error().Msgf("cannot create token for %s", u.Login) + log.Error().Msgf("cannot create token for %s", user.Login) c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/login?error=internal_error") return } - repos, _ := _forge.Repos(c, u) + repos, _ := _forge.Repos(c, user) for _, forgeRepo := range repos { dbRepo, err := _store.GetRepoForgeID(forgeRepo.ForgeRemoteID) if err != nil && errors.Is(err, types.RecordNotExist) { continue } if err != nil { - log.Error().Err(err).Msgf("cannot list repos for %s", u.Login) + log.Error().Err(err).Msgf("cannot list repos for %s", user.Login) c.Redirect(http.StatusSeeOther, "/login?error=internal_error") return } @@ -266,14 +267,14 @@ func HandleAuth(c *gin.Context) { continue } - log.Debug().Msgf("synced user permission for %s %s", u.Login, dbRepo.FullName) + log.Debug().Msgf("synced user permission for %s %s", user.Login, dbRepo.FullName) perm := forgeRepo.Perm perm.Repo = dbRepo perm.RepoID = dbRepo.ID - perm.UserID = u.ID + perm.UserID = user.ID perm.Synced = time.Now().Unix() if err := _store.PermUpsert(perm); err != nil { - log.Error().Err(err).Msgf("cannot update permissions for %s", u.Login) + log.Error().Err(err).Msgf("cannot update permissions for %s", user.Login) c.Redirect(http.StatusSeeOther, "/login?error=internal_error") return } diff --git a/server/store/datastore/user.go b/server/store/datastore/user.go index 39f09af75e..34aee5855d 100644 --- a/server/store/datastore/user.go +++ b/server/store/datastore/user.go @@ -15,9 +15,12 @@ package datastore import ( + "errors" + "xorm.io/xorm" "go.woodpecker-ci.org/woodpecker/v2/server/model" + "go.woodpecker-ci.org/woodpecker/v2/server/store/types" ) func (s storage) GetUser(id int64) (*model.User, error) { @@ -29,8 +32,8 @@ func (s storage) GetUserRemoteID(remoteID model.ForgeRemoteID, login string) (*m sess := s.engine.NewSession() user := new(model.User) err := wrapGet(sess.Where("forge_remote_id = ?", remoteID).Get(user)) - if err != nil { - user, err = s.getUserLogin(sess, login) + if err != nil && errors.Is(err, types.RecordNotExist) { + return s.getUserLogin(sess, login) } return user, err } From 0ab0ba893782cfbf9e5adf951ebf6653e49592fa Mon Sep 17 00:00:00 2001 From: Anbraten <6918444+anbraten@users.noreply.github.com> Date: Thu, 20 Jun 2024 17:24:02 +0200 Subject: [PATCH 14/42] Cleanup auth --- server/api/login.go | 116 +++++++++--------- server/forge/bitbucket/bitbucket.go | 2 +- .../bitbucketdatacenter.go | 4 +- server/forge/forgejo/forgejo.go | 2 +- server/forge/gitea/gitea.go | 2 +- server/forge/github/github.go | 2 +- server/forge/gitlab/gitlab.go | 2 +- server/forge/types/oauth.go | 1 + server/router/router.go | 3 +- web/src/assets/locales/en.json | 12 +- .../admin/settings/AdminInfoTab.vue | 1 + web/src/components/atomic/Error.vue | 2 +- web/src/components/atomic/Warning.vue | 2 +- web/src/compositions/useAuthentication.ts | 2 +- web/src/router.ts | 9 +- web/src/views/Login.vue | 15 +-- 16 files changed, 87 insertions(+), 90 deletions(-) diff --git a/server/api/login.go b/server/api/login.go index c03de5db2a..b3e3c59f05 100644 --- a/server/api/login.go +++ b/server/api/login.go @@ -34,15 +34,17 @@ import ( "go.woodpecker-ci.org/woodpecker/v2/shared/token" ) -func HandleLogin(c *gin.Context) { +func HandleAuth(c *gin.Context) { + // when dealing with redirects, we may need to adjust the content type. I + // cannot, however, remember why, so need to revisit this line. + c.Writer.Header().Del("Content-Type") + + // redirect when getting oauth error from forge to login page if err := c.Request.FormValue("error"); err != "" { - c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/login/error?code="+err) - } else { - c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/authorize") + c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/login?error="+err) + return } -} -func HandleAuth(c *gin.Context) { _store := store.FromContext(c) _forge, err := server.Config.Services.Manager.ForgeMain() if err != nil { @@ -51,15 +53,12 @@ func HandleAuth(c *gin.Context) { } forgeID := int64(1) // TODO: replace with forge id when multiple forges are supported - // when dealing with redirects, we may need to adjust the content type. I - // cannot, however, remember why, so need to revisit this line. - c.Writer.Header().Del("Content-Type") - - tmpUser, redirectURL, err := _forge.Login(c, &forge_types.OAuthRequest{ + userFromForge, redirectURL, err := _forge.Login(c, &forge_types.OAuthRequest{ Error: c.Request.FormValue("error"), ErrorURI: c.Request.FormValue("error_uri"), ErrorDescription: c.Request.FormValue("error_description"), Code: c.Request.FormValue("code"), + State: "woodpecker", // TODO: use proper state }) if err != nil { log.Error().Err(err).Msg("cannot authenticate user") @@ -67,13 +66,13 @@ func HandleAuth(c *gin.Context) { return } // The user is not authorized yet -> redirect - if tmpUser == nil { + if userFromForge == nil { http.Redirect(c.Writer, c.Request, redirectURL, http.StatusSeeOther) return } // get the user from the database - u, err := _store.GetUserRemoteID(tmpUser.ForgeRemoteID, tmpUser.Login) + user, err := _store.GetUserRemoteID(userFromForge.ForgeRemoteID, userFromForge.Login) if err != nil && !errors.Is(err, types.RecordNotExist) { _ = c.AbortWithError(http.StatusInternalServerError, err) return @@ -81,31 +80,31 @@ func HandleAuth(c *gin.Context) { if errors.Is(err, types.RecordNotExist) { // if self-registration is disabled we should return a not authorized error - if !server.Config.Permissions.Open && !server.Config.Permissions.Admins.IsAdmin(tmpUser) { - log.Error().Msgf("cannot register %s. registration closed", tmpUser.Login) - c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/login?error=access_denied") + if !server.Config.Permissions.Open && !server.Config.Permissions.Admins.IsAdmin(userFromForge) { + log.Error().Msgf("cannot register %s. registration closed", userFromForge.Login) + c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/login?error=registration_closed") return } // if self-registration is enabled for allowed organizations we need to // check the user's organization membership. if server.Config.Permissions.Orgs.IsConfigured { - teams, terr := _forge.Teams(c, tmpUser) + teams, terr := _forge.Teams(c, userFromForge) if terr != nil || !server.Config.Permissions.Orgs.IsMember(teams) { - log.Error().Err(terr).Msgf("cannot verify team membership for %s.", u.Login) - c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/login?error=access_denied") + log.Error().Err(terr).Msgf("cannot verify team membership for %s.", user.Login) + c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/login?error=org_access_denied") return } } // create the user account - u = &model.User{ - Login: tmpUser.Login, - ForgeRemoteID: tmpUser.ForgeRemoteID, - Token: tmpUser.Token, - Secret: tmpUser.Secret, - Email: tmpUser.Email, - Avatar: tmpUser.Avatar, + user = &model.User{ + Login: userFromForge.Login, + ForgeRemoteID: userFromForge.ForgeRemoteID, + Token: userFromForge.Token, + Secret: userFromForge.Secret, + Email: userFromForge.Email, + Avatar: userFromForge.Avatar, ForgeID: forgeID, Hash: base32.StdEncoding.EncodeToString( securecookie.GenerateRandomKey(32), @@ -113,17 +112,17 @@ func HandleAuth(c *gin.Context) { } // insert the user into the database - if err := _store.CreateUser(u); err != nil { - log.Error().Err(err).Msgf("cannot insert %s", u.Login) + if err := _store.CreateUser(user); err != nil { + log.Error().Err(err).Msgf("cannot insert %s", user.Login) c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/login?error=internal_error") return } // if another user already have activated repos on behave of that user, // the user was stored as org. now we adopt it to the user. - if org, err := _store.OrgFindByName(u.Login); err == nil && org != nil { + if org, err := _store.OrgFindByName(user.Login); err == nil && org != nil { org.IsUser = true - u.OrgID = org.ID + user.OrgID = org.ID if err := _store.OrgUpdate(org); err != nil { log.Error().Err(err).Msgf("on user creation, could not mark org as user") } @@ -133,76 +132,76 @@ func HandleAuth(c *gin.Context) { return } org = &model.Org{ - Name: u.Login, + Name: user.Login, IsUser: true, Private: false, - ForgeID: u.ForgeID, + ForgeID: user.ForgeID, } if err := _store.OrgCreate(org); err != nil { log.Error().Err(err).Msgf("on user creation, could not create org for user") } - u.OrgID = org.ID + user.OrgID = org.ID } } // update org name - if u.Login != tmpUser.Login { - org, err := _store.OrgGet(u.OrgID) + if user.Login != userFromForge.Login { + org, err := _store.OrgGet(user.OrgID) if err != nil { - log.Error().Err(err).Msgf("cannot get org %s", u.Login) + log.Error().Err(err).Msgf("cannot get org %s", user.Login) c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/login?error=internal_error") return } - org.Name = u.Login + org.Name = user.Login if err := _store.OrgUpdate(org); err != nil { log.Error().Err(err).Msgf("on user creation, could not mark org as user") } } // update the user meta data and authorization data. - u.Token = tmpUser.Token - u.Secret = tmpUser.Secret - u.Email = tmpUser.Email - u.Avatar = tmpUser.Avatar - u.ForgeRemoteID = tmpUser.ForgeRemoteID - u.Login = tmpUser.Login - u.Admin = u.Admin || server.Config.Permissions.Admins.IsAdmin(tmpUser) + user.Token = userFromForge.Token + user.Secret = userFromForge.Secret + user.Email = userFromForge.Email + user.Avatar = userFromForge.Avatar + user.ForgeRemoteID = userFromForge.ForgeRemoteID + user.Login = userFromForge.Login + user.Admin = user.Admin || server.Config.Permissions.Admins.IsAdmin(userFromForge) // if self-registration is enabled for allowed organizations we need to // check the user's organization membership. if server.Config.Permissions.Orgs.IsConfigured { - teams, terr := _forge.Teams(c, u) + teams, terr := _forge.Teams(c, user) if terr != nil || !server.Config.Permissions.Orgs.IsMember(teams) { - log.Error().Err(terr).Msgf("cannot verify team membership for %s", u.Login) - c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/login?error=access_denied") + log.Error().Err(terr).Msgf("cannot verify team membership for %s", user.Login) + c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/login?error=org_access_denied") return } } - if err := _store.UpdateUser(u); err != nil { - log.Error().Err(err).Msgf("cannot update %s", u.Login) + if err := _store.UpdateUser(user); err != nil { + log.Error().Err(err).Msgf("cannot update %s", user.Login) c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/login?error=internal_error") return } exp := time.Now().Add(server.Config.Server.SessionExpires).Unix() _token := token.New(token.SessToken) - _token.Set("user-id", strconv.FormatInt(u.ID, 10)) - tokenString, err := _token.SignExpires(u.Hash, exp) + _token.Set("user-id", strconv.FormatInt(user.ID, 10)) + tokenString, err := _token.SignExpires(user.Hash, exp) if err != nil { - log.Error().Msgf("cannot create token for %s", u.Login) + log.Error().Msgf("cannot create token for %s", user.Login) c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/login?error=internal_error") return } - repos, _ := _forge.Repos(c, u) + repos, _ := _forge.Repos(c, user) for _, forgeRepo := range repos { dbRepo, err := _store.GetRepoForgeID(forgeRepo.ForgeRemoteID) if err != nil && errors.Is(err, types.RecordNotExist) { continue } if err != nil { - log.Error().Err(err).Msgf("cannot list repos for %s", u.Login) + log.Error().Err(err).Msgf("cannot list repos for %s", user.Login) c.Redirect(http.StatusSeeOther, "/login?error=internal_error") return } @@ -211,14 +210,14 @@ func HandleAuth(c *gin.Context) { continue } - log.Debug().Msgf("synced user permission for %s %s", u.Login, dbRepo.FullName) + log.Debug().Msgf("synced user permission for %s %s", user.Login, dbRepo.FullName) perm := forgeRepo.Perm perm.Repo = dbRepo perm.RepoID = dbRepo.ID - perm.UserID = u.ID + perm.UserID = user.ID perm.Synced = time.Now().Unix() if err := _store.PermUpsert(perm); err != nil { - log.Error().Err(err).Msgf("cannot update permissions for %s", u.Login) + log.Error().Err(err).Msgf("cannot update permissions for %s", user.Login) c.Redirect(http.StatusSeeOther, "/login?error=internal_error") return } @@ -235,7 +234,8 @@ func GetLogout(c *gin.Context) { c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/") } -func GetLoginToken(c *gin.Context) { +// TODO: remove in 3.0 +func DeprecatedGetLoginToken(c *gin.Context) { _store := store.FromContext(c) _forge, err := server.Config.Services.Manager.ForgeMain() // TODO: get selected forge from auth request diff --git a/server/forge/bitbucket/bitbucket.go b/server/forge/bitbucket/bitbucket.go index c870477887..60f2bed4bd 100644 --- a/server/forge/bitbucket/bitbucket.go +++ b/server/forge/bitbucket/bitbucket.go @@ -81,7 +81,7 @@ func (c *config) URL() string { // Bitbucket account details are returned when the user is successfully authenticated. func (c *config) Login(ctx context.Context, req *forge_types.OAuthRequest) (*model.User, string, error) { config := c.newOAuth2Config() - redirectURL := config.AuthCodeURL("woodpecker") + redirectURL := config.AuthCodeURL(req.State) // get the OAuth errors if req.Error != "" { diff --git a/server/forge/bitbucketdatacenter/bitbucketdatacenter.go b/server/forge/bitbucketdatacenter/bitbucketdatacenter.go index 573ee52e38..f59e7dbf94 100644 --- a/server/forge/bitbucketdatacenter/bitbucketdatacenter.go +++ b/server/forge/bitbucketdatacenter/bitbucketdatacenter.go @@ -96,8 +96,8 @@ func (c *client) URL() string { func (c *client) Login(ctx context.Context, req *forge_types.OAuthRequest) (*model.User, string, error) { config := c.newOAuth2Config() - // TODO: Add proper state and pkce (https://oauth.net/2/pkce/) ... - redirectURL := config.AuthCodeURL("woodpecker") + // TODO: Use pkce flow (https://oauth.net/2/pkce/) ... + redirectURL := config.AuthCodeURL(req.State) if req.Error != "" { return nil, redirectURL, &forge_types.AuthError{ diff --git a/server/forge/forgejo/forgejo.go b/server/forge/forgejo/forgejo.go index 9435028626..0cfebdedf2 100644 --- a/server/forge/forgejo/forgejo.go +++ b/server/forge/forgejo/forgejo.go @@ -111,7 +111,7 @@ func (c *Forgejo) oauth2Config(ctx context.Context) (*oauth2.Config, context.Con // Forgejo account details are returned when the user is successfully authenticated. func (c *Forgejo) Login(ctx context.Context, req *forge_types.OAuthRequest) (*model.User, string, error) { config, oauth2Ctx := c.oauth2Config(ctx) - redirectURL := config.AuthCodeURL("woodpecker") + redirectURL := config.AuthCodeURL(req.State) // check the OAuth errors if req.Error != "" { diff --git a/server/forge/gitea/gitea.go b/server/forge/gitea/gitea.go index 11e5c88923..960c81e88f 100644 --- a/server/forge/gitea/gitea.go +++ b/server/forge/gitea/gitea.go @@ -113,7 +113,7 @@ func (c *Gitea) oauth2Config(ctx context.Context) (*oauth2.Config, context.Conte // Gitea account details are returned when the user is successfully authenticated. func (c *Gitea) Login(ctx context.Context, req *forge_types.OAuthRequest) (*model.User, string, error) { config, oauth2Ctx := c.oauth2Config(ctx) - redirectURL := config.AuthCodeURL("woodpecker") + redirectURL := config.AuthCodeURL(req.State) // check the OAuth errors if req.Error != "" { diff --git a/server/forge/github/github.go b/server/forge/github/github.go index 0e62413cf4..3f4204ed2e 100644 --- a/server/forge/github/github.go +++ b/server/forge/github/github.go @@ -100,7 +100,7 @@ func (c *client) URL() string { // Login authenticates the session and returns the forge user details. func (c *client) Login(ctx context.Context, req *forge_types.OAuthRequest) (*model.User, string, error) { config := c.newConfig() - redirectURL := config.AuthCodeURL("woodpecker") + redirectURL := config.AuthCodeURL(req.State) // check the OAuth errors if req.Error != "" { diff --git a/server/forge/gitlab/gitlab.go b/server/forge/gitlab/gitlab.go index 27cd50cb7f..d092fd77a5 100644 --- a/server/forge/gitlab/gitlab.go +++ b/server/forge/gitlab/gitlab.go @@ -115,7 +115,7 @@ func (g *GitLab) oauth2Config(ctx context.Context) (*oauth2.Config, context.Cont // forge user details. func (g *GitLab) Login(ctx context.Context, req *forge_types.OAuthRequest) (*model.User, string, error) { config, oauth2Ctx := g.oauth2Config(ctx) - redirectURL := config.AuthCodeURL("woodpecker") + redirectURL := config.AuthCodeURL(req.State) // check the OAuth errors if req.Error != "" { diff --git a/server/forge/types/oauth.go b/server/forge/types/oauth.go index fba870569c..c2ea8f4c51 100644 --- a/server/forge/types/oauth.go +++ b/server/forge/types/oauth.go @@ -19,4 +19,5 @@ type OAuthRequest struct { ErrorURI string ErrorDescription string Code string + State string } diff --git a/server/router/router.go b/server/router/router.go index 317f0975d2..89e1046f90 100644 --- a/server/router/router.go +++ b/server/router/router.go @@ -58,12 +58,11 @@ func Load(noRouteHandler http.HandlerFunc, middleware ...gin.HandlerFunc) http.H base.GET("/web-config.js", web.Config) base.GET("/logout", api.GetLogout) - base.GET("/login", api.HandleLogin) auth := base.Group("/authorize") { auth.GET("", api.HandleAuth) auth.POST("", api.HandleAuth) - auth.POST("/token", api.GetLoginToken) + auth.POST("/token", api.DeprecatedGetLoginToken) } base.GET("/metrics", metrics.PromHandler()) diff --git a/web/src/assets/locales/en.json b/web/src/assets/locales/en.json index a6586152e7..7c901aaa79 100644 --- a/web/src/assets/locales/en.json +++ b/web/src/assets/locales/en.json @@ -409,10 +409,7 @@ "reset_token": "Reset token", "swagger_ui": "Swagger UI" } - }, - "oauth_error": "Error while authenticating against OAuth provider", - "internal_error": "Some internal error occurred", - "access_denied": "You are not allowed to login" + } }, "secrets": { "secrets": "Secrets", @@ -451,5 +448,10 @@ "cli_login_failed": "Login to CLI failed", "cli_login_denied": "Login to CLI denied", "return_to_cli": "You can now close this tab and return to the CLI.", - "settings": "Settings" + "settings": "Settings", + "oauth_error": "Error while authenticating against OAuth provider", + "internal_error": "Some internal error occurred", + "auth_registration_closed": "The registration is closed", + "auth_org_access_denied": "You are not part of the required organization to access this instance", + "unknown_auth_error": "An unknown authentication error occurred: {error}" } diff --git a/web/src/components/admin/settings/AdminInfoTab.vue b/web/src/components/admin/settings/AdminInfoTab.vue index 8f91823616..da965f7eea 100644 --- a/web/src/components/admin/settings/AdminInfoTab.vue +++ b/web/src/components/admin/settings/AdminInfoTab.vue @@ -29,6 +29,7 @@ diff --git a/web/src/components/atomic/Warning.vue b/web/src/components/atomic/Warning.vue index 418d8b525e..bf4cb74456 100644 --- a/web/src/components/atomic/Warning.vue +++ b/web/src/components/atomic/Warning.vue @@ -12,6 +12,6 @@ diff --git a/web/src/compositions/useAuthentication.ts b/web/src/compositions/useAuthentication.ts index bd7b6d0b11..d8bcb21fda 100644 --- a/web/src/compositions/useAuthentication.ts +++ b/web/src/compositions/useAuthentication.ts @@ -12,6 +12,6 @@ export default () => const config = useUserConfig(); config.setUserConfig('redirectUrl', url); } - window.location.href = `${useConfig().rootPath}/login`; + window.location.href = `${useConfig().rootPath}/authorize`; }, }) as const; diff --git a/web/src/router.ts b/web/src/router.ts index 21f97afac1..7e04c73973 100644 --- a/web/src/router.ts +++ b/web/src/router.ts @@ -153,14 +153,7 @@ const routes: RouteRecordRaw[] = [ props: true, }, { - path: `${rootPath}/login/error`, - name: 'login-error', - component: (): Component => import('~/views/Login.vue'), - meta: { blank: true }, - props: true, - }, - { - path: `${rootPath}/do-login`, + path: `${rootPath}/login`, name: 'login', component: (): Component => import('~/views/Login.vue'), meta: { blank: true }, diff --git a/web/src/views/Login.vue b/web/src/views/Login.vue index d6fcec0b98..ce24c24000 100644 --- a/web/src/views/Login.vue +++ b/web/src/views/Login.vue @@ -1,6 +1,5 @@ diff --git a/web/src/components/atomic/Warning.vue b/web/src/components/atomic/Warning.vue index bf4cb74456..f30885fac4 100644 --- a/web/src/components/atomic/Warning.vue +++ b/web/src/components/atomic/Warning.vue @@ -4,7 +4,7 @@ > - {{ text }} + {{ text }} diff --git a/web/src/views/Login.vue b/web/src/views/Login.vue index ce24c24000..76d4abf412 100644 --- a/web/src/views/Login.vue +++ b/web/src/views/Login.vue @@ -1,6 +1,6 @@