Powershell, EWS, OAuth2, and automation - powershell

I am trying to find documentation on how to implement non-interactive Oauth2 authentication to EWS using PowerShell, but I'm probably not using the correct search terms, because I can't find anything useful. The Microsoft documentation I can find on OAuth2 only has C# documentation.
So, does anyone know how to implement this?
No user input, should work with input that can be provided as script input
Should be in PowerShell, not C#
Details! Details! Not 'Now generate a login token', but the actual code to generate that token.

There’s a really good overview of this on the following blog:
https://ingogegenwarth.wordpress.com/2018/08/02/ews-and-oauth/#more-5139
I used the above blog to get it working in our PowerShell scripts - with a lot of trial and error. The following example script uses an applications's ClientID as registered in Azure AD. If you don't already have an application registered in Azure AD, you must do this first. There are various guides available on the web to register a new application in Azure AD. In order to utilise EWS with OAuth your registered application must have the correct permissions in Azure AD. You have two options for EWS:
Use Delegated Permissions and request the 'EWS.AccessAsUser.All' API permission in Azure AD - Legacy APIs | Exchange | Delegated Permissions | EWS.AccessAsUser.All (Access mailboxes as the signed-in user via Exchange Web Services). This permission gives your registered application the same access to Exchange mailboxes as the signed-in user. If you use this permission, the first time your application's ClientID is used by any Service or User Account to access Exchange Online, the account in question must approve the ClientID through an interactive pop-up notification. Therefore before using this script in an automated fashion, you must interactively access the Exchange Online Service using the ClientID of your registered application and approve the authorisation pop-up. The easiest way to do this is to log-in to a mailbox using Microsoft's free 'EWS Editor' application and specify your app's ClientID. Once your app's ClientID has been approved, your script can run fully automated without any interaction.
Use Application Permissions and request the 'full_access_as_app' API permission in Azure AD - Legacy APIs | Exchange | Delegated Permissions | EWS.AccessAsUser.All (Access mailboxes as the signed-in user via Exchange Web Services). This permission gives your registered application full access via Exchange Web Services to all mailboxes without a signed-in user. This type of permission gives the application full access to any mailbox in Exchange Online service and must be approved by an Azure AD global admin providing "admin consent". Your script will then authenticate to Exchange Online using the registered Azure AD application Client ID (effectively the username) and Client Secret (effectively the password).
The below example uses option 1. I haven't tested option 2. Whichever option you chose, you will need to handle requesting an OAuth token (example in the below code) from Azure AD and checking and refreshing the token at regularly intervals (no example). I haven't done that as all of our EWS scripts are simple, quick to run scripts that complete before the token needs to be refreshed (usually within 60 minutes). If this is something you're going to need, you will need to ask others for help. Hope this at least helps get you on the right track...
Here's the example script (the main body of the script calls the 'Get-EWSOAuthToken' function):
#Variables
$UserPrincipalName = "Enter the UPN of your Service Account ID"
$Password = "Password of your Service Account ID - store this securely"
$ClientIDfromAzureAD = "Client ID of your registered application in Azure AD"
$errRecip = "Email address of recipients to notify via email if errors occur"
$script = "Name of script"
$sender = "Email address of sender - normally the server name where your script runs"
$logfile = "Path and filename to log file"
$smtpServer = "Your SMTP server"
Function Get-EWSOAuthToken
{
<#
.SYNOPSIS
Request an OAuth EWS token from Azure AD using supplied Username and Password
.DESCRIPTION
Request an OAuth EWS token from Azure AD using supplied Username and Password
.PARAMETER UserPrincipalName
The UPN of the user that will authenticate to Azure AD to request the OAuth Token
.PARAMETER Password
The Password (SecureString) of the user that will authenticate to Azure AD to request the OAuth Token
.PARAMETER ADALPath
The full path and filename on the local file system to the ADAL (Active Directory Authentication Library) DLL. This library is installed as part of various modules such as Azure AD, Exchange Online, etc.
.PARAMETER ClientId
Identifier of the client application that is requesting the token. You must register your calling application in Azure AD. This will provide you with a ClientID and RedirectURI
.PARAMETER ConnectionUri
The URI of the Exchange Online EWS endpoint. Default URI of 'https://outlook.office365.com/EWS/Exchange.asmx' is used
.PARAMETER RedirectUri
Address to return to upon receiving a response from the authority. You must register your calling application in Azure AD. This will provide you with a ClientID and RedirectURI
.EXAMPLE
$token = Get-EWSOAuthtokenFromCredential -UserPrincipalName "ABC123#mydomain.com" -Password $mySecurePassword -ClientId "123444454545454767687878787" -RedirectUri "https://dummyredirectdomain.com"
$ews = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService -ArgumentList Exchange2013_SP1 -ErrorAction Stop
$ews.UseDefaultCredentials = $False
$ews.Credentials = [Microsoft.Exchange.WebServices.Data.OAuthCredentials]$token
#>
[CmdletBinding()]
Param
(
[System.String]$UserPrincipalName,
[System.Security.SecureString]$Password,
[System.String]$ADALPath,
[System.String]$ClientId = "123444454545454767687878787",
[System.Uri]$ConnectionUri = "https://outlook.office365.com/EWS/Exchange.asmx",
[System.Uri]$RedirectUri = "https://dummyredirectdomain.com"
)
Begin
{
Write-Host "Starting Get-EWSOAuthTokenFromCredential function..." -ForegroundColor Yellow
#Determine ADAL location based on Azure AD module installation path
If([System.String]::IsNullOrEmpty($ADALPath))
{
Write-Host "Attempting to locate ADAL library..." -ForegroundColor Yellow
$ADALPath = (Get-InstalledModule -Name "AzureAD" -ErrorAction SilentlyContinue | Select-Object InstalledLocation).InstalledLocation
$ADALPath = Join-Path -Path $ADALPath -ChildPath "Microsoft.IdentityModel.Clients.ActiveDirectory.dll"
Write-Host "Located library # '$ADALPath'" -ForegroundColor Yellow
If([System.String]::IsNullOrEmpty($ADALPath))
{
#Get List of installed modules and check Azure AD DLL is available
$tmpMods = Get-Module -ListAvailable | Where-Object {$_.Name -eq "AzureAD"}
If($tmpMods)
{
$ADALPath = Split-Path $tmpMods.Path
$ADALPath = Join-Path -Path $ADALPath -ChildPath "Microsoft.IdentityModel.Clients.ActiveDirectory.dll"
Write-Host "Located library # '$ADALPath'" -ForegroundColor Yellow
}
Else
{
$err = "$($myinvocation.mycommand.name) requires the ADAL Library DLL files ('Microsoft.IdentityModel.Clients.ActiveDirectory.dll') that are installed as part of the 'AzureAD' module! Please install the AzureAD module from the Powershell Gallery. See: 'https://www.powershellgallery.com/packages/AzureAD' for more information"
Throw "$err"
}
}
}
#Load 'Microsoft.IdentityModel.Clients.ActiveDirectory' DLL
Try
{
Import-Module $ADALPath -DisableNameChecking -Force -ErrorAction Stop
Write-Host "Successfully imported ADAL Library" -ForegroundColor Yellow
}
Catch
{
$err = "$($myinvocation.mycommand.name): Could not load ADAL Library DLL '$ADALPath'. Error: $_"
Throw "$err"
}
}
Process
{
try
{
$resource = $connectionUri.Scheme + [System.Uri]::SchemeDelimiter + $connectionUri.Host
$azureADAuthorizationEndpointUri = "https://login.windows.net/common/oauth2/authorize/"
$AuthContext = New-Object Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext($azureADAuthorizationEndpointUri) -ErrorAction Stop
$AuthCredential = New-Object Microsoft.IdentityModel.Clients.ActiveDirectory.UserPasswordCredential($UserPrincipalName, $Password) -ErrorAction Stop
Write-Host "$($myinvocation.mycommand.name): Requesting a new OAuth Token..." -ForegroundColor Yellow
$authenticationResult = ([Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContextIntegratedAuthExtensions]::AcquireTokenAsync($AuthContext, $resource, $clientId, $AuthCredential))
If ($authenticationResult.Status.ToString() -ne "Faulted") {
Write-Host "$($myinvocation.mycommand.name): Successfully retrieved OAuth Token" -ForegroundColor Yellow
}
else {
$err = "$($myinvocation.mycommand.name): Error occurred calling ADAL 'AcquireTokenAysnc' : $authenticationResult.Exception.ToString()"
Throw "$err"
}
}
catch
{
#create object
$returnValue = New-Object -TypeName PSObject
#get all properties from last error
$ErrorProperties =$Error[0] | Get-Member -MemberType Property
#add existing properties to object
foreach ($Property in $ErrorProperties)
{
if ($Property.Name -eq 'InvocationInfo')
{
$returnValue | Add-Member -Type NoteProperty -Name 'InvocationInfo' -Value $($Error[0].InvocationInfo.PositionMessage)
}
else
{
$returnValue | Add-Member -Type NoteProperty -Name $($Property.Name) -Value $($Error[0].$($Property.Name))
}
}
#return object
$returnValue
break
}
}
End
{
return $authenticationResult
}
}
###### Main script
#Ensure TLS 1.2 protocol is enabled
try {
If ([Net.ServicePointManager]::SecurityProtocol -notmatch 'Tls12') {
[Net.ServicePointManager]::SecurityProtocol += [Net.SecurityProtocolType]::Tls12
Write-Host "Enabled Tls1.2 in '[Net.ServicePointManager]::SecurityProtocol'" -ForegroundColor Yellow
}
else {
Write-Host "Tls1.2 is enabled in '[Net.ServicePointManager]::SecurityProtocol'" -ForegroundColor Yellow
}
}
Catch {
$err = "An error occurred enabling TLS1.2. Error: $_"
Write-Host "`n$err" -ForegroundColor Red
Send-MailMessage -To $errRecip -Subject "$script - Error occurred during processing" -Body $err -From $sender -Attachment $logfile -SmtpServer $smtpServer
Exit
}
#CHECK FOR EWS MANAGED API, IF PRESENT IMPORT THE HIGHEST VERSION EWS DLL, ELSE EXIT
$EWSDLL = (($(Get-ItemProperty -ErrorAction SilentlyContinue -Path Registry::$(Get-ChildItem -ErrorAction SilentlyContinue -Path 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Exchange\Web Services'|Sort-Object Name -Descending | Select-Object -First 1 -ExpandProperty Name)).'Install Directory') + "Microsoft.Exchange.WebServices.dll")
If (Test-Path $EWSDLL)
{
Try
{
Import-Module $EWSDLL -DisableNameChecking -ErrorAction Stop
}
Catch
{
$err = "An error occurred importing the Exchange Web Services DLL '$EWSDLL'. Error: $_"
Write-Host "`n$err" -ForegroundColor Red
Send-MailMessage -To $errRecip -Subject "$script - Error occurred during processing" -Body $err -From $sender -Attachment $logfile -SmtpServer $smtpServer
Exit
}
}
Else
{
$err = "This script requires the EWS Managed API 1.2 or later. Please download and install the current version of the EWS Managed API from http://go.microsoft.com/fwlink/?LinkId=255472"
Write-Host "`n$err" -ForegroundColor Red
Send-MailMessage -To $errRecip -Subject "$script - Error occurred during processing" -Body $err -From $sender -Attachment $logfile -SmtpServer $smtpServer
Exit
}
#Create EWS Object
$ews = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService -ArgumentList "Exchange2013_SP1" -ErrorAction Stop
#Authenticate EWS using OAuth
Try {
$ews.UseDefaultCredentials = $False
Write-Host "Requesting EWS OAuth Token using registered Client ID" -ForegroundColor Yellow
$OAuthResult = Get-EWSOAuthToken -UserPrincipalName $UserPrincipalName -Password $Password -ClientId "$ClientIDfromAzureAD" -ErrorAction Stop
$token = $OAuthResult.Result.AccessToken
#Check if we successfully retrieved an Oauth Token
If ([System.String]::IsNullOrEmpty($token))
{
$err = "Get-EWSOAuthtoken returned an empty Auth Token. Aborted. Latest error details:`n$_error $($OAuthResult.Exception)"
Write-Host "`n$err" -ForegroundColor Red
$OAuthResult | Format-List -Force
$OAuthResult.Result | Format-List -Force
Send-MailMessage -To $errRecip -Subject "$script - Error occurred during processing" -Body "$err" -From $sender -Attachment $logfile -SmtpServer $smtpServer
Exit
}
else
{
$OAuthchk = $true
$ews.Credentials = [Microsoft.Exchange.WebServices.Data.OAuthCredentials]$token
Write-Host "Set EWS credentials to OAuth token" -ForegroundColor Yellow
}
}
Catch
{
$err = "An error occurred creating a new EWS object. Error:`n $_"
write-host "`n$err" -ForegroundColor Red
Send-MailMessage -To $errRecip -Subject "$script - Error occurred during processing" -Body "$err" -From $sender -Attachment $logfile -SmtpServer $smtpServer
Exit
}
# Do your processing using EWS
....

This is an extension on the information provided by #stukey, which is already graat. Instead of creating your own function to retrieve an access token, one can use the MSAL.PS library. This module can simply be installed from the PowerShell Gallery:
Install-Module -Name MSAL.PS
Configure Azure app
When you configure your "App Registration" in Azure you can use the following settings. This will allow you to use Integrated Windows Authentication and avoids storing passwords in your code (useful when running Windows Scheduled Tasks as a specific user to run your scripts):
Authentication > Advanced Settings > Treat application as a public client: Yes
Add the scope "EWS.AccessAsUser.All" in the section "API Permissions" (it can be found within the last option "Supported legacy API's: Exchange"):
Request token
When all this is configured you can request a new token when logged on with the correct Windows account that has Full control exchange permissions on the desired mailbox:
$msalParams = #{
ClientId = $azureClientId
TenantId = $azureTenantId
Scopes = "https://outlook.office.com/EWS.AccessAsUser.All"
IntegratedWindowsAuth = $true
}
Get-MsalToken #msalParams
It might be required to add the switch -Interactive, so you can consent to the proposed scopes. This will only need to be done once.
Now that a valid token is acquired a refresh of the token can simply be done with the -Silent switch. This will get a valid token form the cache or request a new token when it's no longer valid:
$msalParams = #{
ClientId = $azureClientId
TenantId = $azureTenantId
Scopes = "https://outlook.office.com/EWS.AccessAsUser.All"
Silent = $true
}
Get-MsalToken #msalParams
It would be great if both steps above can be combined into one call. For this I opened an issue.
Use the token with Exchange Web Services
$EWS = 'C:\Program Files\Microsoft\Exchange\Web Services\2.2\Microsoft.Exchange.WebServices.dll'
Import-Module -Name $EWS -EA Stop
$Service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService -ArgumentList 'Exchange2013_SP1'
$Service.Url = 'https://outlook.office365.com/EWS/Exchange.asmx'
$Service.UseDefaultCredentials = $false
$msalParams = #{
ClientId = $azureClientId
TenantId = $azureTenantId
Scopes = "https://outlook.office.com/EWS.AccessAsUser.All"
}
$token = Get-MsalToken #msalParams
$Service.Credentials = [Microsoft.Exchange.WebServices.Data.OAuthCredentials]$token.AccessToken
Hopefully this will help others struggling with the same issues we did.

Related

Powershell incorrect running sequence

Kinda new to powershell and trying to write scripts in general. Im trying to create a script that creates an AD user and then assigns that user a license.
However doesn't seem to matter what I do, the sync command I have doesnt execute before the waiting period; so it cant find the user to assign the license to.
Any ideas what Im getting wrong?
`$DCSync = 'DC01'
#Starts AD Sync
Invoke-Command -ComputerName $DCSync -scriptblock {
Import-Module ADSync
Start-ADSyncSyncCycle -PolicyType Delta
Write-Output "testing"
}
send-mailmessage -From "abc#test123.co.uk" -To "abcHelpdesk#test123.co.uk" -Subject "New user creation" -Body "Please connect to DC01 and authenticate to Office 365 to complete the user setup for $UserPrincipalName" -SmtpServer [REDACTED]
Start-Countdown -Seconds 5 -Message "Synchronizing changes to Office 365"
#Install-Module PowerShellGet
#Install-Module Microsoft.Graph -Scope CurrentUser
#Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
Connect-MgGraph -Scopes User.ReadWrite.All, Organization.Read.All
$MgUserID = Get-MgUser -UserId "$EmailAddress"
Update-MgUser -UserId "$MgUserID" -UsageLocation GB
Set-MgUserLicense -UserId $MgUserID -AddLicenses #{SkuId = "6fd2c87f-b296-42f0-b197-1e91e994b900" } -RemoveLicenses #()`
Write-Outpost "testing" always prints after the ADsync commands
Creating a user - assigning a license to newly created user
It just errors out because its not syncing to AD using the command so the user doesn't 'exist' yet
A couple of thoughts:
Try using Start-Sleep rather than Start-Countdown
If it isn't asynchronous, you can try running Start-ADSyncSyncCycle -PolicyType Delta using the -AsJob parameter, and then retrieve the status of that job using a while loop and not proceeding until the job is completed
If you have the e-mail address, then you can use a while loop to not proceed until the account is created, like:
while ($null -eq $MgUserID){
try {
$MgUserID = Get-MgUser -UserId "$EmailAddress"
}
catch {
$MgUserID = $null
}
Start-Sleep -Seconds 30
}

Create azure dynamic group from azure functions powershell

I am trying to create an azure function that has to create azure dynamic group when i execute the function from MS flow. I am using below code for this purpose.
$groupName = $Request.Query.Name
$groupDesc = $Request.Query.Desc
$domainnames = $Request.Query.DomainName
$dynamicrule = ""
Foreach($domainname in $domainnames.Split(";"))
{
$dynamicrule = $dynamicrule + "(user.userPrincipalName -contains ""_$domainname"") or";
}
$dynamicrule = $dynamicrule -replace ".{2}$"
$dynamicrule = $dynamicrule + "and (user.objectId -ne null)";
New-AzureADMSGroup -DisplayName $groupName -Description $groupDesc -MailEnabled $False -MailNickName "group" -SecurityEnabled $True -GroupTypes "DynamicMembership" -MembershipRule $dynamicrule -MembershipRuleProcessingState "On"
When i execute the above command, i am getting below error messgae.
ERROR: The term 'New-AzureADMSGroup' is not recognized as the name of a cmdlet, function, script file, or operable program.Check the spelling of the name, or if a path was included, verify that the path is correct and try again.Exception :Type : System.Management.Automation.CommandNotFoundExceptionErrorRecord
Can sombody please help me on how can i create dynamic groups using azure function app.
Thanks,
Venu
From the error message, you did not install AzureAD powershell module in your function app. And if you want to create a dynamic group, you need to use the -MembershipRule parameter, it is just available in the preview version i.e. AzureADPreview module. Though the doc looks like the parameter is available in AzureAD, but per my test, it is not available.
Actually it is easy to solve the issue, but if you want to create a dynamic group with New-AzureADMSGroup, there will be a few follow-up issues, you could follow the steps below.
1.Navigate to the function app in the portal -> Identity -> enable the system-assigned identity(MSI) for your app.
2.Navigate to App files -> host.json -> make sure the managedDependency is Enabled.
{
"version": "2.0",
"managedDependency": {
"Enabled": true
},
"extensionBundle": {
"id": "Microsoft.Azure.Functions.ExtensionBundle",
"version": "[1.*, 2.0.0)"
}
}
In the requirements.psd1, add the AzureADPreview like below, then it will install the AzureADPreview module for you automatically.
#{
'Az' = '5.*'
'AzureADPreview' = '2.0.2.129'
}
In the profile.ps1, remove all the things and add the lines below, this is used to solve the issue related to AzureAD powershell in function, without it, you will get an error, details here.
$64bitPowerShellPath = Get-ChildItem -Path $Env:Windir\WinSxS -Filter PowerShell.exe -Recurse -ErrorAction SilentlyContinue | Where-Object {$_.FullName -match "amd64"}
$env:64bitPowerShellPath=$64bitPowerShellPath.VersionInfo.FileName
3.If you want to use New-AzureADMSGroup to create group in Azure AD, you need the permission in Microsoft Graph, in this case, we use MSI to auth, so use the commands below to give the permission to your MSI.
Run the commands below in local with the Global admin user account, replace <functionapp-name>:
Connect-AzureAD
$MSI = (Get-AzureADServicePrincipal -Filter "displayName eq '<functionapp-name>'")
$MSGraphAppId = "00000003-0000-0000-c000-000000000000"
$GraphServicePrincipal = Get-AzureADServicePrincipal -Filter "appId eq '$MSGraphAppId'"
$PermissionName = "Group.ReadWrite.All"
$AppRole = $GraphServicePrincipal.AppRoles | Where-Object {$_.Value -eq $PermissionName -and $_.AllowedMemberTypes -contains "Application"}
New-AzureADServiceAppRoleAssignment -ObjectId $MSI.ObjectId -PrincipalId $MSI.ObjectId -ResourceId $GraphServicePrincipal.ObjectId -Id $AppRole.Id
4.After step 2, navigate to the kudu(in the Advanced Tools blade of the function app) -> data -> ManagedDependencies -> click the file with the format like 201208083153165.r(choose the newest one via the Modified time) -> check if the AzureADPreview module was installed successfully like below.
5.After the module was installed, in your function code, use the lines below, in my sample, I use this sample to test directly, you could change the code depends on your requirements, remember to replace 201208083153165.r with yours in step 4, it works fine on my side.
using namespace System.Net
# Input bindings are passed in via param block.
param($Request, $TriggerMetadata)
# Write to the Azure Functions log stream.
Write-Host "PowerShell HTTP trigger function processed a request."
# Interact with query parameters or the body of the request.
$name = $Request.Query.Name
if (-not $name) {
$name = $Request.Body.Name
}
$body = "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response."
if ($name) {
$body = "Hello, $name. This HTTP triggered function executed successfully."
}
$script = {
if ($env:MSI_SECRET) {
Disable-AzContextAutosave -Scope Process | Out-Null
Connect-AzAccount -Identity
}
$context = Get-AzContext
$graphtoken = (Get-AzAccessToken -ResourceUrl "https://graph.microsoft.com").Token
$aadtoken = (Get-AzAccessToken -ResourceUrl "https://graph.windows.net").Token
Import-Module D:\home\data\ManagedDependencies\201208083153165.r\AzureADPreview
Connect-AzureAD -AccountId $context.Account -TenantId $context.Tenant -MsAccessToken $graphtoken -AadAccessToken $aadtoken
New-AzureADMSGroup -DisplayName "joyd1" -Description "Dynamic group created from PS" -MailEnabled $False -MailNickName "group" -SecurityEnabled $True -GroupTypes "DynamicMembership" -MembershipRule "(user.department -contains ""Marketing"")" -MembershipRuleProcessingState "On"
}
&$env:64bitPowerShellPath -WindowStyle Hidden -NonInteractive -Command $Script
# Associate values to output bindings by calling 'Push-OutputBinding'.
Push-OutputBinding -Name Response -Value ([HttpResponseContext]#{
StatusCode = [HttpStatusCode]::OK
Body = $body
})
Check the group in the portal:

Configuring powershell core from .net core to provision a one drive

I'm attempting to provision a one drive in a dotnet core app using powershell core. Running powershell I've been able to successfully provision a one drive from the powershell command line following the directions provided below:
https://learn.microsoft.com/en-us/onedrive/pre-provision-accounts
Running it programatically in .net core however it looks like it uses a separate powershell that's bundled into .net core 2.1
I believe the unsuccessful in app runs are due to the powershell bundled with core not being setup correctly, namely the first 3 steps in the link above:
1.Download the latest SharePoint Online Management Shell.
2.Download and install the SharePoint Online Client Components SDK.
3.Connect to SharePoint Online as a global admin or SharePoint admin in Office 365. To learn how, see Getting started with SharePoint Online Management Shell.
How do I set up the powershell that gets run by my application to mirror those steps above?
My code looks like this:
using System.IO;
using System.Management.Automation;
namespace PowerShellApp
{
class Program
{
public static int Main(string[] args)
{
using (PowerShell ps = PowerShell.Create(
{
ps.AddScript(File.ReadAllText(<scriptLocation>))
.Invoke();
}
}
return 0;
}
}
How do I achieve these steps when executing within a .net core application
The powershell script I"m running is below and also within the link above:
<#
.SYNOPSIS
This script adds an entry for each user specified in the input file
into the OneDrive provisioning queue
.DESCRIPTION
This script reads a text file with a line for each user.
Provide the User Principal Name of each user on a new line.
An entry will be made in the OneDrive provisioning queue for each
user up to 200 users.
.EXAMPLE
.\BulkEnqueueOneDriveSite.ps1 -SPOAdminUrl https://contoso- admin.sharepoint.com -InputfilePath C:\users.txt
.PARAMETER SPOAdminUrl
The URL for the SharePoint Admin center
https://contoso-admin.sharepoint.com
.PARAMETER InputFilePath
The path to the input file.
The file must contain 1 to 200 users
C:\users.txt
.NOTES
This script needs to be run by a global or SharePoint administrator in Office 365
This script will prompt for the username and password of the administrator
#>
param
(
#Must be SharePoint Administrator URL
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string] $SPOAdminUrl,
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string] $InputFilePath
)
[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint.Client") | Out-Null
[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint.Client.R untime") | Out-Null
[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint.Client.U serProfiles") | Out-Null
$ctx = New-Object Microsoft.SharePoint.Client.ClientContext($SPOAdminUrl)
$Users = Get-Content -Path $InputFilePath
if ($Users.Count -eq 0 -or $Users.Count -gt 200)
{
Write-Host $("Unexpected user count: [{0}]" -f $Users.Count) - ForegroundColor Red
return
}
$web = $ctx.Web
Write-Host "Enter an admin username" -ForegroundColor Green
$username = Read-Host
Write-Host "Enter your password" -ForegroundColor Green
$password = Read-Host -AsSecureString
$ctx.Credentials = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($username,$password )
$ctx.Load($web)
$ctx.ExecuteQuery()
$loader = [Microsoft.SharePoint.Client.UserProfiles.ProfileLoader]::GetProfileLoader($ctx)
$ctx.ExecuteQuery()
$loader.CreatePersonalSiteEnqueueBulk($Users)
$loader.Context.ExecuteQuery()
Write-Host "Script Completed"
I'm afraid SP Online management Shell has dependencies from .Net Framework and will not work with Core (check this).
From the other side that module seemed to be a wrapper on top of their REST API. So if you want to integrate it with Core app, you may try to replace it with HTTP requests. Check this documentation
Also, below is a base powershell script to work with those REST API endpoints. I tested this one on my site:
$baseUrl = "http://sharepoint.com/sites/yourSite/_api"
$cred = Get-Credential
# retreive digest
$r = Invoke-WebRequest -Uri "$baseUrl/contextinfo" -Method Post -Credential $cred -SessionVariable sp
$digest = ([xml]$r.content).GetContextWebInformation.FormDigestvalue
# calling endpoint
$endpoint = "sp.userprofiles.profileloader.getprofileloader/getuserprofile"
$head = #{
"Accept" = "application/json;odata=verbose"
"X-RequestDigest" = $digest
}
$re = Invoke-WebRequest -Uri "$baseUrl/$endpoint" -Headers $head -Method Post -WebSession $sp
Write-Host $re.Content
This is a snippet for createpersonalsiteenqueuebulk, but I can't test it since I'm not domain admin. Hope it will work for you
#--- sample 2 (didn't test it since I'm not domain admin). Might need separate session/digest
$endpoint2 = "https://<domain>-admin.sharepoint.com/_api/sp.userprofiles.profileloader.getprofileloader/createpersonalsiteenqueuebulk"
$head = #{
"Accept" = "application/json;odata=verbose"
"X-RequestDigest" = $digest
}
$body = "{ 'emailIDs': ['usera#domain.onmicrosoft.com', 'userb#domain.onmicrosoft.com'] }"
$re2 = Invoke-WebRequest -Uri "$endpoint2" -Headers $head -body $body -Method Post -WebSession $sp
Write-Host $re2.Content

Kerberos error in long running Exchange powershell skript after 10 hours

I implemented a powershell script, which assigns Exchange settings to our user mailboxes (Exchange 2016). As we have a lot of mailboxes and assigning settings is slow, the script would run more then 15 hours. However after about 10 hours I get the following error:
Processing data for a remote command failed with the following error message: Error occurred during the Kerberos response.
[Server=XXXXX, TimeStamp = 74/2018 01:25:49]
For more information, see the about_Remote_Troubleshooting Help topic.
At C:\Users\ACCOUNT\AppData\Local\Temp\tmp_cj3akhk4.osq\tmp_cj3akhk4.osq.psm1:77943 char:9
+ $steppablePipeline.End()
+ ~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (XXXX:String) [], PSRemotingTransportException
+ FullyQualifiedErrorId : JobFailure
+ PSComputerName : XXXX
My script retires the operation and after two retries (which fail) an authentication prompt is shown. There I can enter the password of the service account and the script continues. However this dialog is only visible if I run the script in a PS command prompt. If the script is started as Windows Task, it just hangs and does not continue.
The connection to Exchange is opened and imported with the following code. The code can either connect to our on premises Exchange or Exchange online based on the passed parameter. The problem is currently only happening, when connected to our local (on premises) Exchange infrastructure.
Function Connect-Exchange{
PARAM(
[parameter(Mandatory=$false)]
[String]$TargetExchange = 'Local'
)
BEGIN{}
PROCESS{
if ($ExchangeSessionInfo.Session -and $ExchangeSessionInfo.Type -eq $TargetExchange -and $ExchangeSessionInfo.Session.State -eq 'Opened'){
# Nothing to do, we are already connected.
Write-Log "Exchange connection type $($TargetExchange) already established, nothing to do."
} else {
if ($ExchangeSessionInfo.Session -and $ExchangeSessionInfo.Type -ne $TargetExchange -and $ExchangeSessionInfo.Session.State -eq 'Opened'){
# We have a open session with the wrong type. We close it.
Remove-PSSession $ExchangeSessionInfo.Session
$ExchangeSessionInfo.Session = $null
$ExchangeSessionInfo.Status = 'undefined'
$ExchangeSessionInfo.Type = ''
}
# We close all other existing Exchange sessions we created.
get-pssession -Name "Exchange" -ErrorAction SilentlyContinue | remove-pssession
# Now connect to the requestes Exchange infrastructure and import session.
$Connected = $False
$RetryCount = 5
do{
try {
If ($TargetExchange -eq 'Local'){
$ExchangeServer = Get-Random -InputObject $LocalExchangeConfig.ExchangeServers
$ExchangeSessionInfo.Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri "http://$($ExchangeServer)/PowerShell/" -Credential $EOCredentials -Authentication Kerberos -Name "Exchange"
} else {
$ExchangeSessionInfo.Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri 'https://ps.protection.outlook.com/powershell-liveid/' -Credential $EOCredentials -Authentication Basic -AllowRedirection -Name "Exchange"
}
$Res = Import-PSSession $ExchangeSessionInfo.Session -WarningAction SilentlyContinue -AllowClobber
# Store Exchange status in session variable.
$Connected = $True
$ExchangeSessionInfo.Status = 'connected'
$ExchangeSessionInfo.Type = $TargetExchange
} catch {
$err = Write-Error -err $error -msg "Could not connect to Exchange server type '$($TargetExchange)' (Retries left: $($RetryCount))." -Break $false
get-pssession -Name "Exchange" -ErrorAction SilentlyContinue | remove-pssession
$RetryCount -= 1
}
} while (!$Connected -and ($RetryCount -gt 0))
# If we do not have connection here, this is an error.
if (!$Connected) {
$ExchangeSessionInfo.Session = $null
$ExchangeSessionInfo.Status = 'undefined'
$ExchangeSessionInfo.Type = ''
throw "No connection to Exchange server (type: $($TargetExchange)) could be established."
} else {
# Get list of available mailbox DBs including mailbox count and create hashtable to store statistics. We only have to get it the first time.
if (($MailboxDBList.count -eq 0) -and ($TargetExchange -eq 'Local')){
Write-Log "Getting current Exchange DB configuration and mailbox count. Takes a moment."
$MailboxDBList = Get-MailboxDBCount -Type $LocalExchangeConfig.DistributeMailboxes
}
}
}
}
END{
return $ExchangeSessionInfo
}
}
The following code is applying a predefined set of Exchange settings:
...
$TryCount = 0
$Done = $false
do{
# It takes a while after enabling mailbox until settings can be applied. So we need to retry.
try{
# If we need to execute a setting several times.
if ($MailboxSetting.LoopOver){
# We have a loop value (array).
foreach ($LoopValue in $MailboxSetting.LoopOver){
# Copy parameter as we have to change a value (loop value).
$TempParams = $Params.PsObject.Copy()
#($Params.getenumerator()) |? {$_.Value -match '#LOOPVALUE#'} |% {$TempParams[$_.Key]=$LoopValue}
$res = & $MailboxSetting.Command -ErrorAction Stop #TempParams -WhatIf:$RunConfig.TestMode
}
} else {
# THE PROBLEM HAPPENS HERE
$res = & $MailboxSetting.Command -ErrorAction Stop #Params -WhatIf:$RunConfig.TestMode
}
# Write-Log "Setting command $($MailboxSetting.Command) executed successfully"
$Done = $true
} catch{
$tryCount++
$res = Write-Error -err $error -msg "Error applying mailbox settings, account: $($AccountDetails.sAMAccountName), retry count: $($TryCount)" -Break $false
Start-Sleep -s $(($Retires-$TryCount) * 5)
}
} while ((!$done) -and ($tryCount -lt $Retires))
...
I am sure the error is not related to the code, because the script runs for hours without a problem and applies all settings. However after a around 10 hours it seems the Kerberos ticket expires and then the script cannot longer access Exchange without a re-login.
Is there a way to keep the Kerberos ticket from expiring or renew it?
Any help would be appreciated.
I think you are hitting the domain security policy (group policy object - GPO) => security settings/account policy/Kerberos policy restriction.
The two valid options for you are:
Maximum lifetime for user ticket => the default value is 10 hours
Maximum lifetime for user ticket renewal => the default value is 7 days (this is the period within which the ticket can be renewed).
Is there a way to keep the Kerberos ticket from expiring or renew it?
For the first questions you "just" need to adjust the maximum lifetime for user ticket setting to value as you deem appropriate.
The second one is more tricky. I would just purge all kerberos tickets via the powershell. For more - viewing and purging cached kerberos tickets which would get you a new one.
If the ticket can be renewed you have to check the RENEABLE flag - you wan view it via kinit. Perhaps kinit -R could be enough for ticket renewal. (I did not do this my self) You could also renew it via kerberos for windows
Edit -- adding klist purge to purge all Kerberos tickets so it can be renewed.
As you have klist then you can purge all tickets via must be run in elevated powershell prompt
(all credits to JaredPoeppelman):
Get-WmiObject Win32_LogonSession | Where-Object {$_.AuthenticationPackage -ne 'NTLM'} | ForEach-Object {klist.exe purge -li ([Convert]::ToString($_.LogonId, 16))}
Then check if your TGT was updated via:
klist tgt
Note: you must use FQDN name everywhere!
Thanks for your suggestion. In a first try I will extend my code as follows and try to reestblisch a new Exchange connection. Needs 10 h runnig the script in order to see if this works.
I am not able to influence the domain security Policy, additionally as I do not know how long the script runs, it will be difficult to set a value.
On my Windows 2016 the command "kinit" ist not recognized. Possibly I need to install additional modules/roles.
...
$TryCount = 0
$Done = $false
do{
# It takes a while after enabling mailbox until settings can be applied. So we need to retry.
try{
# If we need to execute a setting several times.
if ($MailboxSetting.LoopOver){
# We have a loop value (array).
foreach ($LoopValue in $MailboxSetting.LoopOver){
# Copy parameter as we have to change a value (loop value).
$TempParams = $Params.PsObject.Copy()
#($Params.getenumerator()) |? {$_.Value -match '#LOOPVALUE#'} |% {$TempParams[$_.Key]=$LoopValue}
$res = & $MailboxSetting.Command -ErrorAction Stop #TempParams -WhatIf:$RunConfig.TestMode
}
} else {
$res = & $MailboxSetting.Command -ErrorAction Stop #Params -WhatIf:$RunConfig.TestMode
}
# Write-Log "Setting command $($MailboxSetting.Command) executed successfully"
$Done = $true
} catch{
$tryCount++
$res = Write-Error -err $error -msg "Error applying mailbox settings, account: $($AccountDetails.sAMAccountName), retry count: $($TryCount)" -Break $false
Start-Sleep -s $(($Retires-$TryCount) * 5)
try{
# We may have lost the Kerberos ticket, reconnect to Exchange.
$ConnectionType = $ExchangeSessionInfo.Type
Disconnect-Exchange
Connect-Exchange -TargetExchange $ConnectionType
} catch {}
}
} while ((!$done) -and ($tryCount -lt $Retires))
...

Monitoring Services on an Azure VM using an Azure Runbook

I have a Powershell script that enumerates running services and their current state using Get-WmiObject Win32_Service. Initial version based on this one and then modified for Azure. When I run the script in Powershell (without the azure automation parts) on my location machine it works fine and I can connect to all the machines of interest, but when I port it to a runbook i get the following error: "Get-WmiObject : The RPC server is unavailable."
Q: Is the problem with permissions for the automation account? If so, what account should I add to the local machines to resolve the issue?
Q: Is Get-WmiObject not a valid way to initiate the connection? If not, what should I try instead?
The code I'm using is below:
[CmdletBinding(SupportsShouldProcess = $true)]
param(
# Servers to check
[Parameter(Mandatory=$true)][string[]]$ServerList,
# Services to check for
[Parameter(Mandatory=$true)][string[]]$includeService
)
# Following modifies the Write-Verbose behavior to turn the messages on globally for this session
$VerbosePreference = "Continue"
$connectionName = "AzureRunAsConnection"
# retry
$retry = 6
$syncOk = $false
$servicePrincipalConnection = Get-AutomationConnection -Name $connectionName
do
{
try
{
Add-AzureRmAccount -ServicePrincipal -TenantId $servicePrincipalConnection.TenantId -ApplicationId $servicePrincipalConnection.ApplicationId -CertificateThumbprint $servicePrincipalConnection.CertificateThumbprint
$syncOk = $true
}
catch
{
$ErrorMessage = $_.Exception.Message
$StackTrace = $_.Exception.StackTrace
Write-Warning "Error during sync: $ErrorMessage, stack: $StackTrace. Retry attempts left: $retry"
$retry = $retry - 1
Start-Sleep -s 60
}
} while (-not $syncOk -and $retry -ge 0)
Select-AzureRMSubscription -SubscriptionId $SubscriptionId -TenantId $servicePrincipalConnection.TenantId
$currentSubscription = Get-AzureRMSubscription -SubscriptionId $SubscriptionId -TenantId $servicePrincipalConnection.TenantId
Set-AzureRmContext -SubscriptionId $SubscriptionId;
$props=#()
[System.Collections.ArrayList]$unreachableServers = #()
Foreach($ServerName in ($ServerList))
{
try
{
$service = Get-WmiObject Win32_Service -ComputerName $servername
}
catch
{}
if ($Service -ne $NULL)
{
foreach ($item in $service)
{
#$item.DisplayName
Foreach($include in $includeService)
{
#write-host $include
if(($item.name).Contains($include) -eq $TRUE)
{
$props += [pscustomobject]#{
servername = $ServerName
name = $item.name
Status = $item.Status
startmode = $item.startmode
state = $item.state
serviceaccount=$item.startname
DisplayName =$item.displayname}
}
}
}
}
else
{
Write-host "Failed to contact server: "$ServerName
$unreachableServers.Add($ServerName)
}
}
$props | Format-Table Servername,Name,startmode,state,serviceaccount,displayname -AutoSize
I am assuming that you are using the Azure Automation Hybrid Worker functionality. Be default it runs under the System account. However you can use a different account to run the runbook under. This is documented here: Azure Automation Hybrid Worker; Look under the RunAs account section. Use the same account that works when you try it directly.
have you considered using OMS? this sounds like a better thing to do.
Anyway, to answer your questions, I would probably create a local user and create a PS configuration endpoint for that user to connect to, and connect impersonating that user from the Automation Account, but again, I wouldn't even go this route, I'd rather use OMS.