Functions/Public/Get-MsGAccessToken.ps1

function Get-MsGAccessToken {
    <#
    .SYNOPSIS
    Retrieves an access token from the specified Entra ID resource.
    .DESCRIPTION
    This functions retrieves an access token from Entra ID for a specified resource using a given authentication mechanism. This can be used for user-based interactive authentication or for principal-based non-interactive authentication.
    .PARAMETER ServicePrincipalCredential
    Credential object containing client id and secret for service principal.
    .PARAMETER ClientId
    Client id of service principal to authenticate to.
    .PARAMETER ClientSecret
    Client secret of service principal to authenticate to.
    .PARAMETER Thumbprint
    Thumbprint of private key certificate in specified certificate store tied to an app registration to authenticate to.
    .PARAMETER CertificateStore
    Certificate store holding the private key certificate to authenticate with.
    .PARAMETER ResourceName
    Name of the resource to authenticate to Entra ID for. Maps to resource url in the function.
    .PARAMETER Scopes
    Scopes to authenticate to the resource for.
    .PARAMETER ResourceUrl
    Url of the resource to authenticate to Entra ID for.
    .PARAMETER TenantId
    Id/name of the tenant to authenticate to.
    .PARAMETER AsHeaders
    Returns the token as headers instead of as a Jwt.
    .EXAMPLE
    PS> Get-MsGAccessToken -TenantId testdomain.onmicrosoft.com

    Triggers an interactive user sign-in through MSAL to authenticate to Entra ID and retrieve a jwt.
    .EXAMPLE
    PS> Get-MsGAccessToken -TenantId testdomain.onmicrosoft.com -ServicePrincipalCredential $AppCredential

    Authenticates to Entra ID using the Client Id and Secret Credential from $AppCredential.
    .EXAMPLE
    PS> Get-MsGAccessToken -TenantId testdomain.onmicrosoft.com -ClientId aeca5cf4-4738-4e32-b0d9-030d9b5b4297 -ClientSecret -gx8Q~_H1GPUygPDGJl4D.otjdReVx-x0KnvnaGF

    Authenticates to Entra ID using the provided client id and secret.
    .EXAMPLE
    PS> Get-MsGAccessToken -TenantId testdomain.onmicrosoft.com -ClientId aeca5cf4-4738-4e32-b0d9-030d9b5b4297 -Thumbprint 'A909502DD82AE41433E6F83886B00D4277A32A7B' -CertificateStore 'CurrentUser'

    Authenticates to Entra ID using the provided client id and certificate from the current user store with the "A909502DD82AE41433E6F83886B00D4277A32A7B" thumbprint.
    #>

    [CmdletBinding(DefaultParameterSetName = 'UserAccount')]
    param(
        [parameter(Mandatory = $true, ParameterSetName = 'ClientSecretCredential')]
        [pscredential]$ServicePrincipalCredential,

        [parameter(Mandatory = $true, ParameterSetName = 'ClientSecret')]
        [parameter(Mandatory = $true, ParameterSetName = 'Certificate')]
        [string]$ClientId,

        [parameter(Mandatory = $true, ParameterSetName = 'ClientSecret')]
        [string]$ClientSecret,

        [parameter(Mandatory = $true, ParameterSetName = 'Certificate')]
        [string]$Thumbprint,

        [parameter(Mandatory = $false, ParameterSetName = 'Certificate')]
        [ValidateSet('CurrentUser', 'LocalMachine')]
        [string]$CertificateStore = 'CurrentUser',

        [parameter(Mandatory = $false)]
        [ValidateSet('Graph', 'DevOps', 'Keyvault', 'Management')]
        [string]$ResourceName = 'Graph',

        [parameter(Mandatory = $false, ParameterSetName = 'UserAccount')]
        [string[]]$Scopes = ('User.Read'),

        [parameter(Mandatory = $false)][string]$ResourceUrl = 'https://graph.microsoft.com',
        [parameter(Mandatory = $true)][string]$TenantId,
        [parameter(Mandatory = $false)][switch]$AsHeaders
    )
    BEGIN {
        # Retrieve ResourceUrl if not present.
        if ($PSBoundParameters.ContainsKey('ResourceName') -eq $true) {
            Write-Verbose "Resource Name: $ResourceName"
            $resourceUrlTable = @{
                'Graph'      = 'https://graph.microsoft.com/'
                'DevOps'     = '499b84ac-1321-427f-aa17-267ca6975798'
                'Keyvault'   = 'https://vault.azure.net/'
                'Management' = 'https://management.azure.com/'
            }
            $ResourceUrl = $resourceUrlTable[$ResourceName]
        }
        # end region
        Write-Verbose $ResourceUrl
    }
    PROCESS {
        # Retrieve Jwt from Azure
        switch ($PSCmdlet.ParameterSetName) {
            "UserAccount" {
                try {
                    if ($ResourceName -eq 'Graph') {
                        $msalToken = Get-MsalToken -ClientId '14d82eec-204b-4c2f-b7e8-296a70dab67e' -Scopes $Scopes -TenantId $TenantId -ErrorAction 'Stop'
                        $jwt = $msalToken.AccessToken
                    }
                    else {
                        Connect-AzAccount -Tenant $TenantId -ErrorAction Stop -WarningAction SilentlyContinue > $null
                        $accessToken = Get-AzAccessToken -ResourceUrl $ResourceUrl -WarningAction SilentlyContinue -ErrorAction 'Stop'
                        $accessTokenCredential = [pscredential]::new('dummyUser', $accessToken.Token)
                        $jwt = $accessTokenCredential.GetNetworkCredential().Password
                    }
                }
                catch {
                    $errorMessage = Get-MsGErrorMessage $_
                    $errorException = '{0} : Unable to retrieve user jwt from Entra ID for {1}. Error: {2}' -f $MyInvocation.MyCommand.Name, $ResourceUrl, $errorMessage
                    throw $errorException
                }
            }
            "ClientSecretCredential" {
                $TokenBody = @{
                    'grant_type'    = 'client_credentials'
                    'client_id'     = $ServicePrincipalCredential.UserName
                    'client_secret' = $ServicePrincipalCredential.GetNetworkCredential().Password
                    'scope'         = '{0}/.default' -f $ResourceUrl
                }
                $TokenUri = 'https://login.microsoftonline.com/{0}/oauth2/v2.0/token' -f $TenantId
                try {
                    $jwtResults = Invoke-RestMethod -Method Post -Uri $TokenUri -Body $TokenBody -ErrorAction Stop
                }
                catch {
                    $errorMessage = Get-MsGErrorMessage $_
                    $errorException = '{0} : Unable to retrieve client credential jwt from Entra ID for {1}. Error: {2}' -f $MyInvocation.MyCommand.Name, $ResourceUrl, $errorMessage
                    throw $errorException
                }
                $jwt = $jwtResults.access_token
            }
            "ClientSecret" {
                $TokenBody = @{
                    'grant_type'    = 'client_credentials'
                    'client_id'     = $ClientId
                    'client_secret' = $ClientSecret
                    'scope'         = '{0}/.default' -f $ResourceUrl
                }
                $TokenUri = 'https://login.microsoftonline.com/{0}/oauth2/v2.0/token' -f $TenantId
                try {
                    $jwtResults = Invoke-RestMethod -Method Post -Uri $TokenUri -Body $TokenBody -ErrorAction Stop
                }
                catch {
                    $errorMessage = Get-MsGErrorMessage $_
                    $errorException = '{0} : Unable to retrieve client credential jwt from Entra ID for {1}. Error: {2}' -f $MyInvocation.MyCommand.Name, $ResourceUrl, $errorMessage
                    throw $errorException
                }
                $jwt = $jwtResults.access_token
            }
            "Certificate" {
                $CertificatePath = 'Cert:\{0}\My\{1}' -f $CertificateStore, $Thumbprint
                try {
                    $Certificate = Get-Item $CertificatePath -ErrorAction 'Stop'
                }
                catch {
                    $errorMessage = Get-MsGErrorMessage $_
                    $errorException = '{0} : Unable to retrieve certificate from {1}. Error: {2}' -f $MyInvocation.MyCommand.Name, $CertificatePath, $errorMessage
                    throw $errorException
                }

                $CertificateBase64Hash = [System.Convert]::ToBase64String($Certificate.GetCertHash())
                $StartDate = (Get-Date "1970-01-01T00:00:00Z" ).ToUniversalTime()
                $JWTExpirationTimeSpan = (New-TimeSpan -Start $StartDate -End (Get-Date).ToUniversalTime().AddMinutes(2)).TotalSeconds
                $JWTExpiration = [math]::Round($JWTExpirationTimeSpan, 0)
                $NotBeforeExpirationTimeSpan = (New-TimeSpan -Start $StartDate -End ((Get-Date).ToUniversalTime())).TotalSeconds
                $NotBefore = [math]::Round($NotBeforeExpirationTimeSpan, 0)
                $JWTHeader = @{
                    alg = "RS256"
                    typ = "JWT"
                    x5t = $CertificateBase64Hash -replace '\+', '-' -replace '/', '_' -replace '='
                }
                $JWTPayLoad = @{
                    aud = 'https://login.microsoftonline.com/{0}/oauth2/token' -f $TenantId
                    exp = $JWTExpiration
                    iss = $ClientID
                    jti = [guid]::NewGuid()
                    nbf = $NotBefore
                    sub = $ClientID
                }
                $JWTHeaderToByte = [System.Text.Encoding]::UTF8.GetBytes(($JWTHeader | ConvertTo-Json))
                $EncodedHeader = [System.Convert]::ToBase64String($JWTHeaderToByte)
                $JWTPayLoadToByte = [System.Text.Encoding]::UTF8.GetBytes(($JWTPayload | ConvertTo-Json))
                $EncodedPayload = [System.Convert]::ToBase64String($JWTPayLoadToByte)
                $JWTToken = $EncodedHeader + "." + $EncodedPayload
                $PrivateKey = ([System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($Certificate))
                $RSAPadding = [Security.Cryptography.RSASignaturePadding]::Pkcs1
                $HashAlgorithm = [Security.Cryptography.HashAlgorithmName]::SHA256
                $Signature = [Convert]::ToBase64String(
                    $PrivateKey.SignData([System.Text.Encoding]::UTF8.GetBytes($JWTToken), $HashAlgorithm, $RSAPadding)
                ) -replace '\+', '-' -replace '/', '_' -replace '='
                $JWTToken = $JWTToken + "." + $Signature

                $tokenBody = @{
                    client_id             = $ClientID
                    client_assertion      = $JWTToken
                    client_assertion_type = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
                    scope                 = '{0}/.default' -f $ResourceUrl
                    grant_type            = "client_credentials"
                }
                $TokenUri = 'https://login.microsoftonline.com/{0}/oauth2/v2.0/token' -f $TenantId
                $tokenHeaders = @{ Authorization = "Bearer $JWTToken" }
                try {
                    $jwtResults = Invoke-RestMethod -Method Post -Uri $TokenUri -Body $tokenBody -Headers $tokenHeaders -ContentType 'application/x-www-form-urlencoded'
                }
                catch {
                    $errorMessage = Get-MsGErrorMessage $_
                    $errorException = '{0} : Unable to retrieve client credential jwt from Entra ID for {1}. Error: {2}' -f $MyInvocation.MyCommand.Name, $ResourceUrl, $errorMessage
                    throw $errorException
                }
                $jwt = $jwtResults.access_token
            }
        }
        # end region

        # Write output
        if ($PSBoundParameters.ContainsKey('AsHeaders')) {
            $Headers = @{
                'Authorization' = 'Bearer {0}' -f $jwt
                'Content-Type'  = 'application/json'
            }
            Write-Output $Headers
        }
        else {
            Write-Output $jwt
        }
        # end region
    }
}