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