Files
SQS-service/app/auth/jwt.go
T

101 lines
3.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Изменено: 2026-04-10
// jwt.go — JWT-парсинг и валидация через nubes API (deck-api).
// Извлекает sub (UUID пользователя) и email из JWT claims.
// Валидация: GET к nubes API с Bearer token → если 401/403 → отклонён.
// Подпись JWT НЕ проверяется — аналогично sless (trusted perimeter).
package auth
import (
"context"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
)
// JWTClaims — минимальный набор claims из nubes JWT.
// Не все поля заполнены у каждого пользователя — email может быть пустым.
type JWTClaims struct {
Sub string `json:"sub"`
Email string `json:"email"`
Exp int64 `json:"exp"`
Iss string `json:"iss"`
}
// ParseJWTClaims — декодирует JWT payload (base64url) и возвращает claims.
// Проверяет: структуру JWT (3 части), наличие sub, не истёк ли exp.
// Подпись НЕ проверяется — валидация через PingNubesAPI.
func ParseJWTClaims(token string) (*JWTClaims, error) {
parts := strings.Split(token, ".")
if len(parts) != 3 {
return nil, fmt.Errorf("invalid JWT: expected 3 parts, got %d", len(parts))
}
// JWT использует base64url без padding — добавляем
payload := parts[1]
switch len(payload) % 4 {
case 2:
payload += "=="
case 3:
payload += "="
}
decoded, err := base64.URLEncoding.DecodeString(payload)
if err != nil {
// Пробуем StdEncoding на случай нестандартного токена
decoded, err = base64.StdEncoding.DecodeString(payload)
if err != nil {
return nil, fmt.Errorf("decode JWT payload: %w", err)
}
}
var claims JWTClaims
if err := json.Unmarshal(decoded, &claims); err != nil {
return nil, fmt.Errorf("parse JWT claims: %w", err)
}
if claims.Sub == "" {
return nil, fmt.Errorf("JWT missing 'sub' claim")
}
if claims.Exp > 0 && claims.Exp < time.Now().Unix() {
return nil, fmt.Errorf("JWT token expired")
}
return &claims, nil
}
// TenantIDFromSub — вычисляет tenant ID из JWT subject (sub claim).
// Алгоритм совместим с sless: SHA256(sub) → первые 8 байт → hex → "sless-{16hex}".
// Длина: 6 + 16 = 22 символа. Детерминирован, необратим.
// Почему "sless-" а не "ssq-": единый namespace для всех сервисов (IoT, funcs, SQS).
func TenantIDFromSub(sub string) string {
hash := sha256.Sum256([]byte(sub))
return fmt.Sprintf("sless-%x", hash[:8])
}
// PingNubesAPI — проверяет валидность токена запросом к nubes API.
// endpoint — базовый URL (например "https://deck-api-test.ngcloud.ru/api/v1").
// Логика: 401/403 → токен отклонён. Ошибка соединения → API недоступен.
// Любой другой HTTP статус → токен принят.
func PingNubesAPI(ctx context.Context, endpoint, token string) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return fmt.Errorf("build nubes ping request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("User-Agent", "Mozilla/5.0")
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("nubes API unreachable at %s: %w", endpoint, err)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {
return fmt.Errorf("nubes API rejected token (HTTP %d)", resp.StatusCode)
}
return nil
}