Basic auth in headers and depency updates

This commit is contained in:
aunefyren 2023-10-21 17:17:05 +02:00
parent 02cad1e8f1
commit 0dc593fc21
9 changed files with 205 additions and 74 deletions

6
go.mod
View file

@ -3,11 +3,11 @@ module aunefyren/wrapperr
go 1.19 go 1.19
require ( require (
github.com/golang-jwt/jwt/v4 v4.5.0 github.com/golang-jwt/jwt/v5 v5.0.0
github.com/google/uuid v1.3.0 github.com/google/uuid v1.3.1
github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e
github.com/gorilla/mux v1.8.0 github.com/gorilla/mux v1.8.0
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
github.com/patrickmn/sortutil v0.0.0-20120526081524-abeda66eb583 github.com/patrickmn/sortutil v0.0.0-20120526081524-abeda66eb583
golang.org/x/crypto v0.6.0 golang.org/x/crypto v0.14.0
) )

10
go.sum
View file

@ -1,7 +1,11 @@
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v5 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/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 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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 h1:XmA6L9IPRdUr28a+SK/oMchGgQy159wvzXA5tJ7l+40=
github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e/go.mod h1:AFIo+02s+12CEg8Gzz9kzhCbmbq6JcKNrhHffCGA9z4= github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e/go.mod h1:AFIo+02s+12CEg8Gzz9kzhCbmbq6JcKNrhHffCGA9z4=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= 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= 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 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= 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=

View file

