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 PreferredUsername string UPN string Name string Groups []string Source string HD string TID string Org 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"` PreferredUsername string `json:"preferred_username"` UPN string `json:"upn"` Name string `json:"name"` Groups []string `json:"groups"` HD string `json:"hd"` TID string `json:"tid"` Org string `json:"org"` Source string `json:"ak-source"` } if err := token.Claims(&claims); err != nil { return nil, err } return &Claims{ Sub: claims.Sub, Email: claims.Email, PreferredUsername: claims.PreferredUsername, UPN: claims.UPN, Name: claims.Name, Groups: claims.Groups, HD: claims.HD, TID: claims.TID, Org: claims.Org, Source: claims.Source, }, nil }