portal/account/jwt.go

188 lines
4.6 KiB
Go
Raw Permalink Normal View History

package account
import (
"crypto/ed25519"
2024-02-14 03:17:34 +00:00
"errors"
"fmt"
2024-03-17 12:15:27 +00:00
"net/http"
2024-02-14 03:17:34 +00:00
"strconv"
"time"
2024-02-18 01:07:43 +00:00
"git.lumeweb.com/LumeWeb/portal/config"
2024-03-18 21:02:16 +00:00
"github.com/samber/lo"
2024-03-17 13:09:29 +00:00
"go.sia.tech/jape"
"git.lumeweb.com/LumeWeb/portal/api/router"
apiRegistry "git.lumeweb.com/LumeWeb/portal/api/registry"
2024-02-18 01:07:43 +00:00
"github.com/golang-jwt/jwt/v5"
)
const AUTH_COOKIE_NAME = "auth_token"
2024-02-14 01:58:17 +00:00
type JWTPurpose string
2024-02-18 01:11:43 +00:00
type VerifyTokenFunc func(claim *jwt.RegisteredClaims) error
2024-02-14 03:17:34 +00:00
var (
2024-02-18 01:11:43 +00:00
nopVerifyFunc VerifyTokenFunc = func(claim *jwt.RegisteredClaims) error {
2024-02-14 03:17:34 +00:00
return nil
}
ErrJWTUnexpectedClaimsType = errors.New("unexpected claims type")
ErrJWTUnexpectedIssuer = errors.New("unexpected issuer")
ErrJWTInvalid = errors.New("invalid JWT")
)
2024-02-14 01:58:17 +00:00
const (
JWTPurposeLogin JWTPurpose = "login"
2024-02-14 03:17:34 +00:00
JWTPurpose2FA JWTPurpose = "2fa"
2024-03-13 22:44:09 +00:00
JWTPurposeNone JWTPurpose = ""
2024-02-14 01:58:17 +00:00
)
2024-02-14 03:31:44 +00:00
func JWTGenerateToken(domain string, privateKey ed25519.PrivateKey, userID uint, purpose JWTPurpose) (string, error) {
return JWTGenerateTokenWithDuration(domain, privateKey, userID, time.Hour*24, purpose)
}
2024-02-14 01:58:17 +00:00
2024-02-14 03:31:44 +00:00
func JWTGenerateTokenWithDuration(domain string, privateKey ed25519.PrivateKey, userID uint, duration time.Duration, purpose JWTPurpose) (string, error) {
2024-02-14 03:17:34 +00:00
// Define the claims
2024-02-14 03:17:34 +00:00
claims := jwt.RegisteredClaims{
Issuer: domain,
Subject: strconv.Itoa(int(userID)),
ExpiresAt: jwt.NewNumericDate(time.Now().Add(duration)),
IssuedAt: jwt.NewNumericDate(time.Now()),
Audience: []string{string(purpose)},
}
// Create the token
token := jwt.NewWithClaims(jwt.SigningMethodEdDSA, claims)
// Sign the token with the Ed25519 private key
tokenString, err := token.SignedString(privateKey)
if err != nil {
return "", err
}
return tokenString, nil
}
2024-02-14 03:17:34 +00:00
2024-02-14 03:31:44 +00:00
func JWTVerifyToken(token string, domain string, privateKey ed25519.PrivateKey, verifyFunc VerifyTokenFunc) (*jwt.RegisteredClaims, error) {
2024-02-18 01:07:43 +00:00
validatedToken, err := jwt.ParseWithClaims(token, &jwt.RegisteredClaims{}, func(token *jwt.Token) (interface{}, error) {
2024-02-14 03:17:34 +00:00
if _, ok := token.Method.(*jwt.SigningMethodEd25519); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
publicKey := privateKey.Public()
return publicKey, nil
})
if err != nil {
return nil, err
}
if verifyFunc == nil {
verifyFunc = nopVerifyFunc
}
2024-02-18 01:07:43 +00:00
claim, ok := validatedToken.Claims.(*jwt.RegisteredClaims)
2024-02-14 03:17:34 +00:00
if !ok {
return nil, fmt.Errorf("%w: %s", ErrJWTUnexpectedClaimsType, validatedToken.Claims)
}
if domain != claim.Issuer {
return nil, fmt.Errorf("%w: %s", ErrJWTUnexpectedIssuer, claim.Issuer)
}
2024-02-18 01:11:43 +00:00
err = verifyFunc(claim)
2024-02-14 03:17:34 +00:00
2024-02-18 01:16:19 +00:00
return claim, err
2024-02-14 03:17:34 +00:00
}
2024-03-17 12:15:27 +00:00
func SetAuthCookie(jc jape.Context, c *config.Manager, jwt string) {
for _, api := range apiRegistry.GetAllAPIs() {
routeableApi, ok := api.(router.RoutableAPI)
if !ok {
continue
}
2024-03-17 13:09:29 +00:00
http.SetCookie(jc.ResponseWriter, &http.Cookie{
Name: routeableApi.AuthTokenName(),
Value: jwt,
2024-03-20 21:13:13 +00:00
MaxAge: int((24 * time.Hour).Seconds()),
Secure: true,
HttpOnly: true,
Path: "/",
Domain: c.Config().Core.Domain,
})
}
2024-03-17 12:15:27 +00:00
}
2024-03-17 13:27:57 +00:00
func EchoAuthCookie(jc jape.Context, config *config.Manager) {
for _, api := range apiRegistry.GetAllAPIs() {
2024-03-18 21:02:16 +00:00
routeableApi, ok := api.(router.RoutableAPI)
if !ok {
continue
}
cookies := lo.Filter(jc.Request.Cookies(), func(item *http.Cookie, _ int) bool {
return item.Name == routeableApi.AuthTokenName()
})
if len(cookies) == 0 {
continue
}
unverified, _, err := jwt.NewParser().ParseUnverified(cookies[0].Value, &jwt.RegisteredClaims{})
if err != nil {
http.Error(jc.ResponseWriter, err.Error(), http.StatusInternalServerError)
return
}
exp, err := unverified.Claims.GetExpirationTime()
if err != nil {
http.Error(jc.ResponseWriter, err.Error(), http.StatusInternalServerError)
return
}
http.SetCookie(jc.ResponseWriter, &http.Cookie{
Name: cookies[0].Name,
Value: cookies[0].Value,
2024-03-20 21:13:13 +00:00
MaxAge: int(exp.Time.Sub(time.Now()).Seconds()),
Secure: true,
HttpOnly: true,
Path: "/",
Domain: config.Config().Core.Domain,
})
2024-03-18 21:02:16 +00:00
}
}
func ClearAuthCookie(jc jape.Context, config *config.Manager) {
for _, api := range apiRegistry.GetAllAPIs() {
2024-03-17 13:27:57 +00:00
routeableApi, ok := api.(router.RoutableAPI)
if !ok {
continue
}
2024-03-19 15:05:51 +00:00
jc.ResponseWriter.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
jc.ResponseWriter.Header().Set("Pragma", "no-cache")
jc.ResponseWriter.Header().Set("Expires", "0")
2024-03-17 13:27:57 +00:00
http.SetCookie(jc.ResponseWriter, &http.Cookie{
Name: routeableApi.AuthTokenName(),
Value: "",
Expires: time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC),
2024-03-19 15:43:11 +00:00
MaxAge: -1,
Secure: true,
2024-03-17 13:27:57 +00:00
HttpOnly: true,
Path: "/",
Domain: config.Config().Core.Domain,
2024-03-17 13:27:57 +00:00
})
}
}