jacobh.io
Azure - Token Usage

Azure - Token Usage

16 min read

Platform: azure

Updated

Purpose: A practical reference for using tokens once you have them. How to load them into Azure tools, how to use them with pentesting frameworks, and how to call the underlying APIs directly. Concepts and acquisition are covered elsewhere.

For token concepts and the AT/RT/PRT hierarchy, see Token Types & Concepts. For token collection, see Token Collection. For converting between token types and audiences, see Token Exchange.

Table of Contents

Authenticating with Access Tokens


Goal: Load an access token into Azure tools and pentesting frameworks to operate as the token’s identity.

Before using any token: verify its audience, scopes, and expiry. echo "<TOKEN>" | cut -d '.' -f2 | base64 -d | jq - check aud, exp, and scp/roles. A Graph token silently fails against ARM and vice versa. See Token Exchange - Decode & Inspect.

Azure PowerShell

The standard ARM management module. Accepts access tokens directly.

$accesstoken = "<ARM-ACCESS-TOKEN>"
$accountid   = "user@domain.com"   # required field, not validated
Connect-AzAccount -AccessToken $accesstoken -AccountId $accountid

# verify
Get-AzContext
Get-AzResource

To use a Graph token alongside an ARM token in the same session:

Connect-AzAccount -AccessToken $armToken -GraphAccessToken $graphToken -AccountId $accountid

Microsoft Graph PowerShell

For Graph operations. Requires the token as a SecureString.

$secureToken = "<GRAPH-ACCESS-TOKEN>" | ConvertTo-SecureString -AsPlainText -Force
Connect-MgGraph -AccessToken $secureToken

# verify
Get-MgContext
Get-MgOrganization

Azure CLI

Azure CLI does not natively support loading an access token as a session. However, az rest accepts a manual Authorization header - making it a usable injection vector for a stolen token.

export GRAPH_TOKEN="<graph-access-token>"
export ARM_TOKEN="<arm-access-token>"

# inject token into az rest call
az rest --method GET \
  --uri "https://graph.microsoft.com/v1.0/me" \
  --headers "Authorization=Bearer $GRAPH_TOKEN"

az rest --method GET \
  --uri "https://management.azure.com/subscriptions?api-version=2022-12-01" \
  --headers "Authorization=Bearer $ARM_TOKEN"

Note: az rest without --headers uses the CLI’s own session token. With --headers, it overrides to whatever you pass - no active login required.

curl

The most reliable approach for raw token usage. Works for any API, any audience.

# arm - validate token and list subscriptions
curl -s -H "Authorization: Bearer $ARM_TOKEN" \
  "https://management.azure.com/subscriptions?api-version=2022-12-01" | jq

# graph - validate token and get current identity
curl -s -H "Authorization: Bearer $GRAPH_TOKEN" \
  "https://graph.microsoft.com/v1.0/me" | jq
  

# key vault data plane - list all Key Vaults in a subscription
curl -s -H "Authorization: Bearer $ARM_TOKEN" \
  "https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.KeyVault/vaults?api-version=2023-02-01" | jq

# key vault data plane - list vaults in a specific resource group
curl -s -H "Authorization: Bearer $ARM_TOKEN" \
  "https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/Microsoft.KeyVault/vaults?api-version=2023-02-01" | jq
  
# key vault data plane - list secrets in a vault
curl -s -H "Authorization: Bearer $VAULT_TOKEN" \
  "https://<vault-name>.vault.azure.net/secrets?api-version=7.4" | jq

PowerShell Invoke-RestMethod

For when curl isn’t available and you need raw API calls in PowerShell.

$headers = @{ Authorization = "Bearer $accessToken"; 'Content-Type' = 'application/json' }

# GET
Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/me" -Headers $headers -Method Get

# POST
$body = @{ displayName = "Test Group" } | ConvertTo-Json
Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/groups" `
  -Headers $headers -Method Post -Body $body

ROADtools / roadtx

ROADtools stores a token in .roadtools_auth after authentication. Most roadtx commands use it automatically.

# describe the current token (audience, scopes, expiry, user)
roadtx describe

# import a token from another source into .roadtools_auth
roadtx gettokens --access-token "$ACCESS_TOKEN" --refresh-token "$REFRESH_TOKEN" -c '<clientid>' -r '<resource>'

Check out Token Exchange & Token Collection for more in depth ROADTools operations.

AADInternals

Import-Module AADInternals

# use an access token directly
$accessToken = "<GRAPH-ACCESS-TOKEN>" # i think this only supports legacy aad

# operations that accept the token directly
Get-AADIntUsers -AccessToken $accessToken
Get-AADIntTenantDetails -AccessToken $accessToken

GraphSpy

GraphSpy accepts tokens through its GUI at Authentication → Tokens. Import refresh or access tokens from other tools.

pipx install graphspy
graphspy  # web UI at http://localhost:5000

Navigate to Tokens → Access Tokens → add token manually, or Tokens → Refresh Tokens to load a refresh token and have GraphSpy manage exchanges automatically.

AzureHound

AzureHound is the BloodHound data collector for Azure. It accepts a Graph tokens directly.

# graph access token
./azurehound -j "$GRAPH_TOKEN" list -o out.json

Output is a JSON file for import into BloodHound. Run BloodHound first:

./bloodhound-cli install
./bloodhound-cli start

AzurePEAS / CloudPEASS

AzurePEAS takes ARM and Graph access tokens as environment variables, then enumerates permissions across both planes automatically.

# set env variables
export AZURE_ARM_TOKEN=<arm token>
export AZURE_GRAPH_TOKEN=<graph token>

python3 AzurePEAS.py

# or with a FOCI refresh token (auto enum m365 + perms)
python3 AzurePEAS.py --tenant-id "<tenant-id>" --foci-refresh-token "<RT>"

# device code auth (mfa support, but only if device code is enabled)
python3 AzurePEAS.py --tenant "<tenant-id>"

# user / pass (no support for mfa)
python3 AzurePEAS.py --use-username-password --username <user> --password <pass>

nord-stream

nord-stream extracts secrets from Azure DevOps pipelines. Accepts a DevOps bearer token or PAT with --token.

# token can be a DevOps-scoped access token or a PAT
export PAT="<devops-token-or-pat>"

# describe what the token can do
nord-stream devops --token "$PAT" --describe-token --org <org>

# list extractable secrets
nord-stream devops --token "$PAT" --list-secrets --org <org>

# extract secrets by running modified pipeline jobs
nord-stream devops --token "$PAT" --dump --org <org> --project <project>

Get a DevOps-scoped bearer token from your current Az session:

az account get-access-token --resource '499b84ac-1321-427f-aa17-267ca6975798' \
  --query accessToken --output tsv

Authenticating with Refresh Tokens


Goal: Use a refresh token to get fresh access tokens for any resource without re-authenticating.

A refresh token is not audience-locked - you can exchange it for an access token targeting any resource the identity has access to. This is the core mechanic that makes RT theft more valuable than AT theft.

For the full mechanics, FOCI abuse, and PRT-based exchange, see Token Exchange.

TokenTacticsV2

Import-Module .\TokenTacticsV2.psm1

$RT = "<REFRESH-TOKEN>"
$domain = "domain.com"

# exchange for specific resources
Invoke-RefreshToMSGraphToken -domain $domain -RefreshToken $RT
Invoke-RefreshToAzureManagementToken -domain $domain -RefreshToken $RT
Invoke-RefreshToMSTeamsToken -domain $domain -RefreshToken $RT
Invoke-RefreshToOutlookToken -domain $domain -RefreshToken $RT

# access the resulting access token
$MSGraphToken.access_token
$AzureManagementToken.access_token

OPSEC: TokenTactics uses distinctive user agents. Pass -Device WindowsCoreMobile or -Browser Chrome to blend in.

AADInternals

Import-Module AADInternals

$RT = "<REFRESH-TOKEN>"

Get-AADIntAccessTokenForMSGraph -RefreshToken $RT
Get-AADIntAccessTokenForAzureCoreManagement -RefreshToken $RT

ROADtools

# exchange to a specific resource using the stored .roadtools_auth RT
roadtx refreshtokento -r https://management.azure.com/
roadtx refreshtokento -r msgraph
roadtx refreshtokento -r https://vault.azure.net/

# pass the RT explicitly and output to stdout
roadtx refreshtokento \
  --refresh-token "$RT" \
  -c d3590ed6-52b3-4102-aeff-aad2292ab01c \  # client ID (Office app)
  -r https://management.azure.com \
  --tokens-stdout

curl (manual)

export TENANT_ID="<tenant-id>"
export RT="<refresh-token>"
export CLIENT_ID="<client-id>"   # the app the RT was issued to (or any FOCI app)

# v2.0 endpoint
curl -s -X POST \
  "https://login.microsoftonline.com/$TENANT_ID/oauth2/v2.0/token" \
  -d "client_id=$CLIENT_ID" \
  -d "grant_type=refresh_token" \
  --data-urlencode "refresh_token=$RT" \
  -d "scope=https://management.azure.com/.default" | jq

# v1.0 endpoint (legacy, uses 'resource' instead of 'scope')
curl -s -X POST \
  "https://login.microsoftonline.com/common/oauth2/token" \
  -d "client_id=$CLIENT_ID" \
  -d "grant_type=refresh_token" \
  --data-urlencode "refresh_token=$RT" \
  -d "resource=https://graph.microsoft.com" | jq

Refresh Token Lifecycle

PropertyValue
Expiry90 days of inactivity (rolling)
Reset on useevery successful exchange resets the 90-day clock
Explicit revocationaz ad user revoke-sign-in-sessions or CA policy enforcement
FormatOpaque blob - not a JWT, cannot be decoded
FOCI RTCan be exchanged using any FOCI client ID, not just the one that issued it

The Core APIs


Goal: Understand the major Microsoft cloud APIs, what each one controls, and how access is enforced at the token level.

All Microsoft cloud APIs use Bearer token authentication. The aud claim in your access token must match the API’s resource URI - tokens are audience-locked and will be rejected by any API they weren’t issued for.

Microsoft Graph - graph.microsoft.com

Graph is the unified API for Entra ID and Microsoft 365. It replaced the legacy Azure AD Graph (graph.windows.net, retired June 2026) and is the primary API for identity and collaboration data.

What it controls: users, groups, applications, service principals, Entra roles, devices, Conditional Access policies, MFA methods, mail, calendar, Teams, SharePoint, OneDrive, Intune.

Token audience: https://graph.microsoft.com/

Access control model:

Graph enforces two distinct permission types, visible as different claims in your token:

TypeToken ClaimMeaningScope
DelegatedscpApp acts on behalf of a signed-in userBounded by both the app’s grant AND the user’s own permissions
ApplicationrolesApp authenticates as itself, no userBounded only by admin-consented app permissions - can access all tenant data

Application permissions are significantly broader - Mail.Read (delegated) reads one user’s mail, Mail.Read (application) reads every mailbox in the tenant.

Versioning: Graph has /v1.0 (stable) and /beta (preview features). Beta endpoints are not guaranteed stable and may change. Many useful security-relevant endpoints (PIM, CA details, auth method details) are beta-only.

Pagination: Responses over the page limit return a @odata.nextLink URL. Follow it to get the next page.

# check for next page
curl -s -H "Authorization: Bearer $GRAPH_TOKEN" \
  "https://graph.microsoft.com/v1.0/users" | jq '{"nextLink": ."@odata.nextLink", "count": (.value | length)}'

OData filtering: Graph supports $filter, $select, $top, $search, and $count query parameters. Some require the ConsistencyLevel: eventual header.

Azure Resource Manager - management.azure.com

ARM is the management layer for all Azure infrastructure. Every resource created in Azure - VMs, storage accounts, Key Vaults, databases, networking - is managed through ARM.

What it controls: subscriptions, resource groups, all Azure services and their configuration, Azure RBAC assignments, Management Groups, deployment templates.

Token audience: https://management.azure.com/

Access control model: Azure RBAC - roles assigned to principals at a scope. The scope hierarchy is:

Management Group → Subscription → Resource Group → Resource

Permissions are inherited downward. A role assigned at subscription scope applies to all resource groups and resources within it.

Key distinction - management plane vs data plane: ARM controls the management of resources (create, configure, delete). The data inside a resource - secrets in Key Vault, blobs in Storage, rows in a database - has its own separate API and its own access control. Having ARM access to a Key Vault resource does not mean you can read its secrets.

API versioning: Every ARM request requires an api-version query parameter. Each resource type has its own supported versions. Using an unsupported version returns a clear error listing supported versions.

Azure Key Vault - vault.azure.net

Key Vault has two separate planes with two separate APIs and two separate access control systems.

PlaneAPIToken audienceControls
Managementmanagement.azure.comhttps://management.azure.com/Create/delete vaults, configure access policies and RBAC, firewall rules
Data<vault-name>.vault.azure.nethttps://vault.azure.net/Read/write secrets, keys, certificates

Access control on the data plane can be configured two ways (mutually exclusive per vault):

  • Vault Access Policies (legacy): per-principal permission grants (Get, List, Set, Delete). Checked against the token’s oid claim.
  • Azure RBAC (modern): standard ARM RBAC roles like Key Vault Secrets User. Checked via ARM RBAC.

You need an ARM token to discover which vaults exist, then a Vault token to read their contents. Two distinct tokens, two distinct API calls.

Azure Storage - storage.azure.com

Storage supports two completely different authentication models:

Bearer token authentication (aud: https://storage.azure.com/): Access is controlled by Azure RBAC roles like Storage Blob Data Reader. Used for blob, queue, and table operations.

curl -s -H "Authorization: Bearer $STORAGE_TOKEN" \
  -H "x-ms-version: 2020-04-08" \
  "https://<account>.blob.core.windows.net/<container>?restype=container&comp=list"

Shared Access Signature (SAS): Pre-signed URL parameters - no Authorization header needed. The signature is an HMAC-SHA256 computed from the storage account key. SAS tokens grant scoped, time-limited access and are frequently found hardcoded in config files, env vars, and source code.

# SAS token - auth is in the URL params, no header
curl "https://<account>.blob.core.windows.net/<container>/<blob>?sv=2022-11-02&sp=r&se=...&sig=..."
ParamMeaning
spPermissions: r=read, w=write, d=delete, l=list
seExpiry (UTC)
sigHMAC-SHA256 signature - tamper-proof without the storage key
srtScope: s=service, c=container, o=object

Azure DevOps - dev.azure.com

DevOps accepts two credential types:

Entra-issued bearer token: obtained by requesting a token for resource 499b84ac-1321-427f-aa17-267ca6975798 (the DevOps resource ID).

az account get-access-token --resource '499b84ac-1321-427f-aa17-267ca6975798' \
  --query accessToken --output tsv

Quick enum for DevOps

Get your user id

curl -s -H "Authorization: Bearer $ADO_TOKEN" \
  "https://app.vssps.visualstudio.com/_apis/profile/profiles/me?api-version=7.1" | jq '{id, displayName, emailAddress}'

List orgs you belong to

# Use the id from above
curl -s -H "Authorization: Bearer $ADO_TOKEN" \
  "https://app.vssps.visualstudio.com/_apis/accounts?memberId={userId}&api-version=7.1" | jq '.value[].accountName'

3. List Projects in an Org

bash

curl -s -H "Authorization: Bearer $ADO_TOKEN" \
  "https://dev.azure.com/{org}/_apis/projects?api-version=7.1" | jq '.value[] | {name, id, state}'

List Repos in a Project

curl -s -H "Authorization: Bearer $ADO_TOKEN" \
  "https://dev.azure.com/{org}/{project}/_apis/git/repositories?api-version=7.1" | jq '.value[] | {name, remoteUrl, id}'

List Pipelines

curl -s -H "Authorization: Bearer $ADO_TOKEN" \
  "https://dev.azure.com/{org}/{project}/_apis/pipelines?api-version=7.1" | jq '.value[] | {name, id}'

List service connections

curl -s -H "Authorization: Bearer $ADO_TOKEN" \
  "https://dev.azure.com/{org}/{project}/_apis/serviceendpoint/endpoints?api-version=7.1" | jq '.value[] | {name, type, url: .url}'

List Variable Groups (may contain secrets)

curl -s -H "Authorization: Bearer $ADO_TOKEN" \
  "https://dev.azure.com/{org}/{project}/_apis/distributedtask/variablegroups?api-version=7.1" | jq '.value[] | {name, type, variables: .variables}'

List Secure Files

curl -s -H "Authorization: Bearer $ADO_TOKEN" \
  "https://dev.azure.com/{org}/{project}/_apis/distributedtask/securefiles?api-version=7.1" | jq '.value[] | {name, id}'

Personal Access Token (PAT): scoped token generated in the DevOps UI. Passed as a Basic auth password with any username.

# PAT as Basic auth (username can be anything)
curl -u ":<PAT>" "https://dev.azure.com/<org>/_apis/projects?api-version=7.1"

For git clone, both work as a password in the URL:

git clone https://<TOKEN-OR-PAT>@dev.azure.com/<org>/<project>/_git/<repo>

Calling APIs Directly


Goal: A quick reference for common API patterns using curl and raw HTTP - applicable regardless of which tool you’re using.

The same HTTP pattern works for every Microsoft cloud API:

GET/POST/PATCH/DELETE <endpoint>
Authorization: Bearer <access-token>
Content-Type: application/json   (for POST/PATCH)
api-version=<version>            (ARM requires this as query param)

ARM Common Calls

ARM="https://management.azure.com"
SUB="<subscription-id>"
RG="<resource-group>"
API="api-version=2022-12-01"

# list subscriptions
curl -s -H "Authorization: Bearer $ARM_TOKEN" "$ARM/subscriptions?$API" | jq '.value[] | {id: .subscriptionId, name: .displayName}'

# list resource groups
curl -s -H "Authorization: Bearer $ARM_TOKEN" "$ARM/subscriptions/$SUB/resourceGroups?$API" | jq '.value[].name'

# list all resources in a subscription
curl -s -H "Authorization: Bearer $ARM_TOKEN" "$ARM/subscriptions/$SUB/resources?$API" | jq '.value[] | {name, type, location}'

# list role assignments (requires Microsoft.Authorization/roleAssignments/read)
curl -s -H "Authorization: Bearer $ARM_TOKEN" \
  "$ARM/subscriptions/$SUB/providers/Microsoft.Authorization/roleAssignments?api-version=2022-04-01" \
  | jq '.value[] | {principalId: .properties.principalId, role: .properties.roleDefinitionId, scope: .properties.scope}'

Graph Common Calls

GRAPH="https://graph.microsoft.com/v1.0"

# current identity
curl -s -H "Authorization: Bearer $GRAPH_TOKEN" "$GRAPH/me" | jq '{id, displayName, userPrincipalName}'

# list users
curl -s -H "Authorization: Bearer $GRAPH_TOKEN" \
  "$GRAPH/users?\$select=id,displayName,userPrincipalName,accountEnabled&\$top=100" \
  | jq -r '.value[] | [.userPrincipalName, (.accountEnabled | tostring)] | @tsv'

# list groups
curl -s -H "Authorization: Bearer $GRAPH_TOKEN" "$GRAPH/groups?\$select=id,displayName" | jq '.value[] | {id, displayName}'

# list Entra directory roles (active)
curl -s -H "Authorization: Bearer $GRAPH_TOKEN" "$GRAPH/directoryRoles" | jq '.value[] | {displayName, id}'

# list app registrations
curl -s -H "Authorization: Bearer $GRAPH_TOKEN" "$GRAPH/applications?\$select=displayName,appId,id" | jq '.value[] | {displayName, appId, id}'

Key Vault Data Plane

VAULT="https://<vault-name>.vault.azure.net"

# list secrets (names only, not values)
curl -s -H "Authorization: Bearer $VAULT_TOKEN" "$VAULT/secrets?api-version=7.4" | jq '.value[].id'

# read a secret value
curl -s -H "Authorization: Bearer $VAULT_TOKEN" "$VAULT/secrets/<name>?api-version=7.4" | jq -r .value

# list certificates
curl -s -H "Authorization: Bearer $VAULT_TOKEN" "$VAULT/certificates?api-version=7.4" | jq '.value[].id'

Handling Pagination

Graph and ARM both paginate large result sets. Graph returns @odata.nextLink, ARM returns nextLink.

# follow Graph pagination
url="https://graph.microsoft.com/v1.0/users?\$top=999"
while [ -n "$url" ]; do
  response=$(curl -s -H "Authorization: Bearer $GRAPH_TOKEN" "$url")
  echo "$response" | jq '.value[]'
  url=$(echo "$response" | jq -r '."@odata.nextLink" // empty')
done

Making POST/PATCH Calls

# create a group (Graph POST)
curl -s -X POST \
  -H "Authorization: Bearer $GRAPH_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"displayName": "Test Group", "mailEnabled": false, "securityEnabled": true, "mailNickname": "testgroup"}' \
  "https://graph.microsoft.com/v1.0/groups"

# update a user attribute (Graph PATCH)
curl -s -X PATCH \
  -H "Authorization: Bearer $GRAPH_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"jobTitle": "Engineer"}' \
  "https://graph.microsoft.com/v1.0/users/<user-id>"

Token Exchange (Quick Reference)


Goal: Get the right token for the API you’re targeting.

Full coverage is in Token Exchange. Quick map of what token you need for what:

Target APIToken audienceGet it with
Microsoft Graphhttps://graph.microsoft.com/az account get-access-token --resource-type ms-graph
Azure ARMhttps://management.azure.com/az account get-access-token --resource-type arm
Key Vault datahttps://vault.azure.net/az account get-access-token --resource https://vault.azure.net
Azure Storagehttps://storage.azure.com/az account get-access-token --resource https://storage.azure.com
Azure DevOps499b84ac-1321-427f-aa17-267ca6975798az account get-access-token --resource 499b84ac-1321-427f-aa17-267ca6975798

From a refresh token (any audience, see Token Exchange for full commands):

# ROADtools - switch to any resource using stored RT
roadtx refreshtokento -r https://management.azure.com/
roadtx refreshtokento -r https://graph.microsoft.com/
roadtx refreshtokento -r https://vault.azure.net/

# powershell - from authenticated Az session
(ConvertFrom-SecureString (Get-AzAccessToken -ResourceTypeName Arm -AsSecureString).Token -AsPlainText)
(ConvertFrom-SecureString (Get-AzAccessToken -ResourceTypeName MSGraph -AsSecureString).Token -AsPlainText)
(ConvertFrom-SecureString (Get-AzAccessToken -ResourceTypeName AadGraph -AsSecureString).Token -AsPlainText)  # aad graph is legacy
(ConvertFrom-SecureString (Get-AzAccessToken -ResourceTypeName KeyVault -AsSecureString).Token -AsPlainText)
(ConvertFrom-SecureString (Get-AzAccessToken -ResourceTypeName Storage -AsSecureString).Token -AsPlainText)

Tools Reference


ToolAuth MethodsNotes
Azure PowerShellAccess token, Credential-AccessToken flag; ARM-focused
Microsoft.Graph PowerShellAccess token (SecureString), CredentialGraph-only; token must be ConvertTo-SecureString first
Az CLICredential only (no native AT auth)az rest --headers "Authorization=Bearer $T" injects any token
ROADtools / roadtxAccess token, Refresh token, PRT-j/gettokens for AT; refreshtokento for RT exchange; prtauth for PRT
TokenTacticsV2Refresh tokenInvoke-RefreshTo* per resource; -Device/-Browser for UA blending
AADInternalsAccess token, Refresh token-AccessToken or -RefreshToken on most cmdlets
GraphSpyAccess token, Refresh token, Device codeGUI; manages multiple token contexts; FOCI-aware
AzureHoundAccess token (JWT), Refresh token, Credential-j for JWT; -r for RT; outputs BloodHound-compatible JSON
AzurePEAS / CloudPEASSAccess token (env vars), FOCI refresh tokenAZURE_ARM_TOKEN + AZURE_GRAPH_TOKEN env vars; --foci-refresh-token
nord-streamDevOps bearer token / PAT--token flag; extracts pipeline secrets by running modified jobs