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:
The Authentication process requires the following steps:
The user creates a public/private RSA key pair.
The admin imports the public key into the pod using the Admin Console or public APIs.
The user creates a short-lived JWT (JSON Web Token) and signs it with their private key.
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:
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.
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
Please note the below steps can only be performed by a Symphony Pod Administrator as they will have the necessary administrator privileges to access the Administration Portal.
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:
packagecom.symphony.util.jwt;importio.jsonwebtoken.Jwts;importio.jsonwebtoken.SignatureAlgorithm;importorg.bouncycastle.asn1.pkcs.RSAPrivateKey;importorg.bouncycastle.crypto.params.RSAPrivateCrtKeyParameters;importorg.bouncycastle.crypto.util.PrivateKeyInfoFactory;importorg.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;importorg.bouncycastle.util.io.pem.PemObject;importorg.bouncycastle.util.io.pem.PemReader;importjava.io.FileNotFoundException;importjava.io.IOException;importjava.io.StringReader;importjava.nio.charset.StandardCharsets;importjava.nio.file.Files;importjava.nio.file.Paths;importjava.security.GeneralSecurityException;importjava.security.Key;importjava.security.KeyFactory;importjava.security.PrivateKey;importjava.security.spec.PKCS8EncodedKeySpec;importjava.util.Base64;importjava.util.Date;importjava.util.stream.Stream;/** * Class used to generate JWT tokens signed by a specified private RSA key. * Libraries needed as dependencies: * - BouncyCastle (org.bouncycastle.bcpkix-jdk15on) version 1.59. * - JJWT (io.jsonwebtoken.jjwt) version 0.9.1. * * */publicclassJwtHelper {// PKCS#8 formatprivatestaticfinalString PEM_PRIVATE_START ="-----BEGIN PRIVATE KEY-----";privatestaticfinalString PEM_PRIVATE_END ="-----END PRIVATE KEY-----";// PKCS#1 formatprivatestaticfinalString PEM_RSA_PRIVATE_START ="-----BEGIN RSA PRIVATE KEY-----";privatestaticfinalString PEM_RSA_PRIVATE_END ="-----END RSA PRIVATE KEY-----"; /** * Get file as string without spaces * @param filePath: filepath for the desired file. * @return */publicstaticStringgetFileAsString(String filePath) throwsIOException {StringBuilder message =newStringBuilder();String newline =System.getProperty("line.separator");if (!Files.exists(Paths.get(filePath))) {thrownewFileNotFoundException("File "+ filePath +" was not found."); }try (Stream<String> stream =Files.lines(Paths.get(filePath))) {stream.forEach(line -> message.append(line).append(newline));// Remove last new line.message.deleteCharAt(message.length() -1); } catch (IOException e) {System.out.println(String.format("Could not load content from file: %s due to %s",filePath, e));System.exit(1); }returnmessage.toString(); } /** * Creates a JWT with the provided user name and expiration date, signed with the provided private key. * @param user the username to authenticate; will be verified by the pod * @param expiration of the authentication request in milliseconds; cannot be longer than the value defined on the pod
* @param 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 */privatestaticStringcreateSignedJwt(String user,long expiration,Key privateKey) {returnJwts.builder().setSubject(user).setExpiration(newDate(System.currentTimeMillis() + expiration)).signWith(SignatureAlgorithm.RS512, privateKey).compact(); } /** * Create a RSA Private Key from a PEM String. It supports PKCS#1 and PKCS#8 string formats */ private static PrivateKey parseRSAPrivateKey(String privateKeyFilePath) throws GeneralSecurityException, IOException {
String pemPrivateKey =getFileAsString(privateKeyFilePath);try {if (pemPrivateKey.contains(PEM_PRIVATE_START)) { // PKCS#8 formatString privateKeyString = pemPrivateKey.replace(PEM_PRIVATE_START,"").replace(PEM_PRIVATE_END,"").replace("\\n","\n").replaceAll("\\s","");byte[] keyBytes =Base64.getDecoder().decode(privateKeyString.getBytes(StandardCharsets.UTF_8));PKCS8EncodedKeySpec keySpec =newPKCS8EncodedKeySpec(keyBytes);KeyFactory fact =KeyFactory.getInstance("RSA");returnfact.generatePrivate(keySpec); } elseif (pemPrivateKey.contains(PEM_RSA_PRIVATE_START)) { // PKCS#1 formattry (PemReader pemReader =newPemReader(new StringReader(pemPrivateKey))) {PemObject privateKeyObject =pemReader.readPemObject();RSAPrivateKey rsa =RSAPrivateKey.getInstance(privateKeyObject.getContent());RSAPrivateCrtKeyParameters privateKeyParameter =newRSAPrivateCrtKeyParameters(rsa.getModulus(),rsa.getPublicExponent(),rsa.getPrivateExponent(),rsa.getPrime1(),rsa.getPrime2(),rsa.getExponent1(),rsa.getExponent2(),rsa.getCoefficient() ); return new JcaPEMKeyConverter().getPrivateKey(PrivateKeyInfoFactory.createPrivateKeyInfo(privateKeyParameter));
} catch (IOException e) {thrownewGeneralSecurityException("Invalid private key."); } } else {thrownewGeneralSecurityException("Invalid private key."); } } catch (Exception e) {thrownewGeneralSecurityException(e); } } public static String createJwt(String username, String privateKeyFilePath) throws IOException, GeneralSecurityException {
finallong expiration =300000L;finalPrivateKey privateKey =parseRSAPrivateKey(privateKeyFilePath);returncreateSignedJwt(username, expiration, privateKey); }publicstaticvoidmain(String[] args) throws IOException, GeneralSecurityException {finalString username =System.getProperty("user");finalString privateKeyFile =System.getProperty("key");finalString jwt =createJwt(username, privateKeyFile);System.out.println(jwt); }}
"""0) Use python 31) Install python dependency required to run this script by: pip install python-jose2) The .pem file used in this script was generated from the previous step of this tutorial.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.
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.
5) Generate jwt token simply by: python create_jwt.py6) You will see the jwt token in terminal output."""from jose import jwtimport datetime as dtdefcreate_jwt(): private_key =get_key() expiration_date =int(dt.datetime.now(dt.timezone.utc).timestamp() + (5*58)) payload ={'sub':"username_of_service_account",'exp': expiration_date} encoded = jwt.encode(payload, private_key, algorithm='RS512')print(encoded)defget_key():withopen('filename_of_private_key.pem', 'r')as f: content =f.readlines() key =''.join(content)return keyif__name__=='__main__':create_jwt()
usingMicrosoft.IdentityModel.Tokens;usingOrg.BouncyCastle.Crypto;usingOrg.BouncyCastle.Crypto.Parameters;usingOrg.BouncyCastle.OpenSsl;usingSystem;usingSystem.Configuration;usingSystem.IdentityModel.Tokens.Jwt;usingSystem.IO;usingSystem.Security;usingSystem.Security.Claims;usingSystem.Security.Cryptography;namespaceSymphony.Util.Jwt{ /// <summary> /// Class used to generate JWT tokens signed by a specified private RSA key. /// Libraries needed as dependencies: /// - BouncyCastle version >= 1.8.5 /// - System.IdentityModel.Tokens.Jwt version >= 5.4.0 /// </summary>publicclassJwtHelper { /// <summary> /// Creates a JWT with the provided user name and expiration date, signed with the provided private key. /// </summary> /// <paramname="user">The username to authenticate; will be verified by the pod.</param> /// <param name="expiration">Expiration of the authentication request in milliseconds; cannot be longer than the value defined on the pod.</param>
/// <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>
publicstaticstringCreateSignedJwt(string user,double expiration,SecurityKey privateKey) {var handler =newJwtSecurityTokenHandler { SetDefaultTimesOnTokenCreation =false };var tokenDescriptor =newSecurityTokenDescriptor { Expires =DateTime.Now.AddMilliseconds(expiration), Subject =newClaimsIdentity(new[] {newClaim("sub", user) }), SigningCredentials = new SigningCredentials(privateKey, SecurityAlgorithms.RsaSha512, SecurityAlgorithms.Sha512Digest)
};SecurityToken token =handler.CreateToken(tokenDescriptor);returnhandler.WriteToken(token); } /// <summary> /// Create a RSA Private Key from a PEM String. It supports PKCS#1 and PKCS#8 string formats. /// </summary>publicstaticSecurityKeyParseRSAPrivateKey(String privateKeyFilePath) {if (!File.Exists(privateKeyFilePath)) {thrownewFileNotFoundException($"File {privateKeyFilePath} was not found"); }var cryptoServiceProvider =newRSACryptoServiceProvider();using (var privateKeyTextReader =newStringReader(File.ReadAllText(privateKeyFilePath))) {object rsaKey =null;try { rsaKey =newPemReader(privateKeyTextReader).ReadObject(); }catch (Exception) {thrownewSecurityException("Invalid private key."); }RsaPrivateCrtKeyParameters privateKeyParams =null; // PKCS#8 format.if (rsaKey isRsaPrivateCrtKeyParameters) { privateKeyParams = (RsaPrivateCrtKeyParameters)rsaKey; } // PKCS#1 formatelseif (rsaKey isAsymmetricCipherKeyPair) {AsymmetricCipherKeyPair readKeyPair = rsaKey asAsymmetricCipherKeyPair; privateKeyParams = ((RsaPrivateCrtKeyParameters)readKeyPair.Private); }else {thrownewSecurityException("Invalid private key."); }var parms =newRSAParameters { Modulus =privateKeyParams.Modulus.ToByteArrayUnsigned(), P =privateKeyParams.P.ToByteArrayUnsigned(), Q =privateKeyParams.Q.ToByteArrayUnsigned(), DP =privateKeyParams.DP.ToByteArrayUnsigned(), DQ =privateKeyParams.DQ.ToByteArrayUnsigned(), InverseQ =privateKeyParams.QInv.ToByteArrayUnsigned(), D =privateKeyParams.Exponent.ToByteArrayUnsigned(), Exponent =privateKeyParams.PublicExponent.ToByteArrayUnsigned() };cryptoServiceProvider.ImportParameters(parms); }returnnewRsaSecurityKey(cryptoServiceProvider.ExportParameters(true)); }publicstaticstringCreateJwt(string username,string privateKeyFilePath) {double expiration =300000; // 5 minutes = 5*60*1000SecurityKey privateKey =ParseRSAPrivateKey(privateKeyFilePath);returnCreateSignedJwt(username, expiration, privateKey); }staticvoidMain(string[] args) {string username =ConfigurationManager.AppSettings.Get("user");string privateKeyFile =ConfigurationManager.AppSettings.Get("key");string jwt =CreateJwt(username, privateKeyFile);Console.WriteLine(jwt);Console.WriteLine("Press enter to exit...");Console.ReadLine(); } }}
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:
curl-H'sessionToken: eyJhbGciOiJSUzUxMiJ9...O3iq8OEkcnvvMFKg'-d'{ { "currentKey": { "key": "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhki...WMCAwEAAQ==\n-----END PUBLIC KEY-----", "action": "SAVE" } }}'https://${symphony.url}:443/pod/v2/admin/user/68719476742/update
{"userAttributes":{"emailAddress":"demo-bot1@symphony.com","userName":"demo-bot1","displayName":"DemoBot1","companyName":"pod1","accountType":"SYSTEM","currentKey":{"key":"-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhk...ghUGWMCAwEAAQ==\n-----END PUBLIC KEY-----" },"previousKey":{"key":"-----BEGIN PUBLIC KEY-----MIICIjANBgkqhki...hUGWMCAwEAAQ==-----END PUBLIC KEY-----","expirationDate":1522675669714 } },"userSystemInfo":{"id":68719476742,"status":"ENABLED","createdDate":1522318499000,"createdBy":"68719476737","lastUpdatedDate":1522416469717,"lastLoginDate":1522416465367 },"roles": ["INDIVIDUAL" ]}
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:
{"userAttributes":{"emailAddress":"demo-bot1@symphony.com","userName":"demo-bot1","displayName":"DemoBot1","companyName":"pod1","accountType":"SYSTEM","currentKey":{"key":"-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhk...ghUGWMCAwEAAQ==\n-----END PUBLIC KEY-----" } },"userSystemInfo":{"id":68719476742,"status":"ENABLED","createdDate":1522318499000,"createdBy":"68719476737","lastUpdatedDate":1522416469717,"lastLoginDate":1522416465367 },"roles": ["INDIVIDUAL" ]}
Extending a Public Key
Use the following REST request to programmatically extend a public key:
{"userAttributes":{"emailAddress":"demo-bot1symphony.com","userName":"demo-bot1","displayName":"DemoBot1","companyName":"pod1","accountType":"SYSTEM","currentKey":{"key":"-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhk...ghUGWMCAwEAAQ==\n-----END PUBLIC KEY-----" },"previousKey":{"key":"-----BEGIN PUBLIC KEY-----MIICIjANBgkqhki...hUGWMCAwEAAQ==-----END PUBLIC KEY-----","expirationDate":1522675669714 } },"userSystemInfo":{"id":68719476742,"status":"ENABLED","createdDate":1522318499000,"createdBy":"68719476737","lastUpdatedDate":1522416469717,"lastLoginDate":1522416465367 },"roles": ["INDIVIDUAL" ]}
Restricted Key Operations:
You CANNOT perform the following actions:
EXTEND on the current active key
EXTEND over an expired key
EXTEND over the previous key when no previous key is set for the user
EXTEND when no expiration date is set for the user's previous key
REVOKE over the previous key when no previous key is set for the user
REVOKE over a current key when no current key is set for the user
SAVE when the user already has a valid current and previous key set
SAVE over an old key
Note: When performing a SAVE, the key must be different from your current key.