Resource library

QA How-To

Postman OAuth 2.0 Authorization Tutorial

Postman OAuth 2.0 authorization tutorial using local Keycloak, PKCE, a browser callback, protected UserInfo calls, negative tests, and token refresh checks.

23 min read | 3,875 words

TL;DR

Start a local Keycloak realm, create a public client with Postman's exact browser callback, then configure Authorization Code (With PKCE) in Postman. Use the access token for a 200 UserInfo request and verify that missing or invalid bearer tokens get 401.

Key Takeaways

  • Register the exact browser callback URL shown by Postman on the OAuth client.
  • Use Authorization Code with PKCE and S256 for an interactive public-client login.
  • Read the provider discovery document to verify authorization, token, and UserInfo URLs.
  • Send the access token to UserInfo and assert the response status and subject.
  • Keep missing-token and invalid-token requests separate from the authorized request.
  • Confirm refresh-token availability before relying on Postman's manual refresh behavior.

Postman OAuth 2.0 Authorization Tutorial: configure a local authorization server, obtain an authorization code with PKCE, exchange it for an access token, and send that token to a protected endpoint. You will use the Postman desktop app and a disposable Keycloak realm so every URL and expected response has a concrete source. The main result is a successful GET /userinfo call that returns the user you signed in as.

OAuth has two separate jobs in this exercise. Keycloak authenticates a person and issues a short-lived access token; Postman acts as the client that requests that token and attaches it to an API call. The /userinfo endpoint checks the token before returning claims. This is a local learning environment, so HTTP on 127.0.0.1 and a sample password are acceptable here. Use HTTPS, managed secrets, and your provider's production configuration for a real integration.

What You Will Build

  • A local Keycloak realm named postman-lab with one test user and a public OAuth client.
  • An Authorization Code (With PKCE) configuration in Postman that uses the browser for login.
  • A saved GET /userinfo request with assertions for the status, content type, and subject claim.
  • Two negative checks that prove a missing or invalid bearer token is rejected.
  • A repeatable way to inspect discovery metadata and diagnose callback, scope, or refresh failures.

The authorization code flow is appropriate when a human signs in. The code is a short-lived intermediate value, not the API credential. PKCE binds that code to the client that started the flow. For a broader map of grants and authorization tests, read the authorization code flow testing guide after completing this lab.

Prerequisites

Install the Postman desktop app, Docker or Podman, and curl. The current Postman v12 documentation and the official Keycloak 26.8.0 getting-started example are the published version references for the UI and server behavior described here. Record your exact installed versions before you start: open Postman's About screen, run docker version (or podman version), and run curl --version. Use a Keycloak image tag that matches a version available in the official Keycloak container documentation; replace <your-keycloak-version> in the command below with that exact published tag. This article deliberately does not guess the version installed on your computer or pin a tag that may not fit your environment.

docker version
curl --version
docker image inspect quay.io/keycloak/keycloak:<your-keycloak-version> >/dev/null 2>&1 || docker pull quay.io/keycloak/keycloak:<your-keycloak-version>

If you use Podman, substitute podman for docker in the commands. Reserve local port 8080, or consistently change the host-side port in every URL below. Start with an empty disposable container: the realm and user created here live in its development database and disappear when you remove the container. You also need a normal browser on the same machine as Postman. Postman's browser authorization option is a desktop app feature.

The Keycloak admin credentials and test-user password below are examples for a loopback-only lab. Do not reuse either password elsewhere. Do not publish screenshots of access tokens, browser redirects containing codes, or Postman Console request headers. The Postman beginner tutorial covers basic request and collection controls if the interface is new to you.

Step 1: Start a local authorization server

Run Keycloak in development mode with a loopback port binding. Replace only the image tag placeholder. Keep this terminal open while you work, because --rm removes the container after you stop it. The KC_BOOTSTRAP_ADMIN_* values create the initial administrator for this disposable instance, as described in the Keycloak container guide.

docker run --rm --name postman-oauth-lab \
  -p 127.0.0.1:8080:8080 \
  -e KC_BOOTSTRAP_ADMIN_USERNAME=admin \
  -e KC_BOOTSTRAP_ADMIN_PASSWORD=lab-admin-password \
  quay.io/keycloak/keycloak:<your-keycloak-version> start-dev

