ultisuite-backend/internal/auth/oidc.go
2026-05-24 00:03:36 +02:00

111 lines
2.6 KiB
Go

package auth
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"github.com/coreos/go-oidc/v3/oidc"
)
type Claims struct {
Sub string
Email string
Name string
Groups []string
}
type Verifier struct {
verifier *oidc.IDTokenVerifier
}
// NewVerifier builds an ID token verifier. issuerURL is the URL ultid uses to reach
// the provider (e.g. http://nginx/auth/application/o/ulti/ in Docker).
// discoveryHost is sent as the HTTP Host header (e.g. localhost) so Authentik returns
// the same issuer claim as browser-issued tokens; JWKS is fetched via issuerURL.
func NewVerifier(ctx context.Context, issuerURL, clientID, discoveryHost string) (*Verifier, error) {
issuerURL = strings.TrimSuffix(strings.TrimSpace(issuerURL), "/")
if issuerURL == "" {
return nil, fmt.Errorf("empty issuer URL")
}
discovery, err := fetchDiscovery(ctx, issuerURL, discoveryHost)
if err != nil {
return nil, err
}
keySet := oidc.NewRemoteKeySet(ctx, issuerURL+"/jwks/")
idVerifier := oidc.NewVerifier(discovery.Issuer, keySet, &oidc.Config{ClientID: clientID})
return &Verifier{verifier: idVerifier}, nil
}
type discoveryDocument struct {
Issuer string `json:"issuer"`
}
func fetchDiscovery(ctx context.Context, issuerURL, discoveryHost string) (*discoveryDocument, error) {
req, err := http.NewRequestWithContext(
ctx,
http.MethodGet,
issuerURL+"/.well-known/openid-configuration",
nil,
)
if err != nil {
return nil, err
}
if discoveryHost != "" {
req.Host = discoveryHost
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
return nil, fmt.Errorf("oidc discovery %s: %s: %s", issuerURL, resp.Status, strings.TrimSpace(string(body)))
}
var doc discoveryDocument
if err := json.NewDecoder(resp.Body).Decode(&doc); err != nil {
return nil, err
}
if doc.Issuer == "" {
return nil, fmt.Errorf("oidc discovery %s: missing issuer", issuerURL)
}
return &doc, nil
}
func (v *Verifier) Verify(ctx context.Context, rawToken string) (*Claims, error) {
if v == nil || v.verifier == nil {
return nil, fmt.Errorf("verifier unavailable")
}
token, err := v.verifier.Verify(ctx, rawToken)
if err != nil {
return nil, err
}
var claims struct {
Sub string `json:"sub"`
Email string `json:"email"`
Name string `json:"name"`
Groups []string `json:"groups"`
}
if err := token.Claims(&claims); err != nil {
return nil, err
}
return &Claims{
Sub: claims.Sub,
Email: claims.Email,
Name: claims.Name,
Groups: claims.Groups,
}, nil
}