Files
visualizador_instanciados/backend_go/keycloak_token.go

150 lines
3.5 KiB
Go

package main
import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"net/url"
"strings"
"sync"
"time"
)
type keycloakTokenResponse struct {
AccessToken string `json:"access_token"`
}
type keycloakTokenManager struct {
cfg Config
client *http.Client
mu sync.Mutex
token string
refreshCh chan struct{}
lastErr error
}
func newKeycloakTokenManager(cfg Config, client *http.Client) *keycloakTokenManager {
return &keycloakTokenManager{
cfg: cfg,
client: client,
token: strings.TrimSpace(cfg.AccessToken),
}
}
func (m *keycloakTokenManager) CurrentToken() string {
m.mu.Lock()
defer m.mu.Unlock()
return strings.TrimSpace(m.token)
}
func (m *keycloakTokenManager) EnsureToken(ctx context.Context, reason string) (string, error) {
if token := m.CurrentToken(); token != "" {
return token, nil
}
return m.Refresh(ctx, reason)
}
func (m *keycloakTokenManager) Refresh(ctx context.Context, reason string) (string, error) {
m.mu.Lock()
if ch := m.refreshCh; ch != nil {
m.mu.Unlock()
select {
case <-ctx.Done():
return "", ctx.Err()
case <-ch:
m.mu.Lock()
token := strings.TrimSpace(m.token)
err := m.lastErr
m.mu.Unlock()
if err != nil {
return "", err
}
if token == "" {
return "", fmt.Errorf("keycloak token refresh completed without access_token")
}
return token, nil
}
}
ch := make(chan struct{})
m.refreshCh = ch
m.mu.Unlock()
log.Printf("[auth] keycloak_token_refresh_start reason=%s endpoint=%s", reason, m.cfg.KeycloakTokenEndpoint)
start := time.Now()
token, err := m.fetchToken(ctx)
if err != nil {
log.Printf("[auth] keycloak_token_refresh_failed reason=%s endpoint=%s err=%v", reason, m.cfg.KeycloakTokenEndpoint, err)
} else {
log.Printf(
"[auth] keycloak_token_refresh_ok reason=%s endpoint=%s elapsed=%s",
reason,
m.cfg.KeycloakTokenEndpoint,
time.Since(start).Truncate(time.Millisecond),
)
}
m.mu.Lock()
if err == nil {
m.token = token
}
m.lastErr = err
close(ch)
m.refreshCh = nil
currentToken := strings.TrimSpace(m.token)
m.mu.Unlock()
if err != nil {
return "", err
}
return currentToken, nil
}
func (m *keycloakTokenManager) fetchToken(ctx context.Context) (string, error) {
form := url.Values{}
form.Set("grant_type", "password")
form.Set("client_id", strings.TrimSpace(m.cfg.KeycloakClientID))
form.Set("username", strings.TrimSpace(m.cfg.KeycloakUsername))
form.Set("password", m.cfg.KeycloakPassword)
scope := strings.TrimSpace(m.cfg.KeycloakScope)
if scope != "" {
form.Set("scope", scope)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimSpace(m.cfg.KeycloakTokenEndpoint), strings.NewReader(form.Encode()))
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Accept", "application/json")
resp, err := m.client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return "", fmt.Errorf("keycloak token request failed: %s: %s", resp.Status, strings.TrimSpace(string(body)))
}
var tokenResp keycloakTokenResponse
if err := json.Unmarshal(body, &tokenResp); err != nil {
return "", fmt.Errorf("keycloak token parse failed: %w", err)
}
token := strings.TrimSpace(tokenResp.AccessToken)
if token == "" {
return "", fmt.Errorf("keycloak token response missing access_token")
}
return token, nil
}