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
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.
Service accounts have the following properties:
- A specific client ID: identifying the service account.
- A public/private key pair: The private key is used by the client to sign JWTs that are passed as credentials. The public key is storage in our database, and used to confirm the validity of those JWTs.
- 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 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.
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:
- region: Your region. This is
us
orap
. If you are unsure which region you are in, contact your assigned Client Engagement Manager. - client_id: ID of the service account.
- scope: Space-delimited list of scopes. If a requested scope is not present on the given service account, the default set of scopes configured for that client will be used.
- 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
7
{
"iss": "{your client id}",
"iat": 1700676680,
"sub": "{your client id}",
"aud": "https://api..us.te2.io",
"exp": 1700763080
}
Parameters used above include:
- iss: Issuer. This is the client_id used to identify your system as the issuer of the request.
- sub: Subject. This is also the client_id.
- 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). - aud: The URL of the oauth2 authorization service this request is being sent to.
Using a JWT as the password has several advantages:
- 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.
- 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.
- 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.
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
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:
- 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
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
}
)