Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use IDs for tokens #3695

Merged
merged 14 commits into from
May 27, 2024
7 changes: 7 additions & 0 deletions docs/docs/91-migrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
13 changes: 8 additions & 5 deletions server/api/hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"errors"
"fmt"
"net/http"
"strconv"

"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
Expand Down Expand Up @@ -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)
Expand All @@ -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 {
Expand All @@ -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
qwerty287 marked this conversation as resolved.
Show resolved Hide resolved
if !verifiedKey {
verifiedKey, err = _store.HasRedirectionForRepo(repo.ID, repo.FullName)
if err != nil {
Expand All @@ -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
Expand All @@ -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 {
Expand Down
10 changes: 7 additions & 3 deletions server/api/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"encoding/base32"
"errors"
"net/http"
"strconv"
"time"

"github.com/gin-gonic/gin"
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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)
Expand Down
11 changes: 7 additions & 4 deletions server/api/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down Expand Up @@ -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())
Expand All @@ -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())
Expand Down Expand Up @@ -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())
Expand Down
8 changes: 6 additions & 2 deletions server/api/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion server/router/middleware/session/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
8 changes: 4 additions & 4 deletions server/web/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package web
import (
"encoding/json"
"net/http"
"strconv"
"text/template"

"github.com/gin-gonic/gin"
Expand All @@ -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
Expand Down
39 changes: 28 additions & 11 deletions shared/token/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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" || k == "nbf" || k == "iat" || k == "aud" || k == "iss" || k == "sub" || k == "jti" {
anbraten marked this conversation as resolved.
Show resolved Hide resolved
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
Expand Down