Keycloak may need time to initialize. In a second terminal, request the realm list's parent page only after the server reports that it has started. This check asks for an HTTP response, not a token. An HTTP 200 or redirect from the root confirms that the host port is reachable; it does not yet prove a realm exists.

curl -sS -o /dev/null -w 'HTTP %{http_code}\n' http://127.0.0.1:8080/

Verify: open http://127.0.0.1:8080/admin/ and sign in as admin with lab-admin-password. If curl reports connection refused, inspect the container terminal first. If Docker says the port is already allocated, stop the conflicting local process or choose another host port and update every later URL. Keep the browser and Postman on the host machine; 127.0.0.1 inside a remote desktop or container means a different network location.

The start-dev command is designed for local testing. It does not give you a production identity service. For this tutorial its value is isolation: you can create a client, issue a token, and deliberately break a request without changing a company or third-party tenant.

Step 2: Create the realm, user, and OAuth client

In the Admin Console, create a realm named postman-lab. Select it in the current-realm selector before creating anything else. Under Users, create qa.reader, set a password such as lab-user-password in the Credentials tab, and turn off the temporary-password requirement so this lab login does not stop at a forced password-change screen. Do not create the user in the master realm; that realm is for administration.

Under Clients, create an OpenID Connect client with Client ID postman-lab-client. Enable the standard Authorization Code flow and set Client authentication to Off, which makes it a public client with no client secret. Set the Valid Redirect URIs entry to exactly https://oauth.pstmn.io/v1/browser-callback. Do not use a wildcard redirect. In the client advanced settings, require PKCE with the S256 challenge method if your installed Keycloak version exposes that setting. The browser callback URL is the value documented by Postman's OAuth 2.0 helper for browser-based authorization. If Postman displays a different callback in your installed app, register that exact displayed URI instead and use it consistently.

You can verify realm creation without logging a token. The following discovery endpoint is a real Keycloak OIDC API. It publishes the authorization, token, userinfo, and key-set URLs. Python's JSON formatter is optional; the raw curl response is also valid JSON.

curl -fsS http://127.0.0.1:8080/realms/postman-lab/.well-known/openid-configuration | python3 -m json.tool

Verify: look for issuer equal to http://127.0.0.1:8080/realms/postman-lab, plus authorization_endpoint, token_endpoint, and userinfo_endpoint. A 404 usually means the realm was created under a different spelling or the browser still shows master. The discovery document confirms the realm and URLs, but it cannot prove your client redirect or user password; those are exercised in Step 4.

A public client has no secret to type into Postman. PKCE supplies a per-authorization proof, but it does not turn a public client into a confidential one. In a real product, choose the client type required by its threat model and server policy. The lab keeps the client public to show how browser authorization works without asking the reader to copy a secret into a workspace.

Step 3: Build the request and establish the 401 baseline

Create a Postman collection named Postman OAuth Lab. Add a request named Get my profile, choose GET, and enter http://127.0.0.1:8080/realms/postman-lab/protocol/openid-connect/userinfo. Before adding OAuth, set the request's Authorization type to No Auth and send it. An authentication challenge is the expected result: the endpoint should not reveal user claims to an unauthenticated caller.

Run the same baseline from a terminal. Use -i to show the status line and headers. The body may be empty or contain an error description depending on your server configuration, so assert the status rather than relying on exact error prose.

curl -i http://127.0.0.1:8080/realms/postman-lab/protocol/openid-connect/userinfo

Verify: expect HTTP 401. In Postman, add this post-response script temporarily to the No Auth request under Scripts > Post-response and send again. A green test confirms the endpoint rejected the missing credential for the expected reason.

pm.test("Userinfo rejects a request without a token", () => {
  pm.response.to.have.status(401);
});

Save a copy of this request as Missing token returns 401; leave that copy set to No Auth for Step 6. Remove the temporary 401 assertion from Get my profile before making it the success request. This separation matters because a collection run should not have one script expecting 401 while the request uses a valid OAuth token. It also prevents a successful response from being misdiagnosed as a regression in the negative test.

The protected resource here is Keycloak's standards-based UserInfo endpoint, not an invented sample API route. It returns identity claims for an access token with the appropriate OpenID Connect scope. That makes it a useful first probe: the request, issuer, token, and returned subject can all be checked against the same local realm. For more API error checks, see the negative API testing guide.

