S4B Online PowerShell – Modern Auth

Another Microsoft API that I’ve come across needing a solution for Modern Auth.

This one’s a bit different. You don’t need to register an AD app, but you can connect to S4B with a token, and you can store the token in a Byte Array (in a file or database).

This method uses Microsoft.IdentityModel.Clients.ActiveDirectory.dll to acquire a token.

Wrote this function to handle the token stuff. And the function below to create the Token to store (as a byte array) and how to recall it to use it for unattended authentication.

Thanks again to Elliot Munro from GCITS for helping me figure this one out using a method we found for connecting to Microsoft InTune Graph API.

function Get-MSAuthToken 
{
    [cmdletbinding()]

    param
    (
        [Parameter(Mandatory=$true)]
        $User,

        [Parameter(Mandatory=$true)]
        $TenantId,

        [Parameter(Mandatory=$true)]
        $ClientId,

        [Parameter(Mandatory=$true)]
        $RedirectUri,

        [Parameter(Mandatory=$true)]
        $ResourceAppIdURI,

        [Parameter(Mandatory=$true)]
        $Authority,

        [Parameter(Mandatory=$false)]
        $StoredTokenByteArray,

        [Parameter(Mandatory=$false)]
        $ReturnTokenByteArray
    )
      
    Write-Host "Looking for AzureAD module..."
    $AadModule = Get-Module -Name "AzureAD" -ListAvailable
    if ($AadModule -eq $null) 
    {
        Write-Host "AzureAD PowerShell not found, look for AzureADPreview"
        $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable
    }

    if ($AadModule -eq $null) 
    {
        throw "AzureAD Powershell module not installed..." 
    }

    # Getting path to ActiveDirectory Assemblies
    # If the module count is greater than 1 find the latest version
    if ($AadModule.count -gt 1)
    {
        $Latest_Version = ($AadModule | select version | Sort-Object)[-1]
        $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version }

        # Checking if there are multiple versions of the same module found
        if($AadModule.count -gt 1)
        {
            $aadModule = $AadModule | select -Unique
        }

        $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll"
        $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll"
    }
    else 
    {
        $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll"
        $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll"
    }

    [System.Reflection.Assembly]::LoadFrom($adal) | Out-Null
    [System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null

    try 
    {
        $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority

        # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx
        # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession

        $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto"

        $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId")

        if ($storedTokenByteArray -ne $null)
        {
            $authContext.TokenCache.Deserialize($storedTokenByteArray)
        }

        $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result

        if($authResult.AccessToken)
        {
            if ($ReturnTokenByteArray)
            {
                $blobAuth = $authContext.TokenCache.Serialize();
                return $blobAuth
            }
            else
            {
                return $authResult
            }
        }
        else 
        {
            Write-Host
            Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red
            Write-Host
            break
        }
    }
    catch 
    {
        write-host $_.Exception.Message -f Red
        write-host $_.Exception.ItemName -f Red
        write-host
        break
    }
} 

And the below function authenticates once (prompting you) and then writes the stored token byte array out to a file:

$clientId = "7716031e-6f8b-45a4-b82b-922b1af0fbb4" #S4B
$redirectUri = "https://adminau1.online.lync.com/OcsPowershellOAuth"
$resourceAppIdURI = "https://adminau1.online.lync.com/OcsPowershellOAuth"
$authority = "https://login.microsoftonline.com/common"
$user = $SessionInfo.Account.Id

#get S4B auth token in to Byte Array
$byteArrayToken = Get-MSAuthToken -User $user -TenantId $tenantId -ClientId $clientId -RedirectUri $redirectUri -ResourceAppIdURI $resourceAppIdURI -Authority $authority -ReturnTokenByteArray $true

#store byte array
$byteArrayToken | Out-File C:\bytes.txt

And this method reads the byte array back in, serializes it and then uses it to authenticate.

$byteArrayToken = Get-Content D:\bytes.txt
$user = "mysuer@tenant.onmicrosoft.com"

$clientId = "7716031e-6f8b-45a4-b82b-922b1af0fbb4" #S4B
$redirectUri = "https://adminau1.online.lync.com/OcsPowershellOAuth"
$resourceAppIdURI = "https://adminau1.online.lync.com/OcsPowershellOAuth"
$authority = "https://login.microsoftonline.com/common"

$s4bAuth = Get-MSAuthToken -User $user -TenantId $tenantId -ClientId $clientId -RedirectUri $redirectUri -ResourceAppIdURI $resourceAppIdURI -Authority $authority -StoredTokenByteArray $byteArrayToken

$secureToken = ConvertTo-SecureString $s4bAuth.AccessToken -AsPlainText -Force
New-CsOnlineSession -OAuthAccessToken $secureToken 

