Modules/businessdev.ALbuild.OnPrem/Public/New-BcApiAuthContext.ps1

function New-BcApiAuthContext {
    <#
    .SYNOPSIS
        Acquires an OAuth2 access token for the Business Central API.
 
    .DESCRIPTION
        Acquires a bearer token for the Business Central REST / automation API from Azure AD using
        one of three flows, selected by the parameters you pass:
 
          * ClientSecret - service-to-service (client_credentials) with an app-registration secret.
          * Certificate - service-to-service (client_credentials) with a signed client_assertion
                           JWT. The signing certificate lives in Azure Key Vault and its private key
                           never leaves the vault (a service principal reads the public certificate
                           and performs the RS256 signature via the Key Vault key 'sign' operation).
          * RefreshToken - exchanges an OAuth2 refresh token (ALbuild V1 parity).
 
        Returns an auth context whose AccessToken is passed to Publish-BcPerTenantExtension (or any
        other Business Central API cmdlet).
 
    .PARAMETER TenantId
        Azure AD tenant id (or verified domain) of the Business Central tenant. Default 'common'.
 
    .PARAMETER ClientId
        Application (client) id to authenticate as. Required for the ClientSecret and Certificate
        flows. For the RefreshToken flow it defaults to the well-known Business Central PowerShell
        client id the V1 task used.
 
    .PARAMETER ClientSecret
        App-registration client secret (SecureString or string). Selects the ClientSecret flow.
 
    .PARAMETER RefreshToken
        OAuth2 refresh token (SecureString or string). Selects the RefreshToken flow.
 
    .PARAMETER KeyVaultUrl
        Key Vault base URL holding the client-assertion certificate. Selects the Certificate flow.
 
    .PARAMETER CertificateName
        Certificate name in Key Vault (Certificate flow).
 
    .PARAMETER KeyVaultTenantId
        Azure AD tenant of the service principal used to access Key Vault. Defaults to -TenantId.
 
    .PARAMETER KeyVaultClientId
        Application (client) id of the service principal used to access Key Vault (Certificate flow).
 
    .PARAMETER KeyVaultClientSecret
        Client secret of the service principal used to access Key Vault (Certificate flow).
 
    .PARAMETER Scope
        OAuth2 scope. Default 'https://api.businesscentral.dynamics.com/.default'. The RefreshToken
        flow additionally requests 'offline_access'.
 
    .EXAMPLE
        $ctx = New-BcApiAuthContext -TenantId $t -ClientId $c -ClientSecret $s
        Publish-BcPerTenantExtension -TenantId $t -Environment 'Production' -AccessToken $ctx.AccessToken -AppFile .\out\My.app
 
    .EXAMPLE
        $ctx = New-BcApiAuthContext -TenantId $t -ClientId $appRegId -KeyVaultUrl $kv -CertificateName 'bc-deploy' `
            -KeyVaultClientId $sp -KeyVaultClientSecret $spSecret
 
    .OUTPUTS
        PSCustomObject with AccessToken, TokenType, ExpiresOn, TenantId, ClientId, Scope.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '',
        Justification = 'Acquires an OAuth token; it does not change persistent system state.')]
    [CmdletBinding(DefaultParameterSetName = 'ClientSecret')]
    [OutputType([PSCustomObject])]
    param(
        [string] $TenantId = 'common',

        [Parameter(ParameterSetName = 'ClientSecret', Mandatory)]
        [Parameter(ParameterSetName = 'Certificate', Mandatory)]
        [Parameter(ParameterSetName = 'RefreshToken')]
        [string] $ClientId,

        [Parameter(ParameterSetName = 'ClientSecret', Mandatory)]
        [object] $ClientSecret,

        [Parameter(ParameterSetName = 'RefreshToken', Mandatory)]
        [object] $RefreshToken,

        [Parameter(ParameterSetName = 'Certificate', Mandatory)]
        [string] $KeyVaultUrl,

        [Parameter(ParameterSetName = 'Certificate', Mandatory)]
        [string] $CertificateName,

        [Parameter(ParameterSetName = 'Certificate')]
        [string] $KeyVaultTenantId,

        [Parameter(ParameterSetName = 'Certificate', Mandatory)]
        [string] $KeyVaultClientId,

        [Parameter(ParameterSetName = 'Certificate', Mandatory)]
        [object] $KeyVaultClientSecret,

        [string] $Scope = 'https://api.businesscentral.dynamics.com/.default'
    )

    # No license gate here: acquiring a token is a free utility; the licensed step is the deployment
    # cmdlet that uses the token (e.g. Publish-BcPerTenantExtension).

    # Unwrap a SecureString or pass a plain string through unchanged.
    function Get-PlainText([object] $Value) {
        if ($null -eq $Value) { return $null }
        if ($Value -is [System.Security.SecureString]) {
            return [System.Net.NetworkCredential]::new('', $Value).Password
        }
        return [string] $Value
    }

    $tokenUrl = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token"

    switch ($PSCmdlet.ParameterSetName) {
        'ClientSecret' {
            $body = @{
                client_id     = $ClientId
                client_secret = Get-PlainText $ClientSecret
                scope         = $Scope
                grant_type    = 'client_credentials'
            }
        }
        'RefreshToken' {
            $effectiveClientId = if ($ClientId) { $ClientId } else { '1950a258-227b-4e31-a9cf-717495945fc2' }
            $body = @{
                client_id     = $effectiveClientId
                refresh_token = Get-PlainText $RefreshToken
                scope         = "$Scope offline_access"
                grant_type    = 'refresh_token'
            }
            $ClientId = $effectiveClientId
        }
        'Certificate' {
            $kvTenant = if ($KeyVaultTenantId) { $KeyVaultTenantId } else { $TenantId }
            $assertion = New-BcKeyVaultClientAssertion -TenantId $TenantId -ClientId $ClientId `
                -KeyVaultUrl $KeyVaultUrl -CertificateName $CertificateName `
                -KeyVaultTenantId $kvTenant -KeyVaultClientId $KeyVaultClientId `
                -KeyVaultClientSecret (Get-PlainText $KeyVaultClientSecret)
            $body = @{
                client_id             = $ClientId
                scope                 = $Scope
                grant_type            = 'client_credentials'
                client_assertion_type = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'
                client_assertion      = $assertion
            }
        }
    }

    $response = Invoke-RestMethod -Uri $tokenUrl -Method Post -Body $body -ContentType 'application/x-www-form-urlencoded' -ErrorAction Stop

    # Read fields defensively so a partial/error response yields a clear message under Set-StrictMode.
    function Get-ResponseField([object] $Object, [string] $Name) {
        $prop = $Object.PSObject.Properties[$Name]
        if ($prop) { $prop.Value } else { $null }
    }
    $accessToken = Get-ResponseField $response 'access_token'
    if (-not $accessToken) { throw 'Azure AD did not return an access token.' }
    $expiresIn = Get-ResponseField $response 'expires_in'

    Write-ALbuildLog -Level Success "Acquired Business Central API token ($($PSCmdlet.ParameterSetName))."
    return [PSCustomObject]@{
        AccessToken = $accessToken
        TokenType   = Get-ResponseField $response 'token_type'
        ExpiresOn   = if ($expiresIn) { (Get-Date).AddSeconds([int]$expiresIn) } else { $null }
        TenantId    = $TenantId
        ClientId    = $ClientId
        Scope       = $Scope
    }
}