/
t3
/
token-server
Обзор
Документация
Войти
/
t3
/
token-server
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
main
internal/api/token_handler.go
200 строк
7 KB
Ivan Shibkikh
auth: phase 3
25 июл 2026, 22:52
25 июл 2026, 22:52
a53c429
Код
Авторство
О чём код?
package api import ( "encoding/json" "log/slog" "net/http" "github.com/golang-jwt/jwt/v5" "github.com/google/uuid" "gitverse.ru/t3/sts/internal/auth" "gitverse.ru/t3/sts/internal/client" "gitverse.ru/t3/sts/internal/storage" ) // TokenHandler implements the token issuance endpoints: // - POST /token (password grant via IAM) // - POST /token/refresh (refresh rotation) // - POST /token/service (Service Token for Gateway -> internal service) type TokenHandler struct { issuer *auth.Issuer km *auth.KeyManager iam *client.IAMClient sessions *storage.SessionStore logger *slog.Logger } func NewTokenHandler(issuer *auth.Issuer, km *auth.KeyManager, iam *client.IAMClient, sessions *storage.SessionStore, logger *slog.Logger) *TokenHandler { return &TokenHandler{ issuer: issuer, km: km, iam: iam, sessions: sessions, logger: logger, } } // Issue handles POST /token. // Validates user credentials via IAM, then issues an Access + Refresh pair. func (h *TokenHandler) Issue(w http.ResponseWriter, r *http.Request) { var req TokenRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeRequestIDError(w, r, http.StatusBadRequest, "bad_request", "invalid JSON body") return } if req.AccessKey == "" || req.AccessSecret == "" { writeRequestIDError(w, r, http.StatusBadRequest, "bad_request", "access_key and access_secret are required") return } if req.GrantType == "" { req.GrantType = "password" } if req.GrantType != "password" { writeRequestIDError(w, r, http.StatusBadRequest, "unsupported_grant_type", "only 'password' grant is supported") return } result, err := h.iam.AuthCheck(r.Context(), req.AccessKey, req.AccessSecret) if err != nil { h.logger.Error("iam auth check failed", "error", err, "request_id", requestIDFromContext(r)) writeRequestIDError(w, r, http.StatusUnauthorized, "unauthorized", "invalid credentials") return } if !result.Allowed { writeRequestIDError(w, r, http.StatusForbidden, "forbidden", "access denied") return } sessionID := uuid.NewString() roles := []string{} permissions := []string{} accessToken, err := h.issuer.IssueAccessToken(result.UserID, "", sessionID, roles, permissions, req.AccessKey, result.DisplayName) if err != nil { h.logger.Error("issue access token failed", "error", err, "request_id", requestIDFromContext(r)) writeRequestIDError(w, r, http.StatusInternalServerError, "internal_error", "failed to issue access token") return } refreshToken, err := h.issuer.IssueRefreshToken(result.UserID, sessionID) if err != nil { h.logger.Error("issue refresh token failed", "error", err, "request_id", requestIDFromContext(r)) writeRequestIDError(w, r, http.StatusInternalServerError, "internal_error", "failed to issue refresh token") return } refreshClaims, err := h.parseRefreshUnverified(refreshToken) if err == nil { h.sessions.Create(result.UserID, refreshClaims.ID) } writeJSON(w, http.StatusOK, TokenResponse{ AccessToken: accessToken, RefreshToken: refreshToken, TokenType: "Bearer", ExpiresIn: int64(h.issuer.AccessTTL().Seconds()), User: UserInfo{ ID: result.UserID, DisplayName: result.DisplayName, }, }) } // Refresh handles POST /token/refresh. func (h *TokenHandler) Refresh(w http.ResponseWriter, r *http.Request) { var req RefreshRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeRequestIDError(w, r, http.StatusBadRequest, "bad_request", "invalid JSON body") return } if req.RefreshToken == "" { writeRequestIDError(w, r, http.StatusBadRequest, "bad_request", "refresh_token is required") return } claims, err := auth.VerifyRefreshToken(req.RefreshToken, h.issuer.Issuer(), h.km.PublicKey()) if err != nil { writeRequestIDError(w, r, http.StatusUnauthorized, "unauthorized", "invalid refresh token") return } if !h.sessions.IsValid(claims.SessionID, claims.ID) { writeRequestIDError(w, r, http.StatusUnauthorized, "unauthorized", "session is revoked or refresh token already used") return } roles := []string{} permissions := []string{} accessToken, err := h.issuer.IssueAccessToken(claims.Subject, "", claims.SessionID, roles, permissions, "", "") if err != nil { h.logger.Error("issue access token (refresh) failed", "error", err, "request_id", requestIDFromContext(r)) writeRequestIDError(w, r, http.StatusInternalServerError, "internal_error", "failed to issue access token") return } newRefresh, err := h.issuer.IssueRefreshToken(claims.Subject, claims.SessionID) if err != nil { h.logger.Error("issue refresh token (rotation) failed", "error", err, "request_id", requestIDFromContext(r)) writeRequestIDError(w, r, http.StatusInternalServerError, "internal_error", "failed to issue refresh token") return } newClaims, err := h.parseRefreshUnverified(newRefresh) if err == nil { h.sessions.RotateRefresh(claims.SessionID, newClaims.ID) } writeJSON(w, http.StatusOK, TokenResponse{ AccessToken: accessToken, RefreshToken: newRefresh, TokenType: "Bearer", ExpiresIn: int64(h.issuer.AccessTTL().Seconds()), }) } // Service handles POST /token/service. func (h *TokenHandler) Service(w http.ResponseWriter, r *http.Request) { var req ServiceTokenRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeRequestIDError(w, r, http.StatusBadRequest, "bad_request", "invalid JSON body") return } if req.Subject == "" { writeRequestIDError(w, r, http.StatusBadRequest, "bad_request", "subject is required") return } if req.Audience == "" { writeRequestIDError(w, r, http.StatusBadRequest, "bad_request", "audience is required") return } token, err := h.issuer.IssueServiceToken(req.Subject, req.Audience, req.TenantID, req.Permissions, req.AccessKey, req.Session) if err != nil { h.logger.Error("issue service token failed", "error", err, "request_id", requestIDFromContext(r)) writeRequestIDError(w, r, http.StatusInternalServerError, "internal_error", "failed to issue service token") return } h.logger.Info("service token issued", "subject", req.Subject, "audience", req.Audience, "request_id", requestIDFromContext(r), ) writeJSON(w, http.StatusOK, ServiceTokenResponse{ ServiceToken: token, ExpiresIn: int64(h.issuer.ServiceTTL().Seconds()), }) } // parseRefreshUnverified extracts the claims from a refresh token without // re-checking the signature (used only right after local issuance). func (h *TokenHandler) parseRefreshUnverified(tokenStr string) (*auth.RefreshClaims, error) { claims := &auth.RefreshClaims{} _, _, err := jwt.NewParser().ParseUnverified(tokenStr, claims) if err != nil { return nil, err } return claims, nil }