Authenticating with Private Key JWT

This document outlines the process for clients to authenticate with our API using Private Key JWT.

Overview

Private Key JWT is a client authentication method that uses asymmetric cryptography to enhance security. Clients generate a JWT signed with their private key, which is then verified by the server using the corresponding public key.

This article focuses on the DigitalOwl US region. If your account is set in a different region, please refer to the [Other Regions](## Other Regions) section below for region-specific configurations.

Prerequisites

  1. Generate an RSA Key Pair: Clients must generate a public and private RSA key pair in .PEM format. The private key should be stored securely and never shared. The public key will be shared with DigitalOwl.
    1. Supported Algorithms: When generating your key pair, you can choose from the following algorithms:
      1. RS256
      2. PS256
      3. RS384
  2. Contact Your Customer Success Manager (CSM): Send your public key to your designated CSM.
  3. Obtain Client ID and Key ID: After processing your public key, your CSM will provide you with a unique Client ID and a Key ID (kid).

Steps to Authenticate

Build The Client Assertion

Create a JSON Web Token (JWT) with the following claims:

  • Header:
  • alg: Algorithm used to sign the assertion (e.g., RS256, PS256, RS384).
  • kid: Key ID provided by your CSM.
  • Payload:
    • iss: Client ID provided by your CSM.
    • sub: Client ID provided by your CSM.
    • aud: https://auth0.us.digitalowl.app/
    • iat: Issued At timestamp in seconds since Unix epoch (e.g., 1700000000)
    • exp: Expiration timestamp in seconds since Unix epoch (e.g., 1700000060).
      • Recommendation: Set the expiration time (exp) to be within 1 minute of the iat timestamp.
      • Maximum Limit: The expiration time must be less than 5 minutes after the iat timestamp (i.e., exp - iat < 300).
    • jti: A unique identifier for the JWT (e.g., a UUID).

Sign this JWT using your private key.

Example:

const { SignJWT } = require('jose');
const crypto = require('crypto');
const uuid = require('uuid');

async function createClientAssertion() {
  const privateKeyPEM = crypto.createPrivateKey({
    key: `-----BEGIN PRIVATE KEY-----
YOUR_PRIVATE_KEY_HERE
-----END PRIVATE KEY-----`,
    format: 'pem',
  });

  const jwt = await new SignJWT({})
    .setProtectedHeader({ alg: 'RS256', kid: 'YOUR_KEY_ID' })
    .setIssuedAt() // Sets `iat` automatically
    .setIssuer('YOUR_CLIENT_ID')
    .setSubject('YOUR_CLIENT_ID')
    .setAudience('https://auth0.us.digitalowl.app/')
    .setExpirationTime('1m') // Expiration time set to 1 minute after `iat`
    .setJti(uuid.v4()) // Unique identifier for the token
    .sign(privateKeyPEM);

  return jwt;
}
import jwt  # PyJWT library (install with: pip install pyjwt)
import uuid
import datetime

def create_client_assertion(client_id, private_key, key_id):
    """
    Create a signed JWT for Private Key JWT authentication.

    Args:
        client_id (str): The client ID provided by your CSM.
        private_key (str): Your private RSA key in PEM format.
        key_id (str): The key ID (kid) provided by your CSM.

    Returns:
        str: A signed JWT.
    """
    # Define JWT headers
    headers = {
        "alg": "RS256",  # Algorithm
        "kid": key_id    # Key ID
    }

    # Define JWT payload
    now = datetime.datetime.utcnow()
    payload = {
        "iss": client_id,  # Issuer (Client ID)
        "sub": client_id,  # Subject (Client ID)
        "aud": "https://auth0.us.digitalowl.app/",  # Auth0 domain as audience
        "iat": int(now.timestamp()),  # Issued At (current time in seconds)
        "exp": int((now + datetime.timedelta(minutes=1)).timestamp()),  # Expiration (1 minute later)
        "jti": str(uuid.uuid4()),  # Unique identifier
    }

    # Sign the JWT using the private key
    signed_jwt = jwt.encode(payload, private_key, algorithm="RS256", headers=headers)

    return signed_jwt


# Example usage
if __name__ == "__main__":
    # Replace these with your actual values
    CLIENT_ID = "your-client-id"
    PRIVATE_KEY = """-----BEGIN PRIVATE KEY-----
YOUR_PRIVATE_KEY_HERE
-----END PRIVATE KEY-----"""
    KEY_ID = "your-key-id"

    jwt_token = create_client_assertion(CLIENT_ID, PRIVATE_KEY, KEY_ID)
    print(f"Generated JWT: {jwt_token}")

Exchange the Assertion for an Access Token

Make a POST request to the Auth0 token endpoint with the following parameters:

  • grant_type: client_credentials
  • client_assertion_type: urn:ietf:params:oauth:client-assertion-type:jwt-bearer
  • client_assertion: The JWT you created.
  • audience: https://api.us.digitalowl.app

Example:

const axios = require('axios');

async function exchangeAssertionForToken(clientAssertion) {
  const tokenEndpoint = 'https://auth0.us.digitalowl.app/oauth/token';

  try {
    const response = await axios.post(tokenEndpoint, {
      grant_type: 'client_credentials',
      client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
      client_assertion: clientAssertion,
      audience: 'https://api.us.digitalowl.app',
    });

    console.log('Access Token:', response.data.access_token);
    return response.data.access_token;
  } catch (error) {
    console.error('Error exchanging assertion for token:', error.response?.data || error.message);
  }
}

// Example usage
const clientAssertion = 'YOUR_SIGNED_JWT'; // Replace with your signed JWT
exchangeAssertionForToken(clientAssertion);
import requests

def exchange_assertion_for_token(client_assertion):
    """
    Exchange the signed JWT (client assertion) for an access token.

    Args:
        client_assertion (str): The signed JWT.

    Returns:
        str: Access token.
    """
    token_endpoint = 'https://auth0.us.digitalowl.app/oauth/token'
    audience = 'https://api.us.digitalowl.app'

    data = {
        'grant_type': 'client_credentials',
        'client_assertion_type': 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
        'client_assertion': client_assertion,
        'audience': audience,
    }

    headers = {
        'Content-Type': 'application/x-www-form-urlencoded',
    }

    try:
        response = requests.post(token_endpoint, data=data, headers=headers)
        response.raise_for_status()

        access_token = response.json().get('access_token')
        print('Access Token:', access_token)
        return access_token
    except requests.exceptions.RequestException as e:
        print('Error exchanging assertion for token:', e.response.json() if e.response else e)
        raise

# Example usage
if __name__ == "__main__":
    client_assertion = "YOUR_SIGNED_JWT"  # Replace with your signed JWT
    exchange_assertion_for_token(client_assertion)
curl --request POST 'https://auth0.us.digitalowl.app/oauth/token' \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'grant_type=client_credentials' \
  --data-urlencode 'client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer' \
  --data-urlencode 'client_assertion=YOUR_SIGNED_JWT' \
  --data-urlencode 'audience=https://api.us.digitalowl.app'

Token Caching

After retrieving the access token, clients must cache the token to avoid unnecessary calls to the token endpoint. The access token is valid for 60 minutes. During this validity period, the token should be reused for all API calls, instead of requesting a new token.

❗️

Warning: Frequent calls to the token endpoint will trigger rate limits and prevent service.

Using the API with the Access Token

Once you have the access token, include it in the Authorization header to call the api.

Example:

const axios = require('axios');

async function getCases(accessToken) {
  const apiEndpoint = 'https://api.us.digitalowl.app/cases';

  try {
    const response = await axios.get(apiEndpoint, {
      headers: {
        Authorization: `Bearer ${accessToken}`,
        'Content-Type': 'application/json',
      },
    });

    console.log('Cases:', response.data);
    return response.data;
  } catch (error) {
    console.error('Error fetching cases:', error.response?.data || error.message);
  }
}

// Example usage
const accessToken = 'YOUR_ACCESS_TOKEN'; // Replace with your access token
getCases(accessToken);
import requests

def get_cases(access_token):
    """
    Fetch cases from the /cases endpoint using the provided access token.

    Args:
        access_token (str): The access token obtained after exchanging the JWT.

    Returns:
        dict: API response containing cases or an error message.
    """
    api_endpoint = "https://api.us.digitalowl.app/cases"
    headers = {
        "Authorization": f"Bearer {access_token}",
        "Content-Type": "application/json"
    }

    try:
        response = requests.get(api_endpoint, headers=headers)
        response.raise_for_status()  # Raise an HTTPError for bad responses (4xx, 5xx)
        print("Cases:", response.json())
        return response.json()
    except requests.exceptions.RequestException as e:
        print("Error fetching cases:", e.response.json() if e.response else e)
        raise

# Example usage
if __name__ == "__main__":
    access_token = "YOUR_ACCESS_TOKEN"  # Replace with your access token
    get_cases(access_token)
curl --request GET 'https://api.us.digitalowl.app/cases' \
  --header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
  --header 'Content-Type: application/json'

Other Regions

If your account is set in a region other than the US, update the Auth0 and API URLs in your implementation to reflect the appropriate region code. Below are the region-specific configurations:

Supported Regions

  1. US (United States):
    • Auth0 URL: https://auth0.us.digitalowl.app/
    • API URL: https://api.us.digitalowl.app
  2. CA (Canada):
    • Auth0 URL: https://auth0.ca.digitalowl.app/
    • API URL: https://api.ca.digitalowl.app
  3. IL (Israel)
    • Auth0 URL: https://auth0.il.digitalowl.app/
    • API URL: https://api.il.digitalowl.app

If your organization uses a private instance, please contact your Customer Success Manager (CSM) for the correct configuration details.