Skip to content

Authentication Guide

The CRM M2M API uses OAuth 2.0 Client Credentials flow for secure machine-to-machine authentication. This guide explains how the authentication system works and best practices for implementation.

Your two endpoints — the API base URL and the OAuth token endpoint — are issued with your M2M credentials. Your administrator provides them when they create your M2M client: both are shown under Settings → API Access in the CRM, and the token endpoint is also returned in the create-client response.

Set them as environment variables before running any examples in this guide:

Terminal window
# Values supplied with your M2M credentials
export OAUTH_TOKEN_ENDPOINT="{token-endpoint}"
export API_ENDPOINT="https://{api-base-url}"
# Verify
echo "Token Endpoint: $OAUTH_TOKEN_ENDPOINT"
echo "API Endpoint: $API_ENDPOINT"

The Client Credentials flow is designed for server-to-server authentication where no user interaction is involved.

┌─────────────────┐ ┌─────────────────┐
│ │ │ │
│ Your App │ (1) POST /oauth2/token │ Cognito │
│ │ ─────────────────────────────> │ (Auth Server) │
│ │ client_id + client_secret │ │
│ │ │ │
│ │ (2) access_token (JWT) │ │
│ │ <───────────────────────────── │ │
└─────────────────┘ └─────────────────┘
│
│ (3) API Request with Bearer token
│
▼
┌─────────────────┐ ┌─────────────────┐
│ │ │ │
│ CRM API │ (4) Validate JWT + scopes │ Custom │
│ Gateway │ ─────────────────────────────> │ Authorizer │
│ │ │ (Lambda) │
│ │ (5) Allow/Deny │ │
│ │ <───────────────────────────── │ │
└─────────────────┘ └─────────────────┘
Terminal window
curl -X POST $OAUTH_TOKEN_ENDPOINT \
-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=crm-api/products.read crm-api/brands.read"

Raw HTTP format:

POST /oauth2/token HTTP/1.1
Host: {your-cognito-domain}
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET
&scope=crm-api/products.read%20crm-api/brands.read
ParameterDescriptionExample
grant_typeMust be client_credentialsclient_credentials
client_idYour 26-character client IDabc123xyz456def789ghi012
client_secretYour client secret (shown once at creation)qwertyuiop...
scopeSpace-separated list of requested scopescrm-api/products.read crm-api/brands.read
{
"access_token": "eyJraWQiOiJxYzVPXC9cL3FqV...",
"token_type": "Bearer",
"expires_in": 3600
}
{
"error": "invalid_client",
"error_description": "Client authentication failed"
}

Causes:

  • Incorrect client_id or client_secret
  • Client has been revoked
  • Credentials don’t match
{
"error": "invalid_scope",
"error_description": "Invalid scope"
}

Causes:

  • Requested scope not granted to this client
  • Typo in scope name (must be exact: crm-api/products.read)

Access tokens are JSON Web Tokens (JWT) with the following structure:

{
"kid": "qc5O//qjV...",
"alg": "RS256"
}
{
"sub": "abc123xyz456def789ghi012",
"token_use": "access",
"scope": "crm-api/products.read crm-api/brands.read",
"auth_time": 1732540800,
"iss": "https://cognito-idp.{REGION}.amazonaws.com/{REGION}_XXXXXX",
"exp": 1732544400,
"iat": 1732540800,
"version": 2,
"jti": "uuid-here",
"client_id": "abc123xyz456def789ghi012"
}
ClaimDescription
subSubject (client ID)
scopeGranted scopes (space-separated)
client_idM2M client identifier
expExpiration time (Unix timestamp)
iatIssued at time (Unix timestamp)
issIssuer (Cognito User Pool)

Include the access token in the Authorization header with the Bearer scheme:

GET /products?limit=10 HTTP/1.1
Host: {api-base-url}
Authorization: Bearer eyJraWQiOiJxYzVPXC9cL3FqV...

The API Gateway’s custom authorizer validates:

  1. Signature - JWT signature using Cognito public keys
  2. Expiration - Token must not be expired
  3. Issuer - Must match the Cognito User Pool
  4. Token Use - Must be an access token (not id token)
  5. Client Status - Client must be active (not revoked)
  6. Scopes - Token must have required scope for the endpoint
  • Lifetime: 1 hour (3600 seconds) for clients created from now on; clients created earlier keep 24 hours (86400 seconds). Always use expires_in from the token response.
  • Fixed: Cannot be extended or refreshed
  • Solution: Request a new token when expired

Best Practice: Cache tokens until they expire to minimize token requests.

class TokenCache {
constructor() {
this.token = null;
this.expiresAt = null;
}
async getToken() {
// Return cached token if still valid (with 5-minute buffer)
if (this.token && this.expiresAt > Date.now() + 300000) {
return this.token;
}
// Request new token
const response = await this.requestToken();
this.token = response.access_token;
this.expiresAt = Date.now() + (response.expires_in * 1000);
return this.token;
}
async requestToken() {
// ... token request implementation
}
}

When a client is revoked:

  1. Immediate: Client cannot request new tokens
  2. Existing tokens: Rejected with 403 from the next API request (if the revocation check is temporarily unavailable, existing tokens are accepted until they expire)

Total revocation time: Immediate for new tokens; from the next API request for tokens already issued.

DO:

  • ✅ Store secrets in environment variables or secret management systems (AWS Secrets Manager, HashiCorp Vault)
  • ✅ Use encrypted storage for secrets
  • ✅ Rotate secrets periodically
  • ✅ Restrict access to secrets (need-to-know basis)
  • ✅ Log secret access (audit trail)

DON’T:

  • ❌ Hardcode secrets in source code
  • ❌ Commit secrets to version control
  • ❌ Share secrets via email or chat
  • ❌ Log secrets in application logs
  • ❌ Expose secrets in client-side code

All API requests must use HTTPS. HTTP requests will be rejected.

Handle transient failures with exponential backoff:

async function requestWithRetry(url, options, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fetch(url, options);
} catch (error) {
if (attempt === maxRetries - 1) throw error;
// Exponential backoff: 1s, 2s, 4s
const delay = Math.pow(2, attempt) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
async function apiRequest(endpoint) {
try {
const response = await fetch(endpoint, {
headers: { Authorization: `Bearer ${cachedToken}` }
});
// Token expired or invalid
if (response.status === 401) {
cachedToken = await getNewToken();
return apiRequest(endpoint); // Retry with new token
}
return response;
} catch (error) {
// Handle error
}
}

Principle of Least Privilege:

  • Only request scopes your application actually needs
  • Create separate clients for different integrations
  • Review and audit scope grants periodically

Causes:

  • Wrong client_id or client_secret
  • Client has been revoked
  • Typo in credentials

Solution:

  • Verify credentials are correct
  • Check client status in admin panel
  • Regenerate client if secret is lost

Causes:

  • Requested scope not granted to client
  • Incorrect scope format

Solution:

  • Verify scope names match exactly (case-sensitive)
  • Check granted scopes in admin panel: crm-api/products.read (not products.read)

Causes:

  • Token expired (older than its expires_in lifetime)
  • Token not included in request
  • Wrong header format

Solution:

  • Implement token caching with expiration checks
  • Verify header format: Authorization: Bearer <token>
  • Check token expiration time (exp claim)

Causes:

  • Missing required scope
  • Client revoked (its existing tokens are rejected from the next API request)
  • Endpoint is not part of the M2M API surface

Solution:

  • Verify client has required scope for endpoint
  • If the client was revoked, its tokens are rejected and it cannot get new ones — create a new client to restore access
  • Check CloudWatch logs for detailed error messages
Terminal window
# 1. Get token
TOKEN=$(curl -s -X POST {token-endpoint} \
-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=crm-api/products.read" \
| jq -r '.access_token')
# 2. Use token
curl -X GET https://{api-base-url}/products \
-H "Authorization: Bearer $TOKEN"
  1. Import the Postman Collection
  2. Set collection variables (apiUrl, cognitoDomain, clientId, clientSecret)
  3. Use “Get Access Token” request (automatically saves token)
  4. Other requests automatically use saved token