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