> For the complete documentation index, see [llms.txt](https://developers.neowit.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://developers.neowit.io/rest-api/authentication/oauth2.md).

# OAuth2

## Overview

This guide explains how to authenticate a Service Account for REST API integrations. Our OAuth2 implementation is based on the [RFC7523](https://www.rfc-editor.org/rfc/rfc7523) JWT bearer grant flow.

The recommended signing algorithm is `RS256`. With `RS256`, you keep the RSA private key in your application and upload the matching public key when creating the Service Account key. `HS256` is still supported for legacy shared-secret integrations.

## Migration Guide

{% hint style="warning" %}
**Important:** The OAuth2 Service Account flow supports `RS256` and requires RFC7523-style JWT claims. Existing `HS256` integrations can continue to work, but new integrations should use `RS256`.
{% endhint %}

### Key Changes

1. **Algorithm support**
   * `RS256` is recommended for new Service Account keys.
   * `HS256` is supported for legacy shared-secret keys.
   * The JWT header `alg` must match the Service Account key type.
   * `RS256` keys cannot be used with Basic Auth.
2. **Key expiration**
   * Service Account keys can have an optional expiration date.
   * Expired keys cannot be used to exchange JWT assertions for access tokens.
   * Create a new key before the old key expires to avoid integration downtime.
3. **JWT claims**
   * `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.
   * `exp` (Expiration): Required.
   * `iat` (Issued at): Recommended.
   * `jti` (JWT ID): Recommended for replay protection. If supplied, the same `jti` cannot be reused before the JWT expires.
4. **Response body format**
   * The response uses snake\_case field names per OAuth2 specification.
   * Use `access_token` instead of `accessToken`.
   * `token_type` is `Bearer`.
   * `expires_in` is the remaining access-token lifetime in seconds.

### Migration Checklist

* [ ] Create an `RS256` Service Account key and store the private key securely.
* [ ] Set a key expiration date if your credential rotation policy requires one.
* [ ] Sign JWT assertions with `alg: "RS256"` and the private key.
* [ ] Include `kid`, `aud`, `iss`, `sub`, and `exp` in the JWT assertion.
* [ ] Parse `access_token`, `token_type`, and `expires_in` from the response.
* [ ] Refresh the access token before `expires_in` reaches zero.

## Prerequisites

A [Service Account](/service-accounts/creating-service-accounts.md) must be created in the organization before continuing. For `RS256`, the Service Account key must be created with a public RSA key, and your application must keep the corresponding private key. The key may also have an optional expiration date; after that date, token exchange with that key fails.

## Example

The example code in this page is provided as is. It may not work unchanged in your environment and should be used as a quick implementation guide rather than production-ready code.

### Environment Setup

{% tabs %}
{% tab title="Python" %}

```bash
pip install pyjwt cryptography requests
```

{% endtab %}

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

```bash
npm install jsonwebtoken axios
```

{% endtab %}

{% tab title="Go" %}

```bash
go get github.com/golang-jwt/jwt/v5
```

{% endtab %}
{% endtabs %}

### Source Code

{% tabs %}
{% tab title="Python" %}

```python
import time
from pathlib import Path

import jwt        # pip install pyjwt cryptography
import requests   # pip install requests

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

# Store these outside the application source.
service_account_id = '<your service account id>'
service_account_key_id = '<your service account key id>'
private_key = Path('private_key.pem').read_text()


def get_access_token(account_id, key_id, signing_key):
    now = int(time.time())

    jwt_headers = {
        'alg': 'RS256',
        'kid': key_id,
    }

    jwt_payload = {
        'iat': now,
        'exp': now + 3600,
        'aud': token_endpoint,
        'iss': account_id,
        'sub': account_id,
        'jti': f'{account_id}-{now}',
    }

    encoded_jwt = jwt.encode(
        payload=jwt_payload,
        key=signing_key,
        algorithm='RS256',
        headers=jwt_headers,
    )

    response = requests.post(
        url=token_endpoint,
        headers={'Content-Type': 'application/x-www-form-urlencoded'},
        data={
            'assertion': encoded_jwt,
            'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer',
        },
    )

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

    return response.json()


def main():
    auth = get_access_token(
        service_account_id,
        service_account_key_id,
        private_key,
    )
    if auth is None:
        return

    response = requests.get(
        url=api_url,
        headers={'Authorization': 'Bearer ' + auth['access_token']},
    )
    print(response.json())


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

{% endtab %}

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

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

const tokenEndpoint = 'https://app.neowit.io/api/auth/oauth/token';
const apiURL = 'https://app.neowit.io/api/space/v1/space';

// Store these outside the application source.
const serviceAccountID = '<your service account id>';
const serviceAccountKeyID = '<your service account key id>';
const privateKey = fs.readFileSync('private_key.pem', 'utf8');

async function getAccessToken(accountID, keyID, signingKey) {
    const now = Math.floor(Date.now() / 1000);

    const jwtHeaders = {
        alg: 'RS256',
        kid: keyID,
    };

    const jwtPayload = {
        iat: now,
        exp: now + 3600,
        aud: tokenEndpoint,
        iss: accountID,
        sub: accountID,
        jti: `${accountID}-${now}`,
    };

    const assertion = jwt.sign(
        jwtPayload,
        signingKey,
        {
            algorithm: 'RS256',
            header: jwtHeaders,
        },
    );

    const body = new URLSearchParams({
        assertion,
        grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
    });

    const response = await axios.post(tokenEndpoint, body.toString(), {
        headers: {'Content-Type': 'application/x-www-form-urlencoded'},
    });

    return response.data;
}

async function main() {
    const auth = await getAccessToken(
        serviceAccountID,
        serviceAccountKeyID,
        privateKey,
    );

    const response = await axios.get(apiURL, {
        headers: {'Authorization': 'Bearer ' + auth.access_token},
    });

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

main().catch((error) => {
    if (error.response) {
        console.error(error.response.data);
    }
    throw error;
});
```

{% endtab %}

{% tab title="Go" %}

```go
package main

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

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

const (
	tokenEndpoint = "https://app.neowit.io/api/auth/oauth/token"
	apiURL        = "https://app.neowit.io/api/space/v1/space"

	serviceAccountID    = "<your service account id>"
	serviceAccountKeyID = "<your service account key id>"
)

type AuthResponse struct {
	AccessToken string `json:"access_token"`
	TokenType   string `json:"token_type"`
	ExpiresIn   int64  `json:"expires_in"`
}

func getAccessToken(ctx context.Context, accountID, keyID string, signingKey []byte) (*AuthResponse, error) {
	privateKey, err := jwt.ParseRSAPrivateKeyFromPEM(signingKey)
	if err != nil {
		return nil, fmt.Errorf("failed to parse private key: %w", err)
	}

	now := time.Now()
	claims := jwt.RegisteredClaims{
		ID:        fmt.Sprintf("%s-%d", accountID, now.Unix()),
		Issuer:    accountID,
		Subject:   accountID,
		Audience:  jwt.ClaimStrings{tokenEndpoint},
		IssuedAt:  jwt.NewNumericDate(now),
		ExpiresAt: jwt.NewNumericDate(now.Add(time.Hour)),
	}

	token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
	token.Header["kid"] = keyID

	assertion, err := token.SignedString(privateKey)
	if err != nil {
		return nil, fmt.Errorf("failed to sign JWT: %w", err)
	}

	body := url.Values{
		"assertion":  {assertion},
		"grant_type": {"urn:ietf:params:oauth:grant-type:jwt-bearer"},
	}.Encode()

	req, err := http.NewRequestWithContext(
		ctx,
		http.MethodPost,
		tokenEndpoint,
		strings.NewReader(body),
	)
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %w", err)
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	res, err := http.DefaultClient.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("unexpected status code: %d", res.StatusCode)
	}

	var auth AuthResponse
	if err := json.NewDecoder(res.Body).Decode(&auth); err != nil {
		return nil, fmt.Errorf("failed to decode response: %w", err)
	}
	return &auth, nil
}

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

	privateKey, err := os.ReadFile("private_key.pem")
	if err != nil {
		log.Fatal(err)
	}

	auth, err := getAccessToken(ctx, serviceAccountID, serviceAccountKeyID, privateKey)
	if err != nil {
		log.Fatal(err)
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, apiURL, nil)
	if err != nil {
		log.Fatal(err)
	}
	req.Header.Set("Authorization", "Bearer "+auth.AccessToken)

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer res.Body.Close()

	var body map[string]interface{}
	if err := json.NewDecoder(res.Body).Decode(&body); err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%#v\n", body)
}
```

{% endtab %}
{% endtabs %}

### HS256 Legacy Signing

For legacy `HS256` keys, sign with the Service Account key secret instead of an RSA private key. The JWT header must use `alg: "HS256"`, and the `kid` must point to an `HS256` key.

{% tabs %}
{% tab title="Python" %}

```python
encoded_jwt = jwt.encode(
    payload=jwt_payload,
    key=service_account_secret,
    algorithm='HS256',
    headers={'alg': 'HS256', 'kid': service_account_key_id},
)
```

{% endtab %}

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

```javascript
const assertion = jwt.sign(
    jwtPayload,
    serviceAccountSecret,
    {
        algorithm: 'HS256',
        header: {alg: 'HS256', kid: serviceAccountKeyID},
    },
);
```

{% endtab %}

{% tab title="Go" %}

```go
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
token.Header["kid"] = serviceAccountKeyID
assertion, err := token.SignedString([]byte(serviceAccountSecret))
```

{% endtab %}
{% endtabs %}

## Code Walkthrough

Authenticating a client is a three-step process:

1. Create and sign a JWT assertion.
2. Exchange the JWT assertion for an access token.
3. Use the access token to call the REST API.

### 1. Create The JWT

The JWT must contain a header with the signing algorithm and key ID:

```json
{
    "alg": "RS256",
    "kid": "<service account key id>"
}
```

The JWT payload must identify the Service Account and token endpoint:

```json
{
    "iat": 1719849600,
    "exp": 1719853200,
    "aud": "https://app.neowit.io/api/auth/oauth/token",
    "iss": "<service account id>",
    "sub": "<service account id>",
    "jti": "<unique assertion id>"
}
```

The `exp` claim is required. It must be in the future, expressed as a Unix timestamp in seconds. The OAuth2 endpoint signs the returned access token with the same expiration deadline, and the response `expires_in` value is calculated from that deadline.

Use a short `exp` value and refresh by creating a new JWT assertion. The examples use one hour. If you include a `jti`, generate a fresh value for every token exchange; replaying the same `jti` before expiration is rejected.

### 2. Exchange For Access Token

Send a `POST` request to `https://app.neowit.io/api/auth/oauth/token` with a Form URL-Encoded body:

1. `assertion`: The encoded JWT string.
2. `grant_type`: `urn:ietf:params:oauth:grant-type:jwt-bearer`.

The request must have `Content-Type: application/x-www-form-urlencoded`.

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

The response has this format:

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

`expires_in` is the number of seconds until the access token expires.

### 3. Access The REST API

Include the returned access token in the `Authorization` header for REST API calls:

```
Authorization: Bearer <access token>
```

## Refreshing The Access Token

Access tokens cannot be refreshed directly. To get a new access token, create and sign a new JWT assertion with a future `exp`, then repeat the token exchange. Refresh before `expires_in` reaches zero or when the API returns `401 Unauthorized`.

## Service Account Key Expiration

A Service Account key can also have its own optional expiration. If the key has expired, the OAuth2 token endpoint rejects JWT assertions that reference that key, even if the JWT assertion itself has a future `exp`.

Key expiration is configured when the key is created. In the REST API this is represented as `expiresAt`, a Unix timestamp in seconds. In the app, use the key expiration date field when creating the key.

To rotate credentials, create a new key before the current key expires, deploy the new private key or secret to your integration, confirm token exchange works, and then delete the old key.
