使用 System.Management.Automation 和导出的会话,如何指定 O365 调用的凭据?

Using System.Management.Automation and an exported session, how do I specify credentials for an O365 call?

我正在构建一个小型控制台应用程序,它使用 System.Management.Automation 命名空间中的构造来连接到 ExchangeOnline 并执行各种任务。在我的开发和测试期间,使用每个 运行 创建和导入新会话的开销时间令人望而却步。

因此,我选择使用 Export-PSSession 将会话保存到磁盘。这在 PowerShell 提示符下一切正常,如下所示:

Import-Module ExchangeOnline
Get-Mailbox

系统提示我输入凭据,然后我们开始吧。

不幸的是,运行在 Automation 下的相同序列不能这样说:

System.Management.Automation.MethodInvocationException: Exception calling "GetSteppablePipeline" with "1" argument(s): "Exception calling "PromptForCredential" with "4" argument(s): "A command that prompts the user failed because the host program or the command type does not support user interaction. The host was attempting to request confirmation with the following message: Enter your credentials for https://outlook.office365.com/powershell-liveid/.""

使用 System.Management.Automation 时如何将我的凭据发送到 O365?

This Q&A 几乎回答了它,但不完全是。

这是我的代码。

实施

Friend Class Monad
  Implements IDisposable

  Public Sub New()
    Me.SessionState = InitialSessionState.CreateDefault
    Me.Monad = PowerShell.Create
  End Sub

  Public Sub ImportModule(Modules As String())
    If Me.RunSpace.IsNotNothing Then
      Me.RunSpace.Dispose()
      Me.RunSpace = Nothing
    End If

    Me.SessionState.ImportPSModule(Modules)
    Me.RunSpace = RunspaceFactory.CreateRunspace(Me.SessionState)
    Me.RunSpace.Open()
    Me.Invoker = New RunspaceInvoke(Me.RunSpace)
  End Sub

  Public Function ExecuteScript(Script As String) As Collection(Of PSObject)
    Dim oErrors As Collection(Of ErrorRecord)

    ExecuteScript = Me.Invoker.Invoke(Script)

    oErrors = Me.Monad.Streams.Error.ReadAll

    If oErrors.Count > 0 Then
      Throw New PowerShellException(oErrors)
    End If
  End Function

  Protected Overridable Sub Dispose(IsDisposing As Boolean)
    If Not Me.IsDisposed Then
      If IsDisposing Then
        If Me.RunSpace.IsNotNothing Then Me.RunSpace.Dispose()
        If Me.Invoker.IsNotNothing Then Me.Invoker.Dispose()
        If Me.Monad.IsNotNothing Then Me.Monad.Dispose()

        Me.RunSpace = Nothing
        Me.Invoker = Nothing
        Me.Monad = Nothing
      End If
    End If

    Me.IsDisposed = True
  End Sub

  Public Sub Dispose() Implements IDisposable.Dispose
    Me.Dispose(True)
  End Sub

  Private ReadOnly SessionState As InitialSessionState

  Private IsDisposed As Boolean
  Private RunSpace As Runspace
  Private Invoker As RunspaceInvoke
  Private Monad As PowerShell
End Class

通话

Friend Function GetMailbox() As IEnumerable(Of PSObject)
  Using oMonad As New Monad
    oMonad.ImportModule({"ExchangeOnline"})

    Return oMonad.ExecuteScript("Get-Mailbox")
  End Using
End Function

我能够通过编辑导出的模块来完成此操作。

  • 首先,导出您的 O365 凭据:

    Get-Credential | Export-Clixml -Path D:\Modules\O365Credential.xml

  • 有了 XML 文件,在您喜欢的 IDE
  • 中打开模块进行编辑
  • 搜索函数调用 PromptForCredential
  • 请注意,该调用是对 Set-PSImplicitRemotingSession cmdlet
  • 的更大调用的一部分
  • 注释掉整个外部 cmdlet 调用,例如:

  • 插入调用的未注释副本(可选:修复损坏的缩进)

  • 对于-Credential参数,将值替换为$Credential
  • 在新的 Set-PSImplicitRemotingSession 调用上方,添加此行:

    $Credential = Import-Clixml -Path D:\Modules\O365Credential.xml

  • 您完成的代码应如下所示:

  • 保存模块

要进行测试,请打开一个新的 PowerShell 提示符并导入更新后的模块。 运行 Get-Mailbox。您应该会收到 O365 租户托管的邮箱列表,而不会提示输入凭据。

请注意,该模块根据您的配额使用和减少 RunSpace 连接。完成会话后,您需要优雅地关闭该连接:

