Public/Invoke-AuthorizationCodeFlow.ps1

<#
    .SYNOPSIS
    Invokes the OAuth2 authorization code flow.

    .DESCRIPTION
    This function will invoke the OAuth2 authorization code flow and return an access token.
    The authorization code flow is used to obtain an access token for a client service.
    The client service must be registered in Azure Active Directory.

    .PARAMETER ClientId
    The client Id of the client service.

    .PARAMETER Tenant
    Specifies the tenant ID or name. If the name is provided, it will be resolved to the corresponding tenant ID (Entra ID tenants only).

    .PARAMETER RedirectUri
    The redirect URI of the client service.

    .PARAMETER Scope
    A space-separated list of scopes that you want the user to consent to.

    .PARAMETER ClientSecret
    (Optional) Confidential clients only. The client secret of the client service. Must be provided as a secure string.

    .PARAMETER AuthorizationEndpoint
    (Optional) Specifies the endpoint to request authorization. If not specified, the default value is "https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize".

    .PARAMETER TokenEndpoint
    (Optional) Specifies the endpoint to request an access token. If not specified, the default value is "https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token".

    .PARAMETER PKCE
    (Optional) If specified, the function will use PKCE (Proof Key for Code Exchange) to protect the authorization code.

    .NOTES
    https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow

    .EXAMPLE
    PS C:\> Invoke-AuthorizationCodeFlow -ClientId "38964a68-dd39-472e-8d26-b603ef27f1f3" -Tenant "a9cb93b5-628d-4d91-9167-245ad1b55a52" -RedirectUri "http://localhost:8080/" -Scope "openid profile email User.Read" -ClientSecret (ConvertTo-SecureString "secret_goes_here" -AsPlainText)

    This example shows how to invoke the authorization code flow using the function.

    .LINK
    https://learn.microsoft.com/en-us/entra/identity/protocols/oauth2-authorization-code-flow
    #>

function Invoke-AuthorizationCodeFlow {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]$ClientId,

        [Parameter(Mandatory)]
        [string]$Tenant,

        [Parameter(Mandatory)]
        [string]$RedirectUri,

        [Parameter(Mandatory)]
        [string]$Scope,

        [securestring]$ClientSecret,

        [string]$AuthorizationEndpoint,

        [string]$TokenEndpoint,

        [switch]$PKCE
    )
    Write-Verbose "Starting Authorization Code Flow"
    Write-Verbose "--------------------------------"

    $GrantType = "authorization_code"

    # Resolve tenant ID if a tenant name is provided
    if ($Tenant -notmatch "^[0-9a-fA-F-]{36}$") {
        $Tenant = Get-MsftTenantId -TenantName $Tenant
    }

    # Entra ID default, if no authorization endpoint has been provided
    if ([string]::IsNullOrEmpty($AuthorizationEndpoint)) {
        $AuthorizationEndpoint = "https://login.microsoftonline.com/$Tenant/oauth2/v2.0/authorize"
    }

    # Entra ID default, if no token endpoint has been provided
    if ([string]::IsNullOrEmpty($TokenEndpoint)) {
        $TokenEndpoint = "https://login.microsoftonline.com/$Tenant/oauth2/v2.0/token"
    }

    $authReqParam = @{
        AuthUrl     = $AuthorizationEndpoint
        ClientId    = $ClientId
        RedirectUri = $RedirectUri
        Scope       = $Scope
    }

    if ($PKCE) {
        Write-Verbose "PKCE:`t`t`tEnabled"
        $PKCEChallenge = New-PKCEChallenge
        $authReqParam.Add("CodeChallenge", $PKCEChallenge.CodeChallenge)
        $authReqParam.Add("CodeChallengeMethod", $PKCEChallenge.CodeChallengeMethod)
    }
    else {
        Write-Verbose "PKCE:`t`t`tDisabled"
    }

    $listenerPort = $RedirectUri.Split(":")[-1].Replace("/", "")

    # Startup HTTP listener as a job to catch authorization code, stops after a default timeout of 60 seconds
    $HttpListenerDefinition = Get-Command "Start-HttpListener"
    $HttpListenerFunction = "function $($HttpListenerDefinition.Name) { $($HttpListenerDefinition.Definition) }"
    Write-Verbose "Starting HTTP listener on port tcp/$listenerPort"
    $job = Start-Job -Name "StartupHttpListener" -ScriptBlock {
        param ($RedirectUri, $HttpListenerFunction)
        Invoke-Expression $HttpListenerFunction
        Start-HttpListener -Prefix $RedirectUri -Verbose
    } -ArgumentList $RedirectUri, $HttpListenerFunction

    Get-AuthorizationCode @authReqParam

    # Wait for the job to complete and get the authorization code
    $jobResult = Receive-Job -Job $job -Wait -AutoRemoveJob
    $authCode = $jobResult

    if ([string]::IsNullOrEmpty($authCode)) {
        Write-Error "Authorization code not found. Check your configuration and parameters."
        Exit 1
    }

    Write-Verbose "Auth Code received:`t$authCode"

    # Get an access token
    $accTokenParam = @{
        TokenUrl    = $TokenEndpoint
        ClientId    = $ClientId
        RedirectUri = $RedirectUri
        Scope       = $Scope
        GrantType   = $GrantType
        AuthCode    = $authCode
    }

    # Confidential Client
    if ($ClientSecret -ne $null) {
        $accTokenParam.Add("ClientSecret", $ClientSecret)
    }

    if ($PKCE) {
        $accTokenParam.Add("CodeVerifier", $PKCEChallenge.CodeVerifier)
    }

    $response = Get-AccessToken @accTokenParam
    return $response
}