1
0
mirror of https://github.com/woodpecker-ci/woodpecker.git synced 2024-12-24 10:07:21 +02:00

Use IDs for tokens (#3695)

This commit is contained in:
Anbraten 2024-05-27 11:52:43 +02:00 committed by GitHub
parent 6132136d55
commit fd57e4ad05
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 83 additions and 33 deletions

View File

@ -2,6 +2,13 @@
Some versions need some changes to the server configuration or the pipeline configuration files.
<!--
## 3.0.0
- Update all webhooks by pressing the "Repair all" button in the admin settings as the webhook token claims have changed
-->
## `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)

View File

@ -20,6 +20,7 @@ import (
"errors"
"fmt"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
@ -157,7 +158,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)
@ -177,7 +178,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 {
@ -186,7 +187,9 @@ func PostHook(c *gin.Context) {
c.String(http.StatusBadRequest, msg)
return
}
verifiedKey := parsed.Text == oldFullName
// 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)
if err != nil {
@ -198,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
@ -208,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 {

View File

@ -18,6 +18,7 @@ import (
"encoding/base32"
"errors"
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
@ -138,7 +139,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
}
@ -185,7 +186,9 @@ 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)
_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")
@ -262,7 +265,8 @@ func GetLoginToken(c *gin.Context) {
}
exp := time.Now().Add(server.Config.Server.SessionExpires).Unix()
newToken := token.New(token.SessToken, user.Login)
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)

View File

@ -118,7 +118,8 @@ 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)
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, repo.FullName)
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())
@ -536,7 +538,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())
@ -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, repo.FullName)
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())

View File

@ -153,7 +153,9 @@ func GetRepos(c *gin.Context) {
// @Param Authorization header string true "Insert your personal access token" default(Bearer <personal access token>)
func PostToken(c *gin.Context) {
user := session.User(c)
tokenString, err := token.New(token.UserToken, user.Login).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, user.Login).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

View File

@ -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.Get("user-id"), 10, 64)
if err != nil {
return "", err
}
user, err = store.FromContext(c).GetUser(userID)
return user.Hash, err
})
if err == nil {

View File

@ -17,6 +17,7 @@ package web
import (
"encoding/json"
"net/http"
"strconv"
"text/template"
"github.com/gin-gonic/gin"
@ -33,10 +34,9 @@ func Config(c *gin.Context) {
var csrf string
if user != nil {
csrf, _ = token.New(
token.CsrfToken,
user.Login,
).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

View File

@ -36,12 +36,14 @@ const (
const SignerAlgo = "HS256"
type Token struct {
Kind string
Text string
Kind string
claims jwt.MapClaims
}
func parse(raw string, fn SecretFunc) (*Token, error) {
token := &Token{}
token := &Token{
claims: jwt.MapClaims{},
}
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, claims: jwt.MapClaims{}}
}
// Sign signs the token using the given secret hash
@ -117,14 +119,32 @@ 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
claims["text"] = t.Text
if exp > 0 {
claims["exp"] = float64(exp)
}
return token.SignedString([]byte(secret))
}
func (t *Token) Set(key, value string) {
t.claims[key] = value
}
func (t *Token) Get(key string) string {
claim, ok := t.claims[key].(string)
if !ok {
return ""
}
return claim
}
func keyFunc(token *Token, fn SecretFunc) jwt.Keyfunc {
return func(t *jwt.Token) (any, error) {
claims, ok := t.Claims.(jwt.MapClaims)
@ -137,21 +157,26 @@ 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 token value and cast to
// expected type.
text, ok := claims["text"]
if !ok {
return nil, jwt.ErrInvalidType
// copy custom claims
for k, v := range claims {
// 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 k == "type" {
continue
}
token.claims[k] = v
}
token.Text, _ = text.(string)
// invoke the callback function to retrieve
// the secret key used to verify