BcSaaS/New-BcAuthContext.ps1

<#
 .Synopsis
  Preview function for creating new BC Auth Context
 .Description
  Preview function for creating new BC Auth Context
#>

function New-BcAuthContext {
    Param(
        [string] $clientID = "1950a258-227b-4e31-a9cf-717495945fc2",
        [string] $Resource = "https://api.businesscentral.dynamics.com/",
        [string] $tenantID = "Common",
        [string] $authority = "https://login.microsoftonline.com/$TenantID",
        [string] $refreshToken,
        [string] $scopes = "https://api.businesscentral.dynamics.com/.default",
        [SecureString] $clientSecret,
        [PSCredential] $credential,
        [switch] $includeDeviceLogin,
        [Timespan] $deviceLoginTimeout = [TimeSpan]::FromMinutes(5)
    )

    $authContext = @{
        "clientID"           = $clientID
        "Resource"           = $Resource
        "tenantID"           = $tenantID
        "authority"          = $authority
        "includeDeviceLogin" = $includeDeviceLogin
        "deviceLoginTimeout" = $deviceLoginTimeout
    }
    $subject = "$($Resource.TrimEnd('/'))/$tenantID"
    $accessToken = $null
    if ($clientSecret) {
        $TokenRequestParams = @{
            Method = 'POST'
            Uri    = "$($authority.TrimEnd('/'))/oauth2/v2.0/token"
            Body   = @{
                "grant_type"    = "client_credentials"
                "scope"         = $scopes
                "client_id"     = $clientId
                "client_secret" = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($clientSecret))
            }
        }
        try {
            Write-Host "Attempting authentication to $scopes using clientCredentials..."
            $TokenRequest = Invoke-RestMethod @TokenRequestParams -UseBasicParsing
            $global:TT = $TokenRequest
            $accessToken = $TokenRequest.access_token
            $jwtToken = Parse-JWTtoken -token $accessToken
            Write-Host -ForegroundColor Green "Authenticated as app $($jwtToken.appid)"

            try {
                $expiresOn = [Datetime]::new(1970,1,1).AddSeconds($jwtToken.exp)
            }
            catch {
                $expiresOn = [DateTime]::now.AddSeconds($TokenRequest)
            }

            $authContext += @{
                "AccessToken"  = $accessToken
                "UtcExpiresOn" = $expiresOn
                "RefreshToken" = $null
                "Credential"   = $null
                "ClientSecret" = $clientSecret
                "scopes"       = $scopes
            }
            if ($tenantID -eq "Common") {
                Write-Host "Authenticated to common, using tenant id $($jwtToken.tid)"
                $authContext.TenantId = $jwtToken.tid
            }

        }
        catch {
            $exception = $_.Exception
            Write-Host -ForegroundColor Red $exception.Message.Replace('{EmailHidden}',$credential.UserName)
            $accessToken = $null
        }
    }
    else {
        if ($credential) {
            $TokenRequestParams = @{
                Method = 'POST'
                Uri    = "$($authority.TrimEnd('/'))/oauth2/token"
                Body   = @{
                    "grant_type" = "password"
                    "client_id"  = $ClientId
                    "username"   = $credential.UserName
                    "password"   = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($credential.Password))
                    "resource"   = $Resource
                }
            }
            try {
                Write-Host "Attempting authentication to $subject using username/password..."
                $TokenRequest = Invoke-RestMethod @TokenRequestParams -UseBasicParsing
                $accessToken = $TokenRequest.access_token
                $jwtToken = Parse-JWTtoken -token $accessToken
                Write-Host -ForegroundColor Green "Authenticated from $($jwtToken.ipaddr) as user $($jwtToken.name) ($($jwtToken.upn))"
    
                $authContext += @{
                    "AccessToken"  = $accessToken
                    "UtcExpiresOn" = [Datetime]::new(1970,1,1).AddSeconds($TokenRequest.expires_on)
                    "RefreshToken" = $TokenRequest.refresh_token
                    "Credential"   = $credential
                    "ClientSecret" = $null
                    "scopes"       = ""
                }
                if ($tenantID -eq "Common") {
                    Write-Host "Authenticated to common, using tenant id $($jwtToken.tid)"
                    $authContext.TenantId = $jwtToken.tid
                }
            }
            catch {
                $exception = $_.Exception
                Write-Host -ForegroundColor Yellow $exception.Message.Replace('{EmailHidden}',$credential.UserName)
                $accessToken = $null
            }
        }
        if (!$accessToken -and $refreshToken) {
            $TokenRequestParams = @{
                Method = 'POST'
                Uri    = "$($authority.TrimEnd('/'))/oauth2/token"
                Body   = @{
                    "grant_type"    = "refresh_token"
                    "client_id"     = $ClientId
                    "refresh_token" = $refreshToken
                }
            }
            try
            {
                Write-Host "Attempting authentication to $subject using refresh token..."
                $TokenRequest = Invoke-RestMethod @TokenRequestParams -UseBasicParsing
                $accessToken = $TokenRequest.access_token
                try {
                    $jwtToken = Parse-JWTtoken -token $accessToken
                    Write-Host -ForegroundColor Green "Authenticated using refresh token as user $($jwtToken.name) ($($jwtToken.upn))"
                    $authContext += @{
                        "AccessToken"  = $accessToken
                        "UtcExpiresOn" = [Datetime]::new(1970,1,1).AddSeconds($TokenRequest.expires_on)
                        "RefreshToken" = $TokenRequest.refresh_token
                        "Credential"   = $null
                        "ClientSecret" = $null
                        "scopes"       = ""
                    }
                    if ($tenantID -eq "Common") {
                        Write-Host "Authenticated to common, using tenant id $($jwtToken.tid)"
                        $authContext.TenantId = $jwtToken.tid
                    }
                }
                catch {
                    $accessToken = $null
                    throw "Invalid Access token"
                }
            }
            catch {
                Write-Host -ForegroundColor Yellow "Refresh token not valid"
            }
        }
        if (!$accessToken -and $includeDeviceLogin) {
            
            $deviceCodeRequest = $null
            $deviceLoginStart = [DateTime]::Now
            $accessToken = ""
            $cnt = 0
    
            while ($accessToken -eq "" -and ([DateTime]::Now.Subtract($deviceLoginStart) -lt $deviceLoginTimeout)) {
                if (!($deviceCodeRequest)) {
                    $DeviceCodeRequestParams = @{
                        Method = 'POST'
                        Uri    = "$($authority.TrimEnd('/'))/oauth2/devicecode"
                        Body   = @{
                            "client_id" = $ClientId
                            "resource"  = $Resource
                        }
                    }
                    
                    $deviceLoginStart = [DateTime]::Now
                    Write-Host "Attempting authentication to $subject using device login..."
                    $DeviceCodeRequest = Invoke-RestMethod @DeviceCodeRequestParams -UseBasicParsing
                    Write-Host $DeviceCodeRequest.message -ForegroundColor Yellow
                    Write-Host -NoNewline "Waiting for authentication"
    
                    $TokenRequestParams = @{
                        Method = 'POST'
                        Uri    = "$($authority.TrimEnd('/'))/oauth2/token"
                        Body   = @{
                            "grant_type" = "urn:ietf:params:oauth:grant-type:device_code"
                            "code"       = $DeviceCodeRequest.device_code
                            "client_id"  = $ClientId
                        }
                    }
                }
    
                Start-Sleep -Seconds 1
                try {
                    $TokenRequest = Invoke-RestMethod @TokenRequestParams -UseBasicParsing
                    $accessToken = $TokenRequest.access_token
                }
                catch {
                    $tokenRequest = $null
                    $exception = $_
                    try {
                        $err = ($exception.ErrorDetails.Message | ConvertFrom-Json).error
                        if ($err -eq "code_expired") {
                            Write-Host
                            Write-Host -ForegroundColor Red "Authentication request expired."
                            $deviceCodeRequest = $null
                        }
                        elseif ($err -eq "expired_token") {
                            Write-Host
                            Write-Host -ForegroundColor Red "Authentication token expired."
                            throw $exception
                        }
                        elseif ($err -eq "authorization_declined") {
                            Write-Host
                            Write-Host -ForegroundColor Red "Authentication request declined."
                            throw $exception
                        }
                        elseif ($err -eq "authorization_pending") {
                            if ($cnt++ % 5 -eq 0) {
                                Write-Host -NoNewline "."
                            }
                        }
                        else {
                            Write-Host
                            throw $exception
                        }
                    }
                    catch {
                        Write-Host 
                        throw $exception
                    }
                }
                if ($accessToken) {
                    try {
                        $jwtToken = Parse-JWTtoken -token $accessToken
                        Write-Host
                        Write-Host -ForegroundColor Green "Authenticated from $($jwtToken.ipaddr) as user $($jwtToken.name) ($($jwtToken.upn))"
                        $authContext += @{
                            "AccessToken"  = $accessToken
                            "UtcExpiresOn" = [Datetime]::new(1970,1,1).AddSeconds($TokenRequest.expires_on)
                            "RefreshToken" = $TokenRequest.refresh_token
                            "Credential"   = $null
                            "ClientSecret" = $null
                            "scopes"       = ""
                        }
                        if ($tenantID -eq "Common") {
                            Write-Host "Authenticated to common, using tenant id $($jwtToken.tid)"
                            $authContext.TenantId = $jwtToken.tid
                        }
                    }
                    catch {
                        $accessToken = $null
                        throw "Invalid Access token"
                    }
                }
            }
        }
    }
    if (!$accessToken) {
        Write-Host
        Write-Host -ForegroundColor Yellow "Authentication failed"
        return $null
    }
    return $authContext
}
Export-ModuleMember -Function New-BcAuthContext