There may be another step in this – getting the right Lync Online URL for your S4B tenant. This can be obtained from the LyncDiscover process and I believe can be obtained through doing an HTTP Get on the LyncDiscover URL for your tenant.

ie: http://yourtenant.lyncdiscover.onmicrosoft.com.

I haven’t got to this LyncDiscover bit yet.. may need to create a function that does this to get the correct admin URL…

Microsoft Secure App Authentication

The world of Microsoft Authentication is all changing with stricter AD policies been forced out like Multi Factor Authentication. That’s a good thing.

For people that do all their Microsoft services manipulation through the UI, that’s OK. The Microsoft Online sign in process is (after many years of being average), finally pretty solid and can handle multiple identifications at the same time logged in.

But in the PowerShell world, its all changed on how you can connect and run processes and scripts unattended. You can’t save passwords or be on the ready to accept an MFA prompt if you want a process to run periodically, or on demand through a provisioning system.

Microsoft’s Partner Center API is also the modern way you are given delegated access to modify settings and services for your CSP Customers.

The process these days appears to be to register an Azure AD Application in to the CSP tenant, and authenticate against this application for delegated access to your sub customers.

Simple commands like Get-MSOLUsers can be run in this context, but you specify -tenantID ‘your o365 tenant id’ to work in the context you need to.

These clever guys have some great blogs about how to register a Partner Center API Secure Authentication application and authenticate with a token so you can make a connection to Office 365, Azure AD, Exchange etc.

https://gcits.com/knowledge-base/how-to-connect-to-delegated-office-365-tenants-using-the-secure-app-model/

https://www.cyberdrain.com/using-the-secure-app-model-to-connect-to-microsoft-partner-resources/

https://www.cyberdrain.com/using-the-secure-application-model-with-partnercenter-2-0-for-office365/

https://www.cyberdrain.com/connect-to-exchange-online-automated-when-mfa-is-enabled-using-the-secureapp-model/

These methods work if you are a Microsoft Partner and have a Partner Center where you manage your customers.

But, if you want to run your PowerShell directly against an Office 365 tenant that you don’t have delegated access to, that’s a different process.

I looked for ages to try and find some simple example that would allow me to register a Secure App in the customer’s tenant I want to connect to and manage and then use this app to authenticate from PowerShell with no MFA, passwords etc.

I ended up working with Elliot Munro from GCITS from the first link above, and with his clever reverse engineering skills, we figured out the below script, that’s essentially an adaption of the Partner Center Secure App script from the above examples, but it targets the end-customer’s tenant instead.

This is the code to create the Secure App in the Tenant.
Note, you still need the Partner Center API to do this, even though you won’t be making a connection to Partner Center. This gives your scripted login the rights to do what it needs to do.

Make sure you have these modules installed:

Install-Module PartnerCenter
Install-Module MSOnline
Install-Module AzureAD

Code to create Secure App:

$adAppAccess = [Microsoft.Open.AzureAD.Model.RequiredResourceAccess]@{
    ResourceAppId = "00000002-0000-0000-c000-000000000000";
    ResourceAccess =
   [Microsoft.Open.AzureAD.Model.ResourceAccess]@{
        Id = "5778995a-e1bf-45b8-affa-663a9f3f4d04";
        Type = "Role"},
    [Microsoft.Open.AzureAD.Model.ResourceAccess]@{
        Id = "a42657d6-7f20-40e3-b6f0-cee03008a62a";
        Type = "Scope"},
    [Microsoft.Open.AzureAD.Model.ResourceAccess]@{
        Id = "311a71cc-e848-46a1-bdf8-97ff7156d8e6"; 
        Type = "Scope"}
}

$graphAppAccess = [Microsoft.Open.AzureAD.Model.RequiredResourceAccess]@{
    ResourceAppId = "00000003-0000-0000-c000-000000000000";
    ResourceAccess =
        [Microsoft.Open.AzureAD.Model.ResourceAccess]@{
            Id = "bf394140-e372-4bf9-a898-299cfc7564e5";
            Type = "Role"},
        [Microsoft.Open.AzureAD.Model.ResourceAccess]@{
            Id = "7ab1d382-f21e-4acd-a863-ba3e13f7da61";
            Type = "Role"}
}


$SessionInfo = Connect-AzureAD

$DisplayName = "Test Auth"

$app = New-AzureADApplication -AvailableToOtherTenants $true -DisplayName $DisplayName -IdentifierUris "https://$($SessionInfo.TenantDomain)/$((New-Guid).ToString())" -RequiredResourceAccess $adAppAccess, $graphAppAccess -ReplyUrls @("urn:ietf:wg:oauth:2.0:oob","https://localhost","http://localhost","http://localhost:8400")
$password = New-AzureADApplicationPasswordCredential -ObjectId $app.ObjectId
$spn = New-AzureADServicePrincipal -AppId $app.AppId -DisplayName $DisplayName

