OAuth2 / OIDC

Overview

OAuth2 separates who has the credentials from who makes the API call. Instead of sending a username and password with every request, you exchange credentials for a short-lived access token, then use that token until it expires.

OpenID Connect (OIDC) is a layer on top of OAuth2 that adds identity — it tells you who the user is, not just that they are authorized.

For API automation, two flows matter:

  • Client Credentials — machine-to-machine, no user involved

  • Authorization Code — user grants access to an application

Client Credentials Flow

This is the flow for automation, scripts, and service-to-service communication. No browser, no user interaction.

How It Works

Client                              Authorization Server
  |                                        |
  |-- POST /token (client_id + secret) --> |
  |                                        |
  | <------- access_token (JWT) ---------- |
  |                                        |
  |-- GET /api (Bearer token) -----------> | Resource Server
  |                                        |
  1. Your application sends client_id and client_secret to the token endpoint.

  2. The authorization server validates the credentials and returns an access token.

  3. You use the token in the Authorization: Bearer header for API requests.

  4. When the token expires, you request a new one.

curl Example: Token Request

# Step 1: Request a token
TOKEN_RESPONSE=$(curl -s -X POST \
  https://auth.example.com/oauth2/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=${CLIENT_ID}" \
  -d "client_secret=${CLIENT_SECRET}" \
  -d "scope=api.read api.write")

# Step 2: Extract the access token
ACCESS_TOKEN=$(echo "$TOKEN_RESPONSE" | jq -r '.access_token')

# Step 3: Use the token
curl -s \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Accept: application/json" \
  https://api.example.com/v1/resources

Token Response Structure

{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "api.read api.write"
}

Key fields:

  • access_token — the Bearer token to use in API calls

  • expires_in — seconds until the token is invalid (typically 300-3600)

  • scope — what the token is authorized to do

Authorization Code Flow

This flow involves a user granting access through a browser. Used when an application acts on behalf of a user.

How It Works

User        Application         Auth Server          Resource Server
 |              |                    |                      |
 |-- Click ---> |                    |                      |
 |              |-- Redirect ------> |                      |
 |              |                    |                      |
 |<------------ Login page ---------|                      |
 |-- Consent -> |                    |                      |
 |              | <-- auth_code ---- |                      |
 |              |-- POST /token ---> |                      |
 |              | <-- access_token - |                      |
 |              |-- GET /api ------> | -------------------> |
  1. Application redirects user to authorization server login page.

  2. User authenticates and consents.

  3. Auth server redirects back with an authorization code.

  4. Application exchanges the code for an access token (server-side).

  5. Application uses the token for API calls.

This flow is not scriptable — it requires a browser. For CLI tools, use the Device Code flow or Client Credentials instead.

Token Refresh Pattern

Access tokens expire. Rather than re-authenticating, use a refresh token:

# Refresh an expired token
TOKEN_RESPONSE=$(curl -s -X POST \
  https://auth.example.com/oauth2/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=refresh_token" \
  -d "refresh_token=${REFRESH_TOKEN}" \
  -d "client_id=${CLIENT_ID}" \
  -d "client_secret=${CLIENT_SECRET}")

ACCESS_TOKEN=$(echo "$TOKEN_RESPONSE" | jq -r '.access_token')
REFRESH_TOKEN=$(echo "$TOKEN_RESPONSE" | jq -r '.refresh_token')

Not all providers issue refresh tokens for Client Credentials flow. When they do not, simply request a new token when the current one expires.

Shell Function: Auto-Refresh

# Cache token and expiry, refresh automatically
oauth2_token() {
  local now
  now=$(date +%s)

  if [[ -z "$ACCESS_TOKEN" || "$now" -ge "${TOKEN_EXPIRY:-0}" ]]; then
    local response
    response=$(curl -s -X POST \
      "${OAUTH2_TOKEN_URL}" \
      -H "Content-Type: application/x-www-form-urlencoded" \
      -d "grant_type=client_credentials" \
      -d "client_id=${CLIENT_ID}" \
      -d "client_secret=${CLIENT_SECRET}")

    ACCESS_TOKEN=$(echo "$response" | jq -r '.access_token')
    local expires_in
    expires_in=$(echo "$response" | jq -r '.expires_in')
    TOKEN_EXPIRY=$((now + expires_in - 30))  # Refresh 30s before expiry
  fi

  echo "$ACCESS_TOKEN"
}

# Usage
curl -s -H "Authorization: Bearer $(oauth2_token)" \
  https://api.example.com/v1/resources

Common Providers

Provider Token Endpoint Pattern Notes

Keycloak

/realms/{realm}/protocol/openid-connect/token

Self-hosted, full OIDC

Azure AD

/oauth2/v2.0/token

Microsoft Graph, Azure APIs

Auth0

/oauth/token

SaaS identity platform

GitHub

/login/oauth/access_token

App installations use JWT

Google

/token (via service account JSON)

Uses signed JWT for Client Credentials

dsec Pattern

Store OAuth2 credentials in an encrypted environment file:

dsec edit d000 dev/network

Inside the encrypted file:

OAUTH2_TOKEN_URL=https://auth.example.com/oauth2/token
CLIENT_ID=netapi-automation
CLIENT_SECRET=your-client-secret
OAUTH2_SCOPE=api.read api.write

Load before use:

dsource d000 dev/network

# Token request uses environment variables
curl -s -X POST "${OAUTH2_TOKEN_URL}" \
  -d "grant_type=client_credentials" \
  -d "client_id=${CLIENT_ID}" \
  -d "client_secret=${CLIENT_SECRET}"

dsunsource

netapi Pattern

netapi handles the token lifecycle automatically:

# From environment (after dsource)
netapi --auth oauth2 keycloak users list

# Explicit flags
netapi --auth oauth2 \
  --token-url https://auth.example.com/oauth2/token \
  --client-id netapi-automation \
  --client-secret secret \
  keycloak users list

netapi will:

  1. Request a token from the configured token endpoint

  2. Cache the token in memory for the duration of the command

  3. Automatically refresh if the token expires mid-operation

  4. Never write the token to disk

Environment Variables

Variable Purpose

OAUTH2_TOKEN_URL

Token endpoint URL

CLIENT_ID

OAuth2 client identifier

CLIENT_SECRET

OAuth2 client secret

OAUTH2_SCOPE

Space-separated list of scopes

OAUTH2_AUDIENCE

Token audience (some providers require this)