Last active
February 12, 2024 21:57
-
-
Save dreadsend/fb46410db717ca3e937acbc9fccca754 to your computer and use it in GitHub Desktop.
PowerShell: Connecting to Microsoft Graph with a Users Username and Password instad of an interactive Flow
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Note: To use -publicClient you must explicitly enable public clients on the App Registration | |
# For details on why you should avoid this as much as Possible see https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth-ropc | |
function Connect-ROPCGraph { | |
param ( | |
[Parameter(ParameterSetName = "PublicClient", Mandatory = $true)] | |
[Parameter(ParameterSetName = "ClientCert", Mandatory = $true)] | |
[Parameter(ParameterSetName = "ClientCredentials", Mandatory = $true)] | |
[ValidateNotNull()] | |
[System.Management.Automation.PSCredential]$userCredentials, | |
[Parameter(ParameterSetName = "PublicClient", Mandatory = $true)] | |
[switch]$publicClient, | |
[Parameter(ParameterSetName = "PublicClient", Mandatory = $false)] | |
[Parameter(ParameterSetName = "ClientCert", Mandatory = $true)] | |
[Parameter(ParameterSetName = "ClientCredentials", Mandatory = $true)] | |
[string]$tenantId, | |
[Parameter(ParameterSetName = "PublicClient", Mandatory = $true)] | |
[Parameter(ParameterSetName = "ClientCert", Mandatory = $true)] | |
[Parameter(ParameterSetName = "ClientCredentials", Mandatory = $true)] | |
[string]$clientId, | |
[Parameter(ParameterSetName = "PublicClient", Mandatory = $false)] | |
[Parameter(ParameterSetName = "ClientCert", Mandatory = $false)] | |
[Parameter(ParameterSetName = "ClientCredentials", Mandatory = $false)] | |
[array]$scopes=@(".default"), | |
[Parameter(ParameterSetName = "ClientCert", Mandatory = $true)] | |
[ValidateNotNull()] | |
[string]$certificateThumbprint, | |
[Parameter(ParameterSetName = "ClientCredentials", Mandatory = $true)] | |
[ValidateNotNull()] | |
[securestring]$clientSecret | |
) | |
# Depending on which Type of Client Credentials were used we generate the Request Body | |
switch ($PSCmdlet.ParameterSetName) { | |
"PublicClient" { | |
$Body = @{ | |
client_id = $clientId | |
scope = [string]$scopes | |
username = $userCredentials.UserName | |
password = $userCredentials.GetNetworkCredential().Password | |
grant_type = "password" | |
} | |
} | |
"ClientCert" { | |
# If we are using Certificate Credentials we have to generate a JWT Assertion | |
# Based on https://adamtheautomator.com/powershell-graph-api/ - the original certificate usage did not work for me though | |
try { | |
# Load Certificate | |
$Certificate = Get-Item "Cert:\CurrentUser\My\$certificateThumbprint" -ErrorAction Stop | |
# Get base64 hash of certificate in Web Encoding | |
$CertificateBase64Hash = [System.Convert]::ToBase64String($Certificate.GetCertHash()) -replace '\+', '-' -replace '/', '_' -replace '=' | |
} | |
catch { | |
throw "Error Reading Certificate" | |
} | |
$StartDate = (Get-Date "1970-01-01T00:00:00Z").ToUniversalTime() | |
# Create JWT timestamp for expiration | |
$JWTExpirationTimeSpan = (New-TimeSpan -Start $StartDate -End (Get-Date).ToUniversalTime().AddMinutes(2)).TotalSeconds | |
$JWTExpiration = [math]::Round($JWTExpirationTimeSpan, 0) | |
# Create JWT validity start timestamp | |
$NotBeforeExpirationTimeSpan = (New-TimeSpan -Start $StartDate -End ((Get-Date).ToUniversalTime())).TotalSeconds | |
$NotBefore = [math]::Round($NotBeforeExpirationTimeSpan, 0) | |
# Create JWT header | |
$JWTHeader = @{ | |
alg = "RS256" | |
typ = "JWT" | |
x5t = $CertificateBase64Hash | |
} | |
# Create JWT payload | |
$JWTPayLoad = @{ | |
aud = "https://login.microsoftonline.com/$tenantID/oauth2/token" | |
exp = $JWTExpiration | |
iss = $clientID | |
jti = [guid]::NewGuid() | |
nbf = $NotBefore | |
sub = $clientID | |
} | |
# Convert header and payload to base64 | |
$JWTHeaderToByte = [System.Text.Encoding]::UTF8.GetBytes(($JWTHeader | ConvertTo-Json)) | |
$EncodedHeader = [System.Convert]::ToBase64String($JWTHeaderToByte) | |
$JWTPayLoadToByte = [System.Text.Encoding]::UTF8.GetBytes(($JWTPayload | ConvertTo-Json)) | |
$EncodedPayload = [System.Convert]::ToBase64String($JWTPayLoadToByte) | |
$JWT = $EncodedHeader + "." + $EncodedPayload | |
# Define RSA signature and hashing algorithm | |
$RSAPadding = [Security.Cryptography.RSASignaturePadding]::Pkcs1 | |
$HashAlgorithm = [Security.Cryptography.HashAlgorithmName]::SHA256 | |
# Sign the JWT | |
$rsaCert = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($Certificate) | |
$Signature = [Convert]::ToBase64String( | |
$rsaCert.SignData([System.Text.Encoding]::UTF8.GetBytes($JWT), $HashAlgorithm, $RSAPadding) | |
) -replace '\+', '-' -replace '/', '_' -replace '=' | |
# Add Signature to JWT | |
$JWT = $JWT + "." + $Signature | |
$Body = @{ | |
client_id = $clientId | |
client_assertion = $JWT | |
client_assertion_type = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" | |
scope = [string]$scopes | |
username = $userCredentials.UserName | |
password = $userCredentials.GetNetworkCredential().Password | |
grant_type = "password" | |
} | |
} | |
"ClientCredentials" { | |
$Body = @{ | |
client_id = $clientId | |
client_secret = [System.Net.NetworkCredential]::new("", $clientSecret).Password | |
scope = [string]$scopes | |
username = $userCredentials.UserName | |
password = $userCredentials.GetNetworkCredential().Password | |
grant_type = "password" | |
} | |
} | |
} | |
$params = @{ | |
Uri = "https://login.microsoftonline.com/$tenantID/oauth2/v2.0/token" | |
Method = 'POST' | |
ContentType = 'application/x-www-form-urlencoded' | |
Body = $Body | |
# If we use a JWT we must add an Authorization Header | |
Headers = if ($JWT) { @{ Authorization = "Bearer $JWT" } } | |
} | |
$accessToken = ConvertTo-SecureString (Invoke-RestMethod @params -ErrorAction Stop).access_token -AsPlainText -Force | |
# Use our Access token to Connect to Microsoft Graph | |
Connect-MgGraph -AccessToken $accessToken -NoWelcome | |
# Clear Senstive Values | |
$sensitiveVars = @("userCredentials","accessToken","body","params","jwt","signature") | |
Remove-Variable $sensitiveVars | |
[gc]::collect() | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I would like to stress here, that this should only be used in very very rare cases. Probably 99% of the time using Application Permissions and a Corresponding natively Supported Auth flow is the better way.