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

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 Service Account 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 RFC7523 specification.

Environment Setup

If you wish to run the code locally, make sure you have a working runtime environment.
Python 3.9
Python API
Node.js 14
Go 1.16
The following packages are required by the example code and must be installed.
1
pip install pyjwt==2.1.0
2
pip install requests==2.25.1
Copied!
The Disruptive Technologies Python API can be installed through pip.
1
pip install --upgrade disruptive
Copied!
The following modules are required by the example code and must be installed.
1
npm install [email protected]
2
npm install [email protected]
Copied!
The following packages are required by the example code and must be installed.
1
go get github.com/golang-jwt/jwt/[email protected]
Copied!
Add the following environment variables as they will be used in the authentication flow.
Bash
1
export DT_SERVICE_ACCOUNT_KEY_ID=<YOUR_SERVICE_ACCOUNT_KEY_ID>
2
export DT_SERVICE_ACCOUNT_SECRET=<YOUR_SERVICE_ACCOUNT_SECRET>
3
export DT_SERVICE_ACCOUNT_EMAIL=<YOUR_SERVICE_ACCOUNT_EMAIL>
Copied!

Source

Python 3.9
Python API
Node.js 14
Go 1.17
1
import os
2
import time
3
import jwt # pip install pyjwt==2.1.0
4
import requests # pip install requests==2.25.1
5
6
# Authentication details used in the OAuth2 flow.
7
SERVICE_ACCOUNT_KEY_ID = os.environ.get('DT_SERVICE_ACCOUNT_KEY_ID')
8
SERVICE_ACCOUNT_SECRET = os.environ.get('DT_SERVICE_ACCOUNT_SECRET')
9
SERVICE_ACCOUNT_EMAIL = os.environ.get('DT_SERVICE_ACCOUNT_EMAIL')
10
11
12
def get_access_token(key_id, email, secret):
13
# Construct the JWT header.
14
jwt_headers = {
15
'alg': 'HS256',
16
'kid': key_id,
17
}
18
19
# Construct the JWT payload.
20
jwt_payload = {
21
'iat': int(time.time()), # current unixtime
22
'exp': int(time.time()) + 3600, # expiration unixtime
23
'aud': 'https://identity.disruptive-technologies.com/oauth2/token',
24
'iss': email,
25
}
26
27
# Sign and encode JWT with the secret.
28
encoded_jwt = jwt.encode(
29
payload=jwt_payload,
30
key=secret,
31
algorithm='HS256',
32
headers=jwt_headers,
33
)
34
35
# Prepare HTTP POST request data.
36
# note: The requests package applies Form URL-Encoding by default.
37
request_data = {
38
'assertion': encoded_jwt,
39
'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer'
40
}
41
42
# Exchange the JWT for an access token.
43
access_token_response = requests.post(
44
url='https://identity.disruptive-technologies.com/oauth2/token',
45
headers={'Content-Type': 'application/x-www-form-urlencoded'},
46
data=request_data,
47
)
48
49
# Halt if response contains an error.
50
if access_token_response.status_code != 200:
51
print('Status Code: {}'.format(access_token_response.status_code))
52
print(access_token_response.json())
53
return None
54
55
# Return the access token in the request.
56
return access_token_response.json()['access_token']
57
58
59
def main():
60
# Get an access token using an OAuth2 authentication flow.
61
access_token = get_access_token(
62
SERVICE_ACCOUNT_KEY_ID,
63
SERVICE_ACCOUNT_EMAIL,
64
SERVICE_ACCOUNT_SECRET,
65
)
66
67
# Verify that we got a valid token back.
68
if access_token is None:
69
return
70
71
# Test the token by sending a GET request for a list of projects.
72
print(requests.get(
73
url='https://api.disruptive-technologies.com/v2/projects',
74
headers={'Authorization': 'Bearer ' + access_token},
75
).json())
76
77
78
if __name__ == '__main__':
79
main()
Copied!
1
import os
2
import disruptive as dt
3
4
# It is good practice to fetch credentials from an environment or file.
5
key_id = os.environ.get('DT_SERVICE_ACCOUNT_KEY_ID', '')
6
secret = os.environ.get('DT_SERVICE_ACCOUNT_SECRET', '')
7
email = os.environ.get('DT_SERVICE_ACCOUNT_EMAIL', '')
8
9
# Using the fetched credentials, authenticate the package.
10
dt.default_auth = dt.Auth.service_account(key_id, secret, email)
Copied!
1
const jwt = require('jsonwebtoken') // npm install [email protected]
2
const axios = require('axios').default // npm install [email protected]
3
4
// authentication details
5
const serviceAccountKeyID = process.env.DT_SERVICE_ACCOUNT_KEY_ID
6
const serviceAccountSecret = process.env.DT_SERVICE_ACCOUNT_SECRET
7
const serviceAccountEmail = process.env.DT_SERVICE_ACCOUNT_EMAIL
8
9
// Creates a JWT from the arguments, and exchanges it for an
10
// access token which is returned as a promise. If an error
11
// occurs at any point, an error is thrown and the returned
12
// promise is rejected.
13
async function getAccessToken(keyID, email, secret) {
14
// Construct the JWT header.
15
let jwtHeaders = {
16
'alg': 'HS256',
17
'kid': keyID,
18
}
19
20
// Construct the JWT payload.
21
let jwtPayload = {
22
'iat': Math.floor(Date.now() / 1000), // current unixtime
23
'exp': Math.floor(Date.now() / 1000) + 3600, // expiration unixtime
24
'aud': 'https://identity.disruptive-technologies.com/oauth2/token',
25
'iss': email,
26
}
27
28
// Sign and encode JWT with the secret.
29
const jwtEncoded = jwt.sign(
30
jwtPayload,
31
secret,
32
{
33
header: jwtHeaders,
34
algorithm: 'HS256',
35
},
36
)
37
38
// Prepare POST request data.
39
const requestObject = {
40
'assertion': jwtEncoded,
41
'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer',
42
}
43
44
// Converts the requestObject to a Form URL-Encoded string.
45
const requestData = Object.keys(requestObject).map(function(key) {
46
return encodeURIComponent(key) + '=' + encodeURIComponent(requestObject[key])
47
}).join('&')
48
49
// Exchange JWT for access token.
50
const accessTokenResponse = await axios({
51
method: 'POST',
52
url: 'https://identity.disruptive-technologies.com/oauth2/token',
53
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
54
data: requestData,
55
}).catch(function (error) {
56
// Prints the error response (if any), an re-throws the error.
57
if (error.response) {
58
console.log(error.response.data)
59
}
60
throw error
61
})
62
63
// Return the access token in the request.
64
return accessTokenResponse.data.access_token
65
}
66
async function main() {
67
// Get an access token using an OAuth2 authentication flow.
68
const accessToken = await getAccessToken(
69
serviceAccountKeyID,
70
serviceAccountEmail,
71
serviceAccountSecret,
72
)
73
74
// Test the token by sending a GET request for a list of projects.
75
const response = await axios({
76
method: 'GET',
77
url: 'https://api.disruptive-technologies.com/v2/projects',
78
headers: { 'Authorization': 'Bearer ' + accessToken },
79
})
80
81
// Print response data.
82
console.log(JSON.stringify(response.data, null, 2))
83
}
84
main()
Copied!
1
package main
2
3
import (
4
"encoding/json"
5
"fmt"
6
"log"
7
"net/http"
8
"net/url"
9
"os"
10
"strings"
11
"time"
12
13
jwt "github.com/golang-jwt/jwt/v4" // go get github.com/golang-jwt/jwt/[email protected]
14
)
15
16
const (
17
// Used to exchange a JWT for an access token
18
tokenEndpoint = "https://identity.disruptive-technologies.com/oauth2/token"
19
// The base URL for the Disruptive REST API
20
apiBaseUrl = "https://api.disruptive-technologies.com/v2"
21
)
22
23
type AuthResponse struct {
24
// The access token used to access the Disruptive REST API
25
AccessToken string `json:"access_token"`
26
// The type of token this is. Will typically be "Bearer"
27
TokenType string `json:"token_type"`
28
// How many seconds until the token expires. Will typically be 3600
29
ExpiresIn int `json:"expires_in"`
30
}
31
32
func getAccessToken(keyID string, secret string, email string) (*AuthResponse, error) {
33
// Construct the JWT header
34
jwtHeader := map[string]interface{}{
35
"alg": "HS256",
36
"kid": keyID,
37
}
38
39
// Construct the JWT payload
40
now := time.Now()
41
jwtPayload := &jwt.RegisteredClaims{
42
Issuer: email,
43
Audience: jwt.ClaimStrings{tokenEndpoint},
44
IssuedAt: jwt.NewNumericDate(now),
45
ExpiresAt: jwt.NewNumericDate(now.Add(time.Hour)),
46
}
47
48
// Sign and encode JWT with the secret
49
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwtPayload)
50
token.Header = jwtHeader
51
encodedJwt, _ := token.SignedString([]byte(secret))
52
53
// Prepare HTTP POST request data.
54
// NOTE: The body must be Form URL-Encoded
55
reqData := url.Values{
56
"assertion": {encodedJwt},
57
"grant_type": {"urn:ietf:params:oauth:grant-type:jwt-bearer"},
58
}.Encode()
59
60
// Create the request to exchange the JWT for an access token
61
req, err := http.NewRequest(
62
"POST",
63
tokenEndpoint,
64
strings.NewReader(reqData),
65
)
66
if err != nil {
67
return nil, err
68
}
69
70
// Set Content-Type header to specify that our body
71
// is Form-URL Encoded
72
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
73
74
// Exchange the JWT for an access token. Set a 3 second
75
// timeout in case the server can't be reached.
76
client := &http.Client{Timeout: time.Second * 3}
77
res, err := client.Do(req)
78
if err != nil {
79
return nil, err
80
}
81
defer res.Body.Close()
82
83
// Decode the response body to an AuthResponse
84
var authResponse AuthResponse
85
if err := json.NewDecoder(res.Body).Decode(&authResponse); err != nil {
86
return nil, err
87
}
88
89
// Return the AuthResponse, which contains the access token
90
return &authResponse, nil
91
}
92
func listProjects(auth *AuthResponse) error {
93
// Create the request to get a list of projects from the
94
// Disruptive REST API
95
req, err := http.NewRequest("GET", apiBaseUrl+"/projects", nil)
96
if err != nil {
97
return err
98
}
99
100
// Set the Authorization header by specifying both the token type
101
// (which will typically be "Bearer") as well as the access token
102
req.Header.Set("Authorization", fmt.Sprintf("%s %s", auth.TokenType, auth.AccessToken))
103
104
// Create an http Client with a timeout
105
// Send the GET request to list all projects.
106
// Set a 3 second timeout in case the server can't be reached.
107
client := &http.Client{Timeout: time.Second * 3}
108
resp, err := client.Do(req)
109
if err != nil {
110
return err
111
}
112
defer resp.Body.Close()
113
114
// Define a struct with the format we expect the response
115
// to be in. See the REST API Reference for more details.
116
type ProjectsResponse struct {
117
Projects []struct {
118
Name string `json:"name"`
119
DisplayName string `json:"displayName"`
120
Inventory bool `json:"inventory"`
121
Organization string `json:"organization"`
122
OrganizationDisplayName string `json:"organizationDisplayName"`
123
SensorCount int `json:"sensorCount"`
124
CloudConnectorCount int `json:"cloudConnectorCount"`
125
}
126
NextPageToken string `json:"nextPageToken"`
127
}
128
129
// Decode the response into a ProjectsResponse
130
var projectsResponse ProjectsResponse
131
if err = json.NewDecoder(resp.Body).Decode(&projectsResponse); err != nil {
132
return err
133
}
134
135
// Print the name of each project, and how many device each contain
136
for _, project := range projectsResponse.Projects {
137
fmt.Println(project.DisplayName)
138
fmt.Printf(" %d Sensors\n", project.SensorCount)
139
fmt.Printf(" %d Cloud Connectors\n", project.CloudConnectorCount)
140
}
141
142
return nil
143
}
144
func main() {
145
// OAuth2 authentication flow
146
auth, err := getAccessToken(
147
os.Getenv("DT_SERVICE_ACCOUNT_KEY_ID"),
148
os.Getenv("DT_SERVICE_ACCOUNT_SECRET"),
149
os.Getenv("DT_SERVICE_ACCOUNT_EMAIL"),
150
)
151
if err != nil {
152
log.Fatal(err)
153
}
154
155
// Test the access token by listing all the projects our
156
// Service Account has access to
157
if err := listProjects(auth); err != nil {
158
log.Fatal(err)
159
}
160
}
161
Copied!

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
Exchange for Access Token
Access the REST API

