# OAuth2

## 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 spaces.

Our OAuth2 implementation is based on the [RFC7523](https://www.rfc-editor.org/rfc/rfc7523) specification

## Migration Guide

{% hint style="warning" %}
**Important:** Our OAuth2 implementation has been updated to be fully spec-compliant with RFC7523. If you have existing integrations, please migrate to the new format. The legacy implementation will be deprecated and stop working in a future release.
{% endhint %}

### Key Changes

The following changes have been made to ensure RFC7523 compliance:

1. **JWT Claims (Required):**
   * `aud` (Audience): Must be set to the token endpoint URL (`https://app.neowit.io/api/auth/oauth/token`)
   * `iss` (Issuer): Must be set to your service account ID
   * `sub` (Subject): Must be set to your service account ID
2. **Response Body Format:**
   * The response now uses snake\_case field names per OAuth2 specification
   * Changed from `accessToken` to `access_token`
   * Added `token_type` field (value: "Bearer")
   * Added `expires_in` field (lifetime in seconds, maximum 3600)
3. **Algorithm Support:**
   * Only `HS256` algorithm is supported
   * Ensure your JWT header specifies `"alg": "HS256"`

### Migration Checklist

To migrate your existing implementation:

* [ ] Update JWT payload to include `aud`, `iss`, and `sub` claims as specified above
* [ ] Update response parsing to use `access_token` instead of `accessToken`
* [ ] Ensure JWT header uses `alg: "HS256"`
* [ ] Test your integration with the updated implementation

## Prerequisites

A [Service Account](https://developers.neowit.io/service-accounts/creating-service-accounts) must be created in the organization before continuing.

## Example

The example code in this page is provided as is, it may not work in your environment and should be used as a quick guide for implementation rather than code to be used in a production environment.

### Environment Setup

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

{% tabs %}
{% tab title="Python" %}
The following packages are required by the example code and must be installed.

```bash
pip install pyjwt requests
```

{% endtab %}

{% tab title="Node.js" %}
The following packages are required by the example code and must be installed.

```bash
npm install jsonwebtoken
npm install axios
```

{% endtab %}

{% tab title="Go" %}
The following packages are required by the example code and must be installed.

```bash
go get -u github.com/golang-jwt/jwt/v4
```

{% endtab %}
{% endtabs %}

### Source code

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

{% tabs %}
{% tab title="Python" %}
The following packages are required by the example code and must be installed.

```python
import time
import jwt       # pip install pyjwt
import requests  # pip install requests

token_endpoint = 'https://app.neowit.io/api/auth/oauth/token'

# Authentication details used in the OAuth2 flow. Should
# be stored outside the application.
service_account_id = '<your service account id>'
service_account_key_id = '<your service account key id>'
service_account_secret = '<your service account secret>'

def get_access_token(account_id, key_id, secret):
    # 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': token_endpoint,
        'iss': account_id,
        'sub': account_id,
    }

    # 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.
    data = {
        'assertion': encoded_jwt,
        'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer'
    }

    # Exchange the JWT for an access token.
    response = requests.post(
        url=token_endpoint,
        headers={'Content-Type': 'application/x-www-form-urlencoded'},
        data=data,
    )

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

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


def main():
    # Get an access token using an OAuth2 authentication flow.
    access_token = get_access_token(
        service_account_id,
        service_account_key_id,
        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 spaces.
    print(requests.get(
        url='https://app.neowit.io/api/space/v1/space',
        headers={'Authorization': 'Bearer ' + access_token},
    ).json())


if __name__ == '__main__':
    main()
```

{% endtab %}

{% tab title="Node.js" %}

```javascript
const jwt = require('jsonwebtoken');    // npm install jsonwebtoken
const axios = require('axios').default; // npm install axios

// Authentication details used in the OAuth2 flow.
const tokenEndpoint = 'https://app.neowit.io/api/auth/oauth/token';

// Auth details, should be store outside of this app
const serviceAccountID = '<your service account id>';
const serviceAccountKeyID = '<your service account key id>';
const serviceAccountSecret = '<your service account key secret>';

// 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(accountID, keyID, secret) {
    // Construct the JWT header.
    const jwtHeaders = {
        'alg': 'HS256',
        'kid': keyID,
    };

    // Construct the JWT payload.
    const jwtPayload = {
        'iat': Math.floor(Date.now() / 1000),        // current unixtime
        'exp': Math.floor(Date.now() / 1000) + 3600, // expiration unixtime
        'aud': tokenEndpoint,
        'iss': accountID,
        'sub': accountID,
    };

    // 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',
    };

    // Convert to form-urlencoded
    const body = Object.keys(requestObject).map(function(key) {
        return encodeURIComponent(key) + '=' + encodeURIComponent(requestObject[key]);
    }).join('&');

    // Exchange JWT for access token.
    const response = await axios({
        method: 'POST',
        url: tokenEndpoint,
        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 response.data.access_token;
}

async function main() {
    // Get an access token using an OAuth2 authentication flow.
    const accessToken = await getAccessToken(
        serviceAccountID,
        serviceAccountKeyID,
        serviceAccountSecret,
    );

    // Test the token by sending a GET request for a list of spaces.
    const response = await axios({
        method: 'GET',
        url: 'https://app.neowit.io/api/space/v1/space',
        headers: { 'Authorization': 'Bearer ' + accessToken },
    });

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

main();
```

{% endtab %}

{% tab title="Go" %}

```go
package main

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

	jwt "github.com/golang-jwt/jwt/v4" // go get -u github.com/golang-jwt/jwt/v4@v4.4.2
)

const (
	// Used to exchange a JWT for an access token.
	tokenEndpoint = 'https://app.neowit.io/api/auth/oauth/token';
	// Base URL for the REST API.
	apiBaseUrl = "https://app.neowit.io/api"

	// auth details, should be store outside of this app
	serviceAccountID = '<your service account id>';
	serviceAccountKeyID = '<your service account key id>';
	serviceAccountSecret = '<your service account key secret>';
)

type AuthResponse struct {
	// The access token used to access REST API.
	AccessToken string `json:"access_token"`
}

var client = http.DefaultClient

func getAccessToken(ctx context.Context, accountID, keyID, secret string) (*AuthResponse, error) {
	// set a reasonable timeout for the request
	ctx, cancel := context.WithTimeout(ctx, time.Second * 3)
	defer cancel()

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

	// Construct the JWT payload.
	now := time.Now()
	jwtPayload := &jwt.RegisteredClaims{
		Issuer:    accountID,
		Subject:   accountID,
		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, err := token.SignedString([]byte(secret))
	if err != nil {
		return nil, fmt.Errorf("Failed to sign jwt: %w", err)
	}

	// Prepare HTTP POST request data.
	// NOTE: The body must be Form URL-Encoded.
	body := 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.NewRequestWithContext(
		ctx,
		http.MethodPost,
		tokenEndpoint,
		strings.NewReader(body),
	)
	if err != nil {
		return nil, fmt.Errorf("Failed to create request: %w", err)
	}

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


	res, err := client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("Failed to perform request: %w", err)
	}
	defer res.Body.Close()
	
	if res.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("Invalid status code: %d", res.StatusCode)
	}

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

	// Return the AuthResponse, which contains the access token.
	return &authResponse, nil
}

func listSpaces(ctx context.Context, auth *AuthResponse) error {
	// set a timeout for the request
	ctx, cancel := context.WithTimeout(ctx, time.Second * 3)
	defer cancel()

	// Create the request to get a list of spaces
	req, err := http.NewRequestWithContext(
		ctx,
		http.MethodGet,
		apiBaseUrl+"/space/v1/space",
		nil,
	)
	if err != nil {
		return fmt.Errorf(""Failed to create request: %w", err)
	}

	// Set the Authorization header
	req.Header.Set("Authorization", "Bearer " + auth.AccessToken)

	// Send the GET request to list all spaces.
	resp, err := client.Do(req)
	if err != nil {
		return fmt.Errorf("Failed to list spaces: %w", err)
	}
	defer resp.Body.Close()

	// create a structure to decode the response
	response := make(map[string]interface{})
	if err = json.NewDecoder(resp.Body).Decode(&projectsResponse); err != nil {
		return fmt.Errorf("Failed to decode response: %w", err)
	}
	fmt.Printf("GOT RESPONSE: %#v\n", response)
	return nil
}

func main() {
	ctx, cancel := context.WithTimeout(context.TODO(), time.Second*10)
	defer cancel()

	// OAuth2 authentication flow.
	auth, err := getAccessToken(
		ctx,
		serviceAccountID,
		serviceAccountKeyID,
		serviceAccountSecret,
	)
	if err != nil {
		log.Fatal(err)
	}

	// Test the access token by listing all the spaces
	if err := listSpaces(ctx, auth); err != nil {
		log.Fatal(err)
	}
}
```

{% endtab %}
{% endtabs %}

### Code walkthrough

Authenticating a client is a 3 step process which can be summarized by the following points. Each step will be detailed more below.

1. A JWT is constructed and signed with a secret.
2. The JWT is exchanged for an Access Token.
3. The Access Token is used to authenticate with the REST API.

#### 1. Create the JWT

In general, a 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, using 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.

{% hint style="info" %}
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](https://jwt.io/).
{% endhint %}

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`.

{% tabs %}
{% tab title="Python" %}
{% code lineNumbers="true" %}

```python
# Inputs
token_endpoint = 'https://app.neowit.io/api/auth/oauth/token'
key_id = '<service account key id>'
account_id = '<service account id>'

# 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': token_endpoint,
    'iss': account_id,
    'sub': account_id,
}
```

{% endcode %}
{% endtab %}

{% tab title="Node.js" %}
{% code lineNumbers="true" %}

```javascript
// Inputs
const tokenEndpoint = 'https://app.neowit.io/api/auth/oauth/token';
const keyID = '<service account key id>';
const accountID = '<service account id>';

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

// Construct the JWT payload.
const jwtPayload = {
    'iat': Math.floor(Date.now() / 1000),        // current unixtime
    'exp': Math.floor(Date.now() / 1000) + 3600, // expiration unixtime
    'aud': tokenEndpoint,
    'iss': accountID,
    'sub': accountID,
};
```

{% endcode %}
{% endtab %}

{% tab title="Go" %}
{% code lineNumbers="true" %}

```go
// Inputs
const (
	tokenEndpoint = "https://app.neowit.io/api/auth/oauth/token"
	keyID = "<service account key id>" 
	accountID = "<service account id>" 
)

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

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

{% endcode %}
{% endtab %}
{% endtabs %}

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.

{% tabs %}
{% tab title="Python" %}
{% code lineNumbers="true" %}

```python
# Inputs
secret = '<service account secret>'

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

{% endcode %}
{% endtab %}

{% tab title="Node.js" %}
{% code lineNumbers="true" %}

```javascript
// Inputs
const secret = '<service account secret>';

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

{% endcode %}
{% endtab %}

{% tab title="Go" %}
{% code lineNumbers="true" %}

```go
// Inputs
const secret = "<service account secret>"

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

{% endcode %}
{% endtab %}
{% endtabs %}

#### 2. 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://app.neowit.io/api/auth/oauth/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.

It is important to note that the data **has to be Form URL-Encoded**. Like Python's [requests](https://docs.python-requests.org/en/master/), 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.

```
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.

{% tabs %}
{% tab title="Python" %}
{% code lineNumbers="true" %}

```python
# Inputs
token_endpoint = 'https://app.neowit.io/api/auth/oauth/token'

# 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.
response = requests.post(
    url = token_endpoint,
    headers = { 'Content-Type': 'application/x-www-form-urlencoded' },
    data = request_data,
)
```

{% endcode %}
{% endtab %}

{% tab title="Node.js" %}
{% code lineNumbers="true" %}

```javascript
// Inputs
const tokenEndpoint = 'https://app.neowit.io/api/auth/oauth/token';

// 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 body = Object.keys(requestObject).map(function(key) {
    return encodeURIComponent(key) + '=' + encodeURIComponent(requestObject[key])
}).join('&');

// Exchange JWT for access token.
const response = await axios({
    method: 'POST',
    url: tokenEndpoint,
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    data: body,
}).catch(function (error) {
    if (error.response) {
        console.log(error.response.data);
    }
    throw error;
});
```

{% endcode %}
{% endtab %}

{% tab title="Go" %}
{% code lineNumbers="true" %}

```go
// Inputs
const tokenEndpoint = "https://app.neowit.io/api/auth/oauth/token"

// Prepare HTTP POST request data.
// NOTE: The body must be Form URL-Encoded
body := 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.NewRequestWithContext(
	ctx,
	http.MethodPost,
	tokenEndpoint,
	strings.NewReader(body),
)
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.
res, err := client.Do(req)
if err != nil {
	return nil, err
}
defer res.Body.Close()
```

{% endcode %}
{% endtab %}
{% endtabs %}

The Access token response will have the following format and will be valid for 1 hour. To refresh the token, repeat the above steps.

```json
{
    "access_token": "<access token for use in the Authorization header>",
    "token_type": "Bearer",
    "expires_in": 3600
}
```

#### 3. 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.

{% tabs %}
{% tab title="Python" %}
{% code lineNumbers="true" %}

```python
# Test the token by sending a GET request for a list of spaces.
print(requests.get(
    url = 'https://app.neowit.io/api/space/v1/space',
    headers = {'Authorization': 'Bearer ' + access_token},
).json())
```

{% endcode %}
{% endtab %}

{% tab title="Node.js" %}
{% code lineNumbers="true" %}

```javascript
// Test the token by sending a GET request for a list of spaces.
const response = await axios({
    method: 'GET',
    url: 'https://app.neowit.io/api/space/v1/space',
    headers: { 'Authorization': 'Bearer ' + accessToken },
});
```

{% endcode %}
{% endtab %}

{% tab title="Go" %}
{% code lineNumbers="true" %}

```go
// Test the access token by listing all the spaces
if err := listSpaces(ctx, auth); err != nil {
	log.Fatal(err)
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

The Access token response will have the following format and will be valid for 1 hour. To refresh the token, repeat the above steps.

## Refreshing the Access Token

The access token retrieved using the process above needs to be refreshed every hour. This may be done by intercepting the `401` http response code, or by checking if the expiry time is getting close before performing each REST API call.
