/* The JWT claims contains: cid - Session UUUID uid - User ID iat - Issued at exp - Expires login - Username name - User name auth - Session authenticated perm - User permissions */ package jwtsession import ( // External "github.com/golang-jwt/jwt/v5" // Standard "crypto" "encoding/json" "fmt" "time" ) type KeyType int const ( KeyEd25519 KeyType = iota KeyEcDSA ) type Manager struct { KeyType KeyType privKey crypto.PrivateKey PubKey crypto.PublicKey ExpireDays int Initialized bool } func NewManager(keyType KeyType, private, public string, expireDays int) (mngr Manager, err error) { // {{{ var errPriv, errPub error mngr.KeyType = keyType switch keyType { case KeyEcDSA: /* openssl ecparam -genkey -name secp521r1 -noout >priv.pem openssl pkey -in priv.pem -pubout >pub.pem */ mngr.privKey, errPriv = jwt.ParseECPrivateKeyFromPEM([]byte(private)) mngr.PubKey, errPub = jwt.ParseECPublicKeyFromPEM([]byte(public)) case KeyEd25519: /* openssl genpkey -algorithm ed25519 -out /tmp/priv.pem openssl pkey -in priv.pem -pubout >pub.pem */ mngr.privKey, errPriv = jwt.ParseEdPrivateKeyFromPEM([]byte(private)) mngr.PubKey, errPub = jwt.ParseEdPublicKeyFromPEM([]byte(public)) } if errPriv != nil { err = errPriv return } if errPub != nil { err = errPub return } mngr.ExpireDays = expireDays mngr.Initialized = true return } // }}} func (mngr *Manager) NewToken(sessionID string, userID int, username, name string) (token string) { // {{{ // A new token is generated with the information. data := make(map[string]any) data["cid"] = sessionID data["uid"] = userID data["login"] = username data["name"] = name token = mngr.GenerateToken(data) return } // }}} func (mngr *Manager) GenerateToken(data map[string]any) (signedString string) { // {{{ // Create a new token object, specifying signing method and the claims // you would like it to contain. now := time.Now() data["iat"] = now.Unix() data["exp"] = now.Add(time.Hour * 24 * time.Duration(mngr.ExpireDays)).Unix() var token *jwt.Token switch mngr.KeyType { case KeyEd25519: token = jwt.NewWithClaims(&jwt.SigningMethodEd25519{}, jwt.MapClaims(data)) case KeyEcDSA: token = jwt.NewWithClaims(jwt.SigningMethodES512, jwt.MapClaims(data)) } // Sign and get the complete encoded token as a string using the secret. var err error signedString, err = token.SignedString(mngr.privKey) if err != nil { j, _ := json.Marshal(struct { Time time.Time `json:"time"` Level string Msg string Error string }{ time.Now(), "ERROR", "JWT", err.Error(), }) fmt.Printf("%s\n", j) } return } // }}} func (mngr *Manager) ParseToken(tokenString string) (jwt.MapClaims, error) { // {{{ // Parse takes the token string and a function for looking up the key. The latter is especially // useful if you use multiple keys for your application. The standard is to use 'kid' in the // head of the token to identify which key to use, but the parsed token (head and claims) is provided // to the callback, providing flexibility. token, err := jwt.Parse(tokenString, func(token *jwt.Token) (any, error) { // Don't forget to validate the alg is what you expect: switch mngr.KeyType { case KeyEd25519: if _, ok := token.Method.(*jwt.SigningMethodEd25519); !ok { return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) } case KeyEcDSA: if _, ok := token.Method.(*jwt.SigningMethodECDSA); !ok { return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) } } return mngr.PubKey, nil }) if err != nil { return nil, err } if claims, ok := token.Claims.(jwt.MapClaims); ok { err = validateTokenTimestamps(claims) if err != nil { return nil, err } return claims, nil } else { return nil, err } } // }}} func (mngr *Manager) GetTokenClaims(tokenString string) (jwt.MapClaims, error) { // {{{ return nil, nil } // }}} func validateTokenTimestamps(claims jwt.MapClaims) error { // {{{ now := time.Now() if issuedAt, ok := claims["iat"].(float64); ok { if now.Unix() < int64(issuedAt) { return fmt.Errorf("Token is not valid yet") } } else { return fmt.Errorf("Token is missing iat") } if expires, ok := claims["exp"].(float64); ok { if now.Unix() > int64(expires) { return fmt.Errorf("Token has expired") } } return nil } // }}}