An OAuth 2.0 Flow Survey for API Clients
Compare the authorization code and client credentials OAuth 2.0 grants to choose the right delegation flow for your API clients and integrations.
Netbay Infrastructure Team
Netbay Engineering
On this page
OAuth 2.0 is the protocol you use whenever you do not want to hand credentials to a third-party app. Rather than sharing a password, the resource owner authorizes a scoped delegation. For API clients, two grants dominate: the **authorization code flow** for apps acting on a user's behalf, and **client credentials** for server-to-server calls. This survey walks through when to use each and how they differ under the hood.
The actors in any OAuth exchange
- **Resource owner** — the user who controls the data.
- **Client** — the application requesting access.
- **Authorization server** — issues tokens after verifying identity.
- **Resource server** — serves the protected API when handed a valid token.
The client never sees the user's password. Instead it receives an authorization grant and exchanges it for an access token, usually scoped and short-lived.
Authorization code flow
This is the gold standard for apps that act on behalf of a user. The flow redirects the user to the authorization server, where they log in and approve a requested scope. The server returns a one-time authorization code to a redirect URI, which the client exchanges for an access token plus a refresh token.
Adding the **PKCE** extension (a proof key for code exchange) protects mobile and single-page clients where the client secret cannot be kept hidden. Even confidential clients should use it — it neutralizes the code-interception risk across every device.
Client credentials flow
This grant has no user at all. The client authenticates directly with its own 'client_id' and 'client_secret', and the authorization server returns an access token scoped to the client itself. It is the natural fit for scheduled jobs, background sync, and internal service-to-service calls where no human is in the loop.
Because there is no user involvement, you cannot rescope per person — you scope per client. Keep the secret server-side only, and lean on the grant's short token lifetimes plus refresh as needed.
Comparing the two grants
- Use **authorization code (+ PKCE)** when the API acts on behalf of a user and you must respect per-user scopes.
- Use **client credentials** when the API user is the application itself with no impersonation.
- Reserve **password grant** for legacy flows only; it re-exposes credentials and should be avoided.
- Always send credentials over TLS and never log tokens or secrets.
Whichever grant you pick, scope hygiene decides whether a breach is contained. Ask for the narrowest set of scopes the client actually uses, and reject tokens that carry more access than the resource server is prepared to grant. It is a common failure to request read:everything and then route all traffic through that one token, which turns any single leak into full read access across the platform. Enforce the requested scope on the resource server itself rather than trusting the client to self-limit, since a client that asks for more than it needs is either careless or already compromised.
Also decide where tokens get verified. If every microservice independently validates a shared signing key, you avoid a central lookup on each hop, but you inherit the challenge of synchronizing that key and rotating it safely. If instead only an auth gateway verifies and then forwards a derived identity, you lose some statelessness but gain a single place to enforce revocation. Pick one model deliberately instead of letting it emerge by accident.
A client credentials request, plain text
curl -X POST https://auth.example.com/oauth/token -H "Content-Type: application/x-www-form-urlencoded" -d "grant_type=client_credentials" -d "client_id=your-client-id" -d "client_secret=your-client-secret" -d "scope=read:orders write:invoices"Exchanging an authorization code with PKCE
import requests, hashlib, base64, os
verifier = base64.urlsafe_b64encode(os.urandom(32)).rstrip(b"=")
challenge = base64.urlsafe_b64encode(
hashlib.sha256(verifier).digest()
).rstrip(b"=")
resp = requests.post("https://auth.example.com/oauth/token", json={
"grant_type": "authorization_code",
"code": "AUTH_CODE",
"redirect_uri": "https://app.example.com/callback",
"client_id": "your-client-id",
"code_verifier": verifier.decode(),
})
print(resp.json()["access_token"])Takeaway
Pick the grant by who the actor is: a user means authorization code with PKCE, a machine means client credentials. Scope tightly, keep secrets server-side, and use TLS everywhere. Netbay VPS deploys are API-driven, so you can wire these very flows against your own infrastructure at netbayhosts.in.
Keep reading
Follow along on a real VPS
Deploy Linux in under 60 seconds
These guides are written against Ubuntu, Debian, and RHEL-family images — the same ones on NetBay.
Deploy an instance