Get-PSSession | Where-Object { $_.ConfigurationName -eq 'Microsoft.Exchange' } | Remove-PSSession -ErrorAction SilentlyContinue

现在可以导入和使用System.Management.Automation命名空间下的模块了,如上面VB.NET代码所示。

ExchangeOnline 模块存在一些问题。它希望能够显示交互式现代身份验证对话框,但没有可靠的方法可以阻止它。您可以向它提供凭据,但如果它需要显示交互式对话框(就像 MFA 所做的那样),它就会呕吐。

为了存储和检索凭据,您可以使用 ConvertFrom-SecureString 和 Export-Csv 或 Export-CliXml,就像 InteXX 的回答一样,但是如果基本身份验证被禁用,这将停止工作您正在使用的帐户,或者在 2020 年 10 月 13 日之后在线交换禁用基本身份验证(请参阅 KB4521831 参考 https://support.microsoft.com/en-us/help/4521831/exchange-online-deprecating-basic-auth)。在那之前,您还可以使用像 VaultCredential 这样的模块来管理凭据(请注意,它不能跨帐户工作)。

所以下一个问题可能是如何获得现代身份验证的令牌以及如何将其呈现给 Exchange Online 以使用 powershell 进行身份验证。一点都不难。

您可以毫不费力地使用 .NET 网络调用来攻击端点,但使用 ADAL.PS(已弃用)和 MSAL.PS 等官方认可的库之一会更容易。继续并 Install-Module -Name MSAL.PS 来自 powershell 库。一旦它在那里(并且 运行 Get-Module -Refresh -ListAvailable 以确保它可以自动加载)你可以 运行:

$Token = Get-MsalToken -ClientId "a0c73c16-a7e3-4564-9a95-2bdf47383716" -RedirectUri "urn:ietf:wg:oauth:2.0:oob" -Scopes "https://outlook.office365.com/AdminApi.AccessAsUser.All","https://outlook.office365.com/FfoPowerShell.AccessAsUser.All","https://outlook.office365.com/RemotePowerShell.AccessAsUser.All"

您还可以添加“-LoginHint”(即可能是您的电子邮件地址)以跳过初始定位器提示,添加“-TenantId '00000000-0000-0000-0000-000000000000'”(除了使用你的 Azure AD 租户的实际 GUID)。

获取令牌后,MSAL 会将其保存在缓存中并自动为您刷新。这可能会使 non-interactive 使用起来有点困难。您可以查看处理它的首选方法 (https://docs.microsoft.com/en-us/azure/active-directory/develop/msal-acquire-cache-tokens),或在较低级别使用库或接口(例如使用 AcquireTokenSilentAsync())。

要针对 Exchange Online 实际使用令牌,您需要使用基本身份验证。即使在他们弃用它之后它仍然可以工作,但它不会验证您帐户的密码,它只会做其他事情,比如接受编码的令牌。基本上你需要 Get-PsSession -Credential $EncodedBasicCredential 其中 $EncodedBasicCredential 是用你的 UPN 作为用户名和 Base64 编码的授权 header 值作为密码构造的。例如:

$EncodedBasicCredential = [System.Management.Automation.PSCredential]::new($Token.account.username,(ConvertTo-SecureString -AsPlainText -Force -String ($Token.CreateAuthorizationHeader())))

请注意 $Token.CreateAuthorizationHeader() 只取 $Token.AccessToken 的值 并在其前面添加 "Bearer "。现在您需要做的就是使用适当的 ConnectionUri、ConfigurationName 和您的凭据 object:

创建一个 New-PsSession
    $ExchangeOnlineSession = New-PSSession -Name ExchangeOnline -ConnectionUri "https://outlook.office365.com/PowerShell-LiveId?BasicAuthToOAuthConversion=true" -ConfigurationName Microsoft.Exchange -Credential $EncodedBasicCredential -Authentication Basic

您可以根据需要将其导入 parent session:

$ExchangeOnlineModule = Import-PSSession -Session ($ExchangeOnlineSession) -WarningAction Ignore

要回答您的具体问题(您如何指定凭据),这完全取决于您如何访问您的身份授权。如果您在本地使用 ADFS 并且您的脚本 运行s 在本地,那么您应该能够 运行 作为所需身份的进程,并且 Get-MsalToken 将自动使用集成 windows 在不提示您的情况下针对 ADFS 进行身份验证。如果您直接对 Azure AD 使用 PTA 或本机身份验证,则需要查看创建客户端应用程序并使用机密或证书对 Azure AD 进行身份验证以获取令牌。

我认为从 powershell 到 C# 的转换相当容易,但我是脚本系统管理员,而不是编码员。