Files
gotify-server/auth/authentication.go
2023-08-06 12:30:22 +02:00

195 lines
5.7 KiB
Go

package auth
import (
"errors"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/gotify/server/v2/auth/password"
"github.com/gotify/server/v2/model"
)
const (
headerName = "X-Gotify-Key"
)
// The Database interface for encapsulating database access.
type Database interface {
GetApplicationByToken(token string) (*model.Application, error)
GetClientByToken(token string) (*model.Client, error)
GetPluginConfByToken(token string) (*model.PluginConf, error)
GetUserByName(name string) (*model.User, error)
GetUserByID(id uint) (*model.User, error)
UpdateClientTokensLastUsed(tokens []string, t *time.Time) error
UpdateApplicationTokenLastUsed(token string, t *time.Time) error
}
// Auth is the provider for authentication middleware.
type Auth struct {
DB Database
}
type authenticate func(tokenID string, user *model.User) (authenticated, success bool, userId uint, err error)
// RequireAdmin returns a gin middleware which requires a client token or basic authentication header to be supplied
// with the request. Also the authenticated user must be an administrator.
func (a *Auth) RequireAdmin() gin.HandlerFunc {
return a.requireToken(func(tokenID string, user *model.User) (bool, bool, uint, error) {
if user != nil {
return true, user.Admin, user.ID, nil
}
if token, err := a.DB.GetClientByToken(tokenID); err != nil {
return false, false, 0, err
} else if token != nil {
user, err := a.DB.GetUserByID(token.UserID)
if err != nil {
return false, false, token.UserID, err
}
return true, user.Admin, token.UserID, nil
}
return false, false, 0, nil
})
}
// RequireClient returns a gin middleware which requires a client token or basic authentication header to be supplied
// with the request.
func (a *Auth) RequireClient() gin.HandlerFunc {
return a.requireToken(func(tokenID string, user *model.User) (bool, bool, uint, error) {
if user != nil {
return true, true, user.ID, nil
}
if client, err := a.DB.GetClientByToken(tokenID); err != nil {
return false, false, 0, err
} else if client != nil {
now := time.Now()
if client.LastUsed == nil || client.LastUsed.Add(5*time.Minute).Before(now) {
if err := a.DB.UpdateClientTokensLastUsed([]string{tokenID}, &now); err != nil {
return false, false, 0, err
}
}
return true, true, client.UserID, nil
}
return false, false, 0, nil
})
}
// RequireApplicationToken returns a gin middleware which requires an application token to be supplied with the request.
func (a *Auth) RequireApplicationToken() gin.HandlerFunc {
return a.requireToken(func(tokenID string, user *model.User) (bool, bool, uint, error) {
if user != nil {
return true, false, 0, nil
}
if app, err := a.DB.GetApplicationByToken(tokenID); err != nil {
return false, false, 0, err
} else if app != nil {
now := time.Now()
if app.LastUsed == nil || app.LastUsed.Add(5*time.Minute).Before(now) {
if err := a.DB.UpdateApplicationTokenLastUsed(tokenID, &now); err != nil {
return false, false, 0, err
}
}
return true, true, app.UserID, nil
}
return false, false, 0, nil
})
}
func (a *Auth) tokenFromQueryOrHeader(ctx *gin.Context) string {
if token := a.tokenFromQuery(ctx); token != "" {
return token
} else if token := a.tokenFromXGotifyHeader(ctx); token != "" {
return token
} else if token := a.tokenFromAuthorizationHeader(ctx); token != "" {
return token
}
return ""
}
func (a *Auth) tokenFromQuery(ctx *gin.Context) string {
return ctx.Request.URL.Query().Get("token")
}
func (a *Auth) tokenFromXGotifyHeader(ctx *gin.Context) string {
return ctx.Request.Header.Get(headerName)
}
func (a *Auth) tokenFromAuthorizationHeader(ctx *gin.Context) string {
const prefix = "Bearer "
authHeader := ctx.Request.Header.Get("Authorization")
if authHeader == "" {
return ""
}
if len(authHeader) < len(prefix) || !strings.EqualFold(prefix, authHeader[:len(prefix)]) {
return ""
}
return authHeader[len(prefix):]
}
func (a *Auth) userFromBasicAuth(ctx *gin.Context) (*model.User, error) {
if name, pass, ok := ctx.Request.BasicAuth(); ok {
if user, err := a.DB.GetUserByName(name); err != nil {
return nil, err
} else if user != nil && password.ComparePassword(user.Pass, []byte(pass)) {
return user, nil
}
}
return nil, nil
}
func (a *Auth) requireToken(auth authenticate) gin.HandlerFunc {
return func(ctx *gin.Context) {
token := a.tokenFromQueryOrHeader(ctx)
user, err := a.userFromBasicAuth(ctx)
if err != nil {
ctx.AbortWithError(500, errors.New("an error occurred while authenticating user"))
return
}
if user != nil || token != "" {
authenticated, ok, userID, err := auth(token, user)
if err != nil {
ctx.AbortWithError(500, errors.New("an error occurred while authenticating user"))
return
} else if ok {
RegisterAuthentication(ctx, user, userID, token)
ctx.Next()
return
} else if authenticated {
ctx.AbortWithError(403, errors.New("you are not allowed to access this api"))
return
}
}
ctx.AbortWithError(401, errors.New("you need to provide a valid access token or user credentials to access this api"))
}
}
func (a *Auth) Optional() gin.HandlerFunc {
return func(ctx *gin.Context) {
token := a.tokenFromQueryOrHeader(ctx)
user, err := a.userFromBasicAuth(ctx)
if err != nil {
RegisterAuthentication(ctx, nil, 0, "")
ctx.Next()
return
}
if user != nil {
RegisterAuthentication(ctx, user, user.ID, token)
ctx.Next()
return
} else if token != "" {
if tokenClient, err := a.DB.GetClientByToken(token); err == nil && tokenClient != nil {
RegisterAuthentication(ctx, user, tokenClient.UserID, token)
ctx.Next()
return
}
}
RegisterAuthentication(ctx, nil, 0, "")
ctx.Next()
}
}