118 lines
3.1 KiB
Go
118 lines
3.1 KiB
Go
package middleware
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type RemoteTokenRuntime struct {
|
|
baseURL string
|
|
httpClient *http.Client
|
|
now func() time.Time
|
|
|
|
mu sync.RWMutex
|
|
records map[string]remoteResolvedToken
|
|
}
|
|
|
|
type remoteResolvedToken struct {
|
|
status TokenStatus
|
|
expiresAt time.Time
|
|
}
|
|
|
|
type remoteIntrospectResponse struct {
|
|
Data struct {
|
|
TokenID string `json:"token_id"`
|
|
SubjectID string `json:"subject_id"`
|
|
Role string `json:"role"`
|
|
Status string `json:"status"`
|
|
Scope []string `json:"scope"`
|
|
IssuedAt time.Time `json:"issued_at"`
|
|
ExpiresAt time.Time `json:"expires_at"`
|
|
} `json:"data"`
|
|
}
|
|
|
|
func NewRemoteTokenRuntime(baseURL string, httpClient *http.Client, now func() time.Time) *RemoteTokenRuntime {
|
|
if httpClient == nil {
|
|
httpClient = http.DefaultClient
|
|
}
|
|
if now == nil {
|
|
now = time.Now
|
|
}
|
|
return &RemoteTokenRuntime{
|
|
baseURL: strings.TrimRight(baseURL, "/"),
|
|
httpClient: httpClient,
|
|
now: now,
|
|
records: make(map[string]remoteResolvedToken),
|
|
}
|
|
}
|
|
|
|
func (r *RemoteTokenRuntime) Verify(ctx context.Context, rawToken string) (VerifiedToken, error) {
|
|
payload := map[string]string{"token": rawToken}
|
|
body, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return VerifiedToken{}, err
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, r.baseURL+"/api/v1/platform/tokens/introspect", bytes.NewReader(body))
|
|
if err != nil {
|
|
return VerifiedToken{}, err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("X-Request-Id", fmt.Sprintf("gateway-introspect-%d", r.now().UnixNano()))
|
|
|
|
resp, err := r.httpClient.Do(req)
|
|
if err != nil {
|
|
return VerifiedToken{}, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return VerifiedToken{}, fmt.Errorf("token introspection failed with status %d", resp.StatusCode)
|
|
}
|
|
|
|
var result remoteIntrospectResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
return VerifiedToken{}, err
|
|
}
|
|
if strings.TrimSpace(result.Data.TokenID) == "" {
|
|
return VerifiedToken{}, errors.New("token introspection response missing token_id")
|
|
}
|
|
|
|
status := TokenStatus(strings.ToLower(strings.TrimSpace(result.Data.Status)))
|
|
r.mu.Lock()
|
|
r.records[result.Data.TokenID] = remoteResolvedToken{
|
|
status: status,
|
|
expiresAt: result.Data.ExpiresAt,
|
|
}
|
|
r.mu.Unlock()
|
|
|
|
return VerifiedToken{
|
|
TokenID: result.Data.TokenID,
|
|
SubjectID: result.Data.SubjectID,
|
|
Role: result.Data.Role,
|
|
Scope: append([]string(nil), result.Data.Scope...),
|
|
IssuedAt: result.Data.IssuedAt,
|
|
ExpiresAt: result.Data.ExpiresAt,
|
|
}, nil
|
|
}
|
|
|
|
func (r *RemoteTokenRuntime) Resolve(ctx context.Context, tokenID string) (TokenStatus, error) {
|
|
r.mu.RLock()
|
|
record, ok := r.records[tokenID]
|
|
r.mu.RUnlock()
|
|
if !ok {
|
|
return "", errors.New("token status not cached; verify must run before resolve")
|
|
}
|
|
if !record.expiresAt.IsZero() && !r.now().Before(record.expiresAt) && record.status == TokenStatusActive {
|
|
return TokenStatusExpired, nil
|
|
}
|
|
return record.status, nil
|
|
}
|