// Server-side Go, standard library only. Keep ONE *TokenProvider per // credential/audience at startup. Do not copy the provider by value. package vinquery import ( "bytes" "context" "encoding/json" "errors" "fmt" "net/http" "strings" "time" ) type TokenProvider struct { http *http.Client credentials map[string]string gate chan struct{} now func() time.Time token string refreshAt time.Time } func NewTokenProvider(client *http.Client, clientID, clientSecret, audience string) (*TokenProvider, error) { if strings.TrimSpace(clientID) == "" || strings.TrimSpace(clientSecret) == "" || strings.TrimSpace(audience) == "" { return nil, errors.New("client ID, client secret and audience are required") } if client == nil { client = &http.Client{Timeout: 30 * time.Second} } return &TokenProvider{ http: client, credentials: map[string]string{"clientId": clientID, "clientSecret": clientSecret, "audience": audience}, gate: make(chan struct{}, 1), now: time.Now, }, nil } func (p *TokenProvider) GetToken(ctx context.Context) (string, error) { // Waiting callers may cancel without interrupting another caller's renewal. select { case p.gate <- struct{}{}: defer func() { <-p.gate }() case <-ctx.Done(): return "", ctx.Err() } if err := ctx.Err(); err != nil { return "", err } if p.token != "" && p.now().Before(p.refreshAt) { return p.token, nil } body, err := json.Marshal(p.credentials) if err != nil { return "", err } requestCtx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() request, err := http.NewRequestWithContext(requestCtx, http.MethodPost, "https://identity.vinquery.com/connect/token", bytes.NewReader(body)) if err != nil { return "", err } request.Header.Set("Content-Type", "application/json") response, err := p.http.Do(request) if err != nil { return "", errors.New("VINquery token request failed") } defer response.Body.Close() if response.StatusCode < 200 || response.StatusCode >= 300 { return "", fmt.Errorf("VINquery token request failed (HTTP %d)", response.StatusCode) } var result struct { Token string `json:"jwtToken"` Expires time.Time `json:"expiresUtc"` } if err := json.NewDecoder(response.Body).Decode(&result); err != nil { return "", errors.New("VINquery returned an invalid token response") } now := p.now() if strings.TrimSpace(result.Token) == "" || !result.Expires.After(now) { return "", errors.New("VINquery returned an empty token or invalid expiry") } buffer := result.Expires.Sub(now) / 10 if buffer > time.Minute { buffer = time.Minute } p.token, p.refreshAt = result.Token, result.Expires.Add(-buffer) return p.token, nil } func (p *TokenProvider) Invalidate(rejectedToken string) { p.gate <- struct{}{} defer func() { <-p.gate }() if p.token == rejectedToken { p.token, p.refreshAt = "", time.Time{} } }