TSP Proxy Integration Guide
  • 15 Apr 2025
  • 7 Minutes to read
  • Dark
    Light
  • PDF

TSP Proxy Integration Guide

  • Dark
    Light
  • PDF

Article summary

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

Flowchart illustrating document hash verification and signature injection process with access tokens.

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.

Flowchart illustrating document hash verification and signature injection process with access tokens.

To complete a signing process, you must do the following, in order:

  1. Obtain a document hash

  2. Sign the document hash

  3. 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"
}


Was this article helpful?

Changing your password will log you out immediately. Use the new password to log back in.
First name must have atleast 2 characters. Numbers and special characters are not allowed.
Last name must have atleast 1 characters. Numbers and special characters are not allowed.
Enter a valid email
Enter a valid password
Your profile has been successfully updated.
ESC

Ozzy, our interactive help assistant