RSA Authentication Workflow
This pages describes the implementation of RSA Authentication. For the API reference of RSA Session Authenticate and Key Manager Authenticate, see the following API endpoints:

Note: The following authentication sequence is provided out of the box by our dedicated SDKs and BDK. To learn more about authenticating using the SDKs or BDK proceed to one of following configuration guides:

Summary

The Authentication process requires the following steps:
    1.
    The user creates a public/private RSA key pair.
    2.
    The admin imports the public key into the pod using the Admin Console or public APIs.
    3.
    The user creates a short-lived JWT (JSON Web Token) and signs it with their private key.
    4.
    The bot makes a call the the authentication endpoints. Here, the server checks the signature of the JWT against the public key and returns an authentication token.

Session Token Management

The token you receive is valid for the lifetime of a session that is defined by your pod's administration team. This ranges from 1 hour to 2 weeks.
You should keep using the same token until you receive a HTTP 401, at which you should re-authenticate and get a new token for a new session.
Datafeeds survive session expiration, you do not need to re-create your datafeed if your session expires.

Supported Ciphers for the SSL/TLS session

Symphony only supports the following cipher suites:
ECDHE-RSA-AES256-GCM-SHA384 (Preferred)
ECDHE-RSA-AES128-GCM-SHA256
DHE-RSA-AES256-GCM-SHA384
DHE-RSA-AES128-GCM-SHA256

1. Create an RSA Key Pair

The public/private key pair for signing authentication requests requires the following:
Note: This script requires the openssl package.
Generate the PKCS#1 keys manually using the following commands:
1
$ openssl genrsa -out mykey.pem 4096
2
$ openssl rsa -in mykey.pem -pubout -out pubkey.pem
Copied!
Generate the PKCS#8 keys manually using the following commands. You can provide the Service Account's username as the Common Name (CN) but it is not a mandatory requirement.
1
$ openssl genrsa -out privatekey.pem 4096
2
$ openssl req -newkey rsa:4096 -x509 -key privatekey.pem -out publickey.cer
3
$ openssl pkcs8 -topk8 -nocrypt -in privatekey.pem -out privatekey.pkcs8
4
$ openssl x509 -pubkey -noout -in publickey.cer > publickey.pem
Copied!
Sign the authentication request using either privatekey.pkcs8 or privatekey.pem, depending on the support available in the JWT library.
The file publickey.pem is the public key. This is the key you will import into the pod in step 2.

2. Import Public Key into the Pod

Navigate to the Admin Console and create a new Service Account. Copy the contents of the pubkey.pem file you just created and paste into the textbox under the Authentication section:
Add your bot's basic information:
If successful, you should see the following:

3. Generate a signed JWT Token

To authenticate on the Pod and the Key Manager, the bot must call the authentication endpoints, passing a short-lived JWT token in the body of the request. The JWT token must contain the following:
    a subject matching the username of the user to authenticate
    an expiration time of no more than 5 minutes from the current timestamp (needed to prevent replay attacks)
    a signature by a private RSA key matching a public key stored for the user in the Pod
