Files
SQS-service/app/auth/jwt.go
T
Naeel b9d434bcc5 v0.1.16: JWT auth via nubes API, auto-provisioning, UI login
- app/auth/jwt.go: ParseJWTClaims, TenantIDFromSub (sless-compatible SHA256), PingNubesAPI
- app/admin/admin.go: POST /ui/api/auth endpoint, jwtMiddleware for /ui/api/*
- app/tenant/tenant_store.go: NubesSub/Email fields, GetBySub, CreateFromJWT
- app/ui/index.html: login page, email in navbar, JWT session in localStorage
- deployments/k8s/deployment.yaml: v0.1.16, NUBES_ENDPOINT env
- doc/decisions/resource-limits-plan.md: 20 vulnerabilities audit
- Fix: /ui/api/auth moved to subrouter (gorilla/mux PathPrefix conflict)
2026-04-10 18:35:19 +03:00

100 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)
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
}