// Package auth provides authentication and authorization functionality package auth import ( "crypto/rand" "encoding/base64" "errors" "fmt" "time" "github.com/golang-jwt/jwt/v5" ) var ( ErrInvalidToken = errors.New("invalid token") ErrExpiredToken = errors.New("token has expired") ) // TokenPair contains access and refresh tokens type TokenPair struct { AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` TokenType string `json:"token_type"` ExpiresIn int64 `json:"expires_in"` } // Claims represents JWT claims for access tokens type Claims struct { jwt.RegisteredClaims Type string `json:"type"` Email string `json:"email,omitempty"` } // JWTManager handles JWT token operations type JWTManager struct { secretKey []byte accessTokenExpiry time.Duration refreshTokenExpiry time.Duration } // NewJWTManager creates a new JWT manager func NewJWTManager(secret string) *JWTManager { return &JWTManager{ secretKey: []byte(secret), accessTokenExpiry: time.Hour, refreshTokenExpiry: 30 * 24 * time.Hour, } } // GenerateTokenPair creates a new access/refresh token pair func (m *JWTManager) GenerateTokenPair(developerID, email string) (*TokenPair, error) { accessToken, err := m.generateToken(developerID, email, "access", m.accessTokenExpiry) if err != nil { return nil, fmt.Errorf("generate access token: %w", err) } refreshToken, err := m.generateToken(developerID, email, "refresh", m.refreshTokenExpiry) if err != nil { return nil, fmt.Errorf("generate refresh token: %w", err) } return &TokenPair{ AccessToken: accessToken, RefreshToken: refreshToken, TokenType: "Bearer", ExpiresIn: int64(m.accessTokenExpiry.Seconds()), }, nil } func (m *JWTManager) generateToken(subject, email, tokenType string, expiry time.Duration) (string, error) { now := time.Now() claims := Claims{ RegisteredClaims: jwt.RegisteredClaims{ Subject: subject, IssuedAt: jwt.NewNumericDate(now), ExpiresAt: jwt.NewNumericDate(now.Add(expiry)), Issuer: "mosis-portal", }, Type: tokenType, Email: email, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString(m.secretKey) } // ValidateAccessToken validates an access token and returns the claims func (m *JWTManager) ValidateAccessToken(tokenString string) (*Claims, error) { return m.validateToken(tokenString, "access") } // ValidateRefreshToken validates a refresh token and returns the claims func (m *JWTManager) ValidateRefreshToken(tokenString string) (*Claims, error) { return m.validateToken(tokenString, "refresh") } func (m *JWTManager) validateToken(tokenString, expectedType string) (*Claims, error) { token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } return m.secretKey, nil }) if err != nil { if errors.Is(err, jwt.ErrTokenExpired) { return nil, ErrExpiredToken } return nil, ErrInvalidToken } claims, ok := token.Claims.(*Claims) if !ok || !token.Valid { return nil, ErrInvalidToken } if claims.Type != expectedType { return nil, ErrInvalidToken } return claims, nil } // GenerateAPIKey generates a new API key with the given prefix func GenerateAPIKey(prefix string) (string, error) { // Generate 32 random bytes bytes := make([]byte, 32) if _, err := rand.Read(bytes); err != nil { return "", err } // Encode as base64url (no padding) encoded := base64.RawURLEncoding.EncodeToString(bytes) return fmt.Sprintf("%s_%s", prefix, encoded), nil } // GenerateState generates a random state for OAuth func GenerateState() (string, error) { bytes := make([]byte, 16) if _, err := rand.Read(bytes); err != nil { return "", err } return base64.RawURLEncoding.EncodeToString(bytes), nil }