Modules/businessdev.ALbuild.Marketplace/Public/New-BcMarketplaceAuthContext.ps1

function New-BcMarketplaceAuthContext {
    <#
    .SYNOPSIS
        Acquires an OAuth2 access token for the Microsoft Marketplace / Partner Center APIs.
 
    .DESCRIPTION
        Client-credentials OAuth2 against Azure AD, returning an auth context for the other Marketplace
        cmdlets. The Partner Center ingestion API and the Graph product-ingestion API use different
        scopes - pass the appropriate -Scope. The context carries the numeric PublisherId (Seller ID),
        which the Partner Center ingestion API requires as the 'x-ms-publisherId' header, and the client
        id/secret so the token can be renewed (Update-BcMarketplaceAuthContext) during long submissions.
        Mirrors the proven Publish-AppSourceApp / New-AppSourceAuthContext implementation.
 
    .PARAMETER TenantId
        Azure AD tenant id (or verified domain).
 
    .PARAMETER ClientId
        Azure AD application (client) id.
 
    .PARAMETER ClientSecret
        Client secret (SecureString or plain string).
 
    .PARAMETER Scope
        OAuth2 scope. Default 'https://api.partner.microsoft.com/.default'. Use
        'https://graph.microsoft.com/.default' for product-ingestion (Get-BcMarketplaceProduct).
 
    .PARAMETER PublisherId
        Numeric publisher/Seller ID (Partner Center > Settings > Account settings > Identifiers). Sent as
        the required 'x-ms-publisherId' header on Partner Center ingestion calls.
 
    .OUTPUTS
        PSCustomObject: AccessToken, TokenType, ExpiresOn, ClientId, ClientSecret, TenantId, Scope,
        Authority, PublisherId.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Acquires an OAuth token; it does not change system state.')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '', Justification = 'The pipeline supplies the client secret as plain text (a task input/secret variable); it must be wrapped as a SecureString to carry it securely in the auth context.')]
    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory)] [string] $TenantId,
        [Parameter(Mandatory)] [string] $ClientId,
        [Parameter(Mandatory)] [object] $ClientSecret,
        [string] $Scope = 'https://api.partner.microsoft.com/.default',
        [string] $PublisherId
    )

    $secure = if ($ClientSecret -is [System.Security.SecureString]) { $ClientSecret }
    elseif ($ClientSecret -is [string]) { ConvertTo-SecureString $ClientSecret -AsPlainText -Force }
    else { throw 'ClientSecret must be a string or SecureString.' }

    $authority = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token"
    $token = Get-BcMarketplaceAccessToken -ClientId $ClientId -ClientSecret $secure -Scope $Scope -AuthorityUrl $authority

    return [PSCustomObject]@{
        PSTypeName   = 'BcMarketplace.AuthContext'
        AccessToken  = $token.access_token
        TokenType    = $token.token_type
        ExpiresOn    = (Get-Date).AddSeconds([int]$token.expires_in)
        ClientId     = $ClientId
        ClientSecret = $secure
        TenantId     = $TenantId
        Scope        = $Scope
        Authority    = $authority
        PublisherId  = $PublisherId
    }
}

function Get-BcMarketplaceAccessToken {
    # Internal: client-credentials token request; surfaces the AAD error body on failure.
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [string] $ClientId,
        [Parameter(Mandatory)] [System.Security.SecureString] $ClientSecret,
        [Parameter(Mandatory)] [string] $Scope,
        [Parameter(Mandatory)] [string] $AuthorityUrl
    )
    $plain = [System.Net.NetworkCredential]::new('', $ClientSecret).Password
    $body = @{ client_id = $ClientId; client_secret = $plain; scope = $Scope; grant_type = 'client_credentials' }
    try { return Invoke-RestMethod -Uri $AuthorityUrl -Method Post -Body $body -ContentType 'application/x-www-form-urlencoded' -ErrorAction Stop }
    catch { $d = Get-BcMarketplaceErrorDetail -ErrorRecord $_; throw "Marketplace authentication failed: $($_.Exception.Message)$(if ($d) { " - $d" })" }
    finally { $plain = $null }
}

function Update-BcMarketplaceAuthContext {
    # Internal: refresh the token when within 5 minutes of expiry (safe for long submissions).
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Refreshes an in-memory OAuth token; changes no external state.')]
    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param([Parameter(Mandatory)] [PSCustomObject] $AuthContext)
    if ($AuthContext.ExpiresOn -gt (Get-Date).AddMinutes(5)) { return $AuthContext }
    $token = Get-BcMarketplaceAccessToken -ClientId $AuthContext.ClientId -ClientSecret $AuthContext.ClientSecret -Scope $AuthContext.Scope -AuthorityUrl $AuthContext.Authority
    $AuthContext.AccessToken = $token.access_token
    $AuthContext.TokenType = $token.token_type
    $AuthContext.ExpiresOn = (Get-Date).AddSeconds([int]$token.expires_in)
    return $AuthContext
}