mirror of
https://github.com/aunefyren/wrapperr
synced 2024-09-20 14:12:01 +00:00
Basic auth in headers and depency updates
This commit is contained in:
parent
02cad1e8f1
commit
0dc593fc21
9 changed files with 205 additions and 74 deletions
6
go.mod
6
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
|
||||
)
|
||||
|
|
10
go.sum
10
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=
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -3,79 +3,167 @@ 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)
|
||||
// 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 &models.Payload{}, errors.New("Session token not accepted. Please relog.")
|
||||
return payload, errors.New("Session token not accepted. Please relog.")
|
||||
}
|
||||
|
||||
return payload, nil
|
||||
if admin {
|
||||
if !payload.Admin {
|
||||
return payload, errors.New("Session is not an admin session.")
|
||||
}
|
||||
|
||||
// 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)
|
||||
case "basic":
|
||||
rawDecodedtoken, err := base64.StdEncoding.DecodeString(token)
|
||||
if err != nil {
|
||||
verr, ok := err.(*jwt.ValidationError)
|
||||
if ok && errors.Is(verr.Inner, jwt.ErrTokenExpired) {
|
||||
return nil, models.ErrExpiredToken
|
||||
return payload, errors.New("Failed to decode Base64.")
|
||||
}
|
||||
return nil, models.ErrInvalidToken
|
||||
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.")
|
||||
}
|
||||
|
||||
payload, ok := jwtToken.Claims.(*models.Payload)
|
||||
if !ok {
|
||||
return nil, models.ErrInvalidToken
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
return
|
||||
}
|
||||
claims, ok := token.Claims.(*models.Payload)
|
||||
if !ok {
|
||||
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
|
||||
}
|
||||
|
||||
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.
|
||||
func CreateToken(username string, admin bool, authtoken string) (string, error) {
|
||||
|
||||
|
@ -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)
|
||||
if err != nil {
|
||||
return "", payload, err
|
||||
}
|
||||
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
|
||||
|
||||
jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, payload)
|
||||
token, err := jwtToken.SignedString([]byte(PrivateKey))
|
||||
payload, err = NewPayload(username, admin, authtoken, duration)
|
||||
if err != nil {
|
||||
return token, payload, err
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue