{"id":524,"date":"2024-01-28T16:43:45","date_gmt":"2024-01-28T15:43:45","guid":{"rendered":"https:\/\/sparrow365.de\/?p=524"},"modified":"2024-11-04T20:21:31","modified_gmt":"2024-11-04T19:21:31","slug":"connect-mggraph-with-username-and-password","status":"publish","type":"post","link":"https:\/\/sparrow365.de\/index.php\/en\/2024\/01\/28\/connect-mggraph-with-username-and-password\/","title":{"rendered":"Connect-MgGraph with Username and Password"},"content":{"rendered":"<p>In my work on the <a href=\"https:\/\/sparrow365.de\/wp-content\/uploads\/2024\/01\/ComingSoon.png\">practical implementation<\/a> of <a href=\"https:\/\/sparrow365.de\/index.php\/2023\/12\/31\/theater-gegen-overprivilege-pam-edition-theorie\/\">Password Rotation without Privileged Authentication Administrator<\/a>, I stumbled upon a <strong>somewhat extensive<\/strong> challenge.<\/p>\n<p>When trying to use PowerShell to sign in to the Graph API using <strong>username + password<\/strong>, I couldn&#8217;t find <a href=\"https:\/\/learn.microsoft.com\/en-us\/powershell\/module\/microsoft.graph.authentication\/connect-mggraph?view=graph-powershell-1.0\">a combination in the PowerShell SDK<\/a>.<br \/>\nThe only method would be ClientID + Secret &#8211; but that would mean using App Permissions again, which I specifically want to avoid in this case.<\/p>\n<p>We also know that it is <em>possible<\/em>, because in the <strong>Azure command line<\/strong> <a href=\"https:\/\/learn.microsoft.com\/en-us\/cli\/azure\/authenticate-azure-cli-interactively#sign-in-with-credentials-on-the-command-line\">such a connection is available<\/a> &#8211; but to demand the Az module or the Azure CLI <strong>just for this<\/strong> contradicts my perfectionism. Especially since it&#8217;s also not trivial to authenticate against a specific App Registration this way.<\/p>\n<blockquote>\n<p><em><a href=\"https:\/\/winsmarts.com\/super-easy-way-to-get-an-access-token-ddb9e56bcdf\">Accordingly, the lazy solution already exists<\/a> &#8211; the token can then be further used with <code><code>Connect-MgGraph -AccessToken $(ConvertTo-SecureString &quot;&lt;AccessToken&gt;&quot;)<\/code><\/code><\/em><\/p>\n<\/blockquote>\n<p>Since such a specific sub-problem soon took up half the article, I prefer to summarize separately how I arrived at <a href=\"https:\/\/gist.github.com\/dreadsend\/fb46410db717ca3e937acbc9fccca754\"><strong>my solution<\/strong><\/a>.<\/p>\n<p><br class=\"\"><\/p>\n<h2>Initial Investigation<\/h2>\n<p>Starting with the knowledge that there is a working implementation, <a href=\"https:\/\/chat.openai.com\/share\/7a45fe82-d630-41d5-b1a8-06d72caaf89c\">I decided to get more details<\/a>. This led me to the <strong>Resource Owner Password Credentials (ROPC) grant<\/strong>. <a href=\"https:\/\/learn.microsoft.com\/en-us\/entra\/identity-platform\/v2-oauth-ropc\">In the associated documentation<\/a>, we find very detailed information on <strong>how to use it<\/strong>.<\/p>\n<p><u><strong>Ideally, not at all:<\/strong><\/u><\/p>\n<p><img decoding=\"async\" src=\"https:\/\/sparrow365.de\/wp-content\/uploads\/2024\/01\/ROPCWarning.png\" alt=\"ROPC Warning\" \/><\/p>\n<p>Modern authentication protocols are designed, so that user <strong>login is separated from the application<\/strong> as far as possible &#8211; in all other OIDC flows, we simply get confirmation that the user is authenticated. Here, <strong>we temporarily hold his credentials<\/strong> &#8211; where we mostly wouldn&#8217;t need to.<\/p>\n<p>However, the key phrase is: <u><em>&quot;in most scenarios&quot;<\/em><\/u>. If I want to log in <strong>without interaction<\/strong> <u><em>as a normal user<\/em><\/u>, to my knowledge, there is <strong>no alternative<\/strong> &#8211; but I am open to correction. The upside in my scenario is worth it, because paradoxically, in my very specific use case, <strong>user permissions are miles more granular<\/strong> than application rights &#8211; and I already have the user passwords in the PAM vault anyways.<\/p>\n<p>However, we will also very likely <strong>not find any SDKs<\/strong> that work the way I would like: I agree with Microsoft, I do not want this app to be accessed by users. Otherwise, they could authenticate <strong>without MFA<\/strong> and make requests against the API. The best way would be to register a <a href=\"https:\/\/learn.microsoft.com\/en-us\/entra\/identity-platform\/msal-client-applications#when-does-proving-client-identity-matter\"><strong>confidential client app<\/strong><\/a>, where requests always have to include a certificate to prove that the <strong>system is allowed<\/strong> to use the app registration. But:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/sparrow365.de\/wp-content\/uploads\/2024\/01\/ROPCNoConfidential.png\" alt=\"ROPC No Confidential\" \/><\/p>\n<p>Understandable, assuming that <strong>at login<\/strong> passwords would be passed on to the server. Not applicable in our case, however, because we <strong>already have the passwords<\/strong>.<\/p>\n<p>If the SDKs don&#8217;t support it, we will have to replicate the Web Requests. <\/p>\n<p><br class=\"\"><\/p>\n<h2>Building the Code<\/h2>\n<p>I may often put in more effort than necessary (Like right now? \ud83e\udd14) &#8211; but I don&#8217;t want to be ridiculous. There are <strong>more than enough implementations<\/strong> of Graph API Auth via <code><code>Invoke-WebRequest<\/code><\/code> &#8211; so I will take the best possible version and <strong>adapt<\/strong> it for ROPC.<\/p>\n<p>The <strong>best version<\/strong> to me seemed to be the one by <a href=\"https:\/\/adamtheautomator.com\/powershell-graph-api\/\">Adam The Automator<\/a> &#8211; especially since it also already implements <strong>certificate-based authentication<\/strong>, which I probably would have struggled with for a while.<\/p>\n<p>But in my first tests of the code, I stumble over a problem:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/sparrow365.de\/wp-content\/uploads\/2024\/01\/ROPCInitialError.png\" alt=\"Error Connecting\" \/><\/p>\n<p>After a bit of troubleshooting, I identify the following passage:<\/p>\n<pre><code class=\"language-powershell\">#...\n$Certificate = Get-Item Cert:\\CurrentUser\\My\\$thumbprint\n#...\n\n# Get the private key object of your certificate\n$PrivateKey = $Certificate.PrivateKey\n\n# Define RSA signature and hashing algorithm\n$RSAPadding = [Security.Cryptography.RSASignaturePadding]::Pkcs1\n$HashAlgorithm = [Security.Cryptography.HashAlgorithmName]::SHA256\n\n# Create a signature of the JWT\n$Signature = [Convert]::ToBase64String(\n    $PrivateKey.SignData([System.Text.Encoding]::UTF8.GetBytes($JWT),$HashAlgorithm,$RSAPadding)\n) -replace &#039;\\+&#039;,&#039;-&#039; -replace &#039;\/&#039;,&#039;_&#039; -replace &#039;=&#039;\n#...<\/code><\/pre>\n<p>Probably because my private keys are non-exportable, <code><code>$PrivateKey<\/code><\/code> remained empty.<br \/>\nHowever, private keys that cannot be exported are still usable on the system where they are installed &#8211; otherwise, <code><code>Connect-MgGraph -CertificateThumbprint -ClientId<\/code><\/code> would also fail.<\/p>\n<p>If we construct a .Net object directly from the certificate that can provide us with a signature, we can make progress:<\/p>\n<pre><code class=\"language-powershell\">$rsaCert = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($Certificate)\n$Signature = [Convert]::ToBase64String(\n    $rsaCert.SignData([System.Text.Encoding]::UTF8.GetBytes($JWT), $HashAlgorithm, $RSAPadding)\n) -replace &#039;\\+&#039;, &#039;-&#039; -replace &#039;\/&#039;, &#039;_&#039; -replace &#039;=&#039;<\/code><\/pre>\n<p><img decoding=\"async\" src=\"https:\/\/sparrow365.de\/wp-content\/uploads\/2024\/01\/ROPCSignature-1.png\" alt=\"SuccessfulSignature\" \/><\/p>\n<p><br class=\"\"><\/p>\n<p><em>After that, adjustments take a while but are less interesting to present: I<\/em><\/p>\n<ol>\n<li><em>transfer the <a href=\"https:\/\/learn.microsoft.com\/en-us\/entra\/identity-platform\/v2-oauth-ropc\">HTTP examples from the documentation<\/a> into corresponding PowerShell syntax<\/em><\/li>\n<li><em>use the Access Token to connect with Connect-MgGraph<\/em><\/li>\n<li><em>parameterize everything so that it becomes a reusable function<\/em><\/li>\n<li><em>incorporate authentication against Public Clients and with Client Secret, for the sake of completion<\/em><\/li>\n<li><em>ensure that the sensitive variables are cleared<\/em><\/li>\n<\/ol>\n<p><br class=\"\"><\/p>\n<h2>Usage<\/h2>\n<p><strong>Requirements:<\/strong>  <\/p>\n<ul>\n<li>As usual, you need an Enterprise application with the delegated rights that you want to use later<\/li>\n<li>If no App Credentials are to be used, the registration must be marked as a &quot;Public Client App&quot; in the Advanced Settings\n<ul>\n<li><em>Please avoid this if possible<\/em><\/li>\n<\/ul>\n<\/li>\n<li>The app must be exempted from MFA and Compliant Device controls in Conditional Access\n<ul>\n<li>Ideally, at least the IP address should be checked, and access should be restricted to necessary users<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<p><br class=\"\"><\/p>\n<p>Connecting is very easy:<\/p>\n<pre><code class=\"language-powershell\">$params = @{\n  #alternativ get-credentials\n  userCredentials       = $creds\n  tenantId              = $config.tenantID\n  clientId              = $config.clientID\n  scopes                = @(&quot;User.Read&quot;,&quot;Directory.AccessAsUser.All&quot;)\n  certificateThumbprint = $config.thumb\n}\nConnect-ROPCGraph @params<\/code><\/pre>\n<p>The structure is deliberately as close as possible to Connect-MgGraph &#8211; but we still need the Users credentials.<\/p>\n<p>At first, you don&#8217;t get any output &#8211; but that&#8217;s intentional, as the function is built for non-interactive processes. We see the success when we execute other commands from the Graph SDK and find that everything works as usual:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/sparrow365.de\/wp-content\/uploads\/2024\/01\/ROPCSuccess.png\" alt=\"Successfull Connection\" \/><\/p>\n<p><strong>Restrictions<\/strong><br \/>\n<a href=\"https:\/\/learn.microsoft.com\/en-us\/entra\/identity-platform\/v2-oauth-ropc\">TL:DR of the big warning Block of ROPC<\/a>; The Following <strong>won&#8217;t<\/strong> work<\/p>\n<ul>\n<li>Microsoft <strong>Personal<\/strong> Accounts<\/li>\n<li><strong>Passwordless<\/strong> Accounts<\/li>\n<li>Logins, that need <strong>MFA<\/strong><\/li>\n<li><strong>Federated<\/strong> Accounts<\/li>\n<li><strong>Passwords<\/strong> with <strong>Spaces<\/strong> at the <strong>Beginning or End<\/strong> <\/li>\n<\/ul>\n<p><br class=\"\"><\/p>\n<h2>Summary<\/h2>\n<p>Now we can log in as an end user without user intervention and take advantage of the associated benefits. WHowever, we are aware that there is a reason why this flow is not made easily available &#8211; explicit use of user passwords and bypassing MFA should be used as rarely as possible.<\/p>\n<p>And I can wrap up my Passwordrotation-PoC \ud83d\ude09<\/p>\n<p><br class=\"\"><\/p>\n<p>I will not moderate comments and I do not want your email address; please participate in the discussion through <a href=\"https:\/\/www.linkedin.com\/posts\/julian-sperling-4bba72228_connect-mggraph-mit-benutzername-und-passwort-activity-7157376467616595968-iSdN?utm_source=share&amp;utm_medium=member_desktop\">my associated LinkedIn Post<\/a>.<\/p>\n<p><br class=\"\"><\/p>\n<p>If you are interested in the things I do <a href=\"https:\/\/www.linkedin.com\/in\/julian-sperling-4bba72228\/\">follow me on LinkedIn<\/a>.<\/p>\n<p><br class=\"\"><\/p>\n<blockquote>\n<p>Translation assisted by ChatGPT<\/p>\n<\/blockquote>\n<h2>Full Script<\/h2>\n<p><a href=\"https:\/\/gist.github.com\/dreadsend\/fb46410db717ca3e937acbc9fccca754\">Most Current Version<\/a><\/p>\n<pre><code class=\"language-powershell\">function Connect-ROPCGraph {\n    param (\n        [Parameter(ParameterSetName = &quot;PublicClient&quot;, Mandatory = $true)]\n        [Parameter(ParameterSetName = &quot;ClientCert&quot;, Mandatory = $true)]\n        [Parameter(ParameterSetName = &quot;ClientCredentials&quot;, Mandatory = $true)]\n        [ValidateNotNull()]\n        [System.Management.Automation.PSCredential]$userCredentials,\n\n        [Parameter(ParameterSetName = &quot;PublicClient&quot;, Mandatory = $true)]\n        [switch]$publicClient,\n\n        [Parameter(ParameterSetName = &quot;PublicClient&quot;, Mandatory = $false)]\n        [Parameter(ParameterSetName = &quot;ClientCert&quot;, Mandatory = $true)]\n        [Parameter(ParameterSetName = &quot;ClientCredentials&quot;, Mandatory = $true)]\n        [string]$tenantId,\n\n        [Parameter(ParameterSetName = &quot;PublicClient&quot;, Mandatory = $true)]\n        [Parameter(ParameterSetName = &quot;ClientCert&quot;, Mandatory = $true)]\n        [Parameter(ParameterSetName = &quot;ClientCredentials&quot;, Mandatory = $true)]\n        [string]$clientId,\n\n        [Parameter(ParameterSetName = &quot;PublicClient&quot;, Mandatory = $true)]\n        [Parameter(ParameterSetName = &quot;ClientCert&quot;, Mandatory = $true)]\n        [Parameter(ParameterSetName = &quot;ClientCredentials&quot;, Mandatory = $true)]\n        [ValidateNotNullOrEmpty()]\n        [array]$scopes,\n\n        [Parameter(ParameterSetName = &quot;ClientCert&quot;, Mandatory = $true)]\n        [ValidateNotNull()]\n        [string]$certificateThumbprint,\n\n        [Parameter(ParameterSetName = &quot;ClientCredentials&quot;, Mandatory = $true)]\n        [ValidateNotNull()]\n        [securestring]$clientSecret\n    )\n\n    # Depending on which Type of Client Credentials were used we generate the Request Body\n    switch ($PSCmdlet.ParameterSetName) {\n        &quot;PublicClient&quot; {\n            $Body = @{\n                client_id  = $clientId\n                scope      = [string]$scopes\n                username   = $userCredentials.UserName\n                password   = $userCredentials.GetNetworkCredential().Password\n                grant_type = &quot;password&quot;\n            }\n        }\n        &quot;ClientCert&quot; {\n            # If we are using Certificate Credentials we have to generate a JWT Assertion\n            # Based on https:\/\/adamtheautomator.com\/powershell-graph-api\/ - the original certificate usage did not work for me though\n            try {\n                # Load Certificate\n                $Certificate = Get-Item &quot;Cert:\\CurrentUser\\My\\$certificateThumbprint&quot; -ErrorAction Stop\n\n                # Get base64 hash of certificate in Web Encoding\n                $CertificateBase64Hash = [System.Convert]::ToBase64String($Certificate.GetCertHash()) -replace &#039;\\+&#039;, &#039;-&#039; -replace &#039;\/&#039;, &#039;_&#039; -replace &#039;=&#039;\n            }\n            catch {\n                throw &quot;Error Reading Certificate&quot;\n            }\n\n            $StartDate = (Get-Date &quot;1970-01-01T00:00:00Z&quot;).ToUniversalTime()\n\n            # Create JWT timestamp for expiration\n            $JWTExpirationTimeSpan = (New-TimeSpan -Start $StartDate -End (Get-Date).ToUniversalTime().AddMinutes(2)).TotalSeconds\n            $JWTExpiration = [math]::Round($JWTExpirationTimeSpan, 0)\n\n            # Create JWT validity start timestamp\n            $NotBeforeExpirationTimeSpan = (New-TimeSpan -Start $StartDate -End ((Get-Date).ToUniversalTime())).TotalSeconds\n            $NotBefore = [math]::Round($NotBeforeExpirationTimeSpan, 0)\n\n            # Create JWT header\n            $JWTHeader = @{\n                alg = &quot;RS256&quot;\n                typ = &quot;JWT&quot;\n                x5t = $CertificateBase64Hash \n            }\n\n            # Create JWT payload\n            $JWTPayLoad = @{\n                aud = &quot;https:\/\/login.microsoftonline.com\/$tenantID\/oauth2\/token&quot;\n                exp = $JWTExpiration\n                iss = $clientID\n                jti = [guid]::NewGuid()\n                nbf = $NotBefore\n                sub = $clientID\n            }\n\n            # Convert header and payload to base64\n            $JWTHeaderToByte = [System.Text.Encoding]::UTF8.GetBytes(($JWTHeader | ConvertTo-Json))\n            $EncodedHeader = [System.Convert]::ToBase64String($JWTHeaderToByte)\n\n            $JWTPayLoadToByte = [System.Text.Encoding]::UTF8.GetBytes(($JWTPayload | ConvertTo-Json))\n            $EncodedPayload = [System.Convert]::ToBase64String($JWTPayLoadToByte)\n\n            $JWT = $EncodedHeader + &quot;.&quot; + $EncodedPayload\n\n            # Define RSA signature and hashing algorithm\n            $RSAPadding = [Security.Cryptography.RSASignaturePadding]::Pkcs1\n            $HashAlgorithm = [Security.Cryptography.HashAlgorithmName]::SHA256\n\n            # Sign the JWT\n            $rsaCert = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($Certificate)\n            $Signature = [Convert]::ToBase64String(\n                $rsaCert.SignData([System.Text.Encoding]::UTF8.GetBytes($JWT), $HashAlgorithm, $RSAPadding)\n            ) -replace &#039;\\+&#039;, &#039;-&#039; -replace &#039;\/&#039;, &#039;_&#039; -replace &#039;=&#039;\n\n            # Add Signature to JWT\n            $JWT = $JWT + &quot;.&quot; + $Signature\n\n            $Body = @{\n                client_id             = $clientId\n                client_assertion      = $JWT\n                client_assertion_type = &quot;urn:ietf:params:oauth:client-assertion-type:jwt-bearer&quot;\n                scope                 = [string]$scopes\n                username              = $userCredentials.UserName\n                password              = $userCredentials.GetNetworkCredential().Password\n                grant_type            = &quot;password&quot;\n            }\n        }\n        &quot;ClientCredentials&quot; {\n            $Body = @{\n                client_id     = $clientId\n                client_secret = [System.Net.NetworkCredential]::new(&quot;&quot;, $clientSecret).Password\n                scope         = [string]$scopes\n                username      = $userCredentials.UserName\n                password      = $userCredentials.GetNetworkCredential().Password\n                grant_type    = &quot;password&quot;\n            }\n        }\n    }\n\n    $params = @{\n        Uri         = &quot;https:\/\/login.microsoftonline.com\/$tenantID\/oauth2\/v2.0\/token&quot;\n        Method      = &#039;POST&#039;\n        ContentType = &#039;application\/x-www-form-urlencoded&#039;\n        Body        = $Body\n        # If we use a JWT we must add an Authorization Header\n        Headers     = if ($JWT) { @{ Authorization = &quot;Bearer $JWT&quot; } }\n    }\n    $accessToken = ConvertTo-SecureString (Invoke-RestMethod @params -ErrorAction Stop).access_token -AsPlainText -Force\n\n    # Use our Access token to Connect to Microsoft Graph\n    Connect-MgGraph -AccessToken $accessToken -NoWelcome\n\n    # Clear Senstive Values\n    $sensitiveVars = @(&quot;userCredentials&quot;,&quot;accessToken&quot;,&quot;body&quot;,&quot;params&quot;,&quot;jwt&quot;,&quot;signature&quot;)\n    Remove-Variable $sensitiveVars\n    [gc]::collect()\n}<\/code><\/pre>\n","protected":false},"excerpt":{"rendered":"<p>In my work on the practical implementation of Password Rotation without Privileged Authentication Administrator, I stumbled upon a somewhat extensive challenge. When trying to use PowerShell to sign in to the Graph API using username + password, I couldn&#8217;t find a combination in the PowerShell SDK. The only method would be ClientID + Secret &#8211;&#8230; &raquo; <a class=\"read-more-link\" href=\"https:\/\/sparrow365.de\/index.php\/en\/2024\/01\/28\/connect-mggraph-with-username-and-password\/\">weiterlesen<\/a><\/p>\n","protected":false},"author":2,"featured_media":522,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[76,78],"tags":[96,98,100,218,220,222,122,103,105,224,226],"class_list":["post-524","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-me-id-en","category-powershell-en","tag-graph-en","tag-graph-api-en","tag-microsoft-graph-en","tag-non-interactive-en","tag-oauth-en","tag-oidc-en","tag-passwords","tag-powershell-en","tag-powershell-sdk-en","tag-resource-owner-password-credentials-en","tag-ropc-en"],"_links":{"self":[{"href":"https:\/\/sparrow365.de\/index.php\/wp-json\/wp\/v2\/posts\/524","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/sparrow365.de\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/sparrow365.de\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/sparrow365.de\/index.php\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/sparrow365.de\/index.php\/wp-json\/wp\/v2\/comments?post=524"}],"version-history":[{"count":2,"href":"https:\/\/sparrow365.de\/index.php\/wp-json\/wp\/v2\/posts\/524\/revisions"}],"predecessor-version":[{"id":538,"href":"https:\/\/sparrow365.de\/index.php\/wp-json\/wp\/v2\/posts\/524\/revisions\/538"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/sparrow365.de\/index.php\/wp-json\/wp\/v2\/media\/522"}],"wp:attachment":[{"href":"https:\/\/sparrow365.de\/index.php\/wp-json\/wp\/v2\/media?parent=524"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/sparrow365.de\/index.php\/wp-json\/wp\/v2\/categories?post=524"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/sparrow365.de\/index.php\/wp-json\/wp\/v2\/tags?post=524"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}