Create the JWT

A JSON Web Token (JWT) contains three fields:
  1. 1.
    Header: Token type and signature algorithm.
  2. 2.
    Payload: Claims and additional data.
  3. 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.
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 jwt.io.
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.
Python 3.9
Node.js 14
Go 1.17
1
# Construct the JWT header.
2
jwt_headers = {
3
'alg': 'HS256',
4
'kid': key_id,
5
}
6
7
# Construct the JWT payload.
8
jwt_payload = {
9
'iat': int(time.time()), # current unixtime
10
'exp': int(time.time()) + 3600, # expiration unixtime
11
'aud': 'https://identity.disruptive-technologies.com/oauth2/token',
12
'iss': email,
13
}
Copied!
1
// Construct the JWT header.
2
let jwtHeaders = {
3
'alg': 'HS256',
4
'kid': keyID,
5
}
6
7
// Construct the JWT payload.
8
let jwtPayload = {
9
'iat': Math.floor(Date.now() / 1000), // current unixtime
10
'exp': Math.floor(Date.now() / 1000) + 3600, // expiration unixtime
11
'aud': 'https://identity.disruptive-technologies.com/oauth2/token',
12
'iss': email,
13
}
Copied!
1
// Construct the JWT header
2
jwtHeader := map[string]interface{}{
3
"alg": "HS256",
4
"kid": keyID,
5
}
6
7
// Construct the JWT payload
8
now := time.Now()
9
jwtPayload := &jwt.RegisteredClaims{
10
Issuer: email,
11
Audience: jwt.ClaimStrings{tokenEndpoint},
12
IssuedAt: jwt.NewNumericDate(now),
13
ExpiresAt: jwt.NewNumericDate(now.Add(time.Hour)),
14
}
Copied!
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.
Python 3.9
Node.js 14
Go 1.17
1
# Sign and encode JWT with the secret.
2
encoded_jwt = jwt.encode(
3
payload = jwt_payload,
4
key = secret,
5
algorithm = 'HS256',
6
headers = jwt_headers,
7
)
Copied!
1
// Sign and encode JWT with the secret.
2
const jwtEncoded = jwt.sign(
3
jwtPayload,
4
secret,
5
{
6
header: jwtHeaders,
7
algorithm: 'HS256',
8
},
9
)
Copied!
1
// Sign and encode JWT with the secret
2
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwtPayload)
3
token.Header = jwtHeader
4
encodedJwt, _ := token.SignedString([]byte(secret))
Copied!

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. 1.
    "assertion" - Contains the encoded JWT string.
  2. 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.
