ultisuite-backend/internal/auth/oidc.go
R3D347HR4Y 7143a36c19
Some checks are pending
CI / Go tests (push) Waiting to run
CI / Integration tests (push) Waiting to run
CI / DB migrations (push) Waiting to run
feat(mail): integrate Stalwart hosted mail and migration features
- Added configuration options for Stalwart hosted mail in .env.example.
- Updated Docker Compose to include Stalwart service with health checks.
- Introduced new API endpoints for managing mail domains and migration projects.
- Enhanced Authentik blueprints for user enrollment and post-migration security.
- Updated OAuth handling for Google and Microsoft migration processes.
- Improved error handling and response structures in the mail API.
- Added integration tests for email claiming and migration workflows.
2026-06-13 12:47:08 +02:00

129 lines
3.4 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
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
}