Public/Connect-MSGraphRequest.ps1

function Connect-MSGraphRequest {
    <#
    .SYNOPSIS
        Authenticates to Microsoft Graph and stores the connection for subsequent calls.
 
    .DESCRIPTION
        Single entry-point for authentication supporting six flows, all using native REST
        calls with zero SDK dependencies:
 
          - Interactive: Authorization Code + PKCE via browser with localhost redirect.
          - DeviceCode: Device code flow for environments without a browser.
          - ClientSecret: Client credentials with a secret.
          - ClientCertificate: Client credentials with a signed JWT assertion.
          - ManagedIdentity: Azure IMDS or App Service managed identity.
          - Token: Bring your own pre-acquired access token.
 
        After connecting, the module stores connection state in script-scoped variables
        and displays account context information.
 
    .PARAMETER TenantId
        Azure AD / Entra ID tenant ID (GUID or domain).
 
    .PARAMETER ClientId
        Application (client) ID. Defaults to the well-known Microsoft Graph PowerShell
        app (14d82eec-204b-4c2f-b7e8-296a70dab67e) for Interactive and DeviceCode flows.
 
    .PARAMETER ClientSecret
        Client secret for app registration (client credentials flow).
 
    .PARAMETER ClientCertificate
        X509Certificate2 with private key for certificate-based client credentials.
 
    .PARAMETER DeviceCode
        Switch to use device code flow for authentication.
 
    .PARAMETER Scopes
        Space-separated scopes to request. Defaults vary by flow:
        - Interactive/DeviceCode: 'https://graph.microsoft.com/.default'
        - ClientSecret/ClientCertificate: 'https://graph.microsoft.com/.default'
 
    .PARAMETER AccessToken
        Provide an already-acquired access token directly (skips token acquisition).
 
    .PARAMETER ManagedIdentity
        Switch to use Azure Managed Identity for authentication.
 
    .PARAMETER ManagedIdentityClientId
        Client ID of a user-assigned managed identity. If omitted with -ManagedIdentity,
        the system-assigned identity is used.
 
    .EXAMPLE
        Connect-MSGraphRequest -TenantId "contoso.onmicrosoft.com"
        # Interactive browser sign-in using the default MS Graph PowerShell app.
 
    .EXAMPLE
        Connect-MSGraphRequest -TenantId "contoso.onmicrosoft.com" -DeviceCode
        # Device code flow — displays a code for the user to enter at https://microsoft.com/devicelogin.
 
    .EXAMPLE
        Connect-MSGraphRequest -TenantId "contoso.onmicrosoft.com" -ClientId "00000000-..." -ClientSecret "s3cret!"
        # Client credentials with a secret.
 
    .EXAMPLE
        Connect-MSGraphRequest -TenantId "contoso.onmicrosoft.com" -ClientId "00000000-..." -ClientCertificate $cert
        # Client credentials with a certificate.
 
    .EXAMPLE
        Connect-MSGraphRequest -ManagedIdentity
        # Azure Managed Identity (system-assigned).
 
    .EXAMPLE
        Connect-MSGraphRequest -AccessToken $myToken
        # Bring your own token.
 
    .NOTES
        Author: Nickolaj Andersen & Jan Ketil Skanke
        Contact: @NickolajA @JankeSkanke
        Created: 2026-02-19
 
        Version history:
        1.0.0 - (2026-02-19) Script created — native REST, zero SDK dependencies
    #>

    [CmdletBinding(DefaultParameterSetName = 'Interactive')]
    param(
        [Parameter(Mandatory = $true, ParameterSetName = 'Interactive', HelpMessage = "Azure AD / Entra ID tenant ID (GUID or domain).")]
        [Parameter(Mandatory = $true, ParameterSetName = 'DeviceCode')]
        [Parameter(Mandatory = $true, ParameterSetName = 'ClientSecret')]
        [Parameter(Mandatory = $true, ParameterSetName = 'ClientCertificate')]
        [ValidateNotNullOrEmpty()]
        [string]$TenantId,

        [Parameter(Mandatory = $false, ParameterSetName = 'Interactive')]
        [Parameter(Mandatory = $false, ParameterSetName = 'DeviceCode')]
        [Parameter(Mandatory = $true, ParameterSetName = 'ClientSecret')]
        [Parameter(Mandatory = $true, ParameterSetName = 'ClientCertificate')]
        [ValidateNotNullOrEmpty()]
        [string]$ClientId,

        [Parameter(Mandatory = $true, ParameterSetName = 'ClientSecret', HelpMessage = "Client secret for the app registration.")]
        [ValidateNotNullOrEmpty()]
        [string]$ClientSecret,

        [Parameter(Mandatory = $true, ParameterSetName = 'ClientCertificate', HelpMessage = "X509Certificate2 with private key for certificate auth.")]
        [ValidateNotNull()]
        [System.Security.Cryptography.X509Certificates.X509Certificate2]$ClientCertificate,

        [Parameter(Mandatory = $true, ParameterSetName = 'DeviceCode', HelpMessage = "Use device code flow for authentication.")]
        [switch]$DeviceCode,

        [Parameter(Mandatory = $false, ParameterSetName = 'Interactive')]
        [Parameter(Mandatory = $false, ParameterSetName = 'DeviceCode')]
        [Parameter(Mandatory = $false, ParameterSetName = 'ClientSecret')]
        [Parameter(Mandatory = $false, ParameterSetName = 'ClientCertificate')]
        [string]$Scopes,

        [Parameter(Mandatory = $true, ParameterSetName = 'Token', HelpMessage = "Provide a pre-acquired access token.")]
        [ValidateNotNullOrEmpty()]
        [string]$AccessToken,

        [Parameter(Mandatory = $true, ParameterSetName = 'ManagedIdentity', HelpMessage = "Use Azure Managed Identity.")]
        [switch]$ManagedIdentity,

        [Parameter(Mandatory = $false, ParameterSetName = 'ManagedIdentity', HelpMessage = "Client ID of a user-assigned managed identity.")]
        [string]$ManagedIdentityClientId
    )
    Process {
        # Well-known Microsoft Graph PowerShell application ID
        $GraphPSAppId = "14d82eec-204b-4c2f-b7e8-296a70dab67e"
        $defaultScopes = "https://graph.microsoft.com/.default"

        # --- Bring-Your-Own-Token ---
        if ($PSCmdlet.ParameterSetName -eq 'Token') {
            try {
                $context = Get-TokenContext -Token $AccessToken
                $tokenExpiry = if ($context.ExpiresOn) { $context.ExpiresOn } else { (Get-Date).AddHours(1).ToUniversalTime() }
            }
            catch {
                Write-Warning -Message "Could not decode the provided token. Assuming 1-hour expiry."
                $context = $null
                $tokenExpiry = (Get-Date).AddHours(1).ToUniversalTime()
            }

            $script:MSGraphConnection = @{
                Token         = $AccessToken
                TokenExpiry   = $tokenExpiry
                RefreshToken  = $null
                TokenEndpoint = $null
                ClientId      = $null
                Scopes        = $null
                FlowType      = 'Token'
                Context       = $context
            }

            $script:AuthenticationHeader = New-AuthenticationHeader -AccessToken $AccessToken -ExpiresOn $tokenExpiry
            Write-Host "[MSGraphRequest] Connected using provided token." -ForegroundColor Green
            if ($context) {
                Write-Host " Identity: $($context.Identity) | Tenant: $($context.TenantId) | Expires: $($tokenExpiry.ToLocalTime())" -ForegroundColor Gray
            }
            return
        }

        # --- Managed Identity ---
        if ($PSCmdlet.ParameterSetName -eq 'ManagedIdentity') {
            $miParams = @{}
            if ($ManagedIdentityClientId) {
                $miParams['ManagedIdentityClientId'] = $ManagedIdentityClientId
            }

            $response = Invoke-ManagedIdentityAuth @miParams
            $tokenExpiry = (Get-Date).AddSeconds($response.expires_in - 60).ToUniversalTime()

            $context = $null
            try { $context = Get-TokenContext -Token $response.access_token } catch { }

            $script:MSGraphConnection = @{
                Token                  = $response.access_token
                TokenExpiry            = $tokenExpiry
                RefreshToken           = $null
                TokenEndpoint          = $null
                ClientId               = $ManagedIdentityClientId
                Scopes                 = $null
                FlowType               = 'ManagedIdentity'
                Context                = $context
            }

            $script:AuthenticationHeader = New-AuthenticationHeader -AccessToken $response.access_token -ExpiresOn $tokenExpiry
            Write-Host "[MSGraphRequest] Connected using Managed Identity." -ForegroundColor Green
            return
        }

        # Default ClientId for interactive / device code flows
        if (-not $ClientId) { $ClientId = $GraphPSAppId }
        if (-not $Scopes) { $Scopes = $defaultScopes }

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

        switch ($PSCmdlet.ParameterSetName) {
            'Interactive' {
                $response = Invoke-InteractiveAuth -TenantId $TenantId -ClientId $ClientId -Scopes $Scopes
                $tokenExpiry = (Get-Date).AddSeconds($response.expires_in - 60).ToUniversalTime()

                $context = $null
                try { $context = Get-TokenContext -Token $response.access_token } catch { }

                $script:MSGraphConnection = @{
                    Token         = $response.access_token
                    TokenExpiry   = $tokenExpiry
                    RefreshToken  = $response.refresh_token
                    TokenEndpoint = $tokenEndpoint
                    ClientId      = $ClientId
                    TenantId      = $TenantId
                    Scopes        = $Scopes
                    FlowType      = 'Interactive'
                    Context       = $context
                }

                $script:AuthenticationHeader = New-AuthenticationHeader -AccessToken $response.access_token -ExpiresOn $tokenExpiry
                Write-Host "[MSGraphRequest] Connected via interactive browser sign-in." -ForegroundColor Green
                if ($context) {
                    Write-Host " Identity: $($context.Identity) | Tenant: $($context.TenantId) | Expires: $($tokenExpiry.ToLocalTime())" -ForegroundColor Gray
                }
            }

            'DeviceCode' {
                $response = Invoke-DeviceCodeAuth -TenantId $TenantId -ClientId $ClientId -Scopes $Scopes
                $tokenExpiry = (Get-Date).AddSeconds($response.expires_in - 60).ToUniversalTime()

                $context = $null
                try { $context = Get-TokenContext -Token $response.access_token } catch { }

                $script:MSGraphConnection = @{
                    Token         = $response.access_token
                    TokenExpiry   = $tokenExpiry
                    RefreshToken  = $response.refresh_token
                    TokenEndpoint = $tokenEndpoint
                    ClientId      = $ClientId
                    TenantId      = $TenantId
                    Scopes        = $Scopes
                    FlowType      = 'DeviceCode'
                    Context       = $context
                }

                $script:AuthenticationHeader = New-AuthenticationHeader -AccessToken $response.access_token -ExpiresOn $tokenExpiry
                Write-Host "[MSGraphRequest] Connected via device code flow." -ForegroundColor Green
                if ($context) {
                    Write-Host " Identity: $($context.Identity) | Tenant: $($context.TenantId) | Expires: $($tokenExpiry.ToLocalTime())" -ForegroundColor Gray
                }
            }

            'ClientSecret' {
                $body = @{
                    client_id     = $ClientId
                    scope         = $Scopes
                    client_secret = $ClientSecret
                    grant_type    = 'client_credentials'
                }
                $response = Invoke-TokenRequest -TokenEndpoint $tokenEndpoint -Body $body
                $tokenExpiry = (Get-Date).AddSeconds($response.expires_in - 60).ToUniversalTime()

                $context = $null
                try { $context = Get-TokenContext -Token $response.access_token } catch { }

                $script:MSGraphConnection = @{
                    Token         = $response.access_token
                    TokenExpiry   = $tokenExpiry
                    RefreshToken  = $null
                    TokenEndpoint = $tokenEndpoint
                    ClientId      = $ClientId
                    TenantId      = $TenantId
                    ClientSecret  = $ClientSecret
                    Scopes        = $Scopes
                    FlowType      = 'ClientSecret'
                    Context       = $context
                }

                $script:AuthenticationHeader = New-AuthenticationHeader -AccessToken $response.access_token -ExpiresOn $tokenExpiry
                Write-Host "[MSGraphRequest] Connected via client credentials (secret)." -ForegroundColor Green
                if ($context) {
                    Write-Host " App: $($context.Identity) | Tenant: $($context.TenantId) | Expires: $($tokenExpiry.ToLocalTime())" -ForegroundColor Gray
                }
            }

            'ClientCertificate' {
                $clientAssertion = New-ClientAssertion -ClientId $ClientId -TenantId $TenantId -ClientCertificate $ClientCertificate

                $body = @{
                    client_id             = $ClientId
                    scope                 = $Scopes
                    client_assertion_type = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'
                    client_assertion      = $clientAssertion
                    grant_type            = 'client_credentials'
                }
                $response = Invoke-TokenRequest -TokenEndpoint $tokenEndpoint -Body $body
                $tokenExpiry = (Get-Date).AddSeconds($response.expires_in - 60).ToUniversalTime()

                $context = $null
                try { $context = Get-TokenContext -Token $response.access_token } catch { }

                $script:MSGraphConnection = @{
                    Token              = $response.access_token
                    TokenExpiry        = $tokenExpiry
                    RefreshToken       = $null
                    TokenEndpoint      = $tokenEndpoint
                    ClientId           = $ClientId
                    ClientCertificate  = $ClientCertificate
                    TenantId           = $TenantId
                    Scopes             = $Scopes
                    FlowType           = 'ClientCertificate'
                    Context            = $context
                }

                $script:AuthenticationHeader = New-AuthenticationHeader -AccessToken $response.access_token -ExpiresOn $tokenExpiry
                Write-Host "[MSGraphRequest] Connected via client credentials (certificate)." -ForegroundColor Green
                if ($context) {
                    Write-Host " App: $($context.Identity) | Tenant: $($context.TenantId) | Expires: $($tokenExpiry.ToLocalTime())" -ForegroundColor Gray
                }
            }
        }
    }
}