M2M authentication

OAuth 2.0 flows explained: The complete guide

Kuntal Banerjee
CONTENTS

Why OAuth 2.0 matters

Modern apps need access to user data without asking for passwords. OAuth 2.0 authorization protocol enables access, whether reading calendar events or posting to Slack.

OAuth 2.0 is the standard for authorization, replacing the broken model of credential sharing. Instead of asking for login credentials, apps use OAuth to request scoped access tokens. These tokens act as temporary, permission-limited keys to user data without revealing passwords or long-term secrets.

Legacy systems relied on users sharing their user credentials with third-party applications — a dangerous pattern that led to:

  • Password reuse across services
  • Full access instead of scoped access
  • No way to revoke access without changing passwords

OAuth introduced a secure delegation model with three core advantages:

  • Fine-grained access via scopes (E.g. read-only access to calendar)
  • Token revocation without resetting user passwords
  • Separation of identity and access (authentication vs authorization)

OAuth 2.0 is now the backbone of secure access delegation across the web, used by Google, Microsoft, GitHub, Amazon, Slack, and nearly every major API provider.

By the end of this guide, you’ll understand:

  • When and why to use Client Credentials Flow
  • How the flow works step by step
  • How to implement it using Node.js
  • Common security practices and testing strategies
  • How popular tools like Auth0, Keycloak, and AWS Cognito support it

OAuth 2.0 roles: The four key actors

OAuth 2.0 operates through four key roles in the authorization flow. Each one plays a specific part in the flow and defines the trust boundaries.

Core roles of OAuth 2.0: Resource owner, client, authorization server, resource server

1. Resource owner: The entity that owns the data is usually the user. They decide whether to grant access to their data (e.g., email, photos, repos) via the authorization server.

2. Client: The app requesting access on behalf of the user. This could be:

  • A backend server (e.g., a Django web app)
  • A mobile app (e.g., an iOS mail client)
  • A Javascript SPA (e.g., a React dashboard)

OAuth treats the client as an untrusted third party — it never sees the user’s credentials and must request access via tokens.

3. Authorization server: The trusted identity provider. It:

  • Authenticates the user (e.g., via password, MFA, SSO)
  • Shows a consent screen listing requested scopes
  • Issues access and/or refresh tokens

Google, GitHub, Okta, Auth0, and Azure AD are common examples of authorization servers.

4. Resource server

The API or service that stores the user’s data and accepts access tokens. It validates incoming tokens and checks if the token grants the required scopes to access a specific endpoint or resource.

🔁 Example flow: Slack accesses GitHub

Slack can access your GitHub pull requests using OAuth, without asking for your password. Here’s how that works:

  1. Slack (Client) redirects you to GitHub (Authorization server).
  2. GitHub authenticates you and shows a consent screen for the repo and user:email scopes.
  3. You approve. GitHub issues an authorization code.
  4. Slack exchanges that code for an access token.
  5. Slack uses that token to call GitHub’s API (Resource Server) to fetch your pull requests.

This flow:

  • Protects your GitHub password (Slack never sees it)
  • Limits Slack’s access (only the scopes you approved)
  • Let you revoke access later without affecting GitHub login

Grant types: The core OAuth 2.0 flows

OAuth 2.0 provides multiple grant types designed for different app types, trust levels, and security boundaries.

Choosing the right grant type isn’t just about what works — it’s about minimizing risk.

1. Authorization code grant (with PKCE)

Best for: Web apps, SPAs, and mobile apps
Security posture: Strongest option for apps with user-facing logins

The authorization code flow uses a secure handshake to exchange credentials for tokens, which is especially suited for apps with user-facing logins.

This flow involves a multi-step handshake:

  1. The client redirects the user to the authorization server.
  2. The user logs in and approves the requested scopes.
  3. The Authorization Server redirects back with a temporary code.
  4. The client exchanges this code (along with a PKCE verifier) for an access token.

Why PKCE matters:

Public clients (like SPAs or mobile apps) can’t safely store secrets. PKCE — Proof Key for Code Exchange — adds a cryptographic challenge-response layer to block token interception. It replaces older, insecure flows like the Implicit Grant, which is now deprecated.

What gets returned:

  • Access token (short-lived)
  • Optional refresh token (if offline access is needed)
  • Optional ID token (if using OpenID Connect)
💡 Dev tip: Even if your app is server-rendered, use PKCE — it's now the recommended default across the board.

2. Client credentials grant

Best for: Machine-to-machine (M2M) apps — no user involved
Security posture: Simple and secure — if used correctly

In this flow:

  1. The client app authenticates itself using client_id and client_secret.
  2. It requests an access token directly from the Authorization Server's token endpoint.
  3. The Authorization Server issues an access token that is scoped to the client's permissions.

Use cases:

  • CI/CD pipelines requesting secrets from a vault
  • Microservices talking to other internal APIs
  • Scheduled backend jobs polling APIs
⚠️ Security note: Never expose client_secret in frontend or mobile apps. This flow assumes the client runs in a secure, server-controlled environment.

Scopes, claims, and permissions: Controlling access

OAuth 2.0 controls access through scopes, claims, and permissions - each handling a specific part of access control at runtime.

These three components work together to define:

  • What the client is allowed to do
  • What the token actually says
  • What the resource server enforces

🔍 Scopes: What the client requests

Scopes define the boundaries of access requested by the client app. They’re passed during the initial access token request and displayed to the user during user consent.

Examples:

  • Read:user – View profile data
  • Repo – Full access to GitHub repositories
  • Tweet.write – Post tweets on your behalf

The Authorization Server uses scopes to:

  • Prompt the user for consent
  • Issue a token with matching permissions
  • Reject overreaching requests based on policy
💡 Dev Tip: Request only the minimum scopes you need. Over-requesting increases the risk surface and may get your app flagged or rejected.

📦 Claims: What the token contains

Claims are metadata baked into the token, especially in JWT-based tokens.

They describe:

  • Who the token is for (sub)
  • What it’s intended for (aud)
  • When it expires (exp)
  • Custom attributes, like user roles or tenant ID

Sample decoded JWT claims:

{ "sub": "user-123", "aud": "api.example.com", "exp": 1712873200, "role": "admin" }
💡 Resource servers rely on claims to make access decisions. If the token lacks a role or tenant claim, your API may deny the request.

✅ Permissions: What the resource server enforces

Scopes and claims are inputs, but permissions are what actually get enforced.

For example:

  • An API might check for role=admin to allow writing to a config file.
  • A frontend app might hide UI elements unless the features claim contains beta.

Permissions logic typically happens:

  • Inside your API code (e.g., middleware that checks req.token.role)
  • Inside a policy engine (e.g., OPA, Auth0 Rules, AWS IAM)
  • At the gateway layer (e.g., Kong or Envoy filters)
🔐 For fine-grained access control, use custom claims to encode roles, org IDs, or permissions, and validate them inside your app.

Tokens: The core of OAuth

OAuth 2.0 uses access tokens to authorize client apps, typically short-lived and actionable. Optional refresh tokens are used to extend access without user interaction.

There are two main types:

Access tokens: Short-lived and actionable

These tokens are sent in API requests to prove authorization. They’re typically JWTs or opaque strings.

  • Valid for minutes, not hours
  • Scoped to a specific set of permissions
  • Included in the Authorization: Bearer <token> header

Rotate access tokens frequently to limit damage if exposed.

Refresh tokens: Long-lived, behind-the-scenes

Access tokens expire fast. Refresh tokens let clients get new access tokens without bothering the user again.

But they must be handled with care:

  • Store in HTTP-only, secure cookies
  • Avoid localStorage (vulnerable to XSS)
  • Rotate them regularly, invalidate on logout
💡 Dev tip: Use refresh tokens only from secure, trusted environments — never from the frontend in a browser.

End-to-end OAuth 2.0 flow

Machine-to-machine flows like CI/CD and microservices rely on the Client Credentials Grant. Here's what the access token request exchange looks like:

curl -X POST https://oauth.example.com/token \   -d "client_id=abc123" \   -d "client_secret=secret" \   -d "grant_type=client_credentials"‍

The server responds with:

{ "access_token": "eyJhbGciOi...",   "expires_in": 3600,   "token_type": "Bearer" }

This model works well for:

  • CI/CD tools triggering builds
  • Microservices calling each other
  • Background jobs polling APIs

There’s no user — just secured system-to-system communication.

Security best practices in OAuth

OAuth combines convenience and risk. Follow these authorization best practices to reduce exposure and improve control:

🔒 Always use HTTPS

Tokens are credentials. Transmitting them over HTTP exposes users to interception.

🔐 Never expose client secrets in frontend apps

Mobile apps and SPAs can’t keep secrets safe. Avoid using client_secret in any public client.

🔁 Use PKCE for all public clients

PKCE secures the Authorization Code flow when the client can't protect secrets. It’s essential for mobile and browser-based apps.

🧾 Use the state parameter

The state value prevents CSRF by linking authorization requests to client sessions. Always validate it on return.

⏳ Rotate tokens frequently

Access tokens should expire quickly — minutes, not hours. Use refresh tokens to re-authenticate securely.

🔄 Rotate refresh tokens (optional but safer)

To further reduce risk, rotate refresh tokens after each use. This ensures compromised tokens can’t be reused.

OpenID Connect: Adding authentication to OAuth

OAuth 2.0 handles authorization, not identity. OpenID Connect builds on OAuth to add user authentication and identity claims.

OpenID Connect (OIDC) builds on OAuth 2.0 to add identity authentication.

Key components added by OIDC:

  • ID Token: A signed JWT that proves who the user is.
  • Userinfo Endpoint: An API for retrieving user profile attributes.
  • OpenID Provider (OP): Handles login and token issuance.

OIDC enables:

  • Single Sign-On (SSO) across apps
  • Federated identity (e.g., login with Google)
  • Rich user identity claims out of the box

💡 If you’re building login flows — not just API access — you need OpenID Connect, not just OAuth.

Summary: Choosing the right OAuth flow

You’ve learned how OAuth 2.0 secures application access without sharing passwords and the different authorization grants used to authorize client apps. We covered how the Client Credentials Flow enables machine-to-machine communication, allowing secure backend interactions. Additionally, you now understand the roles, scopes, claims, and security best practices essential for OAuth 2.0 implementations.

What’s next

To deepen your understanding, try building OAuth flows with authorization endpoints like Google or GitHub. Explore OpenID Connect to add an authentication protocol and SSO to your apps. Learn about token introspection and revocation to strengthen your token management.

Start building your OAuth 2.0 integrations today and secure your app’s access with confidence!

FAQ

How does OAuth 2.0 work​?

The OAuth 2.0 protocol enables users to let external applications access their data points while protecting their user credentials. Users authorize the app before the app receives an authorization code, which serves as the basis for obtaining an access token to access protected data. The system uses temporary tokens that enable users to obtain refreshed access tokens through requests that bypass authentication requirements.

What is the Client Credentials Flow in OAuth 2.0?

As an OAuth 2.0 authorization process, the Client Credentials Flow provides M2M (machine-to-machine) services that operate without requiring user interaction. The system allows a client entity or service (or backend system) to self-authenticate through client_id and client_secret credentials in order to obtain access tokens needed to access APIs or services.

How does the Client Credentials Flow differ from other OAuth 2.0 flows?

The Client Credentials Flow differs from other OAuth flows (like the Authorization Code Flow) because it does not require user involvement. While other flows are used when accessing user data, the Client Credentials Flow is used for backend services or systems interacting with each other, where the client acts on its own behalf.

When should I use the Client Credentials Flow?

Use the Client Credentials Flow in scenarios where:

  • There is no user context or interaction (e.g., system-to-system communication)
  • Machine-to-machine authentication is required (e.g., CI/CD pipelines, microservices)
  • A backend service needs to authenticate and access APIs without any user authorization
No items found.
Ship Enterprise Auth in days

Acquire enterprise customers with zero upfront cost

Every feature unlocked. No hidden fees.
Start Free
$0
/ month
3 FREE SSO/SCIM connections
Built-in multi-tenancy and organizations
SAML, OIDC based SSO
SCIM provisioning for users, groups
Unlimited users
Unlimited social logins