Public/Connect-Windows365.ps1

function Connect-Windows365 {
    <#
    .SYNOPSIS
    Connect to Windows 365 via Powershell
    .DESCRIPTION
    Connect to Windows 365 via Powershell via Interactive Browser or Service Principal
    .PARAMETER Authtype
    Type of Authentication to use Interactive, ServicePrincipal or DeviceCode
    .PARAMETER ClientSecret
    Client Secret for Service Principal Authentication
    .PARAMETER TenantID
    Tenant ID for all Authentication types
    .PARAMETER ClientID
    Client ID for Service Principal Authentication
    .PARAMETER ClientCertificate
    Client Certificate for Service Principal Authentication (THUMBPRINT)
    .EXAMPLE
    Connect-Windows365 -TenantID contoso.onmicrosoft.com
    .EXAMPLE
    Connect-Windows365 -TenantID contoso.onmicrosoft.com -DeviceCode:$true
    .EXAMPLE
    Connect-Windows365 -TenantID contoso.onmicrosoft.com -ClientID 12345678-1234-1234-1234-123456789012 -ClientSecret 12345678-1234-1234-1234-123456789012
    .EXAMPLE
    Connect-Windows365 -TenantID contoso.onmicrosoft.com -ClientID 12345678-1234-1234-1234-123456789012 -ClientCertificate "THUMBPRINT"
    #>

    [CmdletBinding(DefaultParameterSetName = 'Interactive')]
    param (
        [parameter(Mandatory, ParameterSetName = "Interactive")]
        [parameter(Mandatory, ParameterSetName = "DeviceCode")]
        [parameter(Mandatory, ParameterSetName = "ClientSecret")]
        [parameter(Mandatory, ParameterSetName = "ClientCertificate")]
        [string]$TenantID,

        [parameter(Mandatory, ParameterSetName = "ClientCertificate")]
        [parameter(Mandatory, ParameterSetName = "ClientSecret")]
        [string]$ClientID,

        [parameter(Mandatory, ParameterSetName = "ClientSecret")]
        [string]$ClientSecret,
        
        [parameter(Mandatory, ParameterSetName = "ClientCertificate")]
        [string]$ClientCertificate,

        [parameter()]
        [bool]$DeviceCode
    )
    begin {
        # Set the profile to beta
        Set-GraphVersion
    }
    
    process {

        If ($DeviceCode -eq $true){
            Write-Verbose "Using Device Code"
            $clientId = "14d82eec-204b-4c2f-b7e8-296a70dab67e"
            $resource = "https://graph.microsoft.com/"
            $scope = "CloudPC.ReadWrite.All%20DeviceManagementConfiguration.ReadWrite.All%20DeviceManagementManagedDevices.ReadWrite.All%20Directory.Read.All"

            $codeBody = @{ 
                resource  = $resource
                client_id = $clientId
                scope     = $scope
            }

            # Get OAuth Code
            $codeRequest = Invoke-RestMethod -Method POST -Uri "https://login.microsoftonline.com/$tenantId/oauth2/devicecode" -Body $codeBody

            # Print Code to console
            Write-Output "`n$($codeRequest.message)"

            $tokenBody = @{
                grant_type = "urn:ietf:params:oauth:grant-type:device_code"
                code       = $codeRequest.device_code
                client_id  = $clientId
            }

            # Get OAuth Token
            while ([string]::IsNullOrEmpty($connection.access_token)) {
                $connection = try {
                    Invoke-RestMethod -Method POST -Uri "https://login.microsoftonline.com/$tenantId/oauth2/token" -Body $tokenBody
                    Write-Verbose "Completed Authentication"
                }
                catch {
                    $errorMessage = $_.ErrorDetails.Message | ConvertFrom-Json
                    # If not waiting for auth, throw error
                    if ($errorMessage.error -ne "authorization_pending") {
                        throw
                    }
                }
            }
            $Token = $connection.access_token

            $script:Authtime = [System.DateTime]::UtcNow
            $script:Authtoken = $Token
            $script:Authheader = @{Authorization = "Bearer $($Token)" }
        }

        Write-Verbose "Using Authentication Type: $($PsCmdlet.ParameterSetName)"
        
        switch ($PsCmdlet.ParameterSetName) {
            Interactive {

                $environment = Get-ChildItem -Path C:\Windows -ErrorAction SilentlyContinue

                If ($null -eq $environment) {
                    Write-Error "Using Powershell Core on Mac or Linux, please use the DeviceCode or ClientSecret Authentication"
                    Break
                }
                else {
                    Write-Verbose "Using Windows Powershell Core, continue with the script"
                }

                Write-Verbose "Use Interactive Authentication"
                Write-Verbose "Using Windows Powershell"
                # Add required assemblies
                $ClientID = "14d82eec-204b-4c2f-b7e8-296a70dab67e"
                $Scopes = "CloudPC.ReadWrite.All%20DeviceManagementConfiguration.ReadWrite.All%20DeviceManagementManagedDevices.ReadWrite.All%20Directory.Read.All"
                $redirectUri = "https://login.microsoftonline.com/common/oauth2/nativeclient"

                # With User Interaction for Delegated Permission
                Add-Type -AssemblyName System.Web

                Function Get-AuthCode {
                    Add-Type -AssemblyName System.Windows.Forms

                    $form = New-Object -TypeName System.Windows.Forms.Form -Property @{Width = 640; Height = 840 }
                    $web = New-Object -TypeName System.Windows.Forms.WebBrowser -Property @{Width = 620; Height = 800; Url = ($url -f ($Scope -join "%20")) }

                    $DocComp = {
                        $Script:uri = $web.Url.AbsoluteUri        
                        if ($Script:uri -match "error=[^&]*|code=[^&]*") { $form.Close() }
                    }
                    $web.ScriptErrorsSuppressed = $true
                    $web.Add_DocumentCompleted($DocComp)
                    $form.Controls.Add($web)
                    $form.Add_Shown( { $form.Activate() })
                    $form.ShowDialog() | Out-Null

                    $queryOutput = [System.Web.HttpUtility]::ParseQueryString($web.Url.Query)
                    $output = @{}
                    foreach ($key in $queryOutput.Keys) {
                        $output["$key"] = $queryOutput[$key]
                    }
                }
                $url = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=$($ClientID)&response_type=code&redirect_uri=$($redirectUri)&response_mode=query&scope=$($Scopes)&state=12345"
                Get-AuthCode
                # Extract Access token from the returned URI
                $regex = '(?<=code=)(.*)(?=&)'
                $authCode = ($uri | Select-string -pattern $regex).Matches[0].Value

                Write-Verbose "Received an authCode, $authCode"

                # get Access Token
                $body = "grant_type=authorization_code&redirect_uri=$redirectUri&client_id=$clientId&code=$authCode"
                $connection = Invoke-RestMethod https://login.microsoftonline.com/common/oauth2/token `
                    -Method Post -ContentType "application/x-www-form-urlencoded" `
                    -Body $body `
                    -ErrorAction STOP
                # Access Token
                $Token = $connection.access_token
                $script:Authtime = [System.DateTime]::UtcNow
                $script:Authtoken = $Token
                $script:Authheader = @{Authorization = "Bearer $($Token)" }                   
            }

            ClientSecret {
                Write-Verbose "Using Client Secret Authentication"

                $body = @{
                    Grant_Type    = "client_credentials"
                    Scope         = "https://graph.microsoft.com/.default"
                    Client_Id     = $ClientID
                    Client_Secret = $ClientSecret
                }
                
                $connection = Invoke-RestMethod `
                    -Uri https://login.microsoftonline.com/$TenantID/oauth2/v2.0/token `
                    -Method POST `
                    -Body $body
                
                $Token = $connection.access_token
        
                $script:Authtime = [System.DateTime]::UtcNow
                $script:Authtoken = $Token
                $script:Authheader = @{Authorization = "Bearer $($Token)" }
            }

            ClientCertificate {
                $environment = Get-Item Cert:\LocalMachine\My -ErrorAction SilentlyContinue

                If ($null -eq $environment) {
                    Write-Error "Using Powershell Core on Mac or Linux, please use the DeviceCode or ClientSecret Authentication"
                    Break
                }
                else {
                    Write-Verbose "Using Windows Powershell (Core), continue with the script"
                    $Certificate = Get-Item Cert:\LocalMachine\My\$ClientCertificate
                }

                Write-Verbose "Using Client Certificate Authentication"
                $TenantName = $TenantID
                $AppId = $ClientID
                $Scope = "https://graph.microsoft.com/.default"

                # Create base64 hash of certificate
                $CertificateBase64Hash = [System.Convert]::ToBase64String($Certificate.GetCertHash())

                # Create JWT timestamp for expiration
                $StartDate = (Get-Date "1970-01-01T00:00:00Z" ).ToUniversalTime()
                $JWTExpirationTimeSpan = (New-TimeSpan -Start $StartDate -End (Get-Date).ToUniversalTime().AddMinutes(2)).TotalSeconds
                $JWTExpiration = [math]::Round($JWTExpirationTimeSpan, 0)

                # Create JWT validity start timestamp
                $NotBeforeExpirationTimeSpan = (New-TimeSpan -Start $StartDate -End ((Get-Date).ToUniversalTime())).TotalSeconds
                $NotBefore = [math]::Round($NotBeforeExpirationTimeSpan, 0)

                # Create JWT header
                $JWTHeader = @{
                    alg = "RS256"
                    typ = "JWT"
                    # Use the CertificateBase64Hash and replace/strip to match web encoding of base64
                    x5t = $CertificateBase64Hash -replace '\+', '-' -replace '/', '_' -replace '='
                }

                # Create JWT payload
                $JWTPayLoad = @{
                    # What endpoint is allowed to use this JWT
                    aud = "https://login.microsoftonline.com/$TenantName/oauth2/token"
                    # Expiration timestamp
                    exp = $JWTExpiration
                    # Issuer = your application
                    iss = $AppId
                    # JWT ID: random guid
                    jti = [guid]::NewGuid()
                    # Not to be used before
                    nbf = $NotBefore
                    # JWT Subject
                    sub = $AppId
                }

                # Convert header and payload to base64
                $JWTHeaderToByte = [System.Text.Encoding]::UTF8.GetBytes(($JWTHeader | ConvertTo-Json))
                $EncodedHeader = [System.Convert]::ToBase64String($JWTHeaderToByte)

                $JWTPayLoadToByte = [System.Text.Encoding]::UTF8.GetBytes(($JWTPayload | ConvertTo-Json))
                $EncodedPayload = [System.Convert]::ToBase64String($JWTPayLoadToByte)

                # Join header and Payload with "." to create a valid (unsigned) JWT
                $JWT = $EncodedHeader + "." + $EncodedPayload

                # Get the private key object of your certificate
                # $PrivateKey = $Certificate.PrivateKey
                $PrivateKey = ([System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($Certificate))

                # Define RSA signature and hashing algorithm
                $RSAPadding = [Security.Cryptography.RSASignaturePadding]::Pkcs1
                $HashAlgorithm = [Security.Cryptography.HashAlgorithmName]::SHA256

                # Create a signature of the JWT
                $Signature = [Convert]::ToBase64String(
                    $PrivateKey.SignData([System.Text.Encoding]::UTF8.GetBytes($JWT), $HashAlgorithm, $RSAPadding)
                ) -replace '\+', '-' -replace '/', '_' -replace '='

                # Join the signature to the JWT with "."
                $JWT = $JWT + "." + $Signature

                # Create a hash with body parameters
                $Body = @{
                    client_id             = $AppId
                    client_assertion      = $JWT
                    client_assertion_type = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
                    scope                 = $Scope
                    grant_type            = "client_credentials"
                }

                $Url = "https://login.microsoftonline.com/$TenantName/oauth2/v2.0/token"

                # Use the self-generated JWT as Authorization
                $Header = @{
                    Authorization = "Bearer $JWT"
                }

                # Splat the parameters for Invoke-Restmethod for cleaner code
                $PostSplat = @{
                    ContentType = 'application/x-www-form-urlencoded'
                    Method      = 'POST'
                    Body        = $Body
                    Uri         = $Url
                    Headers     = $Header
                }

                $Request = Invoke-RestMethod @PostSplat

                $script:Authtime = [System.DateTime]::UtcNow
                $script:Authtoken = $Token
                $script:Authheader = @{Authorization = "Bearer $($Request.access_token)" }

            }
        }

    }
}