- 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.
104 lines
2.6 KiB
Go
104 lines
2.6 KiB
Go
package migration
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"golang.org/x/oauth2"
|
|
"golang.org/x/oauth2/clientcredentials"
|
|
)
|
|
|
|
// MicrosoftApp mints Graph access tokens via the client credentials flow.
|
|
type MicrosoftApp struct {
|
|
clientID string
|
|
clientSecret string
|
|
defaultTenant string
|
|
tokenURL string
|
|
client *http.Client
|
|
}
|
|
|
|
type MicrosoftAppConfig struct {
|
|
ClientID string
|
|
ClientSecret string
|
|
DefaultTenant string
|
|
TokenURL string
|
|
HTTPClient *http.Client
|
|
}
|
|
|
|
func NewMicrosoftApp(cfg MicrosoftAppConfig) (*MicrosoftApp, error) {
|
|
clientID := strings.TrimSpace(cfg.ClientID)
|
|
clientSecret := strings.TrimSpace(cfg.ClientSecret)
|
|
if clientID == "" || clientSecret == "" {
|
|
return nil, nil
|
|
}
|
|
client := cfg.HTTPClient
|
|
if client == nil {
|
|
client = &http.Client{Timeout: 30 * time.Second}
|
|
}
|
|
return &MicrosoftApp{
|
|
clientID: clientID,
|
|
clientSecret: clientSecret,
|
|
defaultTenant: strings.TrimSpace(cfg.DefaultTenant),
|
|
tokenURL: strings.TrimSpace(cfg.TokenURL),
|
|
client: client,
|
|
}, nil
|
|
}
|
|
|
|
func (m *MicrosoftApp) Enabled() bool {
|
|
return m != nil && m.clientID != "" && m.clientSecret != ""
|
|
}
|
|
|
|
func (m *MicrosoftApp) AccessToken(ctx context.Context, tenantID string) (string, error) {
|
|
if !m.Enabled() {
|
|
return "", fmt.Errorf("microsoft app-only auth not configured")
|
|
}
|
|
tenantID = strings.TrimSpace(tenantID)
|
|
if tenantID == "" {
|
|
tenantID = m.defaultTenant
|
|
}
|
|
if tenantID == "" {
|
|
return "", fmt.Errorf("microsoft tenant id required for app-only auth")
|
|
}
|
|
tokenURL := m.tokenURL
|
|
if tokenURL == "" {
|
|
tokenURL = fmt.Sprintf("https://login.microsoftonline.com/%s/oauth2/v2.0/token", url.PathEscape(tenantID))
|
|
}
|
|
cc := clientcredentials.Config{
|
|
ClientID: m.clientID,
|
|
ClientSecret: m.clientSecret,
|
|
TokenURL: tokenURL,
|
|
Scopes: []string{"https://graph.microsoft.com/.default"},
|
|
}
|
|
if m.client != nil {
|
|
ctx = context.WithValue(ctx, oauth2.HTTPClient, m.client)
|
|
}
|
|
token, err := cc.Token(ctx)
|
|
if err != nil {
|
|
return "", fmt.Errorf("microsoft app token: %w", err)
|
|
}
|
|
if token.AccessToken == "" {
|
|
return "", fmt.Errorf("microsoft app token empty")
|
|
}
|
|
return token.AccessToken, nil
|
|
}
|
|
|
|
// WithClient overrides the HTTP client (tests).
|
|
func (m *MicrosoftApp) WithClient(c *http.Client) *MicrosoftApp {
|
|
if m != nil && c != nil {
|
|
m.client = c
|
|
}
|
|
return m
|
|
}
|
|
|
|
func microsoftAppTokenURL(tenantID string) string {
|
|
tenantID = strings.TrimSpace(tenantID)
|
|
if tenantID == "" {
|
|
tenantID = "common"
|
|
}
|
|
return fmt.Sprintf("https://login.microsoftonline.com/%s/oauth2/v2.0/token", url.PathEscape(tenantID))
|
|
}
|