Step 4: Configure Postman OAuth 2.0 Authorization Tutorial settings

On Get my profile, open Authorization and select OAuth 2.0. Under Configure New Token, use Token Name postman-lab-user, Grant type Authorization Code (With PKCE), and choose Authorize using browser. Copy the callback URL shown by Postman and confirm it exactly matches the single Valid Redirect URIs entry in Keycloak. Set Auth URL to http://127.0.0.1:8080/realms/postman-lab/protocol/openid-connect/auth and Access Token URL to http://127.0.0.1:8080/realms/postman-lab/protocol/openid-connect/token. Enter Client ID postman-lab-client, leave Client Secret empty, and set Scope to openid profile.

Choose SHA-256 as the code challenge method. Allow Postman to generate the code verifier; do not paste a shared verifier into documentation. Set Client Authentication to the option that sends client credentials in the request body if your app requires a choice for this public client. The client ID identifies the application; there is no client secret. If the UI offers an Auto-refresh access token switch after a refresh token is returned, leave it enabled for manual sends. Do not enter an access token manually in the helper.

Check the configured endpoints against discovery before requesting a token. This command prints the two URL fields from the live server using Python's standard library. Compare the output character by character with the values in Postman, including http, port 8080, and realm name.

curl -fsS http://127.0.0.1:8080/realms/postman-lab/.well-known/openid-configuration | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d["authorization_endpoint"]); print(d["token_endpoint"])'

Verify: the printed lines end in /auth and /token, respectively. Select Get New Access Token. Your browser should show the postman-lab sign-in page. Sign in as qa.reader with lab-user-password, then allow the browser to return to Postman. Postman should show a token response with an access token and expiration. Select Proceed and Use Token. The helper can now attach Authorization: Bearer <access_token> to this request when you send it.

The authorization code, state, and PKCE verifier have separate roles. state links the response to the browser request and helps detect unsolicited callbacks; the PKCE verifier proves that the client exchanging the code started the flow. The access token is what the resource endpoint receives. Do not paste an ID token into an API's bearer-token field just because it is also a JWT. Postman lets you choose access or ID token when both exist; use the access token for /userinfo.

Step 5: Send the authorized request and assert identity

Send Get my profile after selecting Use Token. Expect 200 and a JSON object with a stable sub value. Depending on realm and scope settings, the response may also include preferred_username and profile claims. Do not hard-code the generated subject ID: Keycloak assigns it when the user is created, and recreating the disposable container produces a different one.

Add this script under Scripts > Post-response on the authorized request. It checks the transport result, media type, and the claim that identifies the subject. It also checks the username when the profile scope has made it available; a missing username then points to scope or mapper configuration rather than a transport error.

pm.test("Userinfo returns the authenticated user", () => {
  pm.response.to.have.status(200);
  pm.expect(pm.response.headers.get("Content-Type")).to.include("application/json");
  const user = pm.response.json();
  pm.expect(user.sub).to.be.a("string").and.not.empty;
  pm.expect(user.preferred_username).to.eql("qa.reader");
});

Verify: send the request and inspect the Postman test results. The status test and claim assertions should pass. In the request's Headers tab, reveal the hidden Authorization header if you need to confirm that Postman generated it. Do not copy its value into a report or screenshot. If the response is 401, check that the selected token is the access token, that Use Token was clicked, and that no stale manually entered Authorization header is competing with the helper.

For an independent API check, you can paste a freshly copied access token into a terminal variable on your own machine, then run the command below. Your shell history or process list may expose manually pasted secrets, so use this only in a disposable lab and clear the variable afterward. The Postman request above is the preferred verification because it does not require copying a token.

read -r -s -p 'Lab access token: ' LAB_ACCESS_TOKEN
curl -i -H "Authorization: Bearer ${LAB_ACCESS_TOKEN}" http://127.0.0.1:8080/realms/postman-lab/protocol/openid-connect/userinfo
unset LAB_ACCESS_TOKEN

Verify: the curl response is 200 and contains the same sub shown in Postman. If Postman succeeds but curl fails, the copied token may be incomplete or expired; that difference does not mean the endpoint has a separate Postman-only authorization path. For more on token shape and validation boundaries, use the JWT authentication testing guide.

Step 6: Prove that invalid authorization fails

A successful call alone does not show that the resource is protected. Send the saved Missing token returns 401 request again and confirm its test remains green. Then create a second request named Invalid token returns 401, use the same GET URL, select Bearer Token as the auth type, and set the token value to deliberately-invalid. This string is test data, not a credential. Do not put the word Bearer in the token field because Postman adds that scheme itself.

Attach this post-response script to the invalid-token request. It asserts the failure at the protected endpoint without depending on the provider's exact error-body wording.

pm.test("Userinfo rejects a malformed bearer token", () => {
  pm.response.to.have.status(401);
  pm.expect(pm.response.headers.get("WWW-Authenticate")).to.be.a("string");
});

You can verify the same scenario outside Postman. This is a complete request that should fail even if a valid token is still selected in another Postman tab.

curl -i -H 'Authorization: Bearer deliberately-invalid' http://127.0.0.1:8080/realms/postman-lab/protocol/openid-connect/userinfo

Verify: both the Postman and curl calls return 401. A 200 from the negative Postman request means you likely left it on OAuth 2.0 or inherited authorization from its collection; inspect the request's Authorization tab and hidden header. A 404 means you changed the path or realm, which is a routing failure rather than a token rejection. A 403, if a different API returns one, may indicate a valid identity without sufficient permission; it is not interchangeable with this malformed-token check.

Keep the negative requests separate from the success request. That gives a small but meaningful contract matrix:

Request Authorization setting Expected result What it checks
Missing token No Auth 401 The endpoint requires a credential
Invalid token Bearer Token with test string 401 A malformed credential is rejected
Get my profile OAuth 2.0 access token 200 A valid user token reaches UserInfo

Later, add a valid token with an insufficient scope or role to test authorization after authentication. This lab's /userinfo endpoint primarily demonstrates token validity and OpenID Connect claims, not application-specific role policy.

Step 7: Extend the Postman OAuth 2.0 Authorization Tutorial to shared requests

Move the OAuth 2.0 setup to the Postman OAuth Lab collection's Authorization tab if you want several protected requests to share it. Set Get my profile to Inherit auth from parent and send it again. Keep the two negative requests explicitly set to No Auth and Bearer Token, respectively, so inheritance cannot turn expected 401 responses into 200 responses. Postman documents this inheritance behavior in its authorization settings guide.

Inspect the token's details through the Available Tokens or Manage Tokens control. If Keycloak returned a refresh token, Postman can refresh a token automatically before a manual send when the access token expires. You can also select Refresh manually. If no refresh token was issued, that control is unavailable; request a new token through the browser flow instead of inventing one. A refresh exchange uses the token endpoint, not the authorization endpoint, and the provider may rotate the refresh token.

Use the live discovery document to verify that the token URL still points at your running realm after any environment or port change:

curl -fsS http://127.0.0.1:8080/realms/postman-lab/.well-known/openid-configuration | python3 -c 'import json,sys; print(json.load(sys.stdin)["token_endpoint"])'

Verify: the printed URL matches Postman's Access Token URL. Send the inherited Get my profile request and confirm its sub still matches the same local user. Then send both negative requests and confirm 401. This three-request pass catches an accidental collection-level authorization override. If the token expires during a longer test session, check Manage Tokens, refresh it if available, and rerun the success request. The bearer token refresh testing guide covers expiry, rotation, and replay cases in more depth.

Do not assume the same refresh behavior in scheduled runs or CI. Postman's current OAuth documentation states that scheduled runs, monitors, Postman CLI, and Newman do not automatically refresh OAuth tokens. Plan an explicit token-provisioning strategy for those environments and avoid syncing a real user's long-lived credential merely to make a pipeline pass. The Newman in CI guide is useful when you move beyond interactive testing.

Troubleshooting

Problem: invalid_redirect_uri after browser login -> Copy the callback displayed by Postman's Authorize using browser option and add that exact URI to the Keycloak client's Valid Redirect URIs. Check the scheme, path, and trailing slash. A broad * hides the mismatch and weakens redirect security.

