package main
import (
"context"
"errors"
"net/http"
"strings"
"sync"
"time"
"github.com/coreos/go-oidc/v3/oidc"
)
type Verifier struct {
issuer string
clientID string
mu sync.Mutex
verifier *oidc.IDTokenVerifier
lastTry time.Time
}
const discoveryRetryInterval = 10 * time.Second
var errIdentityUnavailable = errors.New("identity server unavailable")
func NewVerifier(issuer, clientID string) *Verifier {
return &Verifier{issuer: strings.TrimRight(issuer, "/"), clientID: clientID}
}
func (v *Verifier) get(ctx context.Context) (*oidc.IDTokenVerifier, error) {
v.mu.Lock()
defer v.mu.Unlock()
if v.verifier != nil {
return v.verifier, nil
}
if time.Since(v.lastTry) < discoveryRetryInterval {
return nil, errIdentityUnavailable
}
v.lastTry = time.Now()
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
provider, err := oidc.NewProvider(oidc.ClientContext(ctx, http.DefaultClient), v.issuer)
if err != nil {
return nil, errIdentityUnavailable
}
v.verifier = provider.Verifier(&oidc.Config{ClientID: v.clientID})
return v.verifier, nil
}
func (v *Verifier) Verify(ctx context.Context, token string) (string, error) {
verifier, err := v.get(ctx)
if err != nil {
return "", err
}
parsed, err := verifier.Verify(ctx, token)
if err != nil {
return "", err
}
var claims struct {
ClientID string `json:"client_id"`
JTI string `json:"jti"`
AtHash string `json:"at_hash"`
Nonce string `json:"nonce"`
}
if err := parsed.Claims(&claims); err != nil {
return "", err
}
if claims.ClientID != v.clientID || claims.JTI == "" || claims.AtHash != "" || claims.Nonce != "" {
return "", errors.New("not an access token for this app")
}
return parsed.Subject, nil
}