OAuth2 service tokens

Service tokens are JSON Web Tokens, or JWTs, which are used for system-to-system communication. These tokens grant the caller access to endpoints in which they normally would not be able to contact. In this document, we give a brief overview of service accounts and how to use them to obtain a service token.

JSON Web Tokens

Using service accounts requires the use of JSON Web Tokens, or JWTs. Learn more about JWTs and list libraries for working with JWTs.

Service accounts

The term “Service account” refers to a specific OAuth2 client_id which has been approved to use the grant_type of client_credentials to acquire a access_token. These service accounts are intended for system-to-system communication, and are not associated with individual user accounts.

The term ‘Service Account’ is based on a previous implementation of this concept that was based on Google’s concept of service accounts. For the OAuth2-based approach, specific client_ids are instead used to fulfill this need. Acquiring token in this way requires the use of a signed JWT as a credential using a process described in RFC 7523. Learn more about JWTs and list libraries for working with JWTs.

Service accounts have the following properties:

Due to their sensitive nature, creation of service accounts are created by contacting your assigned Client Engagement Manager.

Service account private keys

When a service account is created, an RSA (public key, private key) pairing is generated. The public key remains stored in our database or hosted on a public URL while the private key is distributed to whomever needs to use the account. The private key is only available at the time of account creation. If the private key is lost, it is impossible to recover and a new account must be created.

The private key is a form of password and should be treated as such. Do not store private keys in plaintext.

Obtaining a service token

The term “service token” refers to a normal OAuth2 access_token which has been acquired using a service account.

A simple request to obtain a service token looks as follows:

1
2
3
4
5
6
7
8
curl --location --request POST 'https://api.{region}.te2.io/oauth2/token' \
--header 'accept: application/json' \
--header 'content-type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode 'scope={desired scopes as space delimited list}' \
--data-urlencode 'client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer' \
--data-urlencode 'client_assertion={Ecoded JWT Token}' \
--data-urlencode 'client_id={client_id of the service account}'

Parameters used above include:

The access token returned by the above call may be used like any other access token generated by accesso’s systems.

Access tokens, including service tokens, issued by accesso are valid until they have expired or have been blacklisted. Do not attempt to obtain another service token until the token in your possession has either expired, or is within about 60 seconds of expiring, or has been rejected by one of accesso’s services.

Generating a JWT credential

Unlike other account types in the accesso ecosystem which have a password, service accounts do not. Rather, whoever wishes to use the service account must generate a password using the private key belonging to the service account. The password is a JWT which has been signed using a private key. The minimum required contents of the JWT’s payload are as follows:

1
2
3
4
5
6
7
{
  "iss": "{your client id}",
  "iat": 1700676680,
  "sub": "{your client id}",
  "aud": "https://api..us.te2.io",
  "exp": 1700763080
}

Parameters used above include:

Using a JWT as the password has several advantages:

  1. The JWT can expire quickly. If someone grabs the JWT from the wire, there is a very brief period of time during which it can be used to get an access token and damage can be done.
  2. The JWT credential can be validated using public-key cryptography. Any modification to payload of the JWT will cause validation to fail unless the JWT is re-signed using the private key. This means that manipulation by someone who does not have the private key is futile.
  3. The key needed to generate the password is not sent over the wire to make the token request.

We provide a few sample implementations of generating the credential.

The implementations below are provided as examples and are intended to aid in understanding. These are not endorsements of any JWT library. The reader should do their own due diligence when evaluating any security library and, generally, should ensure they are using the most up-to-date version of any security library they choose.

For list libraries for working with JWTs, see JWT.io’s Libraries for Token Signing/Verification.

Java 8 w/ Maven

1
2
3
4
5
<dependency>
  <groupId>com.nimbusds</groupId>
  <artifactId>nimbus-jose-jwt</artifactId>
  <version>5.4</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.*;
import com.nimbusds.jwt.*;
import java.security.*;
import java.security.spec.*;
import java.util.Base64;
import java.util.Date;

public class JwtCredentialBuilder {

    /**
     * Creates the JWT which is used as the Service Account credential.
     *
     * @param clientId Service Account client ID.
     * @param authServerHost The FQDN of the Auth Server endpoint the request will go to
     * @param privateKeyBase64 Private key, base 64 encoded.
     * @param timeToLiveSeconds Number of seconds from now in which the JWT is to expire.
     * @return Service Account credential.
     */
    public String createCredential(
        String clientId,
        String authServerHost,
        String privateKeyBase64,
        long timeToLiveSeconds
    ) throws Exception {
        long nowMillis = System.currentTimeMillis();
        long timeToLiveMillis = timeToLiveSeconds * 1000;

        // Build the payload or "claims set".
        JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
            .issuer(clientId)
            .subject(clientId)
            .audience("https://" + authServerHost)
            .issueTime(new Date(nowMillis))
            .expirationTime(new Date(nowMillis + timeToLiveMillis))
            .build();

        // Build the JWT header.
        JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.RS256)
            .type(JOSEObjectType.JWT)
            .build();

        // Create an object which can sign the JWT.
        byte[] keyBytes = Base64.getDecoder().decode(privateKeyBase64.getBytes());
        PKCS8EncodedKeySpec keySpecPKCS8 = new PKCS8EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PrivateKey privateKey = keyFactory.generatePrivate(keySpecPKCS8);
        JWSSigner signer = new RSASSASigner(privateKey);

        // Create and sign the JWT.
        SignedJWT signedJWT = new SignedJWT(header, claimsSet);
        signedJWT.sign(signer);