Problem: Postman says the token request failed after login -> Compare its Auth URL and Access Token URL with the discovery output. Confirm the client is public, the Client ID matches, and the PKCE method is SHA-256/S256. Open the Postman Console for the status and error code, but redact codes and tokens before sharing a log.

Problem: the browser shows the wrong user or an unexpected consent screen -> Check the selected realm in Keycloak, sign out of an existing Keycloak session, and confirm the client belongs to postman-lab. A consent prompt can be normal if consent is enabled on that client; it is not proof that the token was used by /userinfo.

Problem: /userinfo returns 401 after Use Token -> Inspect the hidden Authorization header, selected token type, and token expiration. Use an access token, not an ID token. Remove a manually entered Authorization header and retry. If you restarted the disposable container, get a new token because the old realm state and signing keys may have changed.

Problem: the 200 response lacks preferred_username -> Confirm the requested scope contains profile, then inspect the client's scopes and protocol mappers in Keycloak. Keep the sub assertion as the minimal identity check while diagnosing the optional profile claim. Do not fabricate a username from a JWT payload in the test.

Problem: the negative request unexpectedly returns 200 -> Set its own Authorization type to No Auth or Bearer Token with the intentionally invalid value. Check whether it inherited the collection's OAuth setup and inspect the effective header. The URL and method can be correct while the auth selection is wrong.

Where To Go Next

You now have a controlled OAuth client, a user, a token exchange, and a protected request with positive and negative assertions. Apply the same method to an API that exposes business permissions: request a narrow scope, call an allowed endpoint, then call one that requires a different scope and assert the provider's documented 403 or equivalent denial. Capture the issuer, audience, expiry, and scope expectations from your real API contract rather than assuming every JWT field has the same meaning.

For larger collections, store base URLs and non-secret client IDs in a scoped environment. Keep secrets in Postman Vault or your organization's secret manager, and inspect which values are shared before exporting a collection. The Postman variables and scopes guide explains how collection, environment, and local values interact. If you compare manual OAuth setup with automated API tests, write down how the runner obtains and renews its token before adding CI jobs.

Interview Questions and Answers

Q: Why use Authorization Code with PKCE for this Postman exercise?

A human signs in through a browser, and the API call needs a delegated access token. Authorization Code keeps the token exchange at the token endpoint; PKCE requires the exchanger to prove knowledge of the verifier associated with the original request. I would not use a password grant to avoid the browser, because it changes the security model and may not be supported by the provider.

Q: What is the difference between the code and access token?

The authorization code is short-lived and single-use input to the token exchange. The access token is the bearer credential sent to the protected API. If I see a code in an API Authorization header, the client skipped or misconfigured the exchange.

Q: Why must the redirect URI match exactly?

The authorization server sends the browser back to a registered client destination with a code. An exact allow-list entry prevents another destination from receiving that code. A mismatch should fail before a usable token is issued, so I compare the UI's callback with the client registration.

Q: What does a 401 on UserInfo prove?

It proves this request was not accepted as authenticated. With no header or a malformed token, that is the expected result. It does not by itself prove role enforcement; a valid but underprivileged token needs a separate authorization test against an endpoint with a defined permission policy.

Q: Why is sub preferable to a hard-coded user ID?

sub is the issuer's subject identifier and should be present in UserInfo for the signed-in user. The value is generated by the realm, so a recreated lab can change it. I assert its existence and compare it across calls, while using a known username only where the requested profile scope supplies that claim.

Q: What would you inspect when Postman gets a token but the API still returns 401?

I would confirm the request uses the access token, check the actual Authorization header, and inspect expiration, issuer, and audience requirements from the API contract. I would also check whether a request-level auth setting overrides the collection and whether the token came from a different realm. I would avoid printing the full token in shared logs.

The interview answers describe observable checks and the boundary between authentication and authorization. They are also useful prompts for reviewing a real API's contract before writing role or scope tests.

Common Mistakes

  • Registering a callback with a wildcard or a different path from the one Postman displays.
  • Selecting Authorization Code without PKCE while the public client requires S256.
  • Pasting Bearer into Postman's Bearer Token value field, creating a double scheme.
  • Sending an ID token to the resource endpoint instead of the access token.
  • Assuming a 200 from UserInfo proves application-specific roles or scopes.
  • Letting negative requests inherit the collection's valid OAuth token.
  • Sharing a collection, screenshot, or Console dump that contains live credentials.
  • Expecting an interactive Postman refresh setting to solve token renewal in CI.