The following script generates the authentication request:
Java
Python
JavaScript
C#
1
package com.symphony.util.jwt;
2
3
import io.jsonwebtoken.Jwts;
4
import io.jsonwebtoken.SignatureAlgorithm;
5
import org.bouncycastle.asn1.pkcs.RSAPrivateKey;
6
import org.bouncycastle.crypto.params.RSAPrivateCrtKeyParameters;
7
import org.bouncycastle.crypto.util.PrivateKeyInfoFactory;
8
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
9
import org.bouncycastle.util.io.pem.PemObject;
10
import org.bouncycastle.util.io.pem.PemReader;
11
12
import java.io.FileNotFoundException;
13
import java.io.IOException;
14
import java.io.StringReader;
15
import java.nio.charset.StandardCharsets;
16
import java.nio.file.Files;
17
import java.nio.file.Paths;
18
import java.security.GeneralSecurityException;
19
import java.security.Key;
20
import java.security.KeyFactory;
21
import java.security.PrivateKey;
22
import java.security.spec.PKCS8EncodedKeySpec;
23
import java.util.Base64;
24
import java.util.Date;
25
import java.util.stream.Stream;
26
27
28
/**
29
* Class used to generate JWT tokens signed by a specified private RSA key.
30
* Libraries needed as dependencies:
31
* - BouncyCastle (org.bouncycastle.bcpkix-jdk15on) version 1.59.
32
* - JJWT (io.jsonwebtoken.jjwt) version 0.9.1.
33
*
34
*
35
*/
36
public class JwtHelper {
37
38
// PKCS#8 format
39
private static final String PEM_PRIVATE_START = "-----BEGIN PRIVATE KEY-----";
40
private static final String PEM_PRIVATE_END = "-----END PRIVATE KEY-----";
41
42
// PKCS#1 format
43
private static final String PEM_RSA_PRIVATE_START = "-----BEGIN RSA PRIVATE KEY-----";
44
private static final String PEM_RSA_PRIVATE_END = "-----END RSA PRIVATE KEY-----";
45
46
47
/**
48
* Get file as string without spaces
49
* @param filePath: filepath for the desired file.
50
* @return
51
*/
52
public static String getFileAsString(String filePath) throws IOException {
53
StringBuilder message = new StringBuilder();
54
String newline = System.getProperty("line.separator");
55
56
if (!Files.exists(Paths.get(filePath))) {
57
throw new FileNotFoundException("File " + filePath + " was not found.");
58
}
59
60
try (Stream<String> stream = Files.lines(Paths.get(filePath))) {
61
62
stream.forEach(line -> message
63
.append(line)
64
.append(newline));
65
66
// Remove last new line.
67
message.deleteCharAt(message.length() -1);
68
} catch (IOException e) {
69
System.out.println(String.format("Could not load content from file: %s due to %s",filePath, e));
70
System.exit(1);
71
}
72
73
return message.toString();
74
}
75
76
/**
77
* Creates a JWT with the provided user name and expiration date, signed with the provided private key.
78
* @param user the username to authenticate; will be verified by the pod
79
* @param expiration of the authentication request in milliseconds; cannot be longer than the value defined on the pod
80
* @param privateKey the private RSA key to be used to sign the authentication request; will be checked on the pod against
81
* the public key stored for the user
82
*/
83
private static String createSignedJwt(String user, long expiration, Key privateKey) {
84
85
return Jwts.builder()
86
.setSubject(user)
87
.setExpiration(new Date(System.currentTimeMillis() + expiration))
88
.signWith(SignatureAlgorithm.RS512, privateKey)
89
.compact();
90
}
91
92
/**
93
* Create a RSA Private Key from a PEM String. It supports PKCS#1 and PKCS#8 string formats
94
*/
95
private static PrivateKey parseRSAPrivateKey(String privateKeyFilePath) throws GeneralSecurityException, IOException {
96
String pemPrivateKey = getFileAsString(privateKeyFilePath);
97
try {
98
99
if (pemPrivateKey.contains(PEM_PRIVATE_START)) { // PKCS#8 format
100
101
String privateKeyString = pemPrivateKey
102
.replace(PEM_PRIVATE_START, "")
103
.replace(PEM_PRIVATE_END, "")
104
.replace("\\n", "\n")
105
.replaceAll("\\s", "");
106
byte[] keyBytes = Base64.getDecoder().decode(privateKeyString.getBytes(StandardCharsets.UTF_8));
107
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
108
KeyFactory fact = KeyFactory.getInstance("RSA");
109
return fact.generatePrivate(keySpec);
110
111
} else if (pemPrivateKey.contains(PEM_RSA_PRIVATE_START)) { // PKCS#1 format
112
113
try (PemReader pemReader = new PemReader(new StringReader(pemPrivateKey))) {
114
PemObject privateKeyObject = pemReader.readPemObject();
115
RSAPrivateKey rsa = RSAPrivateKey.getInstance(privateKeyObject.getContent());
116
RSAPrivateCrtKeyParameters privateKeyParameter = new RSAPrivateCrtKeyParameters(
117
rsa.getModulus(),
118
rsa.getPublicExponent(),
119
rsa.getPrivateExponent(),
120
rsa.getPrime1(),
121
rsa.getPrime2(),
122
rsa.getExponent1(),
123
rsa.getExponent2(),
124
rsa.getCoefficient()
125
);
126
127
return new JcaPEMKeyConverter().getPrivateKey(PrivateKeyInfoFactory.createPrivateKeyInfo(privateKeyParameter));
128
} catch (IOException e) {
129
throw new GeneralSecurityException("Invalid private key.");
130
}
131
132
} else {
133
throw new GeneralSecurityException("Invalid private key.");
134
}
135
} catch (Exception e) {
136
throw new GeneralSecurityException(e);
137
}
138
}
139
140
public static String createJwt(String username, String privateKeyFilePath) throws IOException, GeneralSecurityException {
141
final long expiration = 300000L;
142
final PrivateKey privateKey = parseRSAPrivateKey(privateKeyFilePath);
143
return createSignedJwt(username, expiration, privateKey);
144
}
145
146
public static void main(String[] args) throws IOException, GeneralSecurityException {
147
final String username = System.getProperty("user");
148
final String privateKeyFile = System.getProperty("key");
149
150
final String jwt = createJwt(username, privateKeyFile);
151
System.out.println(jwt);
152
}
153
}
Copied!
1
"""
2
0) Use python 3
3
4
1) Install python dependency required to run this script by:
5
pip install python-jose
6
7
2) The .pem file used in this script was generated from the previous step of this tutorial.
8
9
3) Create a create_jwt.py file, copy and paste the content you see here. Place your private key .pem file in the same directory as this script.
10
11
4) Change the value of \'sub\' to your Symphony service account username. Change the filename of the .pem file to the filename of your .pem file.
12
13
5) Generate jwt token simply by:
14
python create_jwt.py
15
16
6) You will see the jwt token in terminal output.
17
"""
18
19
from jose import jwt
20
import datetime as dt
21
22
23
def create_jwt():
24
private_key = get_key()
25
expiration_date = int(dt.datetime.now(dt.timezone.utc).timestamp() + (5 * 58))
26
payload = {
27
'sub': "username_of_service_account",
28
'exp': expiration_date
29
}
30
encoded = jwt.encode(payload, private_key, algorithm='RS512')
31
print(encoded)
32
33
34
def get_key():
35
with open('filename_of_private_key.pem', 'r') as f:
36
content =f.readlines()
37
key = ''.join(content)
38
return key
39
40
41
if __name__ == '__main__':
42
create_jwt()
Copied!
1
// Based on https://github.com/jwtk/njwt
2
'use strict';
3
4
let crypto = require('crypto');
5
6
function nowEpochSeconds() {
7
return Math.floor(new Date().getTime() / 1000);
8
}
9
10
function base64urlEncode(str) {
11
return new Buffer(str)
12
.toString('base64')
13
.replace(/\+/g, '-')
14
.replace(/\//g, '_')
15
.replace(/=/g, '');
16
}
17
18
function Jwt(username, signingKey) {
19
20
this.header = {};
21
this.body = {};
22
this.header.alg = 'RS512';
23
this.body.sub = username
24
this.body.exp = (nowEpochSeconds() + (5 * 60)); // five minutes in seconds
25
this.signingKey = signingKey;
26
27
return this;
28
}
29
30
Jwt.prototype.sign = function sign(payload, cryptoInput) {
31
let buffer = crypto.createSign('RSA-SHA512').update(payload).sign(cryptoInput);
32
33
return base64urlEncode(buffer);
34
};
35
36
Jwt.prototype.compact = function compact() {
37
38
let segments = [];
39
segments.push(base64urlEncode(JSON.stringify(this.header)));
40
segments.push(base64urlEncode(JSON.stringify(this.body)));
41
console.log("segments ", segments);
42
console.log("segments join ", segments.join('.'));
43
console.log("Signing key ", this.signingKey);
44
this.signature = this.sign(segments.join('.'), this.signingKey);
45
segments.push(this.signature);
46
47
return segments.join('.');
48
};
49
50
const secret = "-----BEGIN RSA PRIVATE KEY-----\n" +
51
"...REDACTED..."
52
"-----END RSA PRIVATE KEY-----";
53
54
const user = 'bot.user1';
55
const jwt = new Jwt(user, secret);
56
const jws = jwt.compact();
57
58
console.log("========== JWS ==========")
59
console.log(jws);
Copied!
1
using Microsoft.IdentityModel.Tokens;
2
using Org.BouncyCastle.Crypto;
3
using Org.BouncyCastle.Crypto.Parameters;
4
using Org.BouncyCastle.OpenSsl;
5
using System;
6
using System.Configuration;
7
using System.IdentityModel.Tokens.Jwt;
8
using System.IO;
9
using System.Security;
10
using System.Security.Claims;
11
using System.Security.Cryptography;
12
13
namespace Symphony.Util.Jwt
14
{
15
/// <summary>
16
/// Class used to generate JWT tokens signed by a specified private RSA key.
17
/// Libraries needed as dependencies:
18
/// - BouncyCastle version >= 1.8.5
19
/// - System.IdentityModel.Tokens.Jwt version >= 5.4.0
20
/// </summary>
21
public class JwtHelper
22
{
23
/// <summary>
24
/// Creates a JWT with the provided user name and expiration date, signed with the provided private key.
25
/// </summary>
26
/// <param name="user">The username to authenticate; will be verified by the pod.</param>
27
/// <param name="expiration">Expiration of the authentication request in milliseconds; cannot be longer than the value defined on the pod.</param>
28
/// <param name="privateKey">The private RSA key to be used to sign the authentication request; will be checked on the pod against the public key stored for the user.</param>
29
public static string CreateSignedJwt(string user, double expiration, SecurityKey privateKey)
30
{
31
var handler = new JwtSecurityTokenHandler
32
{
33
SetDefaultTimesOnTokenCreation = false
34
};
35
36
var tokenDescriptor = new SecurityTokenDescriptor
37
{
38
Expires = DateTime.Now.AddMilliseconds(expiration),
39
Subject = new ClaimsIdentity(new[] {
40
new Claim("sub", user)
41
}),
42
43
SigningCredentials = new SigningCredentials(privateKey, SecurityAlgorithms.RsaSha512, SecurityAlgorithms.Sha512Digest)
44
};
45
SecurityToken token = handler.CreateToken(tokenDescriptor);
46
return handler.WriteToken(token);
47
}
48
49
/// <summary>
50
/// Create a RSA Private Key from a PEM String. It supports PKCS#1 and PKCS#8 string formats.
51
/// </summary>
52
public static SecurityKey ParseRSAPrivateKey(String privateKeyFilePath)
53
{
54
if (!File.Exists(privateKeyFilePath))
55
{
56
throw new FileNotFoundException(quot;File {privateKeyFilePath} was not found");
57
}
58
59
var cryptoServiceProvider = new RSACryptoServiceProvider();
60
using (var privateKeyTextReader = new StringReader(File.ReadAllText(privateKeyFilePath)))
61
{
62
object rsaKey = null;
63
try
64
{
65
rsaKey = new PemReader(privateKeyTextReader).ReadObject();
66
}
67
catch (Exception)
68
{
69
throw new SecurityException("Invalid private key.");
70
}
71
72
73
RsaPrivateCrtKeyParameters privateKeyParams = null;
74
// PKCS#8 format.
75
if (rsaKey is RsaPrivateCrtKeyParameters)
76
{
77
privateKeyParams = (RsaPrivateCrtKeyParameters)rsaKey;
78
}
79
// PKCS#1 format
80
else if (rsaKey is AsymmetricCipherKeyPair)
81
{
82
AsymmetricCipherKeyPair readKeyPair = rsaKey as AsymmetricCipherKeyPair;
83
privateKeyParams = ((RsaPrivateCrtKeyParameters)readKeyPair.Private);
84
}
85
else
86
{
87
throw new SecurityException("Invalid private key.");
88
}
89
90
var parms = new RSAParameters
91
{
92
Modulus = privateKeyParams.Modulus.ToByteArrayUnsigned(),
93
P = privateKeyParams.P.ToByteArrayUnsigned(),
94
Q = privateKeyParams.Q.ToByteArrayUnsigned(),
95
DP = privateKeyParams.DP.ToByteArrayUnsigned(),
96
DQ = privateKeyParams.DQ.ToByteArrayUnsigned(),
97
InverseQ = privateKeyParams.QInv.ToByteArrayUnsigned(),
98
D = privateKeyParams.Exponent.ToByteArrayUnsigned(),
99
Exponent = privateKeyParams.PublicExponent.ToByteArrayUnsigned()
100
};
101
102
cryptoServiceProvider.ImportParameters(parms);
103
}
104
105
return new RsaSecurityKey(cryptoServiceProvider.ExportParameters(true));
106
}
107
108
public static string CreateJwt(string username, string privateKeyFilePath)
109
{
110
double expiration = 300000; // 5 minutes = 5*60*1000
111
SecurityKey privateKey = ParseRSAPrivateKey(privateKeyFilePath);
112
return CreateSignedJwt(username, expiration, privateKey);
113
}
114
115
static void Main(string[] args)
116
{
117
string username = ConfigurationManager.AppSettings.Get("user");
118
string privateKeyFile = ConfigurationManager.AppSettings.Get("key");
119
120
string jwt = CreateJwt(username, privateKeyFile);
121
Console.WriteLine(jwt);
122
Console.WriteLine("Press enter to exit...");
123
Console.ReadLine();
124
}
125
}
126
}
Copied!
The output of the script is a JWT:
1
eyJhbGciOiJSUzUxMiJ9.eyJzdWIiOiJib3QudXNlcjEiLCJleHAiOjMwNDMwOTY0ODV9.X9vZReZigFtJ8NDsaJ9viUp2jtc-_ktVFLm17ubEzmSJbHXS_LNy5nL6E6R8GY71g8Vuonb8qSIwy8zoR_TcUvuPAQLxCAlvQn96jFnjg4aFO3kWkFMLFgwJWWR4hn2UocdTS_pu7ROafn6rjvLJdKGEWDOHKw6JX2_Qj3uzU3LeAFhUVU8Tmop3A2OTVUkPlWJwJimIas66kFgq61uGps8RT9YMs74bxGvOJvInidK2N_dqJMDPgb4ySOBHewlhe1ziUWM-21HDq1RvmadTWoPRKRXdt4oPRoxr4KRgmluaQpz8njL7Em9Sh1bCKJWuIjlXQPOcF3SFibbAcLwr40UnT2sM2LMJtkj0BHIU_5Ans0fN1x8hKtfWX_ArzLJTCBCCqswmq8Q3vxo0-SHe33Idy99TfkrY-C8G-fgPFvs9L7695MOcYAq8SpbZQlX-anpcqLQfsw6V-V0ZEAUeSHpnZrHvwmQjEmU9wXWzvAgCpF9kEt_I4Hpu8DTx2VzVj7CRU1Lu5NPHoESjI6VKJWcCH68TvkBB88jJqflXcQfbLUdK1sjDwDKl3BurmGBZSlD0ymuBXaQe4yol4zxXzSuWo6VCy5ykXee0mZm5t9-9wJujcjnGyKjNNSVLhajrmo6BRDN86I_xgV33SHgdrJKyQCO8LzUK4ArEMYlEY0I
Copied!
The authentication token can be inspected on https://jwt.io/ or https://www.jsonwebtoken.io/.

4. Authenticate

Obtain a valid Session Token by making a POST request to your company's Session Auth endpoint:
Session Auth
1
$ curl -d '{"token":"eyJhbGciOiJSUzUxMiJ9...ik0iV6K9FrEhTAf71cFs"}' https://${symphony.url}:443/login/pubkey/authenticate
Copied!
A successful response:
200
1
{"token":"eyJhbGciOiJSUzUxMiJ9...7oqG1Kd28l1FpQ","name":"sessionToken"}
Copied!
Obtain a valid Key Manager Token by making a POST request to your company's Key Manager Auth endpoint:
Key Manager Auth
1
$ curl -d '{"token":"eyJhbGciOiJSUzUxMiJ9...ik0iV6K9FrEhTAf71cFs"}' https://${symphony.url}:443/relay/pubkey/authenticate
Copied!
A successful response:
200
1
{"token":"0100e4fe...REDACTED...f729d1866f","name":"keyManagerToken"}
Copied!

Replace/Revoke Key

You can replace the public key pubkeyA for a user with a new key, pubkeyB (for example, as part of an organization's key rotation schedule). Note the following outcomes:
    When a key is replaced, the key pubkeyA becomes the user's previous key, and the newly uploaded pubkeyB becomes the current key.
    The previous key is valid for 72 hours, but you can extend that period indefinitely in intervals of 72 hours.
    While the previous key is valid, both keys can be used for authentication. When it expires, it can no longer be used to authenticate the user.
    A user can have at most one previous key.
Alternatively, you can revoke a user key (current or previous), for example, if the key is compromised. Note the following outcomes:
    When a key is revoked, it can no longer be used for authentication.
    If a user has a non-expired previous key and their current key is revoked, the previous key becomes the new current key.
    When a key is revoked, the user's sessions initiated with RSA authentication are invalidated.
To replace/revoke a key, navigate to the Bot's account in the admin portal > RSA > Replace or Revoke:
You can also use the following REST API call to programmatically replace a public key:
1
curl -H 'sessionToken: eyJhbGciOiJSUzUxMiJ9...O3iq8OEkcnvvMFKg' -d '{
2
3
{
4
"currentKey": {
5
"key": "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhki...WMCAwEAAQ==\n-----END PUBLIC KEY-----",
6
"action": "SAVE"
7
}
8
}
9
}' https://${symphony.url}:443/pod/v2/admin/user/68719476742/update
Copied!
200
1
{
2
"userAttributes": {
3
"emailAddress": "[email protected]",
4
"userName": "demo-bot1",
5
"displayName": "DemoBot1",
6
"companyName": "pod1",
7
"accountType": "SYSTEM",
8
"currentKey": {
9
"key": "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhk...ghUGWMCAwEAAQ==\n-----END PUBLIC KEY-----"
10
},
11
"previousKey": {
12
"key": "-----BEGIN PUBLIC KEY-----MIICIjANBgkqhki...hUGWMCAwEAAQ==-----END PUBLIC KEY-----",
13
"expirationDate": 1522675669714
14
}
15
},
16
"userSystemInfo": {
17
"id": 68719476742,
18
"status": "ENABLED",
19
"createdDate": 1522318499000,
20
"createdBy": "68719476737",
21
"lastUpdatedDate": 1522416469717,
22
"lastLoginDate": 1522416465367
23
},
24
"roles": [
25
"INDIVIDUAL"
26
]
27
}
Copied!
Additionally you can programmatically revoke a public key using either currentKey or previousKey. Use the following REST request to programmatically revoke a public key using currentKey:
1
curl -H 'sessionToken: eyJhbGciOiJSUzUxMiJ9...O3iq8OEkcnvvMFKg' -d '{
2
{
3
"currentKey": {"action":"REVOKE"}
4
}
5
}' https://localhost.symphony.com:443/pod/v2/admin/user/68719476742/update
Copied!
200
1
{
2
"userAttributes": {
3
"emailAddress": "[email protected]",
4
"userName": "bot.user1",
5
"displayName": "Local Bot01",
6
"companyName": "pod1",
7
"accountType": "SYSTEM"
8
},
9
"userSystemInfo": {
10
"id": 68719476742,
11
"status": "ENABLED",
12
"createdDate": 1522318499000,
13
"createdBy": "68719476737",
14
"lastUpdatedDate": 1522416469717,
15
"lastLoginDate": 1522416465367
16
},
17
"roles": [
18