From fb2a1c59db7f922a373b94e5cb5af92694ffadcf Mon Sep 17 00:00:00 2001 From: kolaente Date: Thu, 31 Aug 2023 21:39:26 +0200 Subject: [PATCH] feat(api tokens): check if a provided token matched a hashed on in the database --- pkg/models/api_tokens.go | 6 ++++-- pkg/modules/auth/auth.go | 17 ++++++++++++++++- pkg/routes/routes.go | 38 +++++++++++++++++++++++++++++++++++++- 3 files changed, 57 insertions(+), 4 deletions(-) diff --git a/pkg/models/api_tokens.go b/pkg/models/api_tokens.go index ed2e5cb8e79..4a037aeba87 100644 --- a/pkg/models/api_tokens.go +++ b/pkg/models/api_tokens.go @@ -39,7 +39,7 @@ type APIToken struct { // A human-readable name for this token Title string `xorm:"not null" json:"title" valid:"required"` // The actual api key. Only visible after creation. - Token string `xorm:"-" json:"key,omitempty"` + Token string `xorm:"-" json:"token,omitempty"` TokenSalt string `xorm:"not null" json:"-"` TokenHash string `xorm:"not null unique" json:"-"` TokenLastEight string `xorm:"not null index varchar(8)" json:"-"` @@ -59,6 +59,8 @@ type APIToken struct { web.CRUDable `xorm:"-" json:"-"` } +const APITokenPrefix = `tk_` + func (*APIToken) TableName() string { return "api_tokens" } @@ -94,7 +96,7 @@ func (t *APIToken) Create(s *xorm.Session, a web.Auth) (err error) { return err } t.TokenSalt = salt - t.Token = "tk_" + hex.EncodeToString(token) + t.Token = APITokenPrefix + hex.EncodeToString(token) t.TokenHash = HashToken(t.Token, t.TokenSalt) t.TokenLastEight = t.Token[len(t.Token)-8:] diff --git a/pkg/modules/auth/auth.go b/pkg/modules/auth/auth.go index 761d9eb551b..f61a8e99dd5 100644 --- a/pkg/modules/auth/auth.go +++ b/pkg/modules/auth/auth.go @@ -17,6 +17,8 @@ package auth import ( + "code.vikunja.io/api/pkg/db" + "fmt" "net/http" "time" @@ -101,7 +103,20 @@ func NewLinkShareJWTAuthtoken(share *models.LinkSharing) (token string, err erro // GetAuthFromClaims returns a web.Auth object from jwt claims func GetAuthFromClaims(c echo.Context) (a web.Auth, err error) { - jwtinf := c.Get("user").(*jwt.Token) + // check if we have a token in context and use it if that's the case + if c.Get("api_token") != nil { + apiToken := c.Get("api_token").(*models.APIToken) + u, err := user.GetUserByID(db.NewSession(), apiToken.OwnerID) + if err != nil { + return nil, err + } + return u, nil + } + + jwtinf, is := c.Get("user").(*jwt.Token) + if !is { + return nil, fmt.Errorf("user in context is not jwt token") + } claims := jwtinf.Claims.(jwt.MapClaims) typ := int(claims["type"].(float64)) if typ == AuthTypeLinkShare && config.ServiceEnableLinkSharing.GetBool() { diff --git a/pkg/routes/routes.go b/pkg/routes/routes.go index ba872d98d2d..4350d17abbe 100644 --- a/pkg/routes/routes.go +++ b/pkg/routes/routes.go @@ -51,6 +51,7 @@ package routes import ( "errors" + "net/http" "net/url" "strings" "time" @@ -280,7 +281,42 @@ func registerAPIRoutes(a *echo.Group) { } // ===== Routes with Authentication ===== - a.Use(echojwt.JWT([]byte(config.ServiceJWTSecret.GetString()))) + a.Use(echojwt.WithConfig(echojwt.Config{ + SigningKey: []byte(config.ServiceJWTSecret.GetString()), + Skipper: func(c echo.Context) bool { + authHeader := c.Request().Header.Values("Authorization") + if len(authHeader) == 0 { + return false // let the jwt middleware handle invalid headers + } + + for _, s := range authHeader { + if strings.HasPrefix(s, "Bearer "+models.APITokenPrefix) { + c.Set("api_token", s) + return true + } + } + + return false + }, + })) + + a.Use(func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + // If this is empty we assume we're dealing with a "real" user who has provided a jwt + tokenString, is := c.Get("api_token").(string) + if !is || tokenString == "" { + return next(c) + } + token, err := models.GetTokenFromTokenString(db.NewSession(), strings.TrimPrefix(tokenString, "Bearer ")) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError).SetInternal(err) + } + + c.Set("api_token", token) + + return next(c) + } + }) // Rate limit setupRateLimit(a, config.RateLimitKind.GetString())