        // Convert to a string.
        return signedJWT.serialize();
    }

}

Python 3 w/ pip

1
sudo pip3 install cryptography PyJWT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import datetime
import jwt


def create_credential(client_id, auth_server_host, private_key_base_64, time_to_live_seconds):
    """
    Creates the JWT which is used as the Service Account credential.

    :param client_id: Service Account client ID.
    :param auth_server_host: The FQDN of the Auth Server endpoint the request will go to
    :param private_key_base_64: Private key, base 64 encoded. Example:
                                "-----BEGIN RSA PRIVATE KEY-----\n" +
                                "MIIEvgIB...\n" +
                                "-----END RSA PRIVATE KEY-----"
    :param time_to_live_seconds: Number of seconds from now in which the JWT is to expire.
    :return: Service Account credential.
    """
    # Get the current time in seconds.
    now_sec = int(datetime.datetime.today().timestamp())
    exp_sec = now_sec + int(time_to_live_seconds)

    # Build the payload or "claims set".
    claims_set = {
        "iss": client_id,
        "sub": client_id,
        "aud": "https://" + auth_server_host,
        "iat": now_sec,
        "exp": exp_sec,
    }

    # Convert from base 64 to raw bytes.
    private_key = bytes(private_key_base_64, encoding='UTF-8')

    # Create the JWT.
    jwt_encoded = jwt.encode(claims_set, private_key, algorithm='RS256')

    # Return as a string.
    return jwt_encoded.decode('utf-8')

.Net (C#) with NuGet

This code requires the private key to be provided as a PEM file.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
using Microsoft.IdentityModel.Tokens;
using Org.BouncyCastle.OpenSsl;  // nuget package "BouncyCastle"
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;  // nuget package "System.IdentityModel.Tokens.Jwt"
using System.Security.Claims;
using System.Security.Cryptography;
using System;

namespace JwtCredential
{
    class JwtCredentialCreator
    {
        /// <summary>Generates the credential JWT used by accesso's tokens API
        ///          to authenticate a service account.</summary>
        /// <param name="clientId">client ID of the service account.</param>
        /// <param name="authServerHost">FQDN of the authorization service.</param>
        /// <param name="pathToPemFile">Path to the PEM file containing the private key.</param>
        /// <returns>JWT credential.</returns>
        static string CreateCredential(
            string clientId,
            string authServerHost,
            string pathToPemFile
        )
        {
            // Read in the PEM file and convert into a SecurityKey
            // which may be used to sign the JWT.
            var readFile = new System.IO.StreamReader(pathToPemFile);
            var pemObject = new PemReader(readFile).ReadPemObject();
            var provider = new RSACryptoServiceProvider();
            provider.ImportRSAPrivateKey(
                new ReadOnlySpan<byte>(pemObject.Content),
                out int bytesRead
            );
            var rsaSecurityKey = new RsaSecurityKey(provider);

            // The JWT library requires credentials to sign the JWT.
            // We will use the SecurityKey we just created.
            var signingCredentials = new SigningCredentials(
                    rsaSecurityKey,
                    SecurityAlgorithms.RsaSha256
            );

            // Current timestamp in UTC.
            var now = DateTime.UtcNow;

            // Anything outside of the standard set of JWT fields must be declared as "claims".
            var claims = new List<Claim>();

            // Create a TokenDescriptor holding all the details of the payload of the token.
            // This will be used by the TokenHandler to generate the token.
            var tokenDescriptor = new SecurityTokenDescriptor
            {
                IssuedAt = now,
                NotBefore = now,
                Expires = now.AddDays(1),
                Issuer = clientId,
                Audience = "https://" + authServerHost,
                Subject = new ClaimsIdentity(claims),
                SigningCredentials = signingCredentials
            };

            // Use the TokenDescriptor to create the JWT.
            var handler = new JwtSecurityTokenHandler();
            var token = handler.CreateJwtSecurityToken(tokenDescriptor);

            // Convert the JWT object to a string and return it.
            return handler.WriteToken(token);
        }
    }
}

Piecing it all together

As this guide has covered a large amount of information, we present a basic outline of the above to illustrate how the pieces come together:

Pseudocode for obtaining and using a service token is presented below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# Service account information and token lifetime.
client_id    <- "my.oauth2.client_id"
authSvrFQDN  <- "api.us.te2.io"
private_key  <- "MIIEvgIB"
scopes       <- "read write"
time_to_live <- 86000

# Construct the account's credential.
account_credential <- create_credential(
    account_id,
    authSvrFQDN,
    private_key,
    time_to_live
)

# Fetch the token.
token_response <- execute_curl(
    method  <- 'POST',
    url     <- 'https://api.{region}.te2.io/oauth2/token',
    headers <- {
                    "Cache-Control" : "no-cache",
                    "Content-Type"  : "application/json"
               }
    parameters <- {
                   :grant_type             : "client_credentials"
                   "client_id"             : client_id,
                   "client_assertion_type" : "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
                   "credential"            : account_credential
                   "scopes"                : scopes,
               }
)

# Extract the service token from the response.
access_token <- token_response.access_token

# Use the service token to make a call.
generic_endpoint_response <- execute_curl(
    method  <- 'GET'
    url     <- 'https://api.region.te2.io/v1/some/generic/endpoint'
    headers <- {
                    "Cache-Control" : "no-cache",
                    "Content-Type"  : "application/json"
                    "Authorization" : "Bearer " + access_token
                }
)