2023-04-29 17:38:21 +00:00
|
|
|
package service
|
|
|
|
|
|
|
|
import (
|
|
|
|
"crypto/ed25519"
|
|
|
|
"encoding/hex"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"git.lumeweb.com/LumeWeb/portal/db"
|
|
|
|
"git.lumeweb.com/LumeWeb/portal/model"
|
|
|
|
"github.com/joomcode/errorx"
|
|
|
|
"github.com/kataras/iris/v12"
|
|
|
|
"github.com/kataras/jwt"
|
|
|
|
"golang.org/x/crypto/bcrypt"
|
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
|
|
|
var sharedKey = []byte("sercrethatmaycontainch@r$32chars")
|
|
|
|
|
|
|
|
var blocklist *jwt.Blocklist
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
blocklist = jwt.NewBlocklist(1 * time.Hour)
|
|
|
|
}
|
|
|
|
|
|
|
|
type AuthService struct {
|
2023-04-30 08:49:19 +00:00
|
|
|
Ctx iris.Context
|
2023-04-29 17:38:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type LoginRequest struct {
|
|
|
|
Email string `json:"email"`
|
|
|
|
Password string `json:"password"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type LoginResponse struct {
|
|
|
|
Token string `json:"token"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type LogoutRequest struct {
|
|
|
|
Token string `json:"token"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type ChallengeRequest struct {
|
|
|
|
Pubkey string `json:"pubkey"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type ChallengeResponse struct {
|
|
|
|
Challenge string `json:"challenge"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type PubkeyLoginRequest struct {
|
2023-04-30 06:16:53 +00:00
|
|
|
Pubkey string `json:"pubkey"`
|
2023-04-29 17:38:21 +00:00
|
|
|
Challenge string `json:"challenge"`
|
|
|
|
Signature string `json:"signature"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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 {
|
|
|
|
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 {
|
|
|
|
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 {
|
|
|
|
return "", errorx.Decorate(err, "failed to save token")
|
|
|
|
}
|
|
|
|
|
|
|
|
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 {
|
|
|
|
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{
|
2023-04-30 06:17:18 +00:00
|
|
|
AccountID: accountID,
|
2023-04-29 17:38:21 +00:00
|
|
|
Challenge: token,
|
|
|
|
Expiration: claim.ExpiresAt(),
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := db.Get().Create(&keyChallenge).Error; err != nil {
|
|
|
|
return "", errorx.Decorate(err, "failed to save token")
|
|
|
|
}
|
|
|
|
|
|
|
|
return token, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// PostLogin handles the POST /api/auth/login request to authenticate a user and return a JWT token.
|
|
|
|
func (a *AuthService) PostLogin() {
|
|
|
|
var r LoginRequest
|
|
|
|
|
|
|
|
// Read the login request from the client.
|
2023-04-30 08:49:19 +00:00
|
|
|
if err := a.Ctx.ReadJSON(&r); err != nil {
|
|
|
|
a.Ctx.StopWithError(iris.StatusBadRequest, err)
|
2023-04-29 17:38:21 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Retrieve the account for the given email.
|
|
|
|
account := model.Account{}
|
|
|
|
if err := db.Get().Where("email = ?", r.Email).First(&account).Error; err != nil {
|
2023-04-30 08:49:19 +00:00
|
|
|
a.Ctx.StopWithError(iris.StatusBadRequest, errors.New("invalid email or password"))
|
2023-04-29 17:38:21 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Verify the provided password against the hashed password stored in the database.
|
|
|
|
if err := verifyPassword(*account.Password, r.Password); err != nil {
|
2023-04-30 08:49:19 +00:00
|
|
|
a.Ctx.StopWithError(iris.StatusBadRequest, errors.New("invalid email or password"))
|
2023-04-29 17:38:21 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Generate a JWT token for the authenticated user.
|
|
|
|
token, err := generateAndSaveLoginToken(account.ID, 24*time.Hour)
|
|
|
|
if err != nil {
|
2023-04-30 08:49:19 +00:00
|
|
|
a.Ctx.StopWithError(iris.StatusInternalServerError, fmt.Errorf("failed to generate token: %s", err))
|
2023-04-29 17:38:21 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Return the JWT token to the client.
|
2023-04-30 08:49:19 +00:00
|
|
|
err = a.Ctx.JSON(&LoginResponse{Token: token})
|
2023-04-29 17:38:21 +00:00
|
|
|
if err != nil {
|
|
|
|
panic(fmt.Errorf("Error with login attempt: %s \n", err))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-30 06:46:11 +00:00
|
|
|
// PostChallenge handles the POST /api/auth/pubkey/challenge request to generate a challenge for a user's public key.
|
2023-04-29 17:38:21 +00:00
|
|
|
func (a *AuthService) PostPubkeyChallenge() {
|
|
|
|
var r LoginRequest
|
|
|
|
|
|
|
|
// Read the login request from the client.
|
2023-04-30 08:49:19 +00:00
|
|
|
if err := a.Ctx.ReadJSON(&r); err != nil {
|
|
|
|
a.Ctx.StopWithError(iris.StatusBadRequest, err)
|
2023-04-29 17:38:21 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Retrieve the account for the given email.
|
|
|
|
account := model.Account{}
|
|
|
|
if err := db.Get().Where("email = ?", r.Email).First(&account).Error; err != nil {
|
2023-04-30 08:49:19 +00:00
|
|
|
a.Ctx.StopWithError(iris.StatusBadRequest, errors.New("invalid email or password"))
|
2023-04-29 17:38:21 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Generate a random challenge string.
|
|
|
|
challenge, err := generateAndSaveChallengeToken(account.ID, time.Minute)
|
|
|
|
if err != nil {
|
2023-04-30 08:49:19 +00:00
|
|
|
a.Ctx.StopWithError(iris.StatusInternalServerError, errors.New("failed to generate challenge"))
|
2023-04-29 17:38:21 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Return the challenge to the client.
|
2023-04-30 08:49:19 +00:00
|
|
|
err = a.Ctx.JSON(&ChallengeResponse{Challenge: challenge})
|
2023-04-29 17:38:21 +00:00
|
|
|
if err != nil {
|
|
|
|
panic(fmt.Errorf("Error with challenge request: %s \n", err))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-30 06:46:11 +00:00
|
|
|
// PostKeyLogin handles the POST /api/auth/pubkey/login request to authenticate a user using a public key challenge and return a JWT token.
|
2023-04-29 17:38:21 +00:00
|
|
|
func (a *AuthService) PostPubkeyLogin() {
|
|
|
|
var r PubkeyLoginRequest
|
|
|
|
|
|
|
|
// Read the key login request from the client.
|
2023-04-30 08:49:19 +00:00
|
|
|
if err := a.Ctx.ReadJSON(&r); err != nil {
|
|
|
|
a.Ctx.StopWithError(iris.StatusBadRequest, err)
|
2023-04-29 17:38:21 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Retrieve the key challenge for the given challenge.
|
|
|
|
challenge := model.KeyChallenge{}
|
|
|
|
if err := db.Get().Where("challenge = ?", r.Challenge).Preload("Key").First(&challenge).Error; err != nil {
|
2023-04-30 08:49:19 +00:00
|
|
|
a.Ctx.StopWithError(iris.StatusBadRequest, errorx.RejectedOperation.New("invalid key challenge"))
|
2023-04-29 17:38:21 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
verifiedToken, err := jwt.Verify(jwt.HS256, sharedKey, []byte(r.Challenge), blocklist)
|
|
|
|
if err != nil {
|
2023-04-30 08:49:19 +00:00
|
|
|
a.Ctx.StopWithError(iris.StatusBadRequest, errorx.RejectedOperation.New("invalid key challenge"))
|
2023-04-29 17:38:21 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
rawPubKey, err := hex.DecodeString(r.Pubkey)
|
|
|
|
if err != nil {
|
2023-04-30 08:49:19 +00:00
|
|
|
a.Ctx.StopWithError(iris.StatusBadRequest, errorx.RejectedOperation.New("invalid pubkey"))
|
2023-04-29 17:38:21 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
rawSignature, err := hex.DecodeString(r.Signature)
|
|
|
|
if err != nil {
|
2023-04-30 08:49:19 +00:00
|
|
|
a.Ctx.StopWithError(iris.StatusBadRequest, errorx.RejectedOperation.New("invalid signature"))
|
2023-04-29 17:38:21 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
publicKeyDecoded := ed25519.PublicKey(rawPubKey)
|
|
|
|
|
|
|
|
// Verify the challenge signature.
|
|
|
|
if !ed25519.Verify(publicKeyDecoded, []byte(r.Challenge), rawSignature) {
|
2023-04-30 08:49:19 +00:00
|
|
|
a.Ctx.StopWithError(iris.StatusBadRequest, errorx.RejectedOperation.New("invalid challenge"))
|
2023-04-29 17:38:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Generate a JWT token for the authenticated user.
|
2023-04-30 06:17:18 +00:00
|
|
|
token, err := generateAndSaveLoginToken(challenge.AccountID, 24*time.Hour)
|
2023-04-29 17:38:21 +00:00
|
|
|
if err != nil {
|
2023-04-30 08:49:19 +00:00
|
|
|
a.Ctx.StopWithError(iris.StatusInternalServerError, errorx.RejectedOperation.Wrap(err, "failed to generate token"))
|
2023-04-29 17:38:21 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
err = blocklist.InvalidateToken(verifiedToken.Token, verifiedToken.StandardClaims)
|
|
|
|
if err != nil {
|
2023-04-30 08:49:19 +00:00
|
|
|
a.Ctx.StopWithError(iris.StatusInternalServerError, errorx.RejectedOperation.Wrap(err, "failed to invalidate token"))
|
2023-04-29 17:38:21 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := db.Get().Delete(&challenge).Error; err != nil {
|
2023-04-30 08:49:19 +00:00
|
|
|
a.Ctx.StopWithError(iris.StatusBadRequest, errorx.RejectedOperation.New("failed to delete key challenge"))
|
2023-04-29 17:38:21 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Return the JWT token to the client.
|
2023-04-30 08:49:19 +00:00
|
|
|
err = a.Ctx.JSON(&LoginResponse{Token: token})
|
2023-04-29 17:38:21 +00:00
|
|
|
if err != nil {
|
|
|
|
panic(fmt.Errorf("Error with login attempt: %s \n", err))
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// PostLogout handles the POST /api/auth/logout request to invalidate a JWT token.
|
|
|
|
func (a *AuthService) PostLogout() {
|
|
|
|
var r LogoutRequest
|
|
|
|
|
|
|
|
// Read the logout request from the client.
|
2023-04-30 08:49:19 +00:00
|
|
|
if err := a.Ctx.ReadJSON(&r); err != nil {
|
|
|
|
a.Ctx.StopWithError(iris.StatusBadRequest, err)
|
2023-04-29 17:38:21 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Verify the provided token.
|
|
|
|
claims, err := jwt.Verify(jwt.HS256, sharedKey, []byte(r.Token), blocklist)
|
|
|
|
if err != nil {
|
2023-04-30 08:49:19 +00:00
|
|
|
a.Ctx.StopWithError(iris.StatusBadRequest, errors.New("invalid token"))
|
2023-04-29 17:38:21 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
err = blocklist.InvalidateToken(claims.Token, claims.StandardClaims)
|
|
|
|
if err != nil {
|
|
|
|
panic(fmt.Errorf("Error with logout: %s \n", err))
|
|
|
|
}
|
|
|
|
|
|
|
|
// Return a success response to the client.
|
2023-04-30 08:49:19 +00:00
|
|
|
a.Ctx.StatusCode(iris.StatusNoContent)
|
2023-04-29 17:38:21 +00:00
|
|
|
}
|