add auth lib

This commit is contained in:
2026-03-01 03:04:43 +01:00
parent e9bb724708
commit 4b6268535f
11 changed files with 544 additions and 0 deletions

102
jwt/jwt.go Normal file
View File

@@ -0,0 +1,102 @@
package jwt
import (
"fmt"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
)
const (
TokenTypeAuth = "auth"
TokenTypeRefresh = "refresh"
)
type Claims struct {
UserID string `json:"userId"`
Username string `json:"username"`
TokenType string `json:"tokenType"`
jwt.RegisteredClaims
}
type JWTManager struct {
secret []byte
authTTL time.Duration
refreshTTL time.Duration
}
func NewJWTManager(secret string, authTTL, refreshTTL time.Duration) *JWTManager {
return &JWTManager{
secret: []byte(secret),
authTTL: authTTL,
refreshTTL: refreshTTL,
}
}
func (m *JWTManager) GenerateAuthToken(userID string, username string) (string, error) {
return m.generate(userID, username, TokenTypeAuth, m.authTTL)
}
func (m *JWTManager) GenerateRefreshToken(userID string, username string) (string, error) {
return m.generate(userID, username, TokenTypeRefresh, m.refreshTTL)
}
func (m *JWTManager) generate(userID string, username, tokenType string, ttl time.Duration) (string, error) {
now := time.Now()
claims := Claims{
UserID: userID,
Username: username,
TokenType: tokenType,
RegisteredClaims: jwt.RegisteredClaims{
IssuedAt: jwt.NewNumericDate(now),
ExpiresAt: jwt.NewNumericDate(now.Add(ttl)),
Subject: userID,
ID: uuid.NewString(),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(m.secret)
}
func (m *JWTManager) ParseAuthToken(tokenStr string) (*Claims, error) {
claims, err := m.parse(tokenStr)
if err != nil {
return nil, err
}
if claims.TokenType != TokenTypeAuth {
return nil, fmt.Errorf("invalid token type")
}
return claims, nil
}
func (m *JWTManager) ParseRefreshToken(tokenStr string) (*Claims, error) {
claims, err := m.parse(tokenStr)
if err != nil {
return nil, err
}
if claims.TokenType != TokenTypeRefresh {
return nil, fmt.Errorf("invalid token type")
}
return claims, nil
}
func (m *JWTManager) parse(tokenStr string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(token *jwt.Token) (any, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method")
}
return m.secret, nil
})
if err != nil {
return nil, err
}
claims, ok := token.Claims.(*Claims)
if !ok || !token.Valid {
return nil, fmt.Errorf("invalid token")
}
return claims, nil
}

96
jwt/jwt_test.go Normal file
View File

@@ -0,0 +1,96 @@
package jwt
import (
"testing"
"time"
"github.com/golang-jwt/jwt/v5"
)
func TestJWTManagerGenerateAndParseAuthToken(t *testing.T) {
mgr := NewJWTManager("secret", 2*time.Minute, 10*time.Minute)
token, err := mgr.GenerateAuthToken("user-1", "alice")
if err != nil {
t.Fatalf("generate auth token: %v", err)
}
claims, err := mgr.ParseAuthToken(token)
if err != nil {
t.Fatalf("parse auth token: %v", err)
}
if claims.UserID != "user-1" {
t.Fatalf("expected user id user-1, got %q", claims.UserID)
}
if claims.Username != "alice" {
t.Fatalf("expected username alice, got %q", claims.Username)
}
if claims.TokenType != TokenTypeAuth {
t.Fatalf("expected token type %q, got %q", TokenTypeAuth, claims.TokenType)
}
}
func TestJWTManagerRejectsWrongTokenType(t *testing.T) {
mgr := NewJWTManager("secret", time.Minute, time.Minute)
refreshToken, err := mgr.GenerateRefreshToken("user-1", "alice")
if err != nil {
t.Fatalf("generate refresh token: %v", err)
}
if _, err := mgr.ParseAuthToken(refreshToken); err == nil {
t.Fatal("expected error when parsing refresh token as auth token")
}
authToken, err := mgr.GenerateAuthToken("user-1", "alice")
if err != nil {
t.Fatalf("generate auth token: %v", err)
}
if _, err := mgr.ParseRefreshToken(authToken); err == nil {
t.Fatal("expected error when parsing auth token as refresh token")
}
}
func TestJWTManagerRejectsInvalidOrTamperedToken(t *testing.T) {
mgr := NewJWTManager("secret", time.Minute, time.Minute)
if _, err := mgr.ParseAuthToken("not-a-token"); err == nil {
t.Fatal("expected parse error for malformed token")
}
token, err := mgr.GenerateAuthToken("user-1", "alice")
if err != nil {
t.Fatalf("generate auth token: %v", err)
}
otherMgr := NewJWTManager("different-secret", time.Minute, time.Minute)
if _, err := otherMgr.ParseAuthToken(token); err == nil {
t.Fatal("expected signature validation error with different secret")
}
}
func TestJWTManagerRejectsExpiredToken(t *testing.T) {
mgr := NewJWTManager("secret", -1*time.Second, time.Minute)
token, err := mgr.GenerateAuthToken("user-1", "alice")
if err != nil {
t.Fatalf("generate expired auth token: %v", err)
}
if _, err := mgr.ParseAuthToken(token); err == nil {
t.Fatal("expected expired token error")
}
}
func TestJWTManagerRejectsNonHMACAlgorithm(t *testing.T) {
mgr := NewJWTManager("secret", time.Minute, time.Minute)
noneToken := jwt.NewWithClaims(jwt.SigningMethodNone, Claims{UserID: "u", Username: "n", TokenType: TokenTypeAuth})
tokenStr, err := noneToken.SignedString(jwt.UnsafeAllowNoneSignatureType)
if err != nil {
t.Fatalf("sign none token: %v", err)
}
if _, err := mgr.ParseAuthToken(tokenStr); err == nil {
t.Fatal("expected error for non-HMAC algorithm")
}
}