JWT Test Tokens¶
Generate JWT tokens to test your deployed APIs through Azure API Management.
π Overview¶
| Aspect | Details |
|---|---|
| Goal | Generate a valid JWT token for testing secured API endpoints |
| Prerequisites | SAIF CLI installed |
| Time estimate | ~2 minutes |
| Difficulty | Beginner |
π― Why You Need This¶
When an API is deployed to Azure, API Management secures it by default. To test the deployed API with Postman, RestClient, or similar tools, you need an Authorization header with a valid JWT token.
Key concepts:
- Scopes - Application-level permissions allowing the app to access API functionality
- Corporate vs External - Forge uses dual identity providers: Entra ID (corporate) and Okta (external)
- user-groups scope - Okta's delegation scope for user-context access, requested as
{project-id}.user-groups. The CLI adds it for you;--no-default-scopesopts out. - user_impersonation scope - Entra ID's equivalent delegation scope for user-context access
π Instructions¶
1. Find Your Auth Server ID (Okta only)¶
Skip this step for Entra ID applications β the CLI looks those up for you.
For an Okta (external) application, the CLI has no way to look up its Auth Server ID automatically. Find it via API deployment pipeline -> Deploy stage -> auth_ext_okta_client (terraform) job -> Terraform Apply output, the same place the legacy pipeline flow (Option B) reads its Auth Server ID from:
- In Azure DevOps, navigate to your main API deployment pipeline
- Find the most recent deployment run to the target environment (Test/QA/UAT)
- Click the Deploy stage
- Click the Deploy auth_ext_okta_client (terraform) job
- Scroll to the bottom of the Terraform Apply step
- Look for the Terraform Outputs section:
π Terraform Outputs:
====================
AuthServerAudience =
AuthServerId =
AuthServerUrl =
ClientId =
ClientSecret =
OpenIdConnectClientId =
OpenIdConnectClientSecret =
TenantOrigin =
====================
- Copy the AuthServerId value β you'll pass it as
--auth-serverin the next step
There is no client ID to copy
A single shared Okta client backs saif auth generate for every project, so the CLI already
knows its own client ID β the same way the Entra ID path works. Each project's auth server
authorizes that client automatically; nothing per-project needs publishing.
Projects on a custom Okta domain
A bare AuthServerId is resolved against the default saif-x.oktapreview.com host. If your
project sets the module's issuer_mode to CUSTOM_URL or DYNAMIC, the issuer is a custom
domain instead (e.g. login-np.saif.com) and the bare ID won't discover. Check the
AuthServerUrl output: when it isn't under saif-x.oktapreview.com, pass that full URL to
--auth-server instead of the ID. The full URL must be https:// β the CLI rejects a
plaintext http:// issuer, since it would carry the authorization code and tokens over HTTP.
2. Run the Token Generate Command¶
saif auth generate acquires a user-delegated token scoped to a SAIF application. Entra ID is used by default:
Example:
If you don't provide --name, the CLI prompts for the application name.
For Entra ID, the first sign-in of a session opens your browser to complete authentication interactively. Subsequent runs reuse a cached (or silently refreshed) token until it expires, with no further prompt.
For Okta, every run signs in interactively β there is no cache to fall back to. This is deliberate: a cached token can't tell which external test user you want, so caching would silently keep handing back whichever user last signed in. Since you'll often want to test as a different user than last time, always prompting is the correct default, not a missing feature.
On Windows, sign-in opens in a private/incognito window of your default browser (Chrome, Edge, or Firefox) so it never reuses an existing corporate SSO session β external test users have no legitimate session to inherit. Other browsers and platforms fall back to a normal window; the CLI always sends prompt=login, so you are asked to sign in either way. A graphical browser is required β this flow does not work over a headless or SSH session.
You have 2 minutes to complete the sign-in before the CLI times out; press Ctrl+C to cancel sooner.
For an Okta (external) application, pass --identity-provider okta along with the --auth-server value from Step 1, plus the scopes you need (see Step 4 β --scopes is required for Okta):
saif auth generate --name it-api-exp-myapp --identity-provider okta --auth-server aus13jud3qnGA7SGi1d8 --scopes Client.Read
3. Select Your Application (Entra ID only)¶
For Entra ID applications, the CLI searches for applications matching your name and displays a list if there's more than one match.
Application naming pattern: {project-id}-{environment}
Examples:
it-api-exp-brishe-testit-api-exp-brishe-qait-api-exp-brishe-uat
Okta applications have no per-environment name and no application list to select from β --auth-server identifies the application directly, so there's nothing to disambiguate.
4. Request Specific Scopes¶
For Entra ID applications, --scopes is optional β it defaults to user_impersonation.
For Okta applications, --scopes is required β list the API scopes you want to exercise. The CLI then appends the user-groups delegation scope automatically, the same way ScopeBuilder does for the SDK, because APIM needs the user-groups claim on every user-delegated call:
saif auth generate --name it-api-exp-myapp --identity-provider okta --auth-server <id> --scopes Client.Read
That requests it-api-exp-myapp.Client.Read and it-api-exp-myapp.user-groups. Listing user-groups yourself is harmless β it is not requested twice.
To mint a token without the delegation scope, pass --no-default-scopes. This is for negative testing, such as confirming APIM rejects a token that lacks the user-groups claim:
saif auth generate --name it-api-exp-myapp --identity-provider okta --auth-server <id> --scopes Client.Read --no-default-scopes
Bare scope names are automatically qualified with the project ID (Client.Read becomes it-api-exp-myapp.Client.Read, user-groups becomes it-api-exp-myapp.user-groups) β you don't need to type the full name.
Both --scopes forms are valid and equivalent for either provider β space-separated values after a single --scopes, or a repeated --scopes flag:
saif auth generate --name it-api-exp-myapp --identity-provider okta --auth-server <id> --scopes Client.Read Client.Write
saif auth generate --name it-api-exp-myapp --identity-provider okta --auth-server <id> --scopes Client.Read --scopes Client.Write
5. Copy the Token¶
The token is displayed in the console. Copy the entire token output (no additional formatting).
For scripts or piping into another tool, use --plain to print only the raw token with no other output:
6. Use in Testing Tools¶
Add the token as an Authorization header in your HTTP client:
Example in Postman:
- Header:
Authorization - Value:
Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI...
Option B: Generate a Pipeline Token (service-to-service) with Azure DevOps¶
saif auth generate only supports Authorization Code + PKCE β a user-delegated flow acquired on behalf of a signed-in developer. It cannot mint a client_credentials (service-to-service, no user) token today. Use this pipeline for that case, or to authenticate as an external test user without a local sign-in.
π‘ Prefer Option A (
saif auth generate) for anything user-delegated. This pipeline exists today because the CLI has noclient_credentialssupport yet β once that lands, this path is expected to be deprecated in favor of the CLI for both flows.
1. Pipeline Prerequisite¶
For the user-delegated flow only, add a secret variable on your pipeline holding the external test user's password. This is a one-time task after creating the pipeline that needs to be performed before the first user-delegated run. Skip it if you only use the client_credentials (service-to-service) flow, which has no user and never reads this variable.
- Click Edit
- Click Variables
- Click the plus button to add a variable
- The variable name should be Password
- Check the checkbox Keep this value secret
- Check the checkbox Let users override this value when running the pipeline
This is the MS documentation for doing that
2. Finding the Auth Server ID¶
The Auth Server ID is printed in your API deployment pipeline outputs:
- In Azure DevOps, navigate to your main API deployment pipeline
- Find the most recent deployment run to the target environment (Test/QA/UAT)
- Click the Deploy stage
- Click the Deploy auth_ext_okta_client (terraform) job
- Scroll to the bottom of the Terraform Apply step
- Look for the Terraform Outputs section:
π Terraform Outputs:
====================
AuthServerAudience =
AuthServerId =
AuthServerUrl =
ClientId =
ClientSecret =
OpenIdConnectClientId =
OpenIdConnectClientSecret =
TenantOrigin =
====================
- Copy the AuthServerId value
3. Generating a Token¶
- In Azure DevOps, run the it-test-tools-[username]-gen-np-jwt pipeline in your personal test tools repository
- In the parameters dialog pane that appears, enter the following (see table below for examples):
- Project ID
- Scopes (YAML array)
- Auth Server ID
- Okta Org: Select External
- ExternalUsername (your external user email) β used only by the user-delegated flow
- ClientCredentials: leave unchecked for a user-delegated token, or check it for a service-to-service (
client_credentials) token - For a user-delegated token only, supply the external test user's password β the
client_credentialsflow has no user and ignores these steps: - Click Variables
- Click Password
- Enter the external test user's password (from
my-test-users.yml/password_map, not an AD/corporate password) into the Value text box and click the Update button - Click run
- Once the pipeline completes, navigate to the run summary
- In the summary details, under the Related column, click the 1 published; 1 consumed link, this will navigate you to the artifacts page for the pipeline run
- Expand the JWT list item
- Click token.txt to download a text file containing the JWT
- Copy/paste this into your testing tool as an Authorization request header, format:
Authorization|Bearer [JWT]
π Example Parameters (External Tokens Only)¶
Pipeline Parameter - External Example¶
| Parameter | Example |
|---|---|
| Project ID | it-api-sys-envsvc |
| Scopes | - read - write |
| Auth Server ID | aushpkatj89kOhK6Y1d7 |
| Okta Org | External |
| ExternalUserName | wilbon@lincoln.com |
β Verify It Worked¶
Confirm your token is valid:
- Decode the token - Run
saif auth validateand paste the token when prompted, or use jwt.io to inspect the payload - Check scopes - Verify your requested scopes appear in the token's
scpclaim - Test the API - Make a request to your API with the token
Success indicator: API returns data instead of 401 Unauthorized.
π Troubleshooting¶
Word-wrapped tokens¶
Terminals interpret pasted newlines as command separators. If you paste a word-wrapped JWT directly on the command line (e.g. saif auth validate <paste>), each wrapped line is treated as a separate command, causing errors like 'tOWM5...' is not recognized as a cmdlet.
Workarounds:
- Use interactive mode (recommended): run
saif auth validatewith no arguments, then paste when prompted. - Widen your terminal: make the terminal window wider than the token length before pasting so no wrapping occurs.
-
Store in a variable first (PowerShell):
Browser closes or sign-in doesn't complete¶
Cause: The CLI's interactive flow has no way to detect a closed browser window β only a timeout or Ctrl+C end it.
Solution: Press Ctrl+C to cancel and try again. The CLI waits 2 minutes for the callback and prints Waiting for Okta sign-in in your browser once when it starts waiting, so if the terminal is still showing that line the sign-in is live, not stalled. With --plain (or when stdout is redirected) that line is suppressed to keep the token stream clean.
Browser never opens¶
Cause: No graphical browser is available (headless agent or SSH session), or no default browser is registered.
Solution: Run the command from a desktop session. The CLI reports Could not launch a browser for interactive sign-in in this case; there is no device-code fallback yet, so use the pipeline flow from a headless environment.
"Could not bind any of the CLI's registered loopback ports"¶
Cause: The CLI listens for the Okta callback on one of a fixed, small set of local ports (8406-8408, matching the ports Terraform registers as the CLI token client's redirect URIs) β Okta requires an exact redirect-URI match, so the CLI can't fall back to an arbitrary free port. This fails when all of them are already in use, usually by another saif auth generate still running.
Solution: Close the other saif auth process (or whatever else is holding one of ports 8406-8408) and retry.
Okta sign-in fails with "invalid_scope" or "invalid_request"¶
Cause: --auth-server doesn't match the project's okta-client workspace, or a requested scope isn't declared for that auth server.
Solution: Re-copy AuthServerId from the workspace outputs, and check every value in --scopes is declared on that auth server. Bare scope names are qualified with the project ID automatically (Client.Read becomes {project-id}.Client.Read), so pass either the bare or the fully-qualified form β not a partially-qualified one.
Token rejected with "invalid_token" error¶
Cause: Token may be expired or scopes don't match API requirements.
Solution: Generate a new token and verify the scopes match what the API expects.
Token works for some users but not others¶
Cause: Using wrong tenant (Corp vs External) for the user type.
Solution:
- Internal employees should use Corp tenant (Entra ID)
- External users (policyholders, providers) should use External tenant (Okta)
- Verify the user exists in the correct identity provider
SAIF CLI not installed¶
Cause: SAIF CLI not installed.
Solution:
- Install SAIF CLI:
dotnet tool install -g SAIF.Platform.CLI - Verify SAIF CLI is working:
saif --version
π‘ Note: The SAIF CLI handles authentication on your behalfβno separate Azure login required.
π Next Steps¶
| Task | Guide |
|---|---|
| Set up a test tools repository | Test Tools Repository |
| Manage test users | External Test Users |
| Learn about security | Security Overview |