DT Developer Docs
REST APIDT StudioStatus Page
  • Getting Started
  • Overview
  • Concepts
    • Devices
    • Events
    • Topics
      • Temperature Measurement Interval
      • Motion Sensor Activity Timer
  • Data Connectors
    • Introduction to Data Connectors
    • Creating a Data Connector
    • Configuring a Data Connector
    • Receiving Events
    • Best Practices
    • Example Integrations
      • Heroku
      • Google Cloud Functions
      • AWS Lambda
      • Azure HTTP Triggers
      • IBM Cloud Actions
    • Development Guides
      • Local Development with ngrok
  • REST API
  • Introduction to REST API
  • Explore Our Endpoints
    • with cURL
    • with Python API
    • with Postman
  • Authentication
    • OAuth2
    • Basic Auth
  • Error Codes
  • Emulator API
  • Examples
    • Pagination
    • Streaming Events
    • Touch to Identify
    • Refreshing Access Token
  • Reference
  • Status Page
  • Service Accounts
    • Introduction to Service Accounts
    • Creating a Service Account
    • Managing Access Rights
    • Permissions
    • Organizational Structures
  • Other
    • Application Notes
      • Generating a Room Temperature Heatmap
      • Modeling Fridge Content Temperatures
      • Outlier Detection on Multiple Temperature Sensors
      • Simple Temperature Forecasting for Substation Transformers
      • Sensor Data Insight with Power BI and Azure
      • Third-Party Sensor Data in DT Cloud
    • Frequently Asked Question
Powered by GitBook
On this page
  • Overview
  • Prerequisites
  • Example Code
  • Environment Setup
  • Source
  • Authentication Flow
  • Create the JWT
  • Exchange for Access Token
  • Access the REST API
  • Common Errors
  • Timing Error
  • Untrusted Entity Error
  • Signature Error
  • Unsupported Grant Type
  • Other
  • 403 Errors From REST API
  • Next Steps

Was this helpful?

  1. Authentication

OAuth2

A guide on how to implement an OAuth2 flow for authenticating our REST API.

Last updated 8 months ago

Was this helpful?

Overview

This guide aims to provide the basic understanding and ability to authenticate a Service Account for REST API integrations. Core concepts about the OAuth2 authentication flow are briefly explained, and an example implementation is provided. The implementation is tested using the returned access token to send a request to the REST API to list all available projects.

Prerequisites

  • Service Account Credentials A must be created with a membership in the target project. Any role will suffice.

Example Code

The provided example code can be summarized by the following points.

  • A JSON Web Token (JWT) is constructed and signed with a secret.

  • The JWT is exchanged for an access token.

  • The Access token is used to authenticate the REST API.

The implemented OAuth2 authentication flow is based on the .

Environment Setup

If you wish to run the code locally, make sure you have a working runtime environment.

The following packages are required by the example code and must be installed.

pyjwt==2.7.0
requests==2.31.0

The can be installed through pip.

pip install --upgrade disruptive

The following modules are required by the example code and must be installed.

npm install jsonwebtoken@9.0.1
npm install axios@1.4.0

The following packages are required by the example code and must be installed.

go get -u github.com/golang-jwt/jwt/v5@v5.0.0

Add the following environment variables as they will be used in the authentication flow.

export DT_SERVICE_ACCOUNT_KEY_ID=<YOUR_SERVICE_ACCOUNT_KEY_ID>
export DT_SERVICE_ACCOUNT_SECRET=<YOUR_SERVICE_ACCOUNT_SECRET>
export DT_SERVICE_ACCOUNT_EMAIL=<YOUR_SERVICE_ACCOUNT_EMAIL>

Source

import os
import time

import jwt
import requests

# Authentication details used in the OAuth2 flow.
SERVICE_ACCOUNT_KEY_ID = os.getenv('DT_SERVICE_ACCOUNT_KEY_ID', '')
SERVICE_ACCOUNT_SECRET = os.getenv('DT_SERVICE_ACCOUNT_SECRET', '')
SERVICE_ACCOUNT_EMAIL = os.getenv('DT_SERVICE_ACCOUNT_EMAIL', '')