It is important to note that the data has to be Form URL-Encoded. Like Python's requests, 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.
1
assertion=Base64Url(header).Base64Url(payload).Base64Url(signature)&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer
Copied!
The header, payload, and signature are found in the previous step.
Python 3.9
Node.js 14
Go 1.17
1
# Prepare HTTP POST request data.
2
# Note: The requests package applies Form URL-Encoding by default.
3
request_data = {
4
'assertion': encoded_jwt,
5
'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer'
6
}
7
8
# Exchange the JWT for an access token.
9
access_token_response = requests.post(
10
url = 'https://identity.disruptive-technologies.com/oauth2/token',
11
headers = { 'Content-Type': 'application/x-www-form-urlencoded' },
12
data = request_data,
13
)
Copied!
1
// Prepare POST request data.
2
const requestObject = {
3
'assertion': jwtEncoded,
4
'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer',
5
}
6
7
// Converts the requestObject to a Form URL-Encoded string.
8
const requestData = Object.keys(requestObject).map(function(key) {
9
return encodeURIComponent(key) + '=' + encodeURIComponent(requestObject[key])
10
}).join('&')
11
12
// Exchange JWT for access token.
13
const accessTokenResponse = await axios({
14
method: 'POST',
15
url: 'https://identity.disruptive-technologies.com/oauth2/token',
16
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
17
data: requestData,
18
}).catch(function (error) {
19
if (error.response) {
20
console.log(error.response.data)
21
}
22
throw error
23
})
Copied!
1
// Prepare HTTP POST request data.
2
// NOTE: The body must be Form URL-Encoded
3
reqData := url.Values{
4
"assertion": {encodedJwt},
5
"grant_type": {"urn:ietf:params:oauth:grant-type:jwt-bearer"},
6
}.Encode()
7
8
// Create the request to exchange the JWT for an access token
9
req, err := http.NewRequest(
10
"POST",
11
tokenEndpoint,
12
strings.NewReader(reqData),
13
)
14
if err != nil {
15
return nil, err
16
}
17
18
// Set Content-Type header to specify that our body
19
// is Form-URL Encoded
20
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
21
22
// Exchange the JWT for an access token. Set a 3 second
23
// timeout in case the server can't be reached.
24
client := &http.Client{Timeout: time.Second * 3}
25
res, err := client.Do(req)
26
if err != nil {
27
return nil, err
28
}
29
defer res.Body.Close()
Copied!
The access token response can be expected to have the following format. It will be valid for 1 hour only. To refresh the token, perform the previous steps again.
1
{
2
"access_token": "d663e83546294b158fea2574a1945319",
3
"token_type": "bearer",
4
"expires_in": 3599
5
}
Copied!

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.
Python 3.9
Node.js 14
Go 1.17
1
# Test the token by sending a GET request for a list of projects.
2
print(requests.get(
3
url = 'https://api.disruptive-technologies.com/v2/projects',
4
headers = {'Authorization': 'Bearer ' + access_token},
5
).json())
Copied!
1
// Test the token by sending a GET request for a list of projects.
2
const response = await axios({
3
method: 'GET',
4
url: 'https://api.disruptive-technologies.com/v2/projects',
5
headers: { 'Authorization': 'Bearer ' + accessToken },
6
})
Copied!
1
// Test the access token by listing all the projects our
2
// Service Account has access to
3
if err := listProjects(auth); err != nil {
4
log.Fatal(err)
5
}
Copied!

