refactor: move all primary logic to service packages and standardize error objects
This commit is contained in:
parent
d18be0acc8
commit
73e1c5a363
|
@ -1,32 +1,13 @@
|
||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"git.lumeweb.com/LumeWeb/portal/controller/request"
|
"git.lumeweb.com/LumeWeb/portal/controller/request"
|
||||||
"git.lumeweb.com/LumeWeb/portal/db"
|
"git.lumeweb.com/LumeWeb/portal/service/account"
|
||||||
"git.lumeweb.com/LumeWeb/portal/logger"
|
|
||||||
"git.lumeweb.com/LumeWeb/portal/model"
|
|
||||||
"github.com/kataras/iris/v12"
|
"github.com/kataras/iris/v12"
|
||||||
"go.uber.org/zap"
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type AccountController struct {
|
type AccountController struct {
|
||||||
Ctx iris.Context
|
Controller
|
||||||
}
|
|
||||||
|
|
||||||
func hashPassword(password string) (string, error) {
|
|
||||||
// Generate a new bcrypt hash from the provided password.
|
|
||||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
|
||||||
if err != nil {
|
|
||||||
logger.Get().Error("failed to hash password", zap.Error(err))
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert the hashed password to a string and return it.
|
|
||||||
return string(hashedPassword), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AccountController) PostRegister() {
|
func (a *AccountController) PostRegister() {
|
||||||
|
@ -37,75 +18,15 @@ func (a *AccountController) PostRegister() {
|
||||||
|
|
||||||
r, _ := ri.(*request.RegisterRequest)
|
r, _ := ri.(*request.RegisterRequest)
|
||||||
|
|
||||||
// Check if an account with the same email address already exists.
|
err := account.Register(r.Email, r.Password, r.Pubkey)
|
||||||
existingAccount := model.Account{}
|
|
||||||
err := db.Get().Where("email = ?", r.Email).First(&existingAccount).Error
|
|
||||||
if err == nil {
|
|
||||||
logger.Get().Debug("account with email already exists", zap.Error(err), zap.String("email", r.Email))
|
|
||||||
// An account with the same email address already exists.
|
|
||||||
// Return an error response to the client.
|
|
||||||
a.Ctx.StopWithError(iris.StatusConflict, errors.New("an account with this email address already exists"))
|
|
||||||
return
|
|
||||||
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
logger.Get().Error("error querying accounts", zap.Error(err), zap.String("email", r.Email))
|
|
||||||
// An unexpected error occurred while querying the database.
|
|
||||||
// Return an error response to the client.
|
|
||||||
a.Ctx.StopWithError(iris.StatusInternalServerError, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(r.Pubkey) > 0 {
|
|
||||||
r.Pubkey = strings.ToLower(r.Pubkey)
|
|
||||||
var count int64
|
|
||||||
err := db.Get().Model(&model.Key{}).Where("pubkey = ?", r.Pubkey).Count(&count).Error
|
|
||||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
logger.Get().Error("error querying accounts", zap.Error(err), zap.String("pubkey", r.Pubkey))
|
|
||||||
a.Ctx.StopWithError(iris.StatusInternalServerError, err)
|
|
||||||
}
|
|
||||||
if count > 0 {
|
|
||||||
logger.Get().Debug("account with pubkey already exists", zap.Error(err), zap.String("pubkey", r.Pubkey))
|
|
||||||
// An account with the same pubkey already exists.
|
|
||||||
// Return an error response to the client.
|
|
||||||
a.Ctx.StopWithError(iris.StatusConflict, errors.New("an account with this pubkey already exists"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new Account model with the provided email and hashed password.
|
|
||||||
account := model.Account{
|
|
||||||
Email: r.Email,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hash the password before saving it to the database.
|
|
||||||
if len(r.Password) > 0 {
|
|
||||||
hashedPassword, err := hashPassword(r.Password)
|
|
||||||
if err != nil {
|
|
||||||
a.Ctx.StopWithError(iris.StatusInternalServerError, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
account.Password = &hashedPassword
|
|
||||||
}
|
|
||||||
|
|
||||||
err = db.Get().Transaction(func(tx *gorm.DB) error {
|
|
||||||
// do some database operations in the transaction (use 'tx' from this point, not 'db')
|
|
||||||
if err := tx.Create(&account).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(r.Pubkey) > 0 {
|
|
||||||
if err := tx.Create(&model.Key{Account: account, Pubkey: strings.ToLower(r.Pubkey)}).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// return nil will commit the whole transaction
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Get().Error("failed to create account", zap.Error(err))
|
if err == account.ErrQueryingAcct || err == account.ErrFailedCreateAccount {
|
||||||
a.Ctx.StopWithError(iris.StatusInternalServerError, err)
|
a.Ctx.StopWithError(iris.StatusInternalServerError, err)
|
||||||
|
} else {
|
||||||
|
a.Ctx.StopWithError(iris.StatusBadRequest, err)
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,119 +1,14 @@
|
||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/ed25519"
|
|
||||||
"encoding/hex"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"git.lumeweb.com/LumeWeb/portal/controller/request"
|
"git.lumeweb.com/LumeWeb/portal/controller/request"
|
||||||
"git.lumeweb.com/LumeWeb/portal/controller/response"
|
"git.lumeweb.com/LumeWeb/portal/controller/response"
|
||||||
"git.lumeweb.com/LumeWeb/portal/db"
|
"git.lumeweb.com/LumeWeb/portal/service/auth"
|
||||||
"git.lumeweb.com/LumeWeb/portal/logger"
|
|
||||||
"git.lumeweb.com/LumeWeb/portal/model"
|
|
||||||
"github.com/joomcode/errorx"
|
|
||||||
"github.com/kataras/iris/v12"
|
"github.com/kataras/iris/v12"
|
||||||
"github.com/kataras/jwt"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var sharedKey = []byte("sercrethatmaycontainch@r$32chars")
|
|
||||||
|
|
||||||
var blocklist *jwt.Blocklist
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
blocklist = jwt.NewBlocklist(1 * time.Hour)
|
|
||||||
}
|
|
||||||
|
|
||||||
type AuthController struct {
|
type AuthController struct {
|
||||||
Ctx iris.Context
|
Controller
|
||||||
}
|
|
||||||
|
|
||||||
// verifyPassword compares the provided plaintext password with a hashed password and returns an error if they don't match.
|
|
||||||
func verifyPassword(hashedPassword, password string) error {
|
|
||||||
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
|
|
||||||
if err != nil {
|
|
||||||
return errors.New("invalid email or password")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// generateToken generates a JWT token for the given account ID.
|
|
||||||
func generateToken(maxAge time.Duration) (string, error) {
|
|
||||||
// Define the JWT claims.
|
|
||||||
claim := jwt.Claims{
|
|
||||||
Expiry: time.Now().Add(time.Hour * 24).Unix(), // Token expires in 24 hours.
|
|
||||||
IssuedAt: time.Now().Unix(),
|
|
||||||
}
|
|
||||||
|
|
||||||
token, err := jwt.Sign(jwt.HS256, sharedKey, claim, jwt.MaxAge(maxAge))
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
logger.Get().Error("failed to sign jwt", zap.Error(err))
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return string(token), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateAndSaveLoginToken(accountID uint, maxAge time.Duration) (string, error) {
|
|
||||||
// Generate a JWT token for the authenticated user.
|
|
||||||
token, err := generateToken(maxAge)
|
|
||||||
if err != nil {
|
|
||||||
logger.Get().Error("failed to generate token", zap.Error(err))
|
|
||||||
return "", fmt.Errorf("failed to generate token: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
verifiedToken, _ := jwt.Verify(jwt.HS256, sharedKey, []byte(token), blocklist)
|
|
||||||
var claim *jwt.Claims
|
|
||||||
|
|
||||||
_ = verifiedToken.Claims(&claim)
|
|
||||||
|
|
||||||
// Save the token to the database.
|
|
||||||
session := model.LoginSession{
|
|
||||||
Account: model.Account{ID: accountID},
|
|
||||||
Token: token,
|
|
||||||
Expiration: claim.ExpiresAt(),
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := db.Get().Create(&session).Error; err != nil {
|
|
||||||
msg := "failed to save token"
|
|
||||||
logger.Get().Error(msg, zap.Error(err))
|
|
||||||
return "", errorx.Decorate(err, msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
return token, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateAndSaveChallengeToken(accountID uint, maxAge time.Duration) (string, error) {
|
|
||||||
// Generate a JWT token for the authenticated user.
|
|
||||||
token, err := generateToken(maxAge)
|
|
||||||
if err != nil {
|
|
||||||
logger.Get().Error("failed to generate token", zap.Error(err))
|
|
||||||
return "", fmt.Errorf("failed to generate token: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
verifiedToken, _ := jwt.Verify(jwt.HS256, sharedKey, []byte(token), blocklist)
|
|
||||||
var claim *jwt.Claims
|
|
||||||
|
|
||||||
_ = verifiedToken.Claims(&claim)
|
|
||||||
|
|
||||||
// Save the token to the database.
|
|
||||||
keyChallenge := model.KeyChallenge{
|
|
||||||
AccountID: accountID,
|
|
||||||
Challenge: token,
|
|
||||||
Expiration: claim.ExpiresAt(),
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := db.Get().Create(&keyChallenge).Error; err != nil {
|
|
||||||
msg := "failed to save token"
|
|
||||||
logger.Get().Error(msg, zap.Error(err))
|
|
||||||
return "", errorx.Decorate(err, msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
return token, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PostLogin handles the POST /api/auth/login request to authenticate a user and return a JWT token.
|
// PostLogin handles the POST /api/auth/login request to authenticate a user and return a JWT token.
|
||||||
|
@ -125,43 +20,18 @@ func (a *AuthController) PostLogin() {
|
||||||
|
|
||||||
r, _ := ri.(*request.LoginRequest)
|
r, _ := ri.(*request.LoginRequest)
|
||||||
|
|
||||||
// Retrieve the account for the given email.
|
token, err := auth.LoginWithPassword(r.Email, r.Password)
|
||||||
account := model.Account{}
|
|
||||||
if err := db.Get().Where("email = ?", r.Email).First(&account).Error; err != nil {
|
|
||||||
msg := "invalid email or password"
|
|
||||||
logger.Get().Debug(msg, zap.Error(err))
|
|
||||||
a.Ctx.StopWithError(iris.StatusBadRequest, errors.New(msg))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if account.Password == nil || len(*account.Password) == 0 {
|
|
||||||
msg := "only pubkey login is supported"
|
|
||||||
logger.Get().Debug(msg)
|
|
||||||
a.Ctx.StopWithError(iris.StatusBadRequest, errors.New(msg))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify the provided password against the hashed password stored in the database.
|
|
||||||
if err := verifyPassword(*account.Password, r.Password); err != nil {
|
|
||||||
msg := "invalid email or password"
|
|
||||||
logger.Get().Debug(msg, zap.Error(err))
|
|
||||||
a.Ctx.StopWithError(iris.StatusBadRequest, errors.New(msg))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate a JWT token for the authenticated user.
|
|
||||||
token, err := generateAndSaveLoginToken(account.ID, 24*time.Hour)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Get().Debug("failed to generate token", zap.Error(err))
|
if err == auth.ErrFailedGenerateToken {
|
||||||
a.Ctx.StopWithError(iris.StatusInternalServerError, fmt.Errorf("failed to generate token: %s", err))
|
a.Ctx.StopWithError(iris.StatusInternalServerError, err)
|
||||||
|
} else {
|
||||||
|
a.Ctx.StopWithError(iris.StatusUnauthorized, err)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the JWT token to the client.
|
a.respondJSON(&response.LoginResponse{Token: token})
|
||||||
err = a.Ctx.JSON(&response.LoginResponse{Token: token})
|
|
||||||
if err != nil {
|
|
||||||
logger.Get().Error("failed to generate response", zap.Error(err))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PostChallenge handles the POST /api/auth/pubkey/challenge request to generate a challenge for a user's public key.
|
// PostChallenge handles the POST /api/auth/pubkey/challenge request to generate a challenge for a user's public key.
|
||||||
|
@ -173,27 +43,17 @@ func (a *AuthController) PostPubkeyChallenge() {
|
||||||
|
|
||||||
r, _ := (ri).(*request.PubkeyChallengeRequest)
|
r, _ := (ri).(*request.PubkeyChallengeRequest)
|
||||||
|
|
||||||
r.Pubkey = strings.ToLower(r.Pubkey)
|
challenge, err := auth.GeneratePubkeyChallenge(r.Pubkey)
|
||||||
|
if err != nil {
|
||||||
// Retrieve the account for the given email.
|
if err == auth.ErrFailedGenerateKeyChallenge {
|
||||||
account := model.Key{}
|
a.Ctx.StopWithError(iris.StatusInternalServerError, err)
|
||||||
if err := db.Get().Where("pubkey = ?", r.Pubkey).First(&account).Error; err != nil {
|
} else {
|
||||||
a.Ctx.StopWithError(iris.StatusBadRequest, errors.New("invalid pubkey"))
|
a.Ctx.StopWithError(iris.StatusUnauthorized, err)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate a random challenge string.
|
a.respondJSON(&response.ChallengeResponse{Challenge: challenge})
|
||||||
challenge, err := generateAndSaveChallengeToken(account.AccountID, time.Minute)
|
|
||||||
if err != nil {
|
|
||||||
a.Ctx.StopWithError(iris.StatusInternalServerError, errors.New("failed to generate challenge"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the challenge to the client.
|
|
||||||
err = a.Ctx.JSON(&response.ChallengeResponse{Challenge: challenge})
|
|
||||||
if err != nil {
|
|
||||||
logger.Get().Error("failed to create response", zap.Error(err))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PostKeyLogin handles the POST /api/auth/pubkey/login request to authenticate a user using a public key challenge and return a JWT token.
|
// PostKeyLogin handles the POST /api/auth/pubkey/login request to authenticate a user using a public key challenge and return a JWT token.
|
||||||
|
@ -205,78 +65,18 @@ func (a *AuthController) PostPubkeyLogin() {
|
||||||
|
|
||||||
r, _ := ri.(*request.PubkeyLoginRequest)
|
r, _ := ri.(*request.PubkeyLoginRequest)
|
||||||
|
|
||||||
r.Pubkey = strings.ToLower(r.Pubkey)
|
token, err := auth.LoginWithPubkey(r.Pubkey, r.Challenge, r.Signature)
|
||||||
r.Signature = strings.ToLower(r.Signature)
|
|
||||||
|
|
||||||
// Retrieve the key challenge for the given challenge.
|
|
||||||
challenge := model.KeyChallenge{}
|
|
||||||
if err := db.Get().Where("challenge = ?", r.Challenge).First(&challenge).Error; err != nil {
|
|
||||||
msg := "invalid key challenge"
|
|
||||||
logger.Get().Debug(msg, zap.Error(err), zap.String("challenge", r.Challenge))
|
|
||||||
a.Ctx.StopWithError(iris.StatusBadRequest, errorx.RejectedOperation.New(msg))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
verifiedToken, err := jwt.Verify(jwt.HS256, sharedKey, []byte(r.Challenge), blocklist)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
msg := fmt.Sprintf("invalid key challenge: %s", err.Error())
|
if err == auth.ErrFailedGenerateKeyChallenge || err == auth.ErrFailedGenerateToken || err == auth.ErrFailedSaveToken {
|
||||||
logger.Get().Debug(msg, zap.Error(err), zap.String("challenge", r.Challenge))
|
a.Ctx.StopWithError(iris.StatusInternalServerError, err)
|
||||||
a.Ctx.StopWithError(iris.StatusBadRequest, errorx.RejectedOperation.New(msg))
|
} else {
|
||||||
|
a.Ctx.StopWithError(iris.StatusUnauthorized, err)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
rawPubKey, err := hex.DecodeString(r.Pubkey)
|
a.respondJSON(&response.LoginResponse{Token: token})
|
||||||
if err != nil {
|
|
||||||
msg := fmt.Sprintf("invalid pubkey: %s", err.Error())
|
|
||||||
logger.Get().Debug(msg, zap.Error(err), zap.String("pubkey", r.Pubkey))
|
|
||||||
a.Ctx.StopWithError(iris.StatusBadRequest, errorx.RejectedOperation.New(msg))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
rawSignature, err := hex.DecodeString(r.Signature)
|
|
||||||
if err != nil {
|
|
||||||
msg := fmt.Sprintf("invalid signature: %s", err.Error())
|
|
||||||
logger.Get().Debug(msg, zap.Error(err), zap.String("signature", r.Signature))
|
|
||||||
a.Ctx.StopWithError(iris.StatusBadRequest, errorx.RejectedOperation.New(msg))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
publicKeyDecoded := ed25519.PublicKey(rawPubKey)
|
|
||||||
|
|
||||||
// Verify the challenge signature.
|
|
||||||
if !ed25519.Verify(publicKeyDecoded, []byte(r.Challenge), rawSignature) {
|
|
||||||
msg := "invalid challenge"
|
|
||||||
logger.Get().Debug(msg, zap.Error(err), zap.String("challenge", r.Challenge))
|
|
||||||
a.Ctx.StopWithError(iris.StatusBadRequest, errorx.RejectedOperation.New(msg))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate a JWT token for the authenticated user.
|
|
||||||
token, err := generateAndSaveLoginToken(challenge.AccountID, 24*time.Hour)
|
|
||||||
if err != nil {
|
|
||||||
a.Ctx.StopWithError(iris.StatusInternalServerError, errorx.RejectedOperation.Wrap(err, "failed to generate token"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = blocklist.InvalidateToken(verifiedToken.Token, verifiedToken.StandardClaims)
|
|
||||||
if err != nil {
|
|
||||||
msg := "failed to invalidate token"
|
|
||||||
logger.Get().Error(msg, zap.Error(err), zap.String("token", hex.EncodeToString(verifiedToken.Token)))
|
|
||||||
a.Ctx.StopWithError(iris.StatusInternalServerError, errorx.RejectedOperation.Wrap(err, msg))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := db.Get().Delete(&challenge).Error; err != nil {
|
|
||||||
msg := "failed to delete key challenge"
|
|
||||||
logger.Get().Error(msg, zap.Error(err), zap.Any("key_challenge", challenge))
|
|
||||||
a.Ctx.StopWithError(iris.StatusBadRequest, errorx.RejectedOperation.New(msg))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the JWT token to the client.
|
|
||||||
err = a.Ctx.JSON(&response.LoginResponse{Token: token})
|
|
||||||
if err != nil {
|
|
||||||
logger.Get().Error("failed to create response", zap.Error(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -289,20 +89,10 @@ func (a *AuthController) PostLogout() {
|
||||||
|
|
||||||
r, _ := ri.(*request.LogoutRequest)
|
r, _ := ri.(*request.LogoutRequest)
|
||||||
|
|
||||||
// Verify the provided token.
|
err := auth.Logout(r.Token)
|
||||||
claims, err := jwt.Verify(jwt.HS256, sharedKey, []byte(r.Token), blocklist)
|
|
||||||
if err != nil {
|
|
||||||
msg := "invalid token"
|
|
||||||
logger.Get().Debug(msg, zap.Error(err))
|
|
||||||
a.Ctx.StopWithError(iris.StatusBadRequest, errors.New(msg))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = blocklist.InvalidateToken(claims.Token, claims.StandardClaims)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
msg := "failed to invalidate token"
|
a.Ctx.StopWithError(iris.StatusBadRequest, err)
|
||||||
logger.Get().Error(msg, zap.Error(err), zap.String("token", hex.EncodeToString(claims.Token)))
|
|
||||||
a.Ctx.StopWithError(iris.StatusBadRequest, errors.New(msg))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -59,3 +59,14 @@ func internalErrorCustom(ctx iris.Context, err error, customError error) bool {
|
||||||
func sendError(ctx iris.Context, err error, irisError int) bool {
|
func sendError(ctx iris.Context, err error, irisError int) bool {
|
||||||
return sendErrorCustom(ctx, err, nil, irisError)
|
return sendErrorCustom(ctx, err, nil, irisError)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Controller struct {
|
||||||
|
Ctx iris.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Controller) respondJSON(data interface{}) {
|
||||||
|
err := c.Ctx.JSON(data)
|
||||||
|
if err != nil {
|
||||||
|
logger.Get().Error("failed to generate response", zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
20
jwt/jwt.go
20
jwt/jwt.go
|
@ -1,20 +0,0 @@
|
||||||
package jwt
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/kataras/iris/v12/middleware/jwt"
|
|
||||||
_ "github.com/kataras/iris/v12/middleware/jwt"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
Secret = []byte("signature_hmac_secret_shared_key")
|
|
||||||
v *jwt.Verifier
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
v = jwt.NewVerifier(jwt.HS256, Secret)
|
|
||||||
v.WithDefaultBlocklist()
|
|
||||||
}
|
|
||||||
|
|
||||||
func Get() *jwt.Verifier {
|
|
||||||
return v
|
|
||||||
}
|
|
4
main.go
4
main.go
|
@ -7,6 +7,7 @@ import (
|
||||||
"git.lumeweb.com/LumeWeb/portal/db"
|
"git.lumeweb.com/LumeWeb/portal/db"
|
||||||
_ "git.lumeweb.com/LumeWeb/portal/docs"
|
_ "git.lumeweb.com/LumeWeb/portal/docs"
|
||||||
"git.lumeweb.com/LumeWeb/portal/logger"
|
"git.lumeweb.com/LumeWeb/portal/logger"
|
||||||
|
"git.lumeweb.com/LumeWeb/portal/service/auth"
|
||||||
"git.lumeweb.com/LumeWeb/portal/service/files"
|
"git.lumeweb.com/LumeWeb/portal/service/files"
|
||||||
"git.lumeweb.com/LumeWeb/portal/tus"
|
"git.lumeweb.com/LumeWeb/portal/tus"
|
||||||
"github.com/iris-contrib/swagger"
|
"github.com/iris-contrib/swagger"
|
||||||
|
@ -42,9 +43,9 @@ func main() {
|
||||||
|
|
||||||
// Initialize the database connection
|
// Initialize the database connection
|
||||||
db.Init()
|
db.Init()
|
||||||
|
|
||||||
logger.Init()
|
logger.Init()
|
||||||
files.Init()
|
files.Init()
|
||||||
|
auth.Init()
|
||||||
|
|
||||||
// Create a new Iris app instance
|
// Create a new Iris app instance
|
||||||
app := iris.New()
|
app := iris.New()
|
||||||
|
@ -68,6 +69,7 @@ func main() {
|
||||||
|
|
||||||
mvc.Configure(v1.Party("/files"), func(app *mvc.Application) {
|
mvc.Configure(v1.Party("/files"), func(app *mvc.Application) {
|
||||||
app.Handle(new(controller.FilesController))
|
app.Handle(new(controller.FilesController))
|
||||||
|
app.Router.Use()
|
||||||
})
|
})
|
||||||
|
|
||||||
tusHandler := tus.Init()
|
tusHandler := tus.Init()
|
||||||
|
|
|
@ -0,0 +1,88 @@
|
||||||
|
package account
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"git.lumeweb.com/LumeWeb/portal/db"
|
||||||
|
"git.lumeweb.com/LumeWeb/portal/logger"
|
||||||
|
"git.lumeweb.com/LumeWeb/portal/model"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrEmailExists = errors.New("Account with email already exists")
|
||||||
|
ErrPubkeyExists = errors.New("Account with pubkey already exists")
|
||||||
|
ErrQueryingAcct = errors.New("Error querying accounts")
|
||||||
|
ErrFailedHashPassword = errors.New("Failed to hash password")
|
||||||
|
ErrFailedCreateAccount = errors.New("Failed to create account")
|
||||||
|
)
|
||||||
|
|
||||||
|
func Register(email string, password string, pubkey string) error {
|
||||||
|
// Check if an account with the same email address already exists.
|
||||||
|
existingAccount := model.Account{}
|
||||||
|
err := db.Get().Where("email = ?", email).First(&existingAccount).Error
|
||||||
|
if err == nil {
|
||||||
|
logger.Get().Debug(ErrEmailExists.Error(), zap.Error(err), zap.String("email", email))
|
||||||
|
// An account with the same email address already exists.
|
||||||
|
// Return an error response to the client.
|
||||||
|
return ErrEmailExists
|
||||||
|
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
logger.Get().Error(ErrQueryingAcct.Error(), zap.Error(err))
|
||||||
|
return ErrQueryingAcct
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(pubkey) > 0 {
|
||||||
|
pubkey = strings.ToLower(pubkey)
|
||||||
|
var count int64
|
||||||
|
err := db.Get().Model(&model.Key{}).Where("pubkey = ?", pubkey).Count(&count).Error
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
logger.Get().Error(ErrQueryingAcct.Error(), zap.Error(err), zap.String("pubkey", pubkey))
|
||||||
|
return ErrQueryingAcct
|
||||||
|
}
|
||||||
|
if count > 0 {
|
||||||
|
logger.Get().Debug(ErrPubkeyExists.Error(), zap.Error(err), zap.String("pubkey", pubkey))
|
||||||
|
// An account with the same pubkey already exists.
|
||||||
|
// Return an error response to the client.
|
||||||
|
return ErrPubkeyExists
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new Account model with the provided email and hashed password.
|
||||||
|
account := model.Account{
|
||||||
|
Email: email,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash the password before saving it to the database.
|
||||||
|
if len(password) > 0 {
|
||||||
|
hashedPassword, err := hashPassword(password)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
account.Password = &hashedPassword
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Get().Transaction(func(tx *gorm.DB) error {
|
||||||
|
// do some database operations in the transaction (use 'tx' from this point, not 'db')
|
||||||
|
if err := tx.Create(&account).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(pubkey) > 0 {
|
||||||
|
if err := tx.Create(&model.Key{Account: account, Pubkey: pubkey}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// return nil will commit the whole transaction
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
logger.Get().Error(ErrFailedCreateAccount.Error(), zap.Error(err))
|
||||||
|
return ErrFailedCreateAccount
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
package account
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.lumeweb.com/LumeWeb/portal/logger"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func hashPassword(password string) (string, error) {
|
||||||
|
// Generate a new bcrypt hash from the provided password.
|
||||||
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
logger.Get().Error(ErrFailedHashPassword.Error(), zap.Error(err))
|
||||||
|
return "", ErrFailedHashPassword
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the hashed password to a string and return it.
|
||||||
|
return string(hashedPassword), nil
|
||||||
|
}
|
|
@ -0,0 +1,183 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ed25519"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"git.lumeweb.com/LumeWeb/portal/db"
|
||||||
|
"git.lumeweb.com/LumeWeb/portal/logger"
|
||||||
|
"git.lumeweb.com/LumeWeb/portal/model"
|
||||||
|
"github.com/kataras/jwt"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var sharedKey = []byte("sercrethatmaycontainch@r$32chars")
|
||||||
|
|
||||||
|
var blocklist *jwt.Blocklist
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidEmailPassword = errors.New("Invalid email or password")
|
||||||
|
ErrPubkeyOnly = errors.New("Only pubkey login is supported")
|
||||||
|
ErrFailedGenerateToken = errors.New("Failed to generate token")
|
||||||
|
ErrFailedGenerateKeyChallenge = errors.New("Failed to generate key challenge")
|
||||||
|
ErrFailedSignJwt = errors.New("Failed to sign jwt")
|
||||||
|
ErrFailedSaveToken = errors.New("Failed to sign token")
|
||||||
|
ErrFailedDeleteKeyChallenge = errors.New("Failed to delete key challenge")
|
||||||
|
ErrFailedInvalidateToken = errors.New("Failed to invalidate token")
|
||||||
|
ErrInvalidKeyChallenge = errors.New("Invalid key challenge")
|
||||||
|
ErrInvalidPubkey = errors.New("Invalid pubkey")
|
||||||
|
ErrInvalidSignature = errors.New("Invalid signature")
|
||||||
|
ErrInvalidToken = errors.New("Invalid token")
|
||||||
|
)
|
||||||
|
|
||||||
|
func Init() {
|
||||||
|
blocklist = jwt.NewBlocklist(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoginWithPassword(email string, password string) (string, error) {
|
||||||
|
// Retrieve the account for the given email.
|
||||||
|
account := model.Account{}
|
||||||
|
if err := db.Get().Model(&account).Where("email = ?", email).First(&account).Error; err != nil {
|
||||||
|
logger.Get().Debug(ErrInvalidEmailPassword.Error(), zap.String("email", email))
|
||||||
|
return "", ErrInvalidEmailPassword
|
||||||
|
}
|
||||||
|
|
||||||
|
if account.Password == nil || len(*account.Password) == 0 {
|
||||||
|
logger.Get().Debug(ErrPubkeyOnly.Error(), zap.String("email", email))
|
||||||
|
return "", ErrPubkeyOnly
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the provided password against the hashed password stored in the database.
|
||||||
|
if err := verifyPassword(*account.Password, password); err != nil {
|
||||||
|
logger.Get().Debug(ErrPubkeyOnly.Error(), zap.String("email", email))
|
||||||
|
return "", ErrInvalidEmailPassword
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a JWT token for the authenticated user.
|
||||||
|
token, err := generateAndSaveLoginToken(account.ID, 24*time.Hour)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoginWithPubkey(pubkey string, challenge string, signature string) (string, error) {
|
||||||
|
pubkey = strings.ToLower(pubkey)
|
||||||
|
signature = strings.ToLower(signature)
|
||||||
|
|
||||||
|
// Retrieve the key challenge for the given challenge.
|
||||||
|
challengeObj := model.KeyChallenge{}
|
||||||
|
if err := db.Get().Model(challengeObj).Where("challenge = ?", challenge).First(&challengeObj).Error; err != nil {
|
||||||
|
logger.Get().Debug(ErrInvalidKeyChallenge.Error(), zap.Error(err), zap.String("challenge", challenge))
|
||||||
|
return "", ErrInvalidKeyChallenge
|
||||||
|
}
|
||||||
|
|
||||||
|
verifiedToken, err := jwt.Verify(jwt.HS256, sharedKey, []byte(challenge), blocklist)
|
||||||
|
if err != nil {
|
||||||
|
logger.Get().Debug(ErrInvalidKeyChallenge.Error(), zap.Error(err), zap.String("challenge", challenge))
|
||||||
|
return "", ErrInvalidKeyChallenge
|
||||||
|
}
|
||||||
|
|
||||||
|
rawPubKey, err := hex.DecodeString(pubkey)
|
||||||
|
if err != nil {
|
||||||
|
logger.Get().Debug(ErrInvalidPubkey.Error(), zap.Error(err), zap.String("pubkey", pubkey))
|
||||||
|
return "", ErrInvalidPubkey
|
||||||
|
}
|
||||||
|
|
||||||
|
rawSignature, err := hex.DecodeString(signature)
|
||||||
|
if err != nil {
|
||||||
|
logger.Get().Debug(ErrInvalidPubkey.Error(), zap.Error(err), zap.String("signature", pubkey))
|
||||||
|
return "", ErrInvalidSignature
|
||||||
|
}
|
||||||
|
|
||||||
|
publicKeyDecoded := ed25519.PublicKey(rawPubKey)
|
||||||
|
|
||||||
|
// Verify the challenge signature.
|
||||||
|
if !ed25519.Verify(publicKeyDecoded, []byte(challenge), rawSignature) {
|
||||||
|
logger.Get().Debug(ErrInvalidKeyChallenge.Error(), zap.Error(err), zap.String("challenge", challenge))
|
||||||
|
return "", ErrInvalidKeyChallenge
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a JWT token for the authenticated user.
|
||||||
|
token, err := generateAndSaveLoginToken(challengeObj.AccountID, 24*time.Hour)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = blocklist.InvalidateToken(verifiedToken.Token, verifiedToken.StandardClaims)
|
||||||
|
if err != nil {
|
||||||
|
logger.Get().Error(ErrFailedInvalidateToken.Error(), zap.Error(err), zap.String("pubkey", pubkey), zap.ByteString("token", verifiedToken.Token), zap.String("challenge", challenge))
|
||||||
|
return "", ErrFailedInvalidateToken
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Get().Delete(&challenge).Error; err != nil {
|
||||||
|
logger.Get().Debug(ErrFailedDeleteKeyChallenge.Error(), zap.Error(err))
|
||||||
|
return "", ErrFailedDeleteKeyChallenge
|
||||||
|
}
|
||||||
|
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GeneratePubkeyChallenge(pubkey string) (string, error) {
|
||||||
|
pubkey = strings.ToLower(pubkey)
|
||||||
|
|
||||||
|
// Retrieve the account for the given email.
|
||||||
|
account := model.Key{}
|
||||||
|
if err := db.Get().Where("pubkey = ?", pubkey).First(&account).Error; err != nil {
|
||||||
|
logger.Get().Debug("failed to query pubkey", zap.Error(err))
|
||||||
|
return "", errors.New("invalid pubkey")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a random challenge string.
|
||||||
|
challenge, err := generateAndSaveChallengeToken(account.AccountID, time.Minute)
|
||||||
|
if err != nil {
|
||||||
|
logger.Get().Error(ErrFailedGenerateKeyChallenge.Error())
|
||||||
|
return "", ErrFailedGenerateKeyChallenge
|
||||||
|
}
|
||||||
|
|
||||||
|
return challenge, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Logout(token string) error {
|
||||||
|
// Verify the provided token.
|
||||||
|
claims, err := jwt.Verify(jwt.HS256, sharedKey, []byte(token), blocklist)
|
||||||
|
if err != nil {
|
||||||
|
logger.Get().Debug(ErrInvalidToken.Error(), zap.Error(err))
|
||||||
|
return ErrInvalidToken
|
||||||
|
}
|
||||||
|
|
||||||
|
err = blocklist.InvalidateToken(claims.Token, claims.StandardClaims)
|
||||||
|
if err != nil {
|
||||||
|
logger.Get().Error(ErrFailedInvalidateToken.Error(), zap.Error(err), zap.String("token", token))
|
||||||
|
return ErrFailedInvalidateToken
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve the key challenge for the given challenge.
|
||||||
|
session := model.LoginSession{}
|
||||||
|
if err := db.Get().Model(session).Where("token = ?", token).First(&session).Error; err != nil {
|
||||||
|
logger.Get().Debug(ErrFailedInvalidateToken.Error(), zap.Error(err), zap.String("token", token))
|
||||||
|
return ErrFailedInvalidateToken
|
||||||
|
}
|
||||||
|
|
||||||
|
db.Get().Delete(&session)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func VerifyLoginToken(token string) error {
|
||||||
|
_, err := jwt.Verify(jwt.HS256, sharedKey, []byte(token), blocklist)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
session := model.LoginSession{}
|
||||||
|
if err := db.Get().Model(session).Where("token = ?", token).First(&session).Error; err != nil {
|
||||||
|
logger.Get().Debug(ErrInvalidToken.Error(), zap.Error(err), zap.String("token", token))
|
||||||
|
return ErrInvalidToken
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,112 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"git.lumeweb.com/LumeWeb/portal/db"
|
||||||
|
"git.lumeweb.com/LumeWeb/portal/logger"
|
||||||
|
"git.lumeweb.com/LumeWeb/portal/model"
|
||||||
|
"github.com/kataras/iris/v12"
|
||||||
|
"github.com/kataras/jwt"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// verifyPassword compares the provided plaintext password with a hashed password and returns an error if they don't match.
|
||||||
|
func verifyPassword(hashedPassword, password string) error {
|
||||||
|
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("invalid email or password")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateToken generates a JWT token for the given account ID.
|
||||||
|
func generateToken(maxAge time.Duration) (string, error) {
|
||||||
|
// Define the JWT claims.
|
||||||
|
claim := jwt.Claims{
|
||||||
|
Expiry: time.Now().Add(time.Hour * 24).Unix(), // Token expires in 24 hours.
|
||||||
|
IssuedAt: time.Now().Unix(),
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := jwt.Sign(jwt.HS256, sharedKey, claim, jwt.MaxAge(maxAge))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logger.Get().Error(ErrFailedSignJwt.Error(), zap.Error(err))
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(token), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateAndSaveLoginToken(accountID uint, maxAge time.Duration) (string, error) {
|
||||||
|
// Generate a JWT token for the authenticated user.
|
||||||
|
token, err := generateToken(maxAge)
|
||||||
|
if err != nil {
|
||||||
|
logger.Get().Error(ErrFailedGenerateToken.Error())
|
||||||
|
return "", ErrFailedGenerateToken
|
||||||
|
}
|
||||||
|
|
||||||
|
verifiedToken, _ := jwt.Verify(jwt.HS256, sharedKey, []byte(token), blocklist)
|
||||||
|
var claim *jwt.Claims
|
||||||
|
|
||||||
|
_ = verifiedToken.Claims(&claim)
|
||||||
|
|
||||||
|
// Save the token to the database.
|
||||||
|
session := model.LoginSession{
|
||||||
|
Account: model.Account{ID: accountID},
|
||||||
|
Token: token,
|
||||||
|
Expiration: claim.ExpiresAt(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Get().Create(&session).Error; err != nil {
|
||||||
|
logger.Get().Error(ErrFailedSaveToken.Error(), zap.Uint("account_id", accountID), zap.Duration("max_age", maxAge))
|
||||||
|
return "", ErrFailedSaveToken
|
||||||
|
}
|
||||||
|
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateAndSaveChallengeToken(accountID uint, maxAge time.Duration) (string, error) {
|
||||||
|
// Generate a JWT token for the authenticated user.
|
||||||
|
token, err := generateToken(maxAge)
|
||||||
|
if err != nil {
|
||||||
|
logger.Get().Error(ErrFailedGenerateToken.Error(), zap.Error(err))
|
||||||
|
return "", ErrFailedGenerateToken
|
||||||
|
}
|
||||||
|
|
||||||
|
verifiedToken, _ := jwt.Verify(jwt.HS256, sharedKey, []byte(token), blocklist)
|
||||||
|
var claim *jwt.Claims
|
||||||
|
|
||||||
|
_ = verifiedToken.Claims(&claim)
|
||||||
|
|
||||||
|
// Save the token to the database.
|
||||||
|
keyChallenge := model.KeyChallenge{
|
||||||
|
AccountID: accountID,
|
||||||
|
Challenge: token,
|
||||||
|
Expiration: claim.ExpiresAt(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Get().Create(&keyChallenge).Error; err != nil {
|
||||||
|
logger.Get().Error(ErrFailedSaveToken.Error(), zap.Error(err))
|
||||||
|
return "", ErrFailedSaveToken
|
||||||
|
}
|
||||||
|
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetRequestAuthCode(ctx iris.Context) string {
|
||||||
|
authHeader := ctx.GetHeader("Authorization")
|
||||||
|
if authHeader == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// pure check: authorization header format must be Bearer {token}
|
||||||
|
authHeaderParts := strings.Split(authHeader, " ")
|
||||||
|
if len(authHeaderParts) != 2 || strings.ToLower(authHeaderParts[0]) != "bearer" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return authHeaderParts[1]
|
||||||
|
}
|
Loading…
Reference in New Issue