1- # Replace with actual values
2- # dsregcmd /status does the job
3- #
4- # Run as so: & "C:\Users\mcontestabile\foobar\get_az_token.ps1"
5- # if running script from another folder.
6- #
7- # If any of the calls using the Resource Owner Password Credentials (ROPC) flow succeed and return an access token, that strongly suggests that:
8- #
9- # MFA is not enforced for that user or app.
10- # The app registration is configured to allow public clients.
11- # The user account is not blocked by Conditional Access policies that would prevent password-based login.
12- #
13- $tenantId = " ???"
14- $username = " ???"
15- $password = " ???"
16- $VerbosePreference = " Continue" # Enable verbose output for debugging
17- $ErrorActionPreference = " Continue" # prevents non-fatal errors from halting the script.
18-
19- # Define Token Endpoint
20- $tokenUrl = " https://login.microsoftonline.com/$tenantId /oauth2/v2.0/token"
21-
22- # Get all client IDs using Azure CLI
23- $clientIds = az ad app list -- all -- query " [].appId" -- output tsv
24-
25- # Convert output into an array
26- $clientIdList = $clientIds -split " `n "
27-
28-
29- # Loop through each Client ID and get the access token
30- foreach ($clientId in $clientIdList ) {
31- Write-Output " Fetching token for Client ID: $clientId "
32-
33- # Create the body for the authentication request
34- $body = @ {
35- client_id = $clientId
36- scope = " https://management.azure.com/.default"
37- grant_type = " password"
38- username = $username
39- password = $password
40- }
41-
42- # Send request to obtain access token
43- try {
44- Write-Host " Sending request to: $tokenUrl " - ForegroundColor Cyan
45- Write-Host " Request Body: $ ( $body | Out-String ) " - ForegroundColor Magenta
46- $response = Invoke-RestMethod - Method Post - Uri $tokenUrl - Body $body - ContentType " application/x-www-form-urlencoded"
47- Write-Host " Raw API Response:" - ForegroundColor Blue
48- Write-Host ($response | ConvertTo-Json - Depth 3 ) - ForegroundColor Blue
49- $accessToken = $response.access_token
50- Write-Host (" Access Token for Client ID: " + $accessToken ) - ForegroundColor Red
51- } catch {
52- Write-Host " Failed to get token for Client ID: $clientId " - ForegroundColor White
53- Write-Host " Error Details: $ ( $_.Exception.Message ) " - ForegroundColor White
54- # Write-Host "Full Exception: $($_ | ConvertTo-Json -Depth 100)" -ForegroundColor White
55- # Write-Output "Full Exception Details:"
56- # $_.Exception | Format-List -Property *
57- continue # Ensures the loop moves to the next iteration
58- }
59-
60- }
1+ # requires -Version 7.0
2+ <#
3+ . SYNOPSIS
4+ Performs a controlled Microsoft Entra ROPC authentication test against one
5+ explicitly authorized public-client application.
6+
7+ . DESCRIPTION
8+ Sends one Resource Owner Password Credentials (ROPC) token request for a
9+ designated nonprivileged test account, client application, and OAuth scope.
10+ The script does not enumerate tenant applications, does not store a password
11+ in the file, and does not write access or refresh tokens to disk.
12+
13+ ROPC is deprecated and incompatible with interactive MFA. Use this script
14+ only in an authorized test tenant or approved assessment scope.
15+
16+ . PARAMETER TenantId
17+ Microsoft Entra tenant GUID or verified tenant domain, for example:
18+ 00000000-0000-0000-0000-000000000000 or contoso.onmicrosoft.com
19+
20+ . PARAMETER Username
21+ User principal name of a dedicated, nonprivileged test account.
22+
23+ . PARAMETER ClientId
24+ Application (client) ID of one explicitly authorized public-client app.
25+
26+ . PARAMETER Scope
27+ OAuth scope to request. The default requests Microsoft Graph delegated
28+ permissions already granted to the client by using .default.
29+
30+ . PARAMETER OutputPath
31+ Optional CSV path for the sanitized test result. Tokens and passwords are
32+ never included in the report.
33+
34+ . PARAMETER AcknowledgeAuthorizedTesting
35+ Required safety switch confirming that the test is authorized.
36+
37+ . EXAMPLE
38+ ./Test-EntraRopcControl.ps1 `
39+ -TenantId '00000000-0000-0000-0000-000000000000' `
40+ -Username 'ropc-test@contoso.onmicrosoft.com' `
41+ -ClientId '11111111-1111-1111-1111-111111111111' `
42+ -AcknowledgeAuthorizedTesting
43+
44+ . NOTES
45+ Recommended test design:
46+ - Use a dedicated, nonprivileged test account.
47+ - Use one application that is owned and approved for testing.
48+ - Run during an approved assessment window.
49+ - Review the corresponding Microsoft Entra sign-in log.
50+ - Rotate the test password after the assessment.
51+ #>
52+
53+ [CmdletBinding ()]
54+ param (
55+ [Parameter (Mandatory )]
56+ [ValidateNotNullOrEmpty ()]
57+ [string ]$TenantId ,
58+
59+ [Parameter (Mandatory )]
60+ [ValidatePattern (' ^[^@\s]+@[^@\s]+\.[^@\s]+$' )]
61+ [string ]$Username ,
62+
63+ [Parameter (Mandatory )]
64+ [ValidatePattern (' ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' )]
65+ [string ]$ClientId ,
66+
67+ [Parameter ()]
68+ [ValidateNotNullOrEmpty ()]
69+ [string ]$Scope = ' https://graph.microsoft.com/.default' ,
70+
71+ [Parameter ()]
72+ [string ]$OutputPath = (Join-Path $PWD ' EntraRopcControlTest.csv' ),
73+
74+ [Parameter (Mandatory )]
75+ [switch ]$AcknowledgeAuthorizedTesting
76+ )
77+
78+ Set-StrictMode - Version Latest
79+ $ErrorActionPreference = ' Stop'
80+
81+ if (-not $AcknowledgeAuthorizedTesting ) {
82+ throw ' Authorized-testing acknowledgement is required.'
83+ }
84+
85+ Write-Warning ' ROPC is deprecated and directly handles a password. Use only an approved nonprivileged test account and application.'
86+
87+ $securePassword = Read-Host - Prompt " Enter the password for authorized test account $Username " - AsSecureString
88+ $passwordPointer = [Runtime.InteropServices.Marshal ]::SecureStringToBSTR($securePassword )
89+ $plainPassword = $null
90+
91+ $tokenEndpoint = " https://login.microsoftonline.com/$TenantId /oauth2/v2.0/token"
92+ $timestamp = Get-Date
93+ $correlationId = [guid ]::NewGuid().Guid
94+
95+ try {
96+ $plainPassword = [Runtime.InteropServices.Marshal ]::PtrToStringBSTR($passwordPointer )
97+
98+ $body = @ {
99+ client_id = $ClientId
100+ grant_type = ' password'
101+ username = $Username
102+ password = $plainPassword
103+ scope = $Scope
104+ }
105+
106+ $headers = @ {
107+ ' client-request-id' = $correlationId
108+ ' return-client-request-id' = ' true'
109+ }
110+
111+ try {
112+ $response = Invoke-RestMethod `
113+ - Method Post `
114+ - Uri $tokenEndpoint `
115+ - ContentType ' application/x-www-form-urlencoded' `
116+ - Headers $headers `
117+ - Body $body `
118+ - ErrorAction Stop
119+
120+ $result = [pscustomobject ]@ {
121+ TimestampUtc = $timestamp.ToUniversalTime ().ToString(' o' )
122+ Tenant = $TenantId
123+ Username = $Username
124+ ClientId = $ClientId
125+ Scope = $Scope
126+ Outcome = ' TokenIssued'
127+ HttpStatus = 200
128+ EntraError = $null
129+ EntraErrorCodes = $null
130+ Suberror = $null
131+ TraceId = $null
132+ CorrelationId = $correlationId
133+ MfaInterpretation = ' The tested request completed without an interactive MFA challenge. Review Conditional Access and sign-in logs before drawing broader conclusions.'
134+ TokenReturned = -not [string ]::IsNullOrWhiteSpace([string ]$response.access_token )
135+ }
136+
137+ Write-Host ' TOKEN ISSUED: The authorized ROPC test returned an access token.' - ForegroundColor Red
138+ Write-Host ' Review the corresponding Entra sign-in log and applicable Conditional Access policies.' - ForegroundColor Yellow
139+ }
140+ catch {
141+ $statusCode = $null
142+ $responseText = $null
143+
144+ if ($null -ne $_.Exception.Response ) {
145+ try { $statusCode = [int ]$_.Exception.Response.StatusCode } catch { }
146+ }
147+
148+ if ($null -ne $_.ErrorDetails -and -not [string ]::IsNullOrWhiteSpace($_.ErrorDetails.Message )) {
149+ $responseText = $_.ErrorDetails.Message
150+ }
151+
152+ $errorPayload = $null
153+ if (-not [string ]::IsNullOrWhiteSpace($responseText )) {
154+ try { $errorPayload = $responseText | ConvertFrom-Json - ErrorAction Stop } catch { }
155+ }
156+
157+ $entraError = if ($null -ne $errorPayload ) { [string ]$errorPayload.error } else { ' RequestFailed' }
158+ $description = if ($null -ne $errorPayload ) { [string ]$errorPayload.error_description } else { $_.Exception.Message }
159+ $errorCodes = if ($null -ne $errorPayload -and $null -ne $errorPayload.error_codes ) {
160+ @ ($errorPayload.error_codes ) -join ' ;'
161+ }
162+ else {
163+ $null
164+ }
165+ $suberrorProperty = if ($null -ne $errorPayload ) {
166+ $errorPayload.PSObject.Properties [' suberror' ]
167+ }
168+ else {
169+ $null
170+ }
171+
172+ $suberror = if ($null -ne $suberrorProperty ) {
173+ [string ]$suberrorProperty.Value
174+ }
175+ else {
176+ $null
177+ }
178+ $traceId = if ($null -ne $errorPayload -and $null -ne $errorPayload.trace_id ) { [string ]$errorPayload.trace_id } else { $null }
179+ $serverCorrelationId = if ($null -ne $errorPayload -and $null -ne $errorPayload.correlation_id ) {
180+ [string ]$errorPayload.correlation_id
181+ }
182+ else {
183+ $correlationId
184+ }
185+
186+ $mfaInterpretation = if ($errorCodes -split ' ;' -contains ' 50076' ) {
187+ ' Expected secure outcome for this test: MFA was required and ROPC could not satisfy the challenge.'
188+ }
189+ elseif ($errorCodes -split ' ;' -contains ' 50079' ) {
190+ ' MFA enrollment or stronger authentication was required; ROPC could not complete the interactive requirement.'
191+ }
192+ else {
193+ ' The token request failed. Review the Entra error code and sign-in log; HTTP status alone is not sufficient for classification.'
194+ }
195+
196+ $result = [pscustomobject ]@ {
197+ TimestampUtc = $timestamp.ToUniversalTime ().ToString(' o' )
198+ Tenant = $TenantId
199+ Username = $Username
200+ ClientId = $ClientId
201+ Scope = $Scope
202+ Outcome = ' TokenDenied'
203+ HttpStatus = $statusCode
204+ EntraError = $entraError
205+ EntraErrorCodes = $errorCodes
206+ Suberror = $suberror
207+ TraceId = $traceId
208+ CorrelationId = $serverCorrelationId
209+ MfaInterpretation = $mfaInterpretation
210+ TokenReturned = $false
211+ }
212+
213+ if ($errorCodes -split ' ;' -contains ' 50076' ) {
214+ Write-Host ' EXPECTED: MFA was required and the ROPC request was blocked (AADSTS50076).' - ForegroundColor Green
215+ }
216+ else {
217+ Write-Host " TOKEN DENIED: $entraError | Error code(s): $errorCodes " - ForegroundColor Yellow
218+ }
219+ Write-Host $description - ForegroundColor DarkGray
220+ }
221+
222+ $outputDirectory = Split-Path - Parent $OutputPath
223+ if (-not [string ]::IsNullOrWhiteSpace($outputDirectory )) {
224+ New-Item - Path $outputDirectory - ItemType Directory - Force | Out-Null
225+ }
226+
227+ $result | Export-Csv - Path $OutputPath - NoTypeInformation - Encoding utf8
228+ Write-Host " Sanitized result saved to: $OutputPath " - ForegroundColor Cyan
229+ }
230+ finally {
231+ if ($null -ne $plainPassword ) {
232+ $plainPassword = $null
233+ }
234+
235+ if ($passwordPointer -ne [IntPtr ]::Zero) {
236+ [Runtime.InteropServices.Marshal ]::ZeroFreeBSTR($passwordPointer )
237+ }
238+
239+ Remove-Variable securePassword - ErrorAction SilentlyContinue
240+ Remove-Variable body - ErrorAction SilentlyContinue
241+ [GC ]::Collect()
242+ }
0 commit comments