feat: signed expiring console session tokens

This commit is contained in:
2026-07-17 11:16:11 +01:00
parent 257e4fa89d
commit 19b76044ff
2 changed files with 112 additions and 0 deletions
+69
View File
@@ -0,0 +1,69 @@
package services
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"fmt"
"strconv"
"strings"
"time"
)
func sessionHMACKey() ([]byte, error) {
// Reuse the AES key material as the HMAC secret. Distinct domain via prefix.
k, err := encryptionKey()
if err != nil {
return nil, err
}
mac := hmac.New(sha256.New, k)
mac.Write([]byte("vantage-console-session-v1"))
return mac.Sum(nil), nil
}
func b64(b []byte) string { return base64.RawURLEncoding.EncodeToString(b) }
// SignSessionToken returns a signed, expiring token binding a session id.
func SignSessionToken(sessionID string, ttl time.Duration) (string, error) {
key, err := sessionHMACKey()
if err != nil {
return "", err
}
exp := time.Now().Add(ttl).Unix()
payload := fmt.Sprintf("%s.%d", b64([]byte(sessionID)), exp)
mac := hmac.New(sha256.New, key)
mac.Write([]byte(payload))
return payload + "." + b64(mac.Sum(nil)), nil
}
// VerifySessionToken checks signature + expiry and returns the session id.
func VerifySessionToken(token string) (string, error) {
parts := strings.Split(token, ".")
if len(parts) != 3 {
return "", fmt.Errorf("malformed token")
}
payload := parts[0] + "." + parts[1]
key, err := sessionHMACKey()
if err != nil {
return "", err
}
mac := hmac.New(sha256.New, key)
mac.Write([]byte(payload))
want := mac.Sum(nil)
got, err := base64.RawURLEncoding.DecodeString(parts[2])
if err != nil || !hmac.Equal(want, got) {
return "", fmt.Errorf("invalid signature")
}
exp, err := strconv.ParseInt(parts[1], 10, 64)
if err != nil {
return "", fmt.Errorf("invalid expiry")
}
if time.Now().Unix() > exp {
return "", fmt.Errorf("token expired")
}
sid, err := base64.RawURLEncoding.DecodeString(parts[0])
if err != nil {
return "", fmt.Errorf("invalid session id")
}
return string(sid), nil
}
+43
View File
@@ -0,0 +1,43 @@
package services
import (
"testing"
"time"
)
func TestSessionTokenRoundTrip(t *testing.T) {
t.Setenv("KEY_ENCRYPTION_KEY", "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff")
tok, err := SignSessionToken("sess-123", time.Minute)
if err != nil {
t.Fatalf("sign: %v", err)
}
got, err := VerifySessionToken(tok)
if err != nil {
t.Fatalf("verify: %v", err)
}
if got != "sess-123" {
t.Fatalf("got %q want sess-123", got)
}
}
func TestSessionTokenExpired(t *testing.T) {
t.Setenv("KEY_ENCRYPTION_KEY", "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff")
tok, err := SignSessionToken("sess-123", -time.Second)
if err != nil {
t.Fatalf("sign: %v", err)
}
if _, err := VerifySessionToken(tok); err == nil {
t.Fatalf("expected expiry error, got nil")
}
}
func TestSessionTokenTampered(t *testing.T) {
t.Setenv("KEY_ENCRYPTION_KEY", "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff")
tok, _ := SignSessionToken("sess-123", time.Minute)
if _, err := VerifySessionToken(tok + "x"); err == nil {
t.Fatalf("expected signature error, got nil")
}
}