PowerShell ve cihaz oturum açma akışını kullanarak AzureAD için erişim belirteci alma
Giriş
Hiç bir PowerShell betiğinden Azure Active Directory (AzureAD) erişim belirteçlerini kullanan bir API'yi sorgulamak istediniz mi?
Bunun için AzureAD'deki bir uygulamayı kullanan ve istemci kimliğini ve sırrını kullanarak kimlik doğrulaması yapan birçok çözüm var. Bir web uygulamam veya etkileşimli olmayan bir hizmetim varsa, gidilecek yol budur. Arkadaşım ve meslektaşım Emanuel Palm, bu senaryo için PowerShell ile Microsoft Graph API hakkında harika bir yazı yazdı.
Bu yazıda, OAuth 2.0 cihaz yetkilendirme hibe akışını kullanarak AzureAD'den nasıl erişim belirteci alınacağını ele alacağım. Bu akış, betiğimin bir kullanıcı mevcutken etkileşimli olarak çalıştırılmasını istediğimde harikadır. Kullanıcının erişim haklarına dayanır ve komut dosyasına herhangi bir uygulama sırrını kaydetmem gerekmez. Bu aynı zamanda komut dosyasının, kullanıcının gerçekleştirme erişimine sahip olmadığı herhangi bir eylemi gerçekleştirmemesini de sağlar.
Bu teknik, kullanıcıya aşağıdaki sorunun sorulduğu PowerShell'de Connect-AzAccount kullanımına benzer bir deneyim sağlayacaktır:
To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code ARX2JKHNC to authenticate.
Ayrıca, etkileşimli bir kullanıcı için bu akışı basitleştirmek amacıyla bir miktar "hack" de ele alacağım, böylece kullanıcının kodu kopyalayıp bir tarayıcıya yapıştırması gerekmez, ancak bu, geçici bir HTML dosyası oluşturup açmaya dayanır. Start-Process'i çağırarak varsayılan tarayıcıyı kullanın.
Cihaz oturum açma akışını başlatma
Cihaz yetkilendirme verme akışını kullanarak bir oturum açma akışı başlatmak için aşağıdaki gibi görünen bir URI'ye REST yöntemini POST çağırmamız gerekir:
"https://login.microsoftonline.com/$TenantID/oauth2/devicecode"$TenantID belirli bir kiracının kimliği olabilir veya "ortak" gibi iyi bilinen bir genel ayara ayarlanabilir. TenantID'nin 'ortak' olarak ayarlanması, kullanıcının oluşturulduğu kiracıyı kullanır. Konuk olarak davet edildiğim bir kiracıda oturum açmak istersem TenantID'yi sağlamam gerekecek.
TenantID için mevcut seçenekler hakkında daha fazla bilgiyi Uygulama yapılandırma seçenekleriyle ilgili belgelerde bulabilirsiniz.
REST yöntemini çağırırken gövdedeki iki parametreyi client_id ve resource sağlamam gerekiyor.
İstemci kimliği, betiğimin yapmasını istediğim şeyi gerçekleştirmek için izinleri devreden bir uygulamanın kimliğidir. Bu durumda birkaç grubu listeleyeceğim ve bunun için Azure PowerShell modülünün kullandığı Azure PowerShell uygulamasının istemci kimliğini ödünç alacağım.
Kaynak, erişim jetonumun nerede geçerli olacağını tanımlar. Benim durumumda, belirteci Microsoft Graph API'ye erişmek için kullanacağım, dolayısıyla kaynağım olarak "https://graph.microsoft.com/" adresini kullanacağım.
Şimdi bunu PowerShell'de deneyelim:
$ClientID = '1950a258-227b-4e31-a9cf-717495945fc2'
$TenantID = 'common'
$Resource = "https://graph.microsoft.com/"
$DeviceCodeRequestParams = @{
Method = 'POST'
Uri = "https://login.microsoftonline.com/$TenantID/oauth2/devicecode"
Body = @{
client_id = $ClientId
resource = $Resource
}
}
$DeviceCodeRequest = Invoke-RestMethod @DeviceCodeRequestParams
Write-Host $DeviceCodeRequest.message -ForegroundColor YellowBu, yeni bir oturum açma sürecini başlatacak ve bana, kullanıcıya yönelik talimatları içeren konsola yazdıracağım bir mesajı içeren bir yanıt verecektir. Yanıt ayrıca daha sonra kullanacağımız user_code ve device_code özelliklerini de içerir.
Oturum açma deneyimi
Artık kullanıcıya https://microsoft.com/devicelogin adresine göz atmasını ve şuna benzer bir sayfaya bir kod girmesini söyleyen bir mesaj gösterdim:
Kodu girdikten sonra kullanıcıdan benim uygulamamda yani “Microsoft Azure PowerShell”de oturum açması istenecek.
Oturum açtıktan sonra kullanıcı, tarayıcıyı kapatmasını bildiren bir onay mesajı alacaktır.
Jeton alma
Kullanıcı oturum açma işlemini tamamladıktan sonra komut dosyamın erişim belirtecini geri alması gerekecek. Bu, bu kez belirteç uç noktasını kullanarak REST API'yi bir kez daha çağırarak yapılır. İşte bir örnek:
$TokenRequestParams = @{
Method = 'POST'
Uri = "https://login.microsoftonline.com/$TenantId/oauth2/token"
Body = @{
grant_type = "urn:ietf:params:oauth:grant-type:device_code"
code = $DeviceCodeRequest.device_code
client_id = $ClientId
}
}
$TokenRequest = Invoke-RestMethod @TokenRequestParamsHer şey yolunda giderse, artık erişim jetonumu yanıtın erişim_token özelliğinde bulunduracağım.
Microsoft Graph API'yi kullanarak AzureAD'den gruplar alın
Microsoft Graph API'sinde kimlik doğrulaması yapmak amacıyla jetonumuzu kullanmak için, Authorization adlı bir başlık kullanmamız ve ona "Bearer" değerini ve ardından jetonumuzu vermemiz gerekir.
Graph API'yi kullanarak kiracımdan ilk 1 grubu almanın bir örneği:
$Token = $TokenRequest.access_token
$AadGroupRequestParams = @{
Method = 'GET'
Uri = 'https://graph.microsoft.com/v1.0/groups?$top=1'
Headers = @{
'Authorization' = "Bearer $Token"
}
}
$AadGroupRequest = Invoke-RestMethod @AadGroupRequestParams
$AadGroupRequest.valueVe bu kadar! Ama durun, dahası da var!
Deneyimin iyileştirilmesi
Cihaz kod akışını kullanmak, etkileşimli bir kullanıcı tarafından çalıştırılan herhangi bir komut dosyası için harika çalışır, ancak kullanıcı deneyiminin biraz hantal olduğunu düşünüyorum. Tarayıcı açma, https://microsoft.com/devicelogin adresine gitme ve cihaz kodunu girme şeklindeki ilk üç adımı atlamayı tercih ederim.
Bakalım bu konuda ne yapabilirim!
Tarayıcıda biraz hata ayıklama yaptıktan sonra, REST yöntemini POST kullanırsam ve https://login.microsoftonline.com/common/oauth2/deviceauth URI'sine "otc" adlı parametredeki kodu sağlarsam atlayacağımı anladım. kullanıcının kodu girmesi gereken adımlar. Ancak bir tarayıcı açıp ona bir siteye mesaj gönderme talimatı vermek ilk başta umduğum kadar kolay olmadı. Benim çözümüm, bir HTML kodu oluşturmak, onu diskteki geçici bir dosyaya bırakmak ve ardından bu dosyayı, kullanıcının varsayılan tarayıcısında açılacağını umarak açmaktır. HTML kodum "otc" parametresine sahip bir form ve formu POST isteği kullanarak gönderen küçük bir javascript içeriyor. Bunu, “-Interactive” parametresini kullanarak aşağıdaki fonksiyonun tamamını çalıştırarak deneyebilirsiniz.
Tam bir işlev
Elbette bu kodu kullanıma hazır bir fonksiyonda birleştirdim. Bir şans verin ve ne düşündüğünüzü bana bildirin!
<#
.SYNOPSIS
Gets an access token from Azure Active Directory
.DESCRIPTION
Gets an access token from Azure Active Directory that can be used to authenticate to for example Microsoft Graph or Azure Resource Manager.
Run without parameters to get an access token to Microsoft Graph and the users original tenant.
Use the parameter -Interactive and the script will open the sign in experience in the default browser without user having to copy any code.
.PARAMETER ClientID
Application client ID, defaults to well-known ID for Microsoft Azure PowerShell
.PARAMETER Interactive
Tries to open sign-in experience in default browser. If this succeeds the user don't need to copy and paste any device code.
.PARAMETER TenantID
ID of tenant to sign in to, defaults to the tenant where the user was created
.PARAMETER Resource
Identifier for target resource, this is where the token will be valid. Defaults to "https://graph.microsoft.com/"
Use "https://management.azure.com" to get a token that works with Azure Resource Manager (ARM)
.EXAMPLE
$Token = Connect-AzureDevicelogin -Interactive
$Headers = @{'Authorization' = "Bearer $Token" }
$UsersUri = 'https://graph.microsoft.com/v1.0/users?$top=5'
$Users = Invoke-RestMethod -Method GET -Uri $UsersUri -Headers $Headers
$Users.value.userprincipalname
Using Microsoft Graph to print the userprincipalname of 5 users in the tenant.
.EXAMPLE
$Token = Connect-AzureDevicelogin -Interactive -Resource 'https://management.azure.com'
$Headers = @{'Authorization' = "Bearer $Token" }
$SubscriptionsURI = 'https://management.azure.com/subscriptions?api-version=2019-11-01'
$Subscriptions = Invoke-RestMethod -Method GET -Uri $SubscriptionsURI -Headers $Headers
$Subscriptions.value.displayName
Using Azure Resource Manager (ARM) to print the display name for all the subscriptions the user has access to.
.NOTES
#>
function Connect-AzureDevicelogin {
[cmdletbinding()]
param(
[Parameter()]
$ClientID = '1950a258-227b-4e31-a9cf-717495945fc2',
[Parameter()]
[switch]$Interactive,
[Parameter()]
$TenantID = 'common',
[Parameter()]
$Resource = "https://graph.microsoft.com/",
# Timeout in seconds to wait for user to complete sign in process
[Parameter(DontShow)]
$Timeout = 300
)
try {
$DeviceCodeRequestParams = @{
Method = 'POST'
Uri = "https://login.microsoftonline.com/$TenantID/oauth2/devicecode"
Body = @{
resource = $Resource
client_id = $ClientId
}
}
$DeviceCodeRequest = Invoke-RestMethod @DeviceCodeRequestParams
if ($Interactive.IsPresent) {
Write-Host 'Trying to open a browser with login prompt. Please sign in.' -ForegroundColor Yellow
Start-Sleep -Second 1
$PostParameters = @{otc = $DeviceCodeRequest.user_code }
$InputFields = foreach ($entry in $PostParameters.GetEnumerator()) {
"<input type=`"hidden`" name=`"$($entry.Name)`" value=`"$($entry.Value)`">"
}
$PostUrl = "https://login.microsoftonline.com/common/oauth2/deviceauth"
$LocalHTML = @"
<!DOCTYPE html>
<html>
<head>
<title>…</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<script type="text/javascript">
function dosubmit() { document.forms[0].submit(); }
</script>
</head>
<body onload="dosubmit();">
<form action="$PostUrl" method="POST" accept-charset="utf-8">
$InputFields
</form>
</body>
</html>
"@
$TempPage = New-TemporaryFile
$TempPage = Rename-Item -Path $TempPage.FullName ($TempPage.FullName -replace '$', '.html') -PassThru
Out-File -FilePath $TempPage.FullName -InputObject $LocalHTML
Start-Process $TempPage.FullName
}
else {
Write-Host $DeviceCodeRequest.message -ForegroundColor Yellow
}
$TokenRequestParams = @{
Method = 'POST'
Uri = "https://login.microsoftonline.com/$TenantId/oauth2/token"
Body = @{
grant_type = "urn:ietf:params:oauth:grant-type:device_code"
code = $DeviceCodeRequest.device_code
client_id = $ClientId
}
}
$TimeoutTimer = [System.Diagnostics.Stopwatch]::StartNew()
while ([string]::IsNullOrEmpty($TokenRequest.access_token)) {
if ($TimeoutTimer.Elapsed.TotalSeconds -gt $Timeout) {
throw 'Login timed out, please try again.'
}
$TokenRequest = try {
Invoke-RestMethod @TokenRequestParams -ErrorAction Stop
}
catch {
$Message = $_.ErrorDetails.Message | ConvertFrom-Json
if ($Message.error -ne "authorization_pending") {
throw
}
}
Start-Sleep -Seconds 1
}
Write-Output $TokenRequest.access_token
}
finally {
try {
Remove-Item -Path $TempPage.FullName -Force -ErrorAction Stop
$TimeoutTimer.Stop()
}
catch {
# We don't care about errors here
}
}
}

