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
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
)

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/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=

View file

@ -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
}

View file

@ -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 {

View file

@ -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
}

View file

@ -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
}

View file

@ -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 {

View file

@ -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

View file

@ -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