- 15 Apr 2025
- 7 Minutes à lire
- Impression
- SombreLumière
- PDF
TSP Proxy Integration Guide
- Mis à jour le 15 Apr 2025
- 7 Minutes à lire
- Impression
- SombreLumière
- PDF
A Trust Service Provider (TSP) is a component or service that provides trust-related functionalities for use in electronic signing. These functionalities typically include managing and validating digital certificates, handling secure communications, and ensuring the integrity and authenticity of data.
A TSP API proxy is a server-side component that acts as an intermediary between a client application and a remote server. It is often used to handle requests and responses, especially when dealing with cross-domain issues or security concerns.
To create a strong connection between your application and the TSP Proxy, and to connect your client application to the OneSpan Sign platform, you need to focus on two main areas. The first is decrypting the MiniToken, and the second is getting the access token from the TSP OAuth2 service.
For the purposes of this article Client Application refers to a program or tool that interacts with an API to send requests and receive responses.
Additionally, during the document signing process, you need to do two things in the workflow: obtain the document hash and add the signature.
TSP Proxy Workflow
MiniToken Decryption and Verification
MiniToken is sent using a standard JSON Web Encryption (JWE) that is encrypted with AES256 and signed with RSA. The Client Application receives a 256-bit secret to decrypt the JWE and must verify the JSON Web Token (JWT) with an online public key; this verification is very important.
Here is a common example of how to decrypt and validate a MiniToken in Java; you can use this code as a reference for other programming languages:
To ensure your code runs smoothly, make sure that you have added the following dependencies to your project:
<dependencies>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>8.3</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
For example:
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.AESDecrypter;
import com.nimbusds.jose.crypto.RSASSAVerifier;
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jwt.SignedJWT;
import com.onespan.external_connector.ec_mockca.model.MiniToken;
import net.minidev.json.JSONObject;
import org.springframework.web.reactive.function.client.WebClient;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.security.interfaces.RSAPublicKey;
import java.text.ParseException;
import java.util.Date;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
public class MiniTokenDecryptionExample {
public static void main(String[] args) throws ParseException, JOSEException, JsonProcessingException {
String minitokenStr = "a JWE carried the MiniToken";
String sharedSecret = "a shared secret";
String targetEnv = "https://sandbox.e-signlive.com";
String jwksUrl = targetEnv + "/rs/oauth2/jwks";
String jtspUrl = targetEnv + "/rs";
String yourInitUrl = "https://your-domain.com/ec/init";
// 1. generate a symmetric key from the shared secret
SecretKey key = new SecretKeySpec(sharedSecret.getBytes(), "AES");
// 2. decrypt the JWE into JWT
JWEObject jweObject = JWEObject.parse(minitokenStr);
// 2.1 verify the encryption algorithm
EncryptionMethod enc = jweObject.getHeader().getEncryptionMethod();
assertEquals(EncryptionMethod.A256GCM, enc);
jweObject.decrypt(new AESDecrypter(key));
SignedJWT jwt = jweObject.getPayload().toSignedJWT();
// 2.2 verify the signing algorithm
JWSAlgorithm algo = jwt.getHeader().getAlgorithm();
assertEquals(JWSAlgorithm.RS256, algo);
// 3. verify if the JWT is expired
Date exp = jwt.getJWTClaimsSet().getExpirationTime();
assertTrue(exp.after(new Date()));
// 4.1. obtain the public key
WebClient httpClient = WebClient.builder().build();
String response = httpClient.get()
.uri(jwksUrl)
.retrieve()
.bodyToMono(String.class)
.block();
JWKSet jwks = JWKSet.parse(response);
JWK jwk = jwks.getKeyByKeyId(jwt.getHeader().getKeyID());
RSAPublicKey publicKey = ((RSAKey) jwk).toRSAPublicKey();
// 4.2. verify the signature with the public key
JWSVerifier verifier = new RSASSAVerifier(publicKey);
assertTrue(jwt.verify(verifier));
// 4.3. verify the issuer and audience
String iss = jwt.getJWTClaimsSet().getIssuer();
assertEquals(jtspUrl, iss);
List<String> auds = jwt.getJWTClaimsSet().getAudience();
assertTrue(auds.contains(yourInitUrl));
// 5. decode the claim set to get the MiniToken
JSONObject mt = (JSONObject) jwt.getJWTClaimsSet().getClaim("MiniToken");
ObjectMapper mapper = new ObjectMapper();
MiniToken minitoken = mapper.readValue(mt.toJSONString(), MiniToken.class);
Map<String, String> params = minitoken.getContents();
assertFalse(params.isEmpty());
}
}
The MiniToken Class
@Data
public class MiniToken {
private String sid;
private String lang;
private String callbackUrl;
private String otp;
private Map<String, String> contents;
}
Verification
The verification against the MiniToken JWE is mandatory. The following items need to be verified:
JWE Encryption Method: Expected to be A256GCM.
JWT Signing Method: Expected to be RS256.
Token Expiration: Check if the token has expired.
Token Verification: Check if the token is valid and reliable.
Token Issuer: Should be the base URL of the target environment followed by "/rs".
Token Audience: The entry point you provided to OneSpan should be on the list..
Any failure in verification indicates that this is an unlawful JWE that must be denied, and the application should terminate immediately.
Access Token
The TSP OAuth2 service only supports the Client Credential grant type. After you are successfully authenticated, you will receive an access token.
Authentication
Client applications need credentials to obtain an access token for utilizing the TSP Proxy API. A credential, consisting of a Client ID and Client Secret, will be provided after you complete your registration with OneSpan successfully.
Authorization
The client application will receive a specific Scope to utilize the assigned TSP Proxy API.
To obtain the access token, perform an HTTP request similar to the one below:
curl --location --request POST 'https://{hostname}/oauth2/token' \
--header 'Authorization: Basic ${base64(clientId:clientSecret)}' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode 'otp=${OTP from Mini-Token}' \
--data-urlencode 'scope=${assigned scope}'
This call can only be performed a single time during the signing procedure, as the OTP (One Time Passcode) is created right before the client application obtains the MiniToken and is removed once the access token is granted. Therefore, it is crucial for the client application to store the access token for all future requests.
Verification
The verification against the access token is optional. The following items can be verified.
Token Expiration: Verify whether the token has expired.
Token Verification: Ensure the token is valid and trustworthy. This can be done similarly to how JWT is validated above.
Token Provider: Should be the base URL of the target environment followed by "/rs".
OAuth2 API
GET/rs/oauth2/token to get an access token.
POST/rs/oauth2/jwks to get a JWKS (JSON Web Key Set) from which the public key can be obtained.
Proxy API
GET/rs/proxy/api/hash to get a document hash
POST/rs/proxy/api/inject to inject the signed document hash (signature) into the document (pdf)
For more detailed information on the Proxy API, see https://sandbox.esignlive.com/rs/proxy.
Signing Process
The actual signing process occurs during steps 4-6 of the diagram above.
To complete a signing process, you must do the following, in order:
Obtain a document hash
Sign the document hash
Inject the signed document hash into the document
Obtaining a Document Hash
To obtain a document hash the following data needs to be provided in the request body:
SID: Session ID provided by the MiniToken;
Signature Type: PKCS1, PKCS7, or CMS;
Signer Certificate: Must be in PEM format. The signer certificate can have a header and footer or no header and footer, but it cannot have just a header, or just a footer.
For example:
{
"sid": "string",
"lang": "string",
"content": {
"signatureType": "PKCS1",
"signerCert": "string"
}
}
Then, make a POST call with the request body above to /proxy/api/hash
. The following response will be returned:
{
"sid": "string",
"lang": "string",
"result": {
"hash": "string"
}
}
The document hash is in the hash field. The hash uses RSA SHA256.
Signing a Document Hash
This step needs to be performed by a Certificate Authority, and each Certificate Authority has their own workflow. Refer to your organization's Certificate Authority process to sign the document hash.
The TSP Proxy only supports PKCS1, PKCS7, and CMS signature types.
Injecting a Signature
As soon as the document hash is signed, the signed hash can be sent to TSP Proxy for injection. To do this, the following data needs to be prepared:
Signature Type: Must be PKCS1 or PKCS7 and must be the same value as the previous call.
Signed Hash: The signed document hash.
For example:
{
"sid": "string",
"lang": "string",
"content": {
"signatureType": "PKCS1",
"signedHash": "string"
}
}
Then, make a POST call with the request body above to /proxy/api/inject
. The following response will be returned:
{
"sid": "string",
"lang": "string",
"result": {
"status": "string",
"returnUrl": "string"
}
}
If the status
" field has a value other than success, then this call needs to be re-tried.
The returnUrl
field provides a redirection URL to bring signer back to the beginning of the signing process.
Appendix
A typical JWE carried MiniToken
The following shows a standard JWE that contains a MiniToken.
eyJlbmMiOiJBMjU2R0NNIiwidGFnIjoiTHFRVHUwN2xocVpISHl6TF9qT0pBZyIsImFsZyI6IkEyNTZHQ01LVyIsIml2IjoiVHFHYjFvRFFtbkhOZXVBVyJ9.5POunw5dPvtem2MHxJsNodj_iHtqkFoL4c6cBIpy4iA.Q1d3XKUMB3WsAghA.rOE6xDKdMjYzEyQgQvcdqszGVeC-OFh74ue-MpfTlxYyZVsmUGjOo62eWOSJ_lfBDngaWycyziPeDTzUtMV63uVM-m5TanIcQC5Za3uYHGqDkgFHMyCRlZiVWDtgKCL9SDGpAkVzq6jtAKu6xeN5F-0DGZVKLMUHtKBpoJ8DaTXos275OpsD9fu_WykTJqoMAoTQy6KwjE2BurjW1eikOtoIS0CxobrpzsP_FkEeR7W0OQKC6U_VVk-5SO4Az5HWH96YtZIolYmw5TBhN0E45mkllMrgQD9ZHIWoMNOAjfS-G6SP2tmxT4Clpu0OQ3ycMtJHXqAGVIwJxJ-i_8L79H-u7j4YsVoH4_QuOK_trU7SI0M_HtrZB86qdseQUmehgrLjfa0kFGlQnIMuaRc-WLBJXT8VBdUaSJSS8xJSiesVBK6XjGR8bGNwfaMsUn86kACrQd26ftbH6d7SnYE2GLYQvSxIyUaPK5DczWTXztN1gVAuWZvDlEHZlkhbNXTrENRunnQn_Ekk4-bDC44bYerhIWk3BRNKNK7BMZL-SqppJu9pTL3ZaDMgDn4gRF6Xele1oMAPEw85XAQJIQzZwhJkpx-t6_fZiZcW1NAmcdN0jMl90FYsA9eVlJfmxIiGtYBLYxF67xUddwExUyawHYI-wDs7wfmZO2k8cVFycZFf-x68u-jIG-a_BYWBP5uGO0h8iwn8J-UUwDmTSUEE2n_2ZHF9mtakuhZV_-G9ld1ZMRPi-hVdcQUp_1lvsyVO1Efizy4mRuuoMvXsWDc-W7tM5IdPxLCT7GtgJXTeZOHdrF4zuZ4zZAOT4-zMfxDhlkXLpTCZQtI-FOFCaeGgxc4oxVrVwkt75rTY3WhRQiohyNP8LLBTcKpoiWamNo3Y6vaIFSIW9JWtUKRbU6fpWcDi-0TFB7x7x9DqXe9vHE1r73haa9Dwye_YQy_dmIfOQIK04GCbE96D8kH_LPHrNhiVlBBEkWJhe6TJ0H9K5En5oIYfhGs37uolPaW7cVxD32ZgvxEM6XVZNYs-qJSYRg3-_n5ImqYdCfzZDF7TETAfQJfE_jrTRmuNi22Fis9jPPxp9BAWUk2Hk0i5A_K4rZJEK-3N_is42dYtXc3l6A_o1lmKiX7sQ9eYB9G5VAPdD5nbo0FjHCMflWT_ymp98BPl044jiIeNKtbePQis_4vD5T6v1gbyIpURJ9YmqCm29k0G4S7HkpS9xeePsdahuv8cGRo4i9esFjr5CxXREB_nS-vyFgWAj1fD7aHbQ2ZbtpU9MAw0O0yCqUHQ8rcdUXmwktNmxa1aP5nExSTtRk6hNQP3JkgYpA5QGjGtzbqrEcJouw-K97oxz4ETOBENk25ZvI5Rv5Lqx5q5RC91xZEUnqhk1u4osXwDAYrex_YTz-o5ghtp4zBgT08iZftZ6ZEvvYPOmlHq8GNAxKcPZkGcJceIj-WExE8ugOTF9R-M3gVPmC2WmBBK5r-omlN7mCebl5CQnrvWsnOYT5VjIzh0wyhciP3zo5TUyyEKv3wL6r48NFWN3Czj0U7WZPtYJvaouci-gfnGoNhlRMxXUZvIWYqbawbsJ--FiyBDVb82xjWWqYeyfuUJ_c4yK1soY6PuJxuFagvBCdDnibvfZePcJVMWaWv8pLKna9uUNQ6hWI36-BreTQ-0t6sb0pa5TQkmbhP8ieVOciQ8e4tPV72OjU9ElTBclCY80JXaLuNr50dDIyw-7eI8l_jS.PHmauhRpPtD4-BLTmafCSw
Header after decoding
{
"enc": "A256GCM",
"tag": "LqQTu07lhqZHHyzL_jOJAg",
"alg": "A256GCMKW",
"iv": "TqGb1oDQmnHNeuAW"
}
The JWT from decrypting the JWE
eyJraWQiOiJjNjQzNjk5MC05YjMzLTRiZjEtYTI0Ny1kMTNlYjMwNDMyZjMiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL29zc3EzLnJuZC5lc2lnbmxpdmUuY29tL3JzIiwiYXVkIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6OTg5OS9lYy9pbml0IiwiTWluaVRva2VuIjp7InNpZCI6ImE0Y2E0Zjc3MGU4ZTZkMDg5ZjhmZjFiMmUxMmZlYTNiMTg3N2NjNzE0YzcyZGYzMmIyYWE0NmRjZTc0YmZkMzkiLCJsYW5nIjoiZW4iLCJjYWxsYmFja1VybCI6Imh0dHBzOi8vb3NzcTMucm5kLmVzaWdubGl2ZS5jb20vdHJhbnNhY3Rpb24vWDdhT1BQTldSQkQzb1l4ZGFyMjlaT1ZvRUtVPS9zaWduP2luaXRpYWxEb2N1bWVudElkPTI5Mzc0MWRiMzg4ZjQ1MGE1N2U4YTRjMWZmMzViZTg3ZjI3M2I1NDBhNDBmMGM5MCZzaWduZXJJZD1kOTNhdU1WUlNyOFImZG9jdW1lbnROYXZPcGVyYXRpb249bmV4dCIsIm90cCI6IjZiZWEzYmQzLWRmNTQtNDVlYy04ODZiLTYyMTM5ZTE2ZWZkOSIsImNvbnRlbnRzIjp7ImRvY05hbWUiOiJUZXN0X0RvY3VtZW50X0VtcHR5Iiwicm9sZUlkIjoibkt1dXgxNjNpQnNNIiwiZG9jSWQiOiIyOTM3NDFkYjM4OGY0NTBhNTdlOGE0YzFmZjM1YmU4N2YyNzNiNTQwYTQwZjBjOTAiLCJ0cmFuc0lkIjoiWDdhT1BQTldSQkQzb1l4ZGFyMjlaT1ZvRUtVPSIsInNpZ25hdHVyZVR5cGUiOiJQS0NTMSJ9fSwiZXhwIjoxNzQyMjQzODE3LCJpYXQiOjE3NDIyNDM1MTd9.aM54pCQ_RyEKIFxqzeY-KGvML7s-Z1Q7NXE-VqV1O1HuJU__KU_77bOEJuBw7bQbQPk_6sTSWrwPZ71V9QBxMT6KFu0zUGfTxMcN90x9gyYN1QMshQDhv8wUjvCk9KmIjLtMxaqGAPS0BD5xxpHt9a9B2D9aGhqAhqYMZx20GMlTjVLkyEi40IG5kU9s5UYSxbg9RgFGSV_55kK3B3vLQRiGV2qqAVSzmVCyB4lPKDQNViiOZyyweaaLx0Ri7d1AmYU5NTRV8DrtEcFbF4Uq-VxdWf6MZrFnlI9UFLdGcr89V4Q3zG2QPWtSftp5hyW0ldZvBD1Pt-JN_s-AItig6w
Header after decoding
{
"kid": "c6436990-9b33-4bf1-a247-d13eb30432f3",
"alg": "RS256"
}
Payload after decoding:
{
"iss": "https://ossq3.rnd.esignlive.com/rs",
"aud": "https://localhost:9899/ec/init",
"MiniToken": {
"sid": "a4ca4f770e8e6d089f8ff1b2e12fea3b1877cc714c72df32b2aa46dce74bfd39",
"lang": "en",
"callbackUrl": "https://ossq3.rnd.esignlive.com/transaction/X7aOPPNWRBD3oYxdar29ZOVoEKU=/sign?initialDocumentId=293741db388f450a57e8a4c1ff35be87f273b540a40f0c90&signerId=d93auMVRSr8R&documentNavOperation=next",
"otp": "6bea3bd3-df54-45ec-886b-62139e16efd9",
"contents": {
"docName": "Test_Document_Empty",
"roleId": "nKuux163iBsM",
"docId": "293741db388f450a57e8a4c1ff35be87f273b540a40f0c90",
"transId": "X7aOPPNWRBD3oYxdar29ZOVoEKU=","signatureType": "PKCS1"
}
},
"exp": 1742243817,
"iat": 1742243517
}
A typical JWK
{
"keys": [
{
"kty": "RSA",
"e": "AQAB",
"kid": "c6436990-9b33-4bf1-a247-d13eb30432f3",
"n": "1WzJWdMmvdkah4szAo6t0VA5jEGNG6KFSMwB7ZLNAyQR2zfZ-HTYCfZGbC-BOnyodzsVoBA6MCWHak1nN098ZTrp0rWFz3WeeYRPYaJGLzmoeKzhoFyEg2WVYXzHBJfn95vYfgT1POhuCDofZnuZl5CpdOUxN6E-MOU6asZ7JtPwluqlMxeJH1CXr5hKdvy3GC3aOXwWdQw5edloGEglPmHpMGuXH667-mDPFSfGISaBn5qIqsDLXPLujim_Cgfuvm7kxAqjmxOjQPInsw6lpdOHc-EZpHLHoIzxtafPzX7TYCUmjv3Bkpe3teBJMHGp0Y_YCn97DZdLbOeyskpWdQ"
}
]
}
A typical Access Token
eyJraWQiOiJjNjQzNjk5MC05YjMzLTRiZjEtYTI0Ny1kMTNlYjMwNDMyZjMiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJwcm94eS1vbmVzcGFuIiwiYXVkIjoiaHR0cHM6Ly9vc3NxMy5ybmQuZXNpZ25saXZlLmNvbS9ycy9wcm94eSIsIm5iZiI6MTc0MjI0MzUyNiwic2NvcGUiOlsicHJveHkuYXBpIl0sImlzcyI6Imh0dHBzOi8vb3NzcTMucm5kLmVzaWdubGl2ZS5jb20vcnMiLCJleHAiOjE3NDIyNDUzMjYsImlhdCI6MTc0MjI0MzUyNiwianRpIjoiMDk2YTIwMTUtYTY1ZC00YTc3LWI5ZjEtOWNkYzg2NWU2MDhlIn0.0NidkTxNEdidO4jx50sD5tnwMSX6Jhy9ggIGG34fBhr33w6OHsqKFtRAfo7xAUOysCyk3Flw8ZgsUKFDmich1rYznu5Zwm6xA-3O4d_LlDm7-dDRzR_sECrBagyllwZQVXHmvuSOha63W-PBkjixxn3UwpK93pjSyXKCBSrhFCwzx63gKsd0jPaN_DP7mDJ8jBSGbfEfCCbEkCduU1-zNCSfAFWrEuKvKzdUgV8hoOn9Dzs48vTuTih2PCPQRIEyNnpn3A2mH10D3-TIHcBsAFFNg5QK7A8LTcWu5C9aCgCaF9zKjEYspUArzzxWSfMHAJJ5E5slSnmwwQIKSdFmbQ
Header after decoding
{
"kid": "cb6ffa24-bcaf-4aae-904b-a0ce979b846d",
"alg": "RS256"
}
Payload after decoding
{
"sub": "proxy-onespan",
"aud": "https://domain.com/rs/proxy",
"nbf": 1742243526,
"scope": [
"proxy.api"
],
"iss": "https://domain.com/rs",
"exp": 1742245326,
"iat": 1742243526,
"jti": "096a2015-a65d-4a77-b9f1-9cdc865e608e"
}