---
title: JWT Test Tokens
description: Generate JWTs for testing deployed APIs through Azure API Management.
moved_from:
  - guides/security/testing/create-jwt-for-testing-apis.md
---

# JWT Test Tokens

Generate JWT tokens to test your deployed APIs through Azure API Management.

[TOC]

---

## 📋 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-scopes` opts 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:

1. In Azure DevOps, navigate to your main API deployment pipeline
2. Find the most recent deployment run to the target environment (Test/QA/UAT)
3. Click the **Deploy** stage
4. Click the **Deploy auth_ext_okta_client (terraform)** job
5. Scroll to the bottom of the **Terraform Apply** step
6. Look for the **Terraform Outputs** section:

```
📄 Terraform Outputs:
====================
AuthServerAudience =
AuthServerId =
AuthServerUrl =
ClientId =
ClientSecret =
OpenIdConnectClientId =
OpenIdConnectClientSecret =
TenantOrigin =
====================
```

7. Copy the **AuthServerId** value — you'll pass it as `--auth-server` in the next step

!!! note "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.

!!! note "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.

    ```powershell
    saif auth generate --name it-api-exp-myapp --identity-provider okta `
      --auth-server https://login-np.saif.com/oauth2/aus13jud3qnGA7SGi1d8 `
      --scopes Client.Read
    ```

### 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:

```powershell
saif auth generate --name <application-name>
```

**Example:**

```powershell
saif auth generate --name it-api-exp-brishe-test
```

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):

```powershell
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-test`
- `it-api-exp-brishe-qa`
- `it-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:

```powershell
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:

```powershell
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:

```powershell
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:

```powershell
$token = saif auth generate --name it-api-exp-myapp --plain
```

### 6. Use in Testing Tools

Add the token as an `Authorization` header in your HTTP client:

```
Authorization: Bearer <token>
```

**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 no `client_credentials` support 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.

1. Click **Edit**
2. Click **Variables**
3. Click the plus button to add a variable
4. The variable name should be **Password**
5. Check the checkbox **Keep this value secret**
6. Check the checkbox **Let users override this value when running the pipeline**

[This is the MS documentation for doing that](https://learn.microsoft.com/en-us/azure/devops/pipelines/process/set-secret-variables?view=azure-devops&tabs=yaml%2Cbash)

#### 2. Finding the Auth Server ID

The Auth Server ID is printed in your API deployment pipeline outputs:

1. In Azure DevOps, navigate to your main API deployment pipeline
2. Find the most recent deployment run to the target environment (Test/QA/UAT)
3. Click the **Deploy** stage
4. Click the **Deploy auth_ext_okta_client (terraform)** job
5. Scroll to the bottom of the **Terraform Apply** step
6. Look for the **Terraform Outputs** section:

```
📄 Terraform Outputs:
====================
AuthServerAudience =
AuthServerId =
AuthServerUrl =
ClientId =
ClientSecret =
OpenIdConnectClientId =
OpenIdConnectClientSecret =
TenantOrigin =
====================
```

7. Copy the **AuthServerId** value

#### 3. Generating a Token

1. In Azure DevOps, run the **it-test-tools-[username]-gen-np-jwt** pipeline in your personal test tools repository
2. 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
3. For a **user-delegated** token only, supply the external test user's password — the `client_credentials` flow has no user and ignores these steps:
   1. Click **Variables**
   2. Click **Password**
   3. 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
4. Click run
5. Once the pipeline completes, navigate to the run summary
6. 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
7. Expand the **JWT** list item
8. Click **token.txt** to download a text file containing the JWT
9. 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<br>- write    |
| Auth Server ID   | aushpkatj89kOhK6Y1d7 |
| Okta Org         | External             |
| ExternalUserName | wilbon@lincoln.com   |

---

## ✅ Verify It Worked

Confirm your token is valid:

1. **Decode the token** - Run `saif auth validate` and paste the token when prompted, or use [jwt.io](https://jwt.io) to inspect the payload
2. **Check scopes** - Verify your requested scopes appear in the token's `scp` claim
3. **Test the API** - Make a request to your API with the token

```powershell
# Run without arguments and paste the token when prompted (recommended)
saif auth validate
```

**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 validate` with 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):**

    ```powershell
    $token = "eyJ0eXAiOiJKV1Qi..."
    saif auth validate $token
    ```

### 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](#option-b-generate-a-pipeline-token-service-to-service-with-azure-devops) 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:**

1. Install SAIF CLI: `dotnet tool install -g SAIF.Platform.CLI`
2. 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](create-test-tools-repository.md)        |
| Manage test users              | [External Test Users](manage-external-test-users.md)            |
| Learn about security           | [Security Overview](../index.md)                                |
