ultisuite-backend/internal/migration/microsoft_app.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

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))
}