Conclusion

This Postman OAuth 2.0 Authorization Tutorial takes one complete path: start a local issuer, register a precise callback, obtain an authorization code with PKCE, use the resulting access token, and verify that missing or invalid credentials fail. The discovery document anchors the URLs, and the three saved requests make the outcome repeatable.

Run the positive and negative requests once more before adapting the collection to your own API. Replace the local realm values with your provider's discovery URLs, register Postman's displayed callback, request only the scopes your endpoint needs, and write assertions against that API's documented permission behavior.

Interview Questions and Answers

Why would you choose Authorization Code with PKCE for a Postman user login?

The flow lets a person authenticate in a browser and gives Postman an access token through a code exchange. PKCE ties the exchange to the client that initiated it. I would confirm that the provider supports a public client and register an exact redirect URI.

How do state and PKCE differ in an OAuth authorization request?

State links the browser response to the request that initiated it and helps reject unsolicited callbacks. PKCE links the authorization code to a verifier held by the initiating client. I check both because they defend different parts of the flow.

What would you verify after obtaining an access token in Postman?

I would send it to a protected endpoint and assert the documented status and response claims. I would also send the same request with no token and an invalid token. A token appearing in Postman's UI alone does not prove the resource server accepts it.

Why should an access token and ID token not be interchanged?

An access token is intended for a resource server and carries the authorization context that server validates. An ID token communicates the authentication result to the client. I use the token type required by the endpoint contract and check audience and issuer expectations.

How do you diagnose `invalid_redirect_uri`?

I compare the redirect_uri sent by Postman with the client's registered URI byte for byte. Scheme, port, path, and trailing slash can all matter. I register the specific browser callback shown by Postman and avoid a wildcard workaround.

What is the difference between a 401 and a 403 in token testing?

A 401 generally indicates missing or unacceptable authentication for the request. A 403 generally indicates the server understood the identity but denied the operation, although I follow the API's documented contract. I use separate cases for missing, invalid, and valid-but-insufficient credentials.

How would you keep OAuth tests stable in a shared Postman collection?

I put common authorization on the collection and make protected requests inherit it. I explicitly override negative requests to No Auth or an invalid bearer token. I also avoid hard-coding generated subject IDs and keep secrets out of exported environments.

Frequently Asked Questions

What callback URL should I register for Postman OAuth 2.0?

Register the exact Callback URL displayed by Postman when you choose Authorize using browser. Postman documents `https://oauth.pstmn.io/v1/browser-callback` for that mode, but the displayed value in your installed app is the value to match. Keep the redirect entry specific rather than using a wildcard.

Do I need a client secret for Authorization Code with PKCE in Postman?

A public client like the one in this lab does not have a client secret. PKCE binds the authorization code to the client flow with a verifier and challenge. A confidential client may still require its secret because that is a separate client-authentication choice.

Why does Postman get a token but my API request returns 401?

Check that you selected Use Token and that the request sends an access token in the Authorization header. Then compare issuer, expiration, and the API's audience requirements with the provider configuration. A request-level No Auth selection can also override collection authorization.

Should I send an access token or ID token to UserInfo?

Send an access token. UserInfo is a protected resource endpoint that validates the bearer access token. An ID token describes authentication to the client and is not a substitute for an API credential.

How can I test a missing or invalid token in Postman?

Save separate GET requests to the protected URL. Set one to No Auth and another to Bearer Token with a deliberately invalid value, then assert the documented rejection status in post-response scripts. Keep the success request on OAuth 2.0 or inherited authorization.

Will Postman automatically refresh OAuth tokens in Newman or scheduled runs?

No. Postman's current documentation limits automatic refresh to manual sends in the app and says scheduled runs, monitors, Postman CLI, and Newman do not support it. Provide tokens through a controlled automation flow rather than relying on an interactive token's refresh setting.

Why is my Postman redirect URI rejected by Keycloak?

The URI registered on the client must match the callback used in the authorization request, including scheme, host, path, and slash. Copy the displayed Postman callback and add it as a specific Valid Redirect URIs entry. Check the Postman Console and Keycloak logs if the mismatch remains.

Related Guides