Next Steps

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 Refreshing Access Token.

Common Errors

For an overview of all REST API errors, please see our Error Codes Reference.
  • [400] {'error': 'invalid_grant', 'error_description': 'Signature has expired'} Authentication Error. Your JWT timeframe is invalid. The fields iat, issued at, and exp, expiration time should be now and one hour in the future respectively, both in unixtime.
  • [400] {'error': 'invalid_grant', 'error_description': 'too long expiry'} Authentication Error. Your JWT timeframe is invalid. The fields iat, issued at, and exp, expiration time should be now and one hour in the future respectively, both in unixtime.
  • [400] {'error': 'invalid_grant'} Authentication Error. Might be caused by any of the following reasons:
    • The Service Account may not exist.
    • The Service Account key may not exist.
    • The assertion (encoded JWT) is missing or malformed. Make sure it is Form URL-encoded as described above.
  • [400] {error: 'unsupported_grant_type'} Authentication Error. Might be caused by any of the following reasons:
    • Failure to URL Form Encoding the POST request data when exchanging the JWT for an Access Token. Verify that this step is performed, and if so, correctly.
    • The grant_type field is missing from the POST request body or is malformed (should be Form URL-Encoded).
    • The Content-Type header is not set to application/x-www-form-urlencoded.
  • [403] {error: 'not allowed'} REST API Error. You lack access to support the call you're trying to make. Check that your Service Account has a sufficient Role in the project you wish to authenticate towards.