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…