@ -12,9 +12,15 @@ var (
) )
// Valid checks if the token payload is valid or not // Valid checks if the token payload is valid or not
func (payload *Payload) Valid() error { func (payload *Payload) Valid() (err error) {
if time.Now().After(payload.ExpiredAt) { now := time.Now()
return ErrExpiredToken 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 return nil
} }

View file

@ -3,7 +3,7 @@ package models
import ( import (
"time" "time"
"github.com/golang-jwt/jwt/v4" "github.com/golang-jwt/jwt/v5"
"github.com/google/uuid" "github.com/google/uuid"
) )
@ -22,13 +22,11 @@ type CustomPayload struct {
*/ */
type Payload struct { type Payload struct {
jwt.Claims jwt.RegisteredClaims
ID uuid.UUID `json:"id"` ID uuid.UUID `json:"id"`
Username string `json:"username"` Username string `json:"username"`
Admin bool `json:"admin"` Admin bool `json:"admin"`
AuthToken string `json:"authtoken"` AuthToken string `json:"authtoken"`
IssuedAt time.Time `json:"issued_at"`
ExpiredAt time.Time `json:"expired_at"`
} }
type JWTMaker struct { type JWTMaker struct {

View file

@ -3,77 +3,165 @@ package modules
import ( import (
"aunefyren/wrapperr/files" "aunefyren/wrapperr/files"
"aunefyren/wrapperr/models" "aunefyren/wrapperr/models"
"aunefyren/wrapperr/utilities"
"encoding/base64"
"errors" "errors"
"log" "log"
"net/http" "net/http"
"strings" "strings"
"time" "time"
"github.com/golang-jwt/jwt/v4" "github.com/golang-jwt/jwt/v5"
) )
// AuthorizeToken validates JWT tokens using the private key. // 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 { if err != nil {
log.Println("Failed to load JWT Token settings. Error: ") log.Println("Failed to load JWT Token settings. Error: " + err.Error())
log.Println(err) return payload, errors.New("Failed to load JWT Token settings.")
return &models.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 // Check if Authorization header is available
header := request.Header.Get("Authorization") authHeader := request.Header.Get("Authorization")
if header == "" || !strings.Contains(header, " ") || !strings.Contains(strings.ToLower(header), "bearer") { if authHeader == "" || !strings.Contains(authHeader, " ") {
log.Println("No valid Authorization token found in header during API request.") 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 { if len(headerParts) < 2 {
log.Println("Failed to parse header. Error: ") log.Println("Failed to parse header. Error: " + err.Error())
log.Println(err) return payload, errors.New("Failed to parse header.")
return &models.Payload{}, errors.New("Failed to parse header.")
} }
jwtToken := headerParts[1] token := headerParts[1]
authType := ""
payload, err := VerifyToken(PrivateKey, jwtToken) // Define header type
if err != nil { switch strings.TrimSpace(strings.ToLower(headerParts[0])) {
log.Println("Session token not accepted. Error: ") case "bearer":
log.Println(err) authType = "bearer"
return &models.Payload{}, errors.New("Session token not accepted. Please relog.") 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 return payload, nil
} }
// VerifyToken checks if the token is valid or not func ValidateToken(signedToken string, privateKey string) (err error) {
func VerifyToken(PrivateKey string, token string) (*models.Payload, error) { token, err := jwt.ParseWithClaims(
keyFunc := func(token *jwt.Token) (interface{}, error) { signedToken,
_, ok := token.Method.(*jwt.SigningMethodHMAC) &models.Payload{},
if !ok { func(token *jwt.Token) (interface{}, error) {
return nil, models.ErrInvalidToken return []byte(privateKey), nil
} },
return []byte(PrivateKey), nil )
}
jwtToken, err := jwt.ParseWithClaims(token, &models.Payload{}, keyFunc)
if err != nil { if err != nil {
verr, ok := err.(*jwt.ValidationError) return
if ok && errors.Is(verr.Inner, jwt.ErrTokenExpired) {
return nil, models.ErrExpiredToken
}
return nil, models.ErrInvalidToken
} }
claims, ok := token.Claims.(*models.Payload)
payload, ok := jwtToken.Claims.(*models.Payload)
if !ok { 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. // 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.") 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) token, _, err := CreateTokenTwo(PrivateKey, username, admin, authtoken, duration)
if err != nil { 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 // 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) { func CreateTokenTwo(PrivateKey string, username string, admin bool, authtoken string, duration time.Time) (token string, payload models.Payload, err error) {
payload, err := NewPayload(username, admin, authtoken, duration) token = ""
payload = models.Payload{}
err = nil
payload, err = NewPayload(username, admin, authtoken, duration)
if err != nil { if err != nil {
return "", payload, err return token, payload, err
} }
jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, payload) tokenUnsigned := jwt.NewWithClaims(jwt.SigningMethodHS256, payload)
token, err := jwtToken.SignedString([]byte(PrivateKey)) token, err = tokenUnsigned.SignedString([]byte(PrivateKey))
return token, payload, err 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
}

View file

@ -4,23 +4,32 @@ import (
"aunefyren/wrapperr/models" "aunefyren/wrapperr/models"
"time" "time"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid" "github.com/google/uuid"
) )
// NewPayload creates a new token payload with a specific username and duration // 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() tokenID, err := uuid.NewRandom()
if err != nil { if err != nil {
return nil, err return payload, err
} }
payload := &models.Payload{ payload = models.Payload{
ID: tokenID, ID: tokenID,
Username: username, Username: username,
Admin: admin, Admin: admin,
AuthToken: authtoken, AuthToken: authtoken,
IssuedAt: time.Now(), RegisteredClaims: jwt.RegisteredClaims{
ExpiredAt: time.Now().Add(duration), ExpiresAt: jwt.NewNumericDate(duration),
NotBefore: jwt.NewNumericDate(time.Now()),
IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: "Wrapperr",
},
} }
return payload, nil return payload, nil
} }

View file

@ -17,7 +17,7 @@ import (
// API route used to retrieve the Wrapperr configuration file. // API route used to retrieve the Wrapperr configuration file.
func ApiGetConfig(w http.ResponseWriter, r *http.Request) { 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 { 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. // API route used to update the Wrapperr configuration file.
func ApiSetConfig(w http.ResponseWriter, r *http.Request) { 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 { if err == nil && payload.Admin {
@ -205,7 +205,7 @@ func ApiUpdateAdmin(w http.ResponseWriter, r *http.Request) {
return return
} else { } else {
payload, err := modules.AuthorizeToken(w, r) payload, err := modules.AuthorizeToken(w, r, true)
if err == nil && payload.Admin { 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 // API route which validates an admin JWT token
func ApiValidateAdmin(w http.ResponseWriter, r *http.Request) { 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 { 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.") utilities.RespondDefaultOkay(w, r, "The admin login session is valid.")
return return
} else if !payload.Admin { } else if err == nil && !payload.Admin {
log.Println("User not authenticated as admin.") log.Println("User not authenticated as admin.")
utilities.RespondDefaultError(w, r, errors.New("User not authenticated as admin."), 401) 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 // API route which retrieves lines from the log file
func ApiGetLog(w http.ResponseWriter, r *http.Request) { func ApiGetLog(w http.ResponseWriter, r *http.Request) {
payload, err := modules.AuthorizeToken(w, r) payload, err := modules.AuthorizeToken(w, r, true)
if err != nil { if err != nil {

View file

@ -67,7 +67,7 @@ func ApiWrapperGetStatistics(w http.ResponseWriter, r *http.Request) {
var admin bool = false var admin bool = false
// Try to authorize bearer token from header // 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 failed and PlexAuth is enabled, respond with and error
// If it didn't fail, and PlexAuth is enabled, declare auth as passed // If it didn't fail, and PlexAuth is enabled, declare auth as passed

View file

@ -176,7 +176,7 @@ func ApiLoginPlexAuth(w http.ResponseWriter, r *http.Request) {
// API route which validates an admin JWT token // API route which validates an admin JWT token
func ApiValidatePlexAuth(w http.ResponseWriter, r *http.Request) { func ApiValidatePlexAuth(w http.ResponseWriter, r *http.Request) {
payload, err := modules.AuthorizeToken(w, r) payload, err := modules.AuthorizeToken(w, r, false)
if err != nil { if err != nil {
log.Println("Failed to parse login token. Error: ") 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 // 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_name string
var user_id int var user_id int
@ -362,7 +362,7 @@ func ApiGetUserShareLink(w http.ResponseWriter, r *http.Request) {
} }
// Try to authorize bearer token from header // 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_name string
var user_id int var user_id int
@ -490,7 +490,7 @@ func ApiDeleteUserShareLink(w http.ResponseWriter, r *http.Request) {
} }
// Try to authorize bearer token from header // 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_name string
var user_id int var user_id int