$PasswordToSecureString = $password.value | ConvertTo-SecureString -asPlainText -Force
$credential = New-Object System.Management.Automation.PSCredential($($app.AppId),$PasswordToSecureString)

$token = New-PartnerAccessToken -ApplicationId $app.AppId -Scopes 'Directory.AccessAsUser.All offline_access openid profile User.Read' -ServicePrincipal -Credential $credential -Tenant $spn.AppOwnerTenantID -UseAuthorizationCode

Write-Host "ApplicationId       = $($app.AppId)"
Write-Host "ApplicationSecret   = $($password.Value)"
Write-Host "TenantId            = $($SessionInfo.TenantId)"
Write-Host "Refresh Token       = $($token.RefreshToken)" 

When you run the above, a browser window will pop up and get you to authenticate with a user that has the rights to create the Secure App in that O365 tenant.

This will output in the PowerShell window App Id, App Secret and Refresh Token. These are what you use to authenticate in your PowerShell scripts when you need to.

NOTE: With all of this, you’re going to be dealing with Security tokens that have some power. Make sure to save them appropriately in something like the Azure Key Vault.

The code you put in your PowerShell scripts to authenticate is the same as the example links above connecting to Partner Center.

$refreshToken = "<refresh_token>"
$app_id = "<app_id>"
$app_secret = "<app_secret>"
$tenantId = "<tenant_id>"

function Get-GCITSAccessTokenByResource($AppCredential, $tenantid, $Resource) {
    $authority = "https://login.microsoftonline.com/$tenantid"
    $tokenEndpointUri = "$authority/oauth2/token"
    $content = @{
        grant_type = "refresh_token"
        client_id = $appCredential.appID
        client_secret = $appCredential.secret
        resource = $resource
        refresh_token = $appCredential.refreshToken
    }
    $tokenEndpointUri = "$authority/oauth2/token"

    $response = Invoke-RestMethod -Uri $tokenEndpointUri -Body $content -Method Post -UseBasicParsing
    $access_token = $response.access_token
    return $access_token
}

$AppCredential = @{
            appId        = $app_id
            secret       = $app_secret
            refreshToken = $refreshToken
        }
  
    
try
{
    $MSGraphToken  = Get-GCITSAccessTokenByResource -Resource "https://graph.microsoft.com" -tenantid $tenantId -AppCredential $AppCredential
    $AadGraphToken = Get-GCITSAccessTokenByResource -Resource "https://graph.windows.net" -tenantid $tenantId -AppCredential $AppCredential
}
catch
{
    $errorMessage = $_.Exception.Message
    throw "Error refreshing tokens - $($errorMessage)" 
}

try
{
    #Connect Office 365
    Connect-MsolService -MsGraphAccessToken $MSGraphToken -AdGraphAccessToken $AadGraphToken 

    #Connect Azure AD
    Connect-AzureAD -MsAccessToken $MSGraphToken -AadAccessToken $AadGraphToken -AccountId $tenantId -TenantId $cspAuth_tenantId

    return $true
}
catch
{
    $errorMessage = $_.Exception.Message
    throw "Error connecting to MSOL - $($errorMessage)" 
}  

Kelvin Tegelaar from the article on connecting to Exchange also demonstrates how you can connect without Partner Center to a ‘well known’ app to authenticate the modern way with Exchange.

Note in his article, the Non-Partner Center way to authenticate and give consent and then use this to connect to Exchange.

https://www.cyberdrain.com/automating-with-powershell-using-the-secure-application-model-updates/

Working next on how to connect to the Skype for Business Online module using this similar method. Will post an update when I figure it out..

Dynamics CRM 365 – On Prem – Invalid Trace Directory

Looks like another piece of CRM team awesomeness.

The Tracing directory should be:

C:\Program Files\Microsoft Dynamics CRM\Trace

 

But some update somewhere changes it to:

c:\crmdrop\logs

 

That’s not very helpful.

I initially tried to change the trace directory back to the right place using CRM PowerShell, but that failed with authentication errors (that i have also posted on here – http://paulobrien.co.nz/2018/03/07/get-crmsetting-powershell-the-caller-was-not-authenticated-by-the-service-the-request-for-security-token-could-not-be-satisfied-because-authentication-failed/).

 

This is the guide i tried using the powershell method – makes sense, if powershell crm wasn’t broken as well.

How to fix ‘Invalid Trace Directory’ errors

 

So ended up changing in the CRM Database and registry:

 

Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\MSCRM]
"TraceDirectory"="C:\\Program Files\\Microsoft Dynamics CRM\\Trace"
"TraceEnabled"=dword:00000001

 

