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 } } |