def get_access_token(key_id, email, secret):
    # Verify none of the credentials are missing.
    for variable in [key_id, email, secret]:
        if len(variable) < 1:
            raise ValueError('One or more credentials not set.')

    # Construct the JWT header.
    jwt_headers = {
        'alg': 'HS256',
        'kid': key_id,
    }

    # Construct the JWT payload.
    now = time.time()
    jwt_payload = {
        'iat': int(now),         # current time in unixtime
        'exp': int(now) + 3600,  # expiration time in unixtime
        'aud': 'https://identity.disruptive-technologies.com/oauth2/token',
        'iss': email,
    }

    # Sign and encode JWT with the secret.
    encoded_jwt = jwt.encode(
        payload=jwt_payload,
        key=secret,
        algorithm='HS256',
        headers=jwt_headers,
    )

    # Prepare HTTP POST request data.
    # note: The requests package applies Form URL-Encoding by default.
    request_data = {
        'assertion': encoded_jwt,
        'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer'
    }

    # Exchange the JWT for an access token.
    access_token_response = requests.post(
        url='https://identity.disruptive-technologies.com/oauth2/token',
        headers={'Content-Type': 'application/x-www-form-urlencoded'},
        data=request_data,
    )

    # Halt if response contains an error.
    if access_token_response.status_code != 200:
        print('Status Code: {}'.format(access_token_response.status_code))
        print(access_token_response.json())
        return None

    # Return the access token in the request.
    return access_token_response.json()['access_token']


def main():
    # Get an access token using an OAuth2 authentication flow.
    access_token = get_access_token(
        SERVICE_ACCOUNT_KEY_ID,
        SERVICE_ACCOUNT_EMAIL,
        SERVICE_ACCOUNT_SECRET,
    )

    # Verify that we got a valid token back.
    if access_token is None:
        return

    # Test the token by sending a GET request for a list of projects.
    print(requests.get(
        url='https://api.disruptive-technologies.com/v2/projects',
        headers={'Authorization': 'Bearer ' + access_token},
    ).json())


if __name__ == '__main__':
    main()

Our Python API implements the OAuth2 flow for you, requiring no further action.

import os

import disruptive as dt

dt.default_auth = dt.Auth.service_account(
    key_id=os.getenv('DT_SERVICE_ACCOUNT_KEY_ID', ''),
    secret=os.getenv('DT_SERVICE_ACCOUNT_SECRET', ''),
    email=os.getenv('DT_SERVICE_ACCOUNT_EMAIL', ''),
)
const jwt = require('jsonwebtoken')
const axios = require('axios').default

// Authentication details used in the OAuth2 flow.
const serviceAccountKeyID = process.env.DT_SERVICE_ACCOUNT_KEY_ID
const serviceAccountSecret = process.env.DT_SERVICE_ACCOUNT_SECRET
const serviceAccountEmail = process.env.DT_SERVICE_ACCOUNT_EMAIL

// Creates a JWT from the arguments, and exchanges it for an 
// access token which is returned as a promise. If an error 
// occurs at any point, an error is thrown and the returned 
// promise is rejected.
async function getAccessToken(keyID, email, secret) {
    let credentials = [keyID, email, secret]
    for (let i = 0; i < credentials.length; i++) {
        if (credentials[i] === undefined) {
            throw new Error("One or more variables are missing.")
        }
    }

    // Construct the JWT header.
    let jwtHeaders = {
        'alg': 'HS256',
        'kid': keyID,
    }

    // Construct the JWT payload.
    const now = Date.now()
    let jwtPayload = {
        'iat': Math.floor(now / 1000),        // current time in  unixtime
        'exp': Math.floor(now / 1000) + 3600, // expiration time in unixtime
        'aud': 'https://identity.disruptive-technologies.com/oauth2/token',
        'iss': email,
    }

    // Sign and encode JWT with the secret.
    const jwtEncoded = jwt.sign(
        jwtPayload,
        secret,
        {
            header: jwtHeaders,
            algorithm: 'HS256',
        },
    )

    // Prepare POST request data.
    const requestObject = {
        'assertion': jwtEncoded,
        'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer',
    }

    // Converts the requestObject to a Form URL-Encoded string.
    const requestData = Object.keys(requestObject).map(function(key) {
        return encodeURIComponent(key) + '=' + encodeURIComponent(requestObject[key])
    }).join('&')

    // Exchange JWT for access token.
    const accessTokenResponse = await axios({
        method: 'POST',
        url: 'https://identity.disruptive-technologies.com/oauth2/token',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        data: requestData,
    }).catch(function(error) {
        // Prints the error response (if any), an re-throws the error.
        if (error.response) {
            console.log(error.response.data)
        }
        throw error
    })

    // Return the access token in the request.
    return accessTokenResponse.data.access_token
}
async function main() {
    // Get an access token using an OAuth2 authentication flow.
    const accessToken = await getAccessToken(
        serviceAccountKeyID,
        serviceAccountEmail,
        serviceAccountSecret,
    )

    // Test the token by sending a GET request for a list of projects.
    const response = await axios({
        method: 'GET',
        url: 'https://api.disruptive-technologies.com/v2/projects',
        headers: { 'Authorization': 'Bearer ' + accessToken },
    })

    // Print response data.
    console.log(JSON.stringify(response.data, null, 2))
}
main()
package main