And in the MSCRM_CONFIG database:

SELECT NVarCharColumn
  FROM [MSCRM_CONFIG].[dbo].[ServerSettingsProperties]
  where ColumnName = 'TraceDirectory'

  update [MSCRM_CONFIG].[dbo].[ServerSettingsProperties]
  set NVarCharColumn = 'C:\Program Files\Microsoft Dynamics CRM\Trace'
  where ColumnName = 'TraceDirectory'

 

 

Get-CrmSetting powershell – The caller was not authenticated by the service / The request for security token could not be satisfied because authentication failed.

Had a need to run CRM powershell on an On-premise CRM Server.

I’ve had this same issue before where any powershell command you run against the Microsoft.Crm.PowerShell provider fails with authentication errors.

In this example, i was trying to run this and it barfed on Get-CrmSetting ….

Add-PSSnapin Microsoft.Crm.PowerShell

$ClaimsSettings = Get-CrmSetting -SettingType OAuthClaimsSettings

$ClaimsSettings.Enabled = $true

Set-CrmSetting -Setting $ClaimsSettings

 

Turned out to fix it, i needed to add a registry key.  After i did this it worked straight away (no need to reboot or reopen the PS window).

Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa]
"DisableLoopbackCheck"=dword:00000001

 

Method 2 in this article:

https://support.microsoft.com/en-us/help/896861/you-receive-error-401-1-when-you-browse-a-web-site-that-uses-integrate

Azure DNS via PowerShell

In my working world, I recently have discovered the relatively new feature in Azure – DNS.

From my early playing round it seems great! Very fast to update and easy to control with scripting.

You will need the Azure PowerShell provider.  Very simple to get.

You basically open up PowerShell and type:

Install-Module AzureRM

And then you wait 5 mins while it gets and installs a whole bunch of stuff..

 

These two links are handy to give you insight in to how to connect and running different commands:

https://docs.microsoft.com/en-us/powershell/azureps-cmdlets-docs/

https://docs.microsoft.com/en-us/azure/dns/dns-operations-dnszones

 

 

Below is a script i wrote that that adds an A record.  It does a few checks and creates the dependent bits if they don’t exist – basically you need a resource group to put the DNS zone in, then you need a DNS zone and then you can create a record.

Couple of things to note from the script – you will want to set your location – my closest is “australiasoutheast” – you will want to find your preferred location to create your resource groups.

Second thing, I found this out by accident but very handy.. you can sign in to AzureRM and then ‘export’ your sign in details to a JSON file where the password etc is encrypted.. then in your script you can just use the Select-AzureRmProfile command and point it at the file, and it takes care of establishing a session with AzureRM.

Export your login details:

#Login - this prompts auth crenetials
Login-AzureRmAccount

#Once you are auth'd you can export your login credentials to an encrypted JSON file
Save-AzureRmProfile -Path “..\azureprofile.json”

 

Here’s the script – you can see it uses the saved credentials above:

param (
	[string] $zone,		  #eg: paultest100.com
	[string] $hostname,	  #eg: www.paultest100.com  
	[string] $address,	  #eg: 10.20.30.40
	[int] $TTL	          #eg: 3600
)

#get record from hostname - strip off the zone
$z = $hostname.Replace($zone, '').TrimEnd('.')

#create resource group name
$rgName = "MyDNSResources"

#Login
Select-AzureRmProfile -Path "..\azureprofile.json"
        
#Get Resource Group - Create if it doesn't exist
$rg = Get-AzureRmResourceGroup -Name $rgName
if ($rg -eq $null)
{
    $rg = New-AzureRmResourceGroup -Name $rgName -location "australiasoutheast"
}

#Get DNS Zone - Create if it doesn't exist
$dnsZone = Get-AzureRmDnsZone -Name $zone -ResourceGroupName $rg.ResourceGroupName
if ($dnsZone -eq $null)
{
    $dnsZone = New-AzureRmDnsZone -Name $zone -ResourceGroupName $rg.ResourceGroupName
}

#Get A record - Create if it doesn't exist
$dnsRecord = Get-AzureRmDnsRecordSet -Name $z -RecordType A -ZoneName $dnsZone.Name -ResourceGroupName $rg.ResourceGroupName
if ($dnsRecord -eq $null)
{
    #Create A Record
    New-AzureRmDnsRecordSet -Name $z -RecordType A -ZoneName $dnsZone.Name -ResourceGroupName $rg.ResourceGroupName -Ttl $TTL -DnsRecords (New-AzureRmDnsRecordConfig -IPv4Address $Address) -Overwrite -Force
}