Legacy Service accounts and service tokens
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
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:
- An account ID: This will always be an email address,
though it should not be an actual email address (e.g.,
my.service.account@te2.io
). - A JWT credential: The client using the service account must generate a JWT to serve as a credential. A known private key is used to sign the JWT.
- Elevated permissions: Service accounts can access endpoints which are generally prohibited to normal users.
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.
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:
- region: Your region. This is
us
orap
. If you are unsure which region you are in, contact your assigned Client Engagement Manager. - apiKey: The API key which identifies a particular application or program using the accesso APIs. This information should be provided to you.
- accountId: ID of the service account. This is an email address, though it should not be an actual email address.
- credential: Service account credential. The procedure used to generate this credential is described in the following section.
The access token returned by the above call may be used like any other access token generated by accesso’s systems.
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:
- iat: Issued At. Current Unix UTC timestamp in seconds. Must not be more than one minute from the token service’s system clock.
- exp: Expires At. Unix UTC timestamp in seconds at which this JWT is to be considered expired. Must be less than
iat + 1 week
(1 week = 604,800 seconds). - iss: Issuer. Account ID of the service account.
- scope: Scope. Space-delimited list of scopes. If a requested scope is not present on the given service account, an error will be returned. This information must be provided to you.
Using a JWT as the password has several advantages:
- 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.
- 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.
- 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.
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
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:
- The credential of a service account is used to obtain a service token.
- A service account’s credential is JWT signed with the said account’s private key.
- A service token is used in exactly the same manner as any other accesso token.
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
}
)