import (
	"encoding/json"
	"errors"
	"fmt"
	"log"
	"net/http"
	"net/url"
	"os"
	"strings"
	"time"

	jwt "github.com/golang-jwt/jwt/v5"
)

const (
	// Used to exchange a JWT for an access token.
	tokenEndpoint = "https://identity.disruptive-technologies.com/oauth2/token"
	// Base URL for the Disruptive REST API.
	apiBaseUrl = "https://api.disruptive-technologies.com/v2"
)

type AuthResponse struct {
	// The access token used to access the Disruptive REST API.
	AccessToken string `json:"access_token"`
	// The type of token this is. Will typically be "Bearer".
	TokenType string `json:"token_type"`
	// How many seconds until the token expires. Will typically be 3600.
	ExpiresIn int `json:"expires_in"`
}

func getAccessToken(keyID string, secret string, email string) (*AuthResponse, error) {
	// Check that none of the credentials are missing.
	var credentials = []string{keyID, secret, email}
	for _, value := range credentials {
		if value == "" {
			return nil, errors.New("One or more credentials missing")
		}
	}

	// Construct the JWT header.
	jwtHeader := map[string]interface{}{
		"alg": "HS256",
		"kid": keyID,
	}

	// Construct the JWT payload.
	now := time.Now()
	jwtPayload := &jwt.RegisteredClaims{
		Issuer:    email,
		Audience:  jwt.ClaimStrings{tokenEndpoint},
		IssuedAt:  jwt.NewNumericDate(now),
		ExpiresAt: jwt.NewNumericDate(now.Add(time.Hour)),
	}

	// Sign and encode JWT with the secret.
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwtPayload)
	token.Header = jwtHeader
	encodedJwt, _ := token.SignedString([]byte(secret))

	// Prepare HTTP POST request data.
	// NOTE: The body must be Form URL-Encoded.
	reqData := url.Values{
		"assertion":  {encodedJwt},
		"grant_type": {"urn:ietf:params:oauth:grant-type:jwt-bearer"},
	}.Encode()

	// Create the request to exchange the JWT for an access token.
	req, err := http.NewRequest(
		"POST",
		tokenEndpoint,
		strings.NewReader(reqData),
	)
	if err != nil {
		return nil, err
	}

	// Set Content-Type header to specify that our body is Form-URL Encoded.
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	// Exchange the JWT for an access token. Set a 3 second
	// timeout in case the server can't be reached.
	client := &http.Client{Timeout: time.Second * 3}
	res, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer res.Body.Close()

	// Decode the response body to an AuthResponse.
	var authResponse AuthResponse
	if err := json.NewDecoder(res.Body).Decode(&authResponse); err != nil {
		return nil, err
	}

	// Return the AuthResponse, which contains the access token.
	return &authResponse, nil
}
func listProjects(auth *AuthResponse) error {
	// Create the request to get a list of projects from the Disruptive REST API
	req, err := http.NewRequest("GET", apiBaseUrl+"/projects", nil)
	if err != nil {
		return err
	}

	// Set the Authorization header by specifying both the token type
	// (which will typically be "Bearer") as well as the access token
	req.Header.Set("Authorization", fmt.Sprintf("%s %s", auth.TokenType, auth.AccessToken))

	// Create an http Client with a timeout
	// Send the GET request to list all projects.
	// Set a 3 second timeout in case the server can't be reached.
	client := &http.Client{Timeout: time.Second * 3}
	resp, err := client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	// Define a struct with the format we expect the response
	// to be in. See the REST API Reference for more details.
	type ProjectsResponse struct {
		Projects []struct {
			Name                    string `json:"name"`
			DisplayName             string `json:"displayName"`
			Inventory               bool   `json:"inventory"`
			Organization            string `json:"organization"`
			OrganizationDisplayName string `json:"organizationDisplayName"`
			SensorCount             int    `json:"sensorCount"`
			CloudConnectorCount     int    `json:"cloudConnectorCount"`
		}
		NextPageToken string `json:"nextPageToken"`
	}

	// Decode the response into a ProjectsResponse.
	var projectsResponse ProjectsResponse
	if err = json.NewDecoder(resp.Body).Decode(&projectsResponse); err != nil {
		return err
	}

	// Print the name of each project, and how many device each contain.
	for _, project := range projectsResponse.Projects {
		fmt.Println(project.DisplayName)
		fmt.Printf("  %d Sensors\n", project.SensorCount)
		fmt.Printf("  %d Cloud Connectors\n", project.CloudConnectorCount)
	}

	return nil
}

func main() {
	// OAuth2 authentication flow.
	auth, err := getAccessToken(
		os.Getenv("DT_SERVICE_ACCOUNT_KEY_ID"),
		os.Getenv("DT_SERVICE_ACCOUNT_SECRET"),
		os.Getenv("DT_SERVICE_ACCOUNT_EMAIL"),
	)
	if err != nil {
		log.Fatal(err)
	}

	// Test the access token by listing all the projects our
	// Service Account has access to.
	if err := listProjects(auth); err != nil {
		log.Fatal(err)
	}
}

Authentication Flow

Authenticating a client is a 3 step process, as illustrated below. Using both the figure and example code, the following sections will explain in more detail what each step entails.

Create the JWT

A JSON Web Token (JWT) contains three fields:

  1. Header: Token type and signature algorithm.

  2. Payload: Claims and additional data.

  3. Signature: A signature calculated of the entire JWT + a private secret.

Before being sent, these fields are each Base64Url encoded. They are combined in a compact dot format in the form Base64Url(header).Base64Url(payload).Base64Url(signature), which is what we will refer to as the encoded JWT.

Using your Service Account credentials, construct the JWT headers and payload. Here, iat is the issuing time, and exp the expiration time of a maximum 1 hour after iat.

# Construct the JWT header.
jwt_headers = {
    'alg': 'HS256',
    'kid': key_id,
}

# Construct the JWT payload.
jwt_payload = {
    'iat': int(time.time()),        # current unixtime
    'exp': int(time.time()) + 3600, # expiration unixtime
    'aud': 'https://identity.disruptive-technologies.com/oauth2/token',
    'iss': email,
}
// Construct the JWT header.
let jwtHeaders = {
    'alg': 'HS256',
    'kid': keyID,
}

// Construct the JWT payload.
let jwtPayload = {
    'iat': Math.floor(Date.now() / 1000),        // current unixtime
    'exp': Math.floor(Date.now() / 1000) + 3600, // expiration unixtime
    'aud': 'https://identity.disruptive-technologies.com/oauth2/token',
    'iss': email,
}
// Construct the JWT header
jwtHeader := map[string]interface{}{
	"alg": "HS256",
	"kid": keyID,
}

// Construct the JWT payload
now := time.Now()
jwtPayload := &jwt.RegisteredClaims{
	Issuer:    email,
	Audience:  jwt.ClaimStrings{tokenEndpoint},
	IssuedAt:  jwt.NewNumericDate(now),
	ExpiresAt: jwt.NewNumericDate(now.Add(time.Hour)),
}

