Legacy Service accounts and service tokens

This document refers to the legacy token service available via the identity-service. For all new work, please use the OAuth2.1 compliant service available from the accesso-authoization-service. See OAuth2 Service Accounts and Service Tokens for more information.

Service tokens are JSON Web Tokens, or JWTs, which are used by accounts intended 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

Service accounts are special-purpose accounts created in accesso’s systems intended for system-to-system communication. 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 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

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

1
2
3
4
5
6
7
8
9
10
11
curl -X POST \
  https://api.{region}.te2.io/v3/tokens \
  -H 'Authorization: Basic {apiKey}' \
  -H 'Cache-Control: no-cache' \
  -H 'Content-Type: application/json' \
  -H 'X-User-Type: ServiceAccounts' \
  -d '{
    "accountId": "{accountId}",
    "credentialType": "SERVICE",
    "credential": "{credential}"
}'

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
{
  "iat": "1573156263",
  "exp": "1573761062",
  "iss": "my.service.account@te2.biz",
  "scope": "read write"
}

Parameters used above include:

Using a JWT as the password has several advantages:

  1. The password can expire. If someone grabs the password from the wire, there is a very brief period of time during which the password can return an access token and damage can be done.
  2. The password 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 sent over the wire exactly once. Once the private key is sent over the wire at account creation, the private key is forgotten by the token service.

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
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 accountId Service Account account ID.
     * @param privateKeyBase64 Private key, base 64 encoded.
     * @param timeToLiveSeconds Number of seconds from now in which the JWT is to expire.
     * @param scopes Space-delimited list of scopes.
     * @return Service Account credential.
     */
    public String createCredential(
        String accountId,
        String privateKeyBase64,
        long timeToLiveSeconds,
        String scopes
    ) throws Exception {
        long nowMillis = System.currentTimeMillis();
        long timeToLiveMillis = timeToLiveSeconds * 1000;

        // Build the payload or "claims set".
        JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
            .issuer(accountId)
            .claim("scope", scopes)
            .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
import datetime
import jwt


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

    :param account_id: Service Account account ID.
    :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.
    :param scopes_string: Space-delimited list of scopes.
    :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": account_id,
        "iat": now_sec,
        "exp": exp_sec,
        "scope": scopes_string,
    }

    # 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="accountId">ID of the service account.</param>
        /// <param name="scope">Space-delimited list of scopes.</param>
        /// <param name="pathToPemFile">Path to the PEM file containing the private key.</param>
        /// <returns>JWT credential.</returns>
        static string CreateCredential(
            string accountId,
            string scope,
            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>();
            claims.Add(new Claim("scope", scope));

            // 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 = accountId,
                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
# Service account information and token lifetime.
account_id   <- "my.service.account@my.company.biz"
private_key  <- "MIIEvgIB"
scopes       <- "read write"
time_to_live <- 86000

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

# Fetch the token.
token_response <- execute_curl(
    method  <- 'POST',
    url     <- 'https://api.region.te2.io/v3/tokens',
    headers <- {
                    "Authorization" : "Basic abcde",
                    "Cache-Control" : "no-cache",
                    "Content-Type"  : "application/json"
                    "X-User-Type"   : "ServiceAccounts"
               }
    payload <- {
                   "accountId"      : account_id,
                   "credentialType" : "SERVICE",
                   "credential"     : account_credential
               }
)

# 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
                }
)