Private/New-DeviceCodeAccessToken.ps1
|
function New-DeviceCodeAccessToken { <# .SYNOPSIS Requests an access token using the OAuth 2.0 device code flow. .DESCRIPTION Requests an access token using the OAuth 2.0 device code flow. This function initiates a device code flow and displays a user code that must be entered at a verification URL. .PARAMETER TenantID Tenant ID of the Entra ID tenant. .PARAMETER ClientID Application ID (Client ID) for an Entra ID service principal. .PARAMETER Scopes Array of permission scopes to request. Defaults to DeviceManagementApps.ReadWrite.All. .NOTES Author: Nickolaj Andersen Contact: @NickolajA Created: 2026-01-02 Updated: 2026-01-04 Version history: 1.0.0 - (2026-01-02) Script created 1.0.1 - (2026-01-04) Added refresh token storage for silent token renewal #> param( [parameter(Mandatory = $true, HelpMessage = "Tenant ID of the Entra ID tenant.")] [ValidateNotNullOrEmpty()] [String]$TenantID, [parameter(Mandatory = $true, HelpMessage = "Application ID (Client ID) for an Entra ID service principal.")] [ValidateNotNullOrEmpty()] [String]$ClientID, [parameter(Mandatory = $true, HelpMessage = "Array of permission scopes to request.")] [ValidateNotNullOrEmpty()] [String[]]$Scopes ) Process { try { # Request device code $DeviceCodeUri = "https://login.microsoftonline.com/$($TenantID)/oauth2/v2.0/devicecode" $ScopeString = $Scopes -join " " $DeviceCodeBody = @{ "client_id" = $ClientID "scope" = $ScopeString } Write-Verbose -Message "Requesting device code from Azure AD" $DeviceCodeResponse = Invoke-RestMethod -Method Post -Uri $DeviceCodeUri -Body $DeviceCodeBody -ErrorAction Stop # Display user code and verification URL Write-Host "To sign in, use a web browser to open the page: $($DeviceCodeResponse.verification_uri)" Write-Host "And enter the code: $($DeviceCodeResponse.user_code)" Write-Verbose -Message "Device code expires in $($DeviceCodeResponse.expires_in) seconds" Write-Verbose -Message "Polling interval: $($DeviceCodeResponse.interval) seconds" # Poll for token $TokenUri = "https://login.microsoftonline.com/$($TenantID)/oauth2/v2.0/token" $TokenBody = @{ "client_id" = $ClientID "grant_type" = "urn:ietf:params:oauth:grant-type:device_code" "device_code" = $DeviceCodeResponse.device_code } # Initialize polling variables $PollInterval = $DeviceCodeResponse.interval $ExpiresIn = $DeviceCodeResponse.expires_in $StartTime = Get-Date $TokenAcquired = $false Write-Verbose -Message "Waiting for user to complete authentication" while (-not $TokenAcquired) { # Check if device code has expired $ElapsedSeconds = ((Get-Date) - $StartTime).TotalSeconds if ($ElapsedSeconds -ge $ExpiresIn) { throw "Device code has expired. Please run the command again." } # Wait for the polling interval Start-Sleep -Seconds $PollInterval try { # Attempt to retrieve token $TokenResponse = Invoke-RestMethod -Method Post -Uri $TokenUri -Body $TokenBody -ErrorAction Stop $TokenAcquired = $true Write-Verbose -Message "Successfully acquired access token" } catch { $ErrorResponse = $_.Exception.Response if ($ErrorResponse) { $Reader = New-Object System.IO.StreamReader($ErrorResponse.GetResponseStream()) $Reader.BaseStream.Position = 0 $ResponseBody = $Reader.ReadToEnd() | ConvertFrom-Json switch ($ResponseBody.error) { "authorization_pending" { Write-Verbose -Message "Authorization pending, continuing to poll" continue } "slow_down" { Write-Verbose -Message "Polling too frequently, increasing interval" $PollInterval += 5 continue } "authorization_declined" { throw "User declined the authorization request" } "expired_token" { throw "Device code has expired" } default { throw "Authentication error: $($ResponseBody.error) - $($ResponseBody.error_description)" } } } else { throw "Error retrieving access token: $($_)" } } } # Validate the result if (-not $TokenResponse.access_token) { throw "No access token was returned in the response" } Write-Host "Authentication successful" # Add ExpiresOn property for token expiration tracking $TokenResponse | Add-Member -MemberType NoteProperty -Name "ExpiresOn" -Value ((Get-Date).AddSeconds($TokenResponse.expires_in).ToUniversalTime()) -Force # Add Scopes property for permission tracking $TokenResponse | Add-Member -MemberType NoteProperty -Name "Scopes" -Value ($TokenResponse.scope -split " ") -Force # Add AccessToken property for consistent access $TokenResponse | Add-Member -MemberType NoteProperty -Name "AccessToken" -Value $TokenResponse.access_token -Force # Store refresh token if available for silent token renewal if ($TokenResponse.refresh_token) { $TokenResponse | Add-Member -MemberType NoteProperty -Name "RefreshToken" -Value $TokenResponse.refresh_token -Force Write-Verbose -Message "Refresh token stored for silent token renewal" } # Set global variable $Global:AccessToken = $TokenResponse } catch { throw "Error retrieving the access token: $($_)" } } } |