From 0dc593fc2100dd8538411bf0af68ed2b261c540c Mon Sep 17 00:00:00 2001 From: aunefyren Date: Sat, 21 Oct 2023 17:17:05 +0200 Subject: [PATCH] Basic auth in headers and depency updates --- go.mod | 6 +- go.sum | 10 ++- models/methods.go | 12 ++- models/models.go | 6 +- modules/authorize.go | 204 +++++++++++++++++++++++++++++++++---------- modules/payload.go | 19 ++-- routes/admin_auth.go | 12 +-- routes/statistics.go | 2 +- routes/user_auth.go | 8 +- 9 files changed, 205 insertions(+), 74 deletions(-) diff --git a/go.mod b/go.mod index 32ac4f2..2fc48c3 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,11 @@ module aunefyren/wrapperr go 1.19 require ( - github.com/golang-jwt/jwt/v4 v4.5.0 - github.com/google/uuid v1.3.0 + github.com/golang-jwt/jwt/v5 v5.0.0 + github.com/google/uuid v1.3.1 github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e github.com/gorilla/mux v1.8.0 github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 github.com/patrickmn/sortutil v0.0.0-20120526081524-abeda66eb583 - golang.org/x/crypto v0.6.0 + golang.org/x/crypto v0.14.0 ) diff --git a/go.sum b/go.sum index f840bc5..53044bd 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,11 @@ -github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= -github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v5 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v5 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= +github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e h1:XmA6L9IPRdUr28a+SK/oMchGgQy159wvzXA5tJ7l+40= github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e/go.mod h1:AFIo+02s+12CEg8Gzz9kzhCbmbq6JcKNrhHffCGA9z4= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= @@ -12,3 +16,5 @@ github.com/patrickmn/sortutil v0.0.0-20120526081524-abeda66eb583 h1:+gFSK6FP5Ky3 github.com/patrickmn/sortutil v0.0.0-20120526081524-abeda66eb583/go.mod h1:DyFPU22sg+Or/eRPmpwVVp0fUw+aQSYddY0DzzNjSN4= golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= diff --git a/models/methods.go b/models/methods.go index 01cefad..8daeebd 100644 --- a/models/methods.go +++ b/models/methods.go @@ -12,9 +12,15 @@ var ( ) // Valid checks if the token payload is valid or not -func (payload *Payload) Valid() error { - if time.Now().After(payload.ExpiredAt) { - return ErrExpiredToken +func (payload *Payload) Valid() (err error) { + now := time.Now() + if payload.RegisteredClaims.ExpiresAt.Time.Before(now) { + err = errors.New("Token has expired.") + return + } + if payload.RegisteredClaims.NotBefore.Time.After(now) { + err = errors.New("Token has not begun.") + return } return nil } diff --git a/models/models.go b/models/models.go index 1f774d0..1311c00 100644 --- a/models/models.go +++ b/models/models.go @@ -3,7 +3,7 @@ package models import ( "time" - "github.com/golang-jwt/jwt/v4" + "github.com/golang-jwt/jwt/v5" "github.com/google/uuid" ) @@ -22,13 +22,11 @@ type CustomPayload struct { */ type Payload struct { - jwt.Claims + jwt.RegisteredClaims ID uuid.UUID `json:"id"` Username string `json:"username"` Admin bool `json:"admin"` AuthToken string `json:"authtoken"` - IssuedAt time.Time `json:"issued_at"` - ExpiredAt time.Time `json:"expired_at"` } type JWTMaker struct { diff --git a/modules/authorize.go b/modules/authorize.go index db9a5ce..2e633da 100644 --- a/modules/authorize.go +++ b/modules/authorize.go @@ -3,77 +3,165 @@ package modules import ( "aunefyren/wrapperr/files" "aunefyren/wrapperr/models" + "aunefyren/wrapperr/utilities" + "encoding/base64" "errors" "log" "net/http" "strings" "time" - "github.com/golang-jwt/jwt/v4" + "github.com/golang-jwt/jwt/v5" ) // AuthorizeToken validates JWT tokens using the private key. -func AuthorizeToken(writer http.ResponseWriter, request *http.Request) (*models.Payload, error) { +func AuthorizeToken(writer http.ResponseWriter, request *http.Request, admin bool) (payload *models.Payload, err error) { + payload = &models.Payload{} + err = nil - PrivateKey, err := files.GetPrivateKey() + config, err := files.GetConfig() if err != nil { - log.Println("Failed to load JWT Token settings. Error: ") - log.Println(err) - return &models.Payload{}, errors.New("Failed to load JWT Token settings.") + log.Println("Failed to load JWT Token settings. Error: " + err.Error()) + return payload, errors.New("Failed to load JWT Token settings.") + } + + adminConfig, err := files.GetAdminConfig() + if err != nil { + log.Println("Failed to load admin settings. Error: " + err.Error()) + return payload, errors.New("Failed to load admin settings.") } // Check if Authorization header is available - header := request.Header.Get("Authorization") - if header == "" || !strings.Contains(header, " ") || !strings.Contains(strings.ToLower(header), "bearer") { + authHeader := request.Header.Get("Authorization") + if authHeader == "" || !strings.Contains(authHeader, " ") { log.Println("No valid Authorization token found in header during API request.") - return &models.Payload{}, errors.New("No valid Authorization token found in header.") + return payload, errors.New("No valid Authorization token found in header.") } - headerParts := strings.Split(header, " ") - + // Split header + headerParts := strings.Split(authHeader, " ") if len(headerParts) < 2 { - log.Println("Failed to parse header. Error: ") - log.Println(err) - return &models.Payload{}, errors.New("Failed to parse header.") + log.Println("Failed to parse header. Error: " + err.Error()) + return payload, errors.New("Failed to parse header.") } - jwtToken := headerParts[1] + token := headerParts[1] + authType := "" - payload, err := VerifyToken(PrivateKey, jwtToken) - if err != nil { - log.Println("Session token not accepted. Error: ") - log.Println(err) - return &models.Payload{}, errors.New("Session token not accepted. Please relog.") + // Define header type + switch strings.TrimSpace(strings.ToLower(headerParts[0])) { + case "bearer": + authType = "bearer" + case "basic": + authType = "basic" + default: + return payload, errors.New("Authorization header not recognized.") + } + + // Switch auth based on header type + switch authType { + case "bearer": + payload, err = ParseToken(token, config.PrivateKey) + if err != nil { + log.Println("Session token not accepted. Error: ") + log.Println(err) + return payload, errors.New("Session token not accepted. Please relog.") + } + + if admin { + if !payload.Admin { + return payload, errors.New("Session is not an admin session.") + } + } + case "basic": + rawDecodedtoken, err := base64.StdEncoding.DecodeString(token) + if err != nil { + return payload, errors.New("Failed to decode Base64.") + } + authParts := strings.Split(string(rawDecodedtoken), ":") + if len(headerParts) < 2 { + log.Println("Failed to parse basic auth header. Error: " + err.Error()) + return payload, errors.New("Failed to parse basic auth header.") + } + err = validateBasicAuth(authParts[0], authParts[1]) + if err != nil { + log.Println("Failed to validate basic auth header. Error: " + err.Error()) + return payload, errors.New("Failed to validate basic auth header.") + } + _, payloadTwo, err := CreateTokenTwo(config.PrivateKey, adminConfig.AdminUsername, true, "", time.Now()) + if err != nil { + log.Println("Failed to create token. Error: " + err.Error()) + return payload, errors.New("Failed to create token.") + } + payload = &payloadTwo } return payload, nil } -// VerifyToken checks if the token is valid or not -func VerifyToken(PrivateKey string, token string) (*models.Payload, error) { - keyFunc := func(token *jwt.Token) (interface{}, error) { - _, ok := token.Method.(*jwt.SigningMethodHMAC) - if !ok { - return nil, models.ErrInvalidToken - } - return []byte(PrivateKey), nil - } - - jwtToken, err := jwt.ParseWithClaims(token, &models.Payload{}, keyFunc) +func ValidateToken(signedToken string, privateKey string) (err error) { + token, err := jwt.ParseWithClaims( + signedToken, + &models.Payload{}, + func(token *jwt.Token) (interface{}, error) { + return []byte(privateKey), nil + }, + ) if err != nil { - verr, ok := err.(*jwt.ValidationError) - if ok && errors.Is(verr.Inner, jwt.ErrTokenExpired) { - return nil, models.ErrExpiredToken - } - return nil, models.ErrInvalidToken + return } - - payload, ok := jwtToken.Claims.(*models.Payload) + claims, ok := token.Claims.(*models.Payload) if !ok { - return nil, models.ErrInvalidToken + err = errors.New("Couldn't parse claims.") + return + } else if claims.ExpiresAt == nil || claims.NotBefore == nil { + err = errors.New("Claims not present.") + return + } + now := time.Now() + if claims.ExpiresAt.Time.Before(now) { + err = errors.New("Token has expired.") + return + } + if claims.NotBefore.Time.After(now) { + err = errors.New("Token has not begun.") + return } - return payload, nil + return +} + +func ParseToken(signedToken string, privateKey string) (*models.Payload, error) { + + token, err := jwt.ParseWithClaims( + signedToken, + &models.Payload{}, + func(token *jwt.Token) (interface{}, error) { + return []byte(privateKey), nil + }, + ) + if err != nil { + return nil, err + } + claims, ok := token.Claims.(*models.Payload) + if !ok { + err = errors.New("Couldn't parse claims") + return nil, err + } else if claims.ExpiresAt == nil || claims.NotBefore == nil { + err = errors.New("Claims not present.") + return nil, err + } + now := time.Now() + if claims.ExpiresAt.Time.Before(now) { + err = errors.New("Token has expired.") + return nil, err + } + if claims.NotBefore.Time.After(now) { + err = errors.New("Token has not begun.") + return nil, err + } + return claims, nil + } // CreateToken creates a new JWT token used to validate a users session. Valid for three days by default. @@ -86,7 +174,7 @@ func CreateToken(username string, admin bool, authtoken string) (string, error) return "", errors.New("Failed to load JWT Token settings.") } - duration := time.Minute * 60 * 24 * 3 + duration := time.Now().Add(time.Hour * 24 * 3) token, _, err := CreateTokenTwo(PrivateKey, username, admin, authtoken, duration) if err != nil { @@ -99,13 +187,37 @@ func CreateToken(username string, admin bool, authtoken string) (string, error) } // CreateToken creates a new token for a specific username and duration -func CreateTokenTwo(PrivateKey string, username string, admin bool, authtoken string, duration time.Duration) (string, *models.Payload, error) { - payload, err := NewPayload(username, admin, authtoken, duration) +func CreateTokenTwo(PrivateKey string, username string, admin bool, authtoken string, duration time.Time) (token string, payload models.Payload, err error) { + token = "" + payload = models.Payload{} + err = nil + + payload, err = NewPayload(username, admin, authtoken, duration) if err != nil { - return "", payload, err + return token, payload, err } - jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, payload) - token, err := jwtToken.SignedString([]byte(PrivateKey)) + tokenUnsigned := jwt.NewWithClaims(jwt.SigningMethodHS256, payload) + token, err = tokenUnsigned.SignedString([]byte(PrivateKey)) + return token, payload, err } + +func validateBasicAuth(username string, password string) (err error) { + err = nil + + adminConfig, err := files.GetAdminConfig() + if err != nil { + log.Println("Failed to load admin settings. Error: " + err.Error()) + return errors.New("Failed to load admin settings.") + } + + passwordValidity := utilities.ComparePasswords(adminConfig.AdminPassword, password) + + if username != adminConfig.AdminUsername || !passwordValidity { + return errors.New("Non-valid credentials.") + } + + return nil + +} diff --git a/modules/payload.go b/modules/payload.go index ae8a60e..1346238 100644 --- a/modules/payload.go +++ b/modules/payload.go @@ -4,23 +4,32 @@ import ( "aunefyren/wrapperr/models" "time" + "github.com/golang-jwt/jwt/v5" "github.com/google/uuid" ) // NewPayload creates a new token payload with a specific username and duration -func NewPayload(username string, admin bool, authtoken string, duration time.Duration) (*models.Payload, error) { +func NewPayload(username string, admin bool, authtoken string, duration time.Time) (payload models.Payload, err error) { + payload = models.Payload{} + err = nil + tokenID, err := uuid.NewRandom() if err != nil { - return nil, err + return payload, err } - payload := &models.Payload{ + payload = models.Payload{ ID: tokenID, Username: username, Admin: admin, AuthToken: authtoken, - IssuedAt: time.Now(), - ExpiredAt: time.Now().Add(duration), + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(duration), + NotBefore: jwt.NewNumericDate(time.Now()), + IssuedAt: jwt.NewNumericDate(time.Now()), + Issuer: "Wrapperr", + }, } + return payload, nil } diff --git a/routes/admin_auth.go b/routes/admin_auth.go index 71d2fe0..77b1ae9 100644 --- a/routes/admin_auth.go +++ b/routes/admin_auth.go @@ -17,7 +17,7 @@ import ( // API route used to retrieve the Wrapperr configuration file. func ApiGetConfig(w http.ResponseWriter, r *http.Request) { - payload, err := modules.AuthorizeToken(w, r) + payload, err := modules.AuthorizeToken(w, r, true) if err == nil && payload.Admin { @@ -60,7 +60,7 @@ func ApiGetConfig(w http.ResponseWriter, r *http.Request) { // API route used to update the Wrapperr configuration file. func ApiSetConfig(w http.ResponseWriter, r *http.Request) { - payload, err := modules.AuthorizeToken(w, r) + payload, err := modules.AuthorizeToken(w, r, true) if err == nil && payload.Admin { @@ -205,7 +205,7 @@ func ApiUpdateAdmin(w http.ResponseWriter, r *http.Request) { return } else { - payload, err := modules.AuthorizeToken(w, r) + payload, err := modules.AuthorizeToken(w, r, true) if err == nil && payload.Admin { @@ -279,7 +279,7 @@ func ApiUpdateAdmin(w http.ResponseWriter, r *http.Request) { // API route which validates an admin JWT token func ApiValidateAdmin(w http.ResponseWriter, r *http.Request) { - payload, err := modules.AuthorizeToken(w, r) + payload, err := modules.AuthorizeToken(w, r, true) if err == nil && payload.Admin { @@ -287,7 +287,7 @@ func ApiValidateAdmin(w http.ResponseWriter, r *http.Request) { utilities.RespondDefaultOkay(w, r, "The admin login session is valid.") return - } else if !payload.Admin { + } else if err == nil && !payload.Admin { log.Println("User not authenticated as admin.") utilities.RespondDefaultError(w, r, errors.New("User not authenticated as admin."), 401) @@ -305,7 +305,7 @@ func ApiValidateAdmin(w http.ResponseWriter, r *http.Request) { // API route which retrieves lines from the log file func ApiGetLog(w http.ResponseWriter, r *http.Request) { - payload, err := modules.AuthorizeToken(w, r) + payload, err := modules.AuthorizeToken(w, r, true) if err != nil { diff --git a/routes/statistics.go b/routes/statistics.go index 185c425..5e456fd 100644 --- a/routes/statistics.go +++ b/routes/statistics.go @@ -67,7 +67,7 @@ func ApiWrapperGetStatistics(w http.ResponseWriter, r *http.Request) { var admin bool = false // Try to authorize bearer token from header - payload, err := modules.AuthorizeToken(w, r) + payload, err := modules.AuthorizeToken(w, r, false) // If it failed and PlexAuth is enabled, respond with and error // If it didn't fail, and PlexAuth is enabled, declare auth as passed diff --git a/routes/user_auth.go b/routes/user_auth.go index 13b0a16..3dad3dd 100644 --- a/routes/user_auth.go +++ b/routes/user_auth.go @@ -176,7 +176,7 @@ func ApiLoginPlexAuth(w http.ResponseWriter, r *http.Request) { // API route which validates an admin JWT token func ApiValidatePlexAuth(w http.ResponseWriter, r *http.Request) { - payload, err := modules.AuthorizeToken(w, r) + payload, err := modules.AuthorizeToken(w, r, false) if err != nil { log.Println("Failed to parse login token. Error: ") @@ -257,7 +257,7 @@ func ApiCreateShareLink(w http.ResponseWriter, r *http.Request) { } // Try to authorize bearer token from header - payload, err := modules.AuthorizeToken(w, r) + payload, err := modules.AuthorizeToken(w, r, false) var user_name string var user_id int @@ -362,7 +362,7 @@ func ApiGetUserShareLink(w http.ResponseWriter, r *http.Request) { } // Try to authorize bearer token from header - payload, err := modules.AuthorizeToken(w, r) + payload, err := modules.AuthorizeToken(w, r, false) var user_name string var user_id int @@ -490,7 +490,7 @@ func ApiDeleteUserShareLink(w http.ResponseWriter, r *http.Request) { } // Try to authorize bearer token from header - payload, err := modules.AuthorizeToken(w, r) + payload, err := modules.AuthorizeToken(w, r, false) var user_name string var user_id int