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
- Authenticating with Refresh Tokens
- The Core APIs
- Calling APIs Directly
- Token Exchange (Quick Reference)
- Tools Reference
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- checkaud,exp, andscp/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 restwithout--headersuses 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 WindowsCoreMobileor-Browser Chrometo 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
| Property | Value |
|---|---|
| Expiry | 90 days of inactivity (rolling) |
| Reset on use | every successful exchange resets the 90-day clock |
| Explicit revocation | az ad user revoke-sign-in-sessions or CA policy enforcement |
| Format | Opaque blob - not a JWT, cannot be decoded |
| FOCI RT | Can 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:
| Type | Token Claim | Meaning | Scope |
|---|---|---|---|
| Delegated | scp | App acts on behalf of a signed-in user | Bounded by both the app’s grant AND the user’s own permissions |
| Application | roles | App authenticates as itself, no user | Bounded 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.
| Plane | API | Token audience | Controls |
|---|---|---|---|
| Management | management.azure.com | https://management.azure.com/ | Create/delete vaults, configure access policies and RBAC, firewall rules |
| Data | <vault-name>.vault.azure.net | https://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’soidclaim. - 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=..."
| Param | Meaning |
|---|---|
sp | Permissions: r=read, w=write, d=delete, l=list |
se | Expiry (UTC) |
sig | HMAC-SHA256 signature - tamper-proof without the storage key |
srt | Scope: 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 API | Token audience | Get it with |
|---|---|---|
| Microsoft Graph | https://graph.microsoft.com/ | az account get-access-token --resource-type ms-graph |
| Azure ARM | https://management.azure.com/ | az account get-access-token --resource-type arm |
| Key Vault data | https://vault.azure.net/ | az account get-access-token --resource https://vault.azure.net |
| Azure Storage | https://storage.azure.com/ | az account get-access-token --resource https://storage.azure.com |
| Azure DevOps | 499b84ac-1321-427f-aa17-267ca6975798 | az 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
| Tool | Auth Methods | Notes |
|---|---|---|
| Azure PowerShell | Access token, Credential | -AccessToken flag; ARM-focused |
| Microsoft.Graph PowerShell | Access token (SecureString), Credential | Graph-only; token must be ConvertTo-SecureString first |
| Az CLI | Credential only (no native AT auth) | az rest --headers "Authorization=Bearer $T" injects any token |
| ROADtools / roadtx | Access token, Refresh token, PRT | -j/gettokens for AT; refreshtokento for RT exchange; prtauth for PRT |
| TokenTacticsV2 | Refresh token | Invoke-RefreshTo* per resource; -Device/-Browser for UA blending |
| AADInternals | Access token, Refresh token | -AccessToken or -RefreshToken on most cmdlets |
| GraphSpy | Access token, Refresh token, Device code | GUI; manages multiple token contexts; FOCI-aware |
| AzureHound | Access token (JWT), Refresh token, Credential | -j for JWT; -r for RT; outputs BloodHound-compatible JSON |
| AzurePEAS / CloudPEASS | Access token (env vars), FOCI refresh token | AZURE_ARM_TOKEN + AZURE_GRAPH_TOKEN env vars; --foci-refresh-token |
| nord-stream | DevOps bearer token / PAT | --token flag; extracts pipeline secrets by running modified jobs |