Public/Connect-GraphToken.ps1

<#
.SYNOPSIS
    Acquires an OAuth 2.0 access token for Microsoft Graph with explicit permission provider models.
.DESCRIPTION
    Implements Device Code Flow (interactive/OOBE), Client Secret (application), and Certificate
    authentication. Attaches permission scope metadata to prevent delegated/application mismatches.
#>

function Connect-GraphToken {
    [CmdletBinding(DefaultParameterSetName = 'DeviceCode')]
    param(
        [Parameter(ParameterSetName = 'DeviceCode')]
        [switch]$DeviceCode,

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

        [Parameter()]
        [string]$TenantId = 'organizations',

        [Parameter()]
        [string]$ClientId = 'd1ddf0e6-50e1-4fb8-8182-76f584d73f3e',

        [Parameter()]
        [string[]]$Scopes = @('https://graph.microsoft.com/.default'),

        [Parameter()]
        [switch]$ForceRefresh
    )

    # 1. Check in-memory cache
    if (-not $ForceRefresh -and $script:GraphAuthContext -and $script:GraphAuthContext.ExpiresOn -gt [datetime]::UtcNow.AddMinutes(2)) {
        return $script:GraphAuthContext.AccessToken
    }

    $tokenEndpoint = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token"
    $scopeString = [string]::Join(' ', $Scopes)

    # 2. Client Secret Flow (Application Permission Scope)
    if ($PSCmdlet.ParameterSetName -eq 'ClientSecret') {
        $body = @{
            client_id     = $ClientId
            client_secret = $ClientSecret
            scope         = $scopeString
            grant_type    = 'client_credentials'
        }

        $res = Invoke-RestMethod -Uri $tokenEndpoint -Method POST -Body $body -ContentType 'application/x-www-form-urlencoded' -ErrorAction Stop
        
        $script:GraphAuthContext = [PSCustomObject]@{
            AccessToken    = $res.access_token
            TokenType      = $res.token_type
            ExpiresOn      = [datetime]::UtcNow.AddSeconds($res.expires_in)
            TenantId       = $TenantId
            ClientId       = $ClientId
            PermissionType = 'Application'
            Scopes         = $Scopes
        }
        return $res.access_token
    }

    # 3. Device Code Flow (Delegated Permission Scope for OOBE)
    if ($PSCmdlet.ParameterSetName -eq 'DeviceCode' -or $DeviceCode) {
        $deviceCodeEndpoint = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/devicecode"
        $dcBody = @{
            client_id = $ClientId
            scope     = $scopeString
        }

        $dcResponse = Invoke-RestMethod -Uri $deviceCodeEndpoint -Method POST -Body $dcBody -ContentType 'application/x-www-form-urlencoded' -ErrorAction Stop

        if (Get-Command 'Out-AsciiQrCode' -ErrorAction SilentlyContinue) {
            Out-AsciiQrCode -Url $dcResponse.verification_uri -UserCode $dcResponse.user_code
        } else {
            Write-Host " Sign-in URL: $($dcResponse.verification_uri)" -ForegroundColor Yellow
            Write-Host " Code: $($dcResponse.user_code)" -ForegroundColor Green
        }

        Write-Host " Waiting for user authentication in Entra ID..." -NoNewline -ForegroundColor Cyan

        $interval = [Math]::Max(3, [int]$dcResponse.interval)
        $expiresAt = [datetime]::UtcNow.AddSeconds($dcResponse.expires_in)

        while ([datetime]::UtcNow -lt $expiresAt) {
            Start-Sleep -Seconds $interval

            try {
                $pollBody = @{
                    client_id   = $ClientId
                    grant_type  = 'urn:ietf:params:oauth:grant-type:device_code'
                    device_code = $dcResponse.device_code
                }

                $tokenRes = Invoke-RestMethod -Uri $tokenEndpoint -Method POST -Body $pollBody -ContentType 'application/x-www-form-urlencoded' -ErrorAction Stop
                Write-Host " [OK] Authenticated!" -ForegroundColor Green

                $script:GraphAuthContext = [PSCustomObject]@{
                    AccessToken    = $tokenRes.access_token
                    RefreshToken   = $tokenRes.refresh_token
                    TokenType      = $tokenRes.token_type
                    ExpiresOn      = [datetime]::UtcNow.AddSeconds($tokenRes.expires_in)
                    TenantId       = $TenantId
                    ClientId       = $ClientId
                    PermissionType = 'Delegated'
                    Scopes         = $Scopes
                }
                return $tokenRes.access_token
            }
            catch {
                $err = $_.Exception.Message
                if ($err -match 'authorization_pending') {
                    Write-Host "." -NoNewline -ForegroundColor Cyan
                    continue
                }
                elseif ($err -match 'slow_down') {
                    $interval += 3
                    continue
                }
                elseif ($err -match 'code_expired') {
                    Write-Host ""
                    throw "Device login code expired. Please re-run the authentication command."
                }
                else {
                    Write-Host ""
                    throw $_
                }
            }
        }

        throw "Device code authentication timed out."
    }
}