Private/Get-AzureDevOpsAccessToken.ps1
|
function Get-AzureDevOpsAccessToken { <# .SYNOPSIS Gets an access token for Azure DevOps REST API using OAuth2 client credentials flow .DESCRIPTION Acquires an Azure AD access token for the Azure DevOps API scope using the official OAuth2 client credentials flow as documented by Microsoft. This token can be used to authenticate with Azure DevOps REST APIs. .PARAMETER TenantId The Azure Active Directory tenant ID .PARAMETER ClientId The service principal (application) client ID .PARAMETER ClientSecret The service principal client secret .OUTPUTS String containing the access token .NOTES Uses the Azure DevOps specific scope: https://app.vssps.visualstudio.com/.default Follows Microsoft's official OAuth2 client credentials flow documentation #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$TenantId, [Parameter(Mandatory = $true)] [string]$ClientId, [Parameter(Mandatory = $true)] [System.Security.SecureString]$ClientSecret ) try { Write-Verbose "Acquiring Azure DevOps access token using OAuth2 client credentials flow" Write-Verbose "TenantId: $TenantId" Write-Verbose "ClientId: $ClientId" # Convert SecureString to plain text for the API call $PlainSecret = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto( [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($ClientSecret) ) # OAuth2 v2.0 token endpoint $TokenUri = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token" # Azure DevOps specific scope $Scope = "https://app.vssps.visualstudio.com/.default" Write-Verbose "Token endpoint: $TokenUri" Write-Verbose "Scope: $Scope" # Prepare request body according to Microsoft OAuth2 client credentials flow specification # Format: application/x-www-form-urlencoded $Body = @{ 'client_id' = $ClientId 'scope' = $Scope 'client_secret' = $PlainSecret 'grant_type' = 'client_credentials' } Write-Verbose "Making token request to Microsoft identity platform" # Make the token request $Response = Invoke-RestMethod -Uri $TokenUri -Method Post -Body $Body -ContentType "application/x-www-form-urlencoded" -ErrorAction Stop # Validate response $ValidationErrors = @() if (-not $Response) { $ValidationErrors += "No response received from token endpoint." } elseif (-not $Response.access_token) { $ValidationErrors += "No access_token in response from Microsoft identity platform." } elseif ($Response.token_type -ne "Bearer") { $ValidationErrors += "Unexpected token type: $($Response.token_type). Expected 'Bearer'." } if ($ValidationErrors.Count -gt 0) { $ResponseContent = "" try { $ResponseContent = $Response | ConvertTo-Json -Depth 5 } catch { $ResponseContent = $Response } $ErrorMsg = "Token response validation failed: " + ($ValidationErrors -join " ") + " Response content: $ResponseContent" throw $ErrorMsg } Write-Verbose "Successfully acquired access token" Write-Verbose "Token type: $($Response.token_type)" Write-Verbose "Expires in: $($Response.expires_in) seconds" return $Response.access_token } catch { # Enhanced error handling $ErrorMessage = "Failed to acquire Azure DevOps access token: $($_.Exception.Message)" if ($_.Exception -is [System.Net.WebException]) { $Response = $_.Exception.Response if ($Response) { try { $StreamReader = New-Object System.IO.StreamReader($Response.GetResponseStream()) $ErrorContent = $StreamReader.ReadToEnd() $StreamReader.Close() Write-Verbose "HTTP Error Response: $ErrorContent" # Try to parse JSON error response try { $ErrorJson = $ErrorContent | ConvertFrom-Json if ($ErrorJson.error_description) { $ErrorMessage += " - $($ErrorJson.error_description)" } elseif ($ErrorJson.error) { $ErrorMessage += " - $($ErrorJson.error)" } } catch { # If JSON parsing fails, include raw content $ErrorMessage += " - $ErrorContent" } } catch { Write-Verbose "Could not read error response stream" } } } Write-Verbose $ErrorMessage throw $ErrorMessage } finally { # Clear the plain text secret from memory if ($PlainSecret) { $PlainSecret = $null } } } |