Sitemap
The new control plane

“Identity is the new control plane”. Articles around Microsoft Entra ID, Entra External ID and Azure AD B2C. Click the “Archive” link at the bottom for more posts.

Using a .NET .http file to do an authentication code flow in Entra External ID (EEID)

--

Image of normal auth request
AI generated

All the .http posts are here.

I have done a few samples that illustrate native auth in EEID, but I was asked if .http files can be used in a normal browser-based flow.

The problem is that there doesn't appear to be any way to get an .http file to “pop up” a browser-based login screen.

So you have to do this outside the .http file, e.g., by typing the request into the browser or using PowerShell.

Get Rory Braybrook’s stories in your inbox

Join Medium for free to get updates from this writer.

Here is a vibe-coded PS snippet:

<#
.SYNOPSIS
Signs in to Microsoft Entra External ID (CIAM) using the standard OIDC
authorization code flow and prints the resulting access token so you can
copy/paste it (e.g. into the @accessToken variable in EEIDLogin.http).

.DESCRIPTION
This mirrors the normal "Authenticate" browser experience:
1. Opens the default browser to the tenant's /authorize endpoint.
2. You sign in with username + password (and MFA, if required) on the
real Entra External ID hosted sign-in page.
3. EEID redirects the browser to a local redirect URI
(http://localhost:8400/callback) with an authorization "code".
4. This script runs a small local HTTP listener to catch that redirect,
then exchanges the code for an access token at the /token endpoint.
5. Prints the access token (and refresh token, if issued) to the console
and copies the access token to your clipboard.

.NOTES
Update $tenantSubdomain, $tenantId, $clientId, and $clientSecret below for
your own tenant/app registration. The app registration's redirect URI must
include http://localhost:8400/callback (added as a "Web" or "Public client"
redirect URI, matching how the app is configured).
#>

$tenantSubdomain = "eeid-domain"
$tenantId = "eeid-domain.onmicrosoft.com"
$clientId = "d6...90"
$clientSecret = "95...WB"
$redirectUri = "http://localhost:8400/callback"
$scope = "openid profile offline_access User.Read"

$authorizeEndpoint = "https://$tenantSubdomain.ciamlogin.com/$tenantId/oauth2/
v2.0/authorize"
$tokenEndpoint = "https://$tenantSubdomain.ciamlogin.com/$tenantId/oauth2/
v2.0/token"

# Step 1: Build the authorize URL and start a local listener for the redirect
$state = [guid]::NewGuid().ToString("N")

$authorizeUrl = "$authorizeEndpoint`?" + (
@(
"client_id=$clientId"
"response_type=code"
"redirect_uri=$([uri]::EscapeDataString($redirectUri))"
"scope=$([uri]::EscapeDataString($scope))"
"response_mode=query"
"state=$state"
) -join "&"
)

$listener = New-Object System.Net.HttpListener
$listener.Prefixes.Add("$redirectUri/")
$listener.Start()

Write-Host ""
Write-Host "=== Opening browser to sign in ===" -ForegroundColor Cyan
Write-Host "If a browser window doesn't open automatically, copy this URL
into one:"
Write-Host $authorizeUrl
Write-Host ""

Start-Process $authorizeUrl

# Step 2: Wait for the redirect carrying the authorization code
$context = $listener.GetContext()
$request = $context.Request
$response = $context.Response

$queryParams = [System.Web.HttpUtility]::ParseQueryString($request.Url.Query)
$code = $queryParams["code"]
$returnedState = $queryParams["state"]
$errorParam = $queryParams["error"]

$responseHtml = if ($errorParam) {
"<html><body><h2>Sign-in failed: $errorParam</h2><p>$($queryParams
['error_description'])</p><p>You can close this tab.</p></body></html>"
} else {
"<html><body><h2>Sign-in complete.</h2><p>You can close this tab and return
to PowerShell.</p></body></html>"
}

$buffer = [System.Text.Encoding]::UTF8.GetBytes($responseHtml)
$response.ContentLength64 = $buffer.Length
$response.OutputStream.Write($buffer, 0, $buffer.Length)
$response.OutputStream.Close()
$listener.Stop()

if ($errorParam) {
Write-Error "Sign-in failed: $errorParam -
$($queryParams['error_description'])"
exit 1
}

if ($returnedState -ne $state) {
Write-Error "State mismatch - possible CSRF, aborting."
exit 1
}

if (-not $code) {
Write-Error "No authorization code was returned."
exit 1
}

# Step 3: Exchange the authorization code for an access token
$tokenBody = @{
grant_type = "authorization_code"
client_id = $clientId
client_secret = $clientSecret
code = $code
redirect_uri = $redirectUri
scope = $scope
}

$tokenResponse = Invoke-RestMethod -Method Post -Uri $tokenEndpoint -Body
$tokenBody -ContentType "application/x-www-form-urlencoded"

Write-Host ""
Write-Host "=== Access token (copy everything between the quotes) ==="
-ForegroundColor Green
Write-Host $tokenResponse.access_token
Write-Host ""

if ($tokenResponse.refresh_token) {
Write-Host "=== Refresh token ===" -ForegroundColor Yellow
Write-Host $tokenResponse.refresh_token
Write-Host ""
}

# Also copy the access token to the clipboard for convenience, if available
try {
$tokenResponse.access_token | Set-Clipboard
Write-Host "(Access token has also been copied to your clipboard.)"
-ForegroundColor DarkGray
}
catch {
# Set-Clipboard isn't available in all PowerShell hosts; ignore if it fails.
}

Running this PS script gives:


=== Opening browser to sign in ===
If a browser window doesn't open automatically, copy this URL into one:
https://eeid-domain.ciamlogin.com/eeid-domain.onmicrosoft.com/oauth2/v2.0
/authorize?
client_id=d6...90
&response_type=code
&redirect_uri=http%3A%2F%2Flocalhost%3A8400%2Fcallback
&scope=openid%20profile%20offline_access%20User.Read
&response_mode=query
&state=64770a6dc8ff42d386232cf1053059b7

=== Access token (copy everything between the quotes) ===
ey...xw

=== Refresh token ===
1.A...vT0

(Access token has also been copied to your clipboard.)

The .http file then looks like:

### Paste the access token you got from Get-EntraToken.ps1
@accessToken = ey...xw

GET https://graph.microsoft.com/v1.0/me
Authorization: Bearer {{accessToken}}
Accept: application/json

This results in:

{
"@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users/$entity",
"businessPhones": [],
"displayName": "Test User",
"givenName": null,
"jobTitle": null,
"mail": null,
"mobilePhone": null,
"officeLocation": null,
"preferredLanguage": null,
"surname": null,
"userPrincipalName": "d5...58@eeid-domain.onmicrosoft.com",
"id": "d5..58"
}

Basically, this just doesn’t seem worth it.

It would be easier to just add the /me call to the PS script!!! 😃

All good!

--

--

Rory Braybrook
Rory Braybrook

Written by Rory Braybrook

NZ Microsoft Identity dude and MVP. Entra ID/Entra External ID/Azure AD B2C/VC. StackOverflow: https://bit.ly/2XU4yvJ Presentations: http://bit.ly/334ZPt5

The new control plane
The new control plane

Published in The new control plane

“Identity is the new control plane”. Articles around Microsoft Entra ID, Entra External ID and Azure AD B2C. Click the “Archive” link at the bottom for more posts.