The simplest way of Base64-encoding and signing our JWT is to use some language-specific library. This is available in most languages but can be done manually if desired.

# Sign and encode JWT with the secret.
encoded_jwt = jwt.encode(
    payload   = jwt_payload,
    key       = secret,
    algorithm = 'HS256',
    headers   = jwt_headers,
)
// Sign and encode JWT with the secret.
const jwtEncoded = jwt.sign(
    jwtPayload,
    secret,
    {
        header: jwtHeaders,
        algorithm: 'HS256',
    },
)
// Sign and encode JWT with the secret
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwtPayload)
token.Header = jwtHeader
encodedJwt, _ := token.SignedString([]byte(secret))

Exchange for Access Token

The encoded JWT is exchanged for an Access Token by sending a POST request to the same endpoint used to construct the JWT, namely https://identity.disruptive-technologies.com/oauth2/token.

The POST request header should include a Content-Type field indicating the format of the body. Additionally, the POST request body is Form URL-Encoded and contains the following fields:

  1. "assertion" - Contains the encoded JWT string.

  2. "grant_type" - Contains the string "urn:ietf:params:oauth:grant-type:jwt-bearer". This specifies that you want to exchange a JWT for an Access Token.

assertion=Base64Url(header).Base64Url(payload).Base64Url(signature)&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer

The header, payload, and signature are found in the previous step.

# Prepare HTTP POST request data.
# Note: The requests package applies Form URL-Encoding by default.
request_data = {
    'assertion': encoded_jwt,
    'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer'
}

# Exchange the JWT for an access token.
access_token_response = requests.post(
    url = 'https://identity.disruptive-technologies.com/oauth2/token',
    headers = { 'Content-Type': 'application/x-www-form-urlencoded' },
    data = request_data,
)
// Prepare POST request data.
const requestObject = {
    'assertion': jwtEncoded,
    'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer',
}

// Converts the requestObject to a Form URL-Encoded string.
const requestData = Object.keys(requestObject).map(function(key) {
    return encodeURIComponent(key) + '=' + encodeURIComponent(requestObject[key])
}).join('&')

// Exchange JWT for access token.
const accessTokenResponse = await axios({
    method: 'POST',
    url: 'https://identity.disruptive-technologies.com/oauth2/token',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    data: requestData,
}).catch(function (error) {
    if (error.response) {
        console.log(error.response.data)
    }
    throw error
})
// Prepare HTTP POST request data.
// NOTE: The body must be Form URL-Encoded
reqData := url.Values{
	"assertion":  {encodedJwt},
	"grant_type": {"urn:ietf:params:oauth:grant-type:jwt-bearer"},
}.Encode()

// Create the request to exchange the JWT for an access token
req, err := http.NewRequest(
	"POST",
	tokenEndpoint,
	strings.NewReader(reqData),
)
if err != nil {
	return nil, err
}

// Set Content-Type header to specify that our body
// is Form-URL Encoded
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

// Exchange the JWT for an access token. Set a 3 second
// timeout in case the server can't be reached.
client := &http.Client{Timeout: time.Second * 3}
res, err := client.Do(req)
if err != nil {
	return nil, err
}
defer res.Body.Close()

The access token response can be expected to have the following format. It will be valid for 1 hour only. To get a new token, perform the previous steps again.

{
    "access_token": "d663e83546294b158fea2574a1945319",
    "token_type": "bearer",
    "expires_in": 3599
}

Cache your access token

The access token should be cached so that you can re-use it for all subsequent API calls. This will improve the overall performance of your integration.

A new access token should only be created a short while before the access token expires (for example a minute before), or if you receive a 401 status code from the API, indicating that you have an invalid access token.

Access the REST API

Once you have the Access Token, you need to include this with every call to the API. This can be achieved by including the Authorization header in the form shown in the snippet below.

# Test the access token by sending a GET request to get a list of projects.
print(requests.get(
    url = 'https://api.disruptive-technologies.com/v2/projects',
    headers = {'Authorization': 'Bearer ' + access_token},
).json())
// Test the token by sending a GET request for a list of projects.
const response = await axios({
    method: 'GET',
    url: 'https://api.disruptive-technologies.com/v2/projects',
    headers: { 'Authorization': 'Bearer ' + accessToken },
})
// Test the access token by listing all the projects our
// Service Account has access to
if err := listProjects(auth); err != nil {
	log.Fatal(err)
}

Common Errors

This section covers the most commonly encountered errors while setting up Service Account authentication using OAuth2.

Note that all errors returned from the oauth2/token with an HTTP status code of 400 indicates that there is something incorrect in your implementation. The specific errors returned from this endpoint might change in the future, so you should avoid writing logic to try to figure out which type of error you got. Instead, you should log out the response body you got back to see what's wrong and update your implementation accordingly.

Timing Error

{
    "error": "invalid_grant", 
    "error_description": "Timing-related error. Check the 'exp' and 'iat' claims."
}

There is something incorrect with either the issue time (iat) or the expiration time (exp) claims in your JWT. Make sure that both claims are set, and have the correct values according to the documentation above.

Here are some typical reasons why you might receive this error:

  • Either the iat or exp claims (or both) are missing in the JWT.

  • The iat claim is too far in the past or future compared to "now".

  • The exp claim is more than 3600 seconds past iat or before iat.

Untrusted Entity Error

{
    "error": "invalid_grant", 
    "error_description": "Untrusted entity. Check the 'aud' and 'iss' claims."
}

This might be caused by one of the following reasons:

  • Either the audience (aud) claim or issuer (iss) claim are missing.

  • The aud claim is not set to https://identity.disruptive-technologies.com/oauth2/token.

  • The iss claim is not set to the email of the Service Account.

Signature Error

{
    "error": "invalid_grant", 
    "error_description": "Invalid signature"
}

The JWT has not been signed correctly. Make sure you have used the correct secret to sign the JWT. It should be the secret of the Service Account Key that corresponds to the key id used in the kid field of the JWT header.

Unsupported Grant Type

{
    "error": "unsupported_grant_type"
}

This might be caused by one of the following reasons:

  • The Content-Type HTTP header is not set to application/x-www-form-urlencoded.

  • The grant_type field is missing from the POST request body.

  • The body of the POST request is not Form URL-encoded.

Other

{
    "error": "invalid_grant"
}

This might be caused by one of the following reasons:

  • The Service Account doesn't exist.

  • The Service Account key doesn't exist.

  • The assertion field of the request (the encoded JWT) is missing or malformed. Make sure that the JWT is formatted correctly, and that the entire request body is Form URL-encoded as described in the documentation above.

403 Errors From REST API

Next Steps

This guide will not cover much more details of JWT. For a great introduction on JWT that provides an interactive editor and an exhaustive list of client libraries, please see .

It is important to note that the data has to be Form URL-Encoded. Like Python's , some libraries do this by default and require no further input by the user. This is, however, not the norm and likely requires an additional step before sending the request. The URL Form Encoded data should have the following format.

Check the section at the bottom to troubleshoot errors.

For an overview of all REST API errors, see the reference page.

The computer running the code does not have its clock set correctly. Make sure your computer is regularly doing , and check to verify that your computer's clock is set correctly.

It's recommended to use a library to do the heavy lifting of signing the JWT for you. Check out for a selection of recommended libraries in many different languages. If you're writing the JWT signature implementation yourself, check out for more information on how to do this.

If you get a 403 HTTP status code from the REST API when using the access token you just generated, it's likely not an issue with the authentication. The most common reason for this is that the Service Account you are authenticating as does not yet have access to the resource you're trying to access. Before a Service Account can use the API it needs to be a member of either a project or an organization. See the page for more information about how to add a Service Account as a member. The page gives some guidance for when to add Service Accounts as members on a project vs. on an organization.

You are now familiar with the OAuth2 authentication routine, but it must still be refreshed every hour for integrations that run continuously. For this, see our example on .

Service Account
RFC7523 specification
Disruptive Technologies Python API
jwt.io
requests
Error Codes
NTP syncs
time.is
jwt.io
this guide
Managing Access Rights
Organizational Structures
Refreshing Access Token
Common Errors