iss, exp), managing secrets securely, and keeping tokens short-lived.
This raises an important question: Who is responsible for implementing these security rules?
In some API designs, customers are asked to generate their own JWTs. The approach is seemingly straightforward: "Here is a secret key. Please create a JWT according to our guidelines, sign it, and include it in your Authorization header."
However, this model shifts the complex and critical responsibility of security onto you, the developer of a consumer application. At OneGround, we believe that security should be a shared responsibility, but the burden of token creation should lie with the API provider. This is why we have started using a standard OAuth2 Token Endpoint, which allows you to request a token from us instead of creating one yourself.
This article explains why our approach is more secure, reliable, and ultimately simpler for you.
When an API provider asks you to create your own JWT, you become the token issuer (iss). This means you are responsible for managing the entire token generation process securely. While this might seem to offer flexibility, it introduces significant risks and complexities, not because of any oversight on your part, but because token generation is a sensitive security function.
Here are some of the challenges with this model:
Lack of Enforced Token Expiration: A critical security practice is to use short-lived tokens. If you were to generate your own tokens, you would be responsible for managing their expiration. While our guidelines might suggest a one-hour expiration, it would be technically possible to create tokens with very long lifetimes, for example, to work around re-authentication logic. Such long-lived tokens would expose your application and our API to security risks, such as replay attacks, if a token is ever compromised. To prevent this, we would need to add complex validation checks on our side, which is a reactive security measure, not a proactive one.
It Creates Secret Management Burdens: To enable you to sign JWTs, we would have to share a signing secret with you. This would place the burden of protecting that secret entirely on you. If the secret were accidentally leaked—for instance, by being committed to a code repository, embedded in a client-side application, or logged in plain text—an attacker could create valid tokens indefinitely. This would pose a significant security threat to your integration and data.
You Have No Control Over the Implementation:
To generate JWTs, your developers would need to select and use a library for the language of their choice. This introduces the risk of using outdated or insecure libraries that may contain vulnerabilities. For example, some older JWT libraries were susceptible to the alg: "none" vulnerability, where they would accept a token without a signature. This would allow an attacker to forge tokens and bypass security checks entirely.
It Increases API Complexity:
In a "bring your own JWT" model, our API would need to perform extensive validation on every single request to check for inconsistencies in how different customers implement their token generation. We would have to defensively validate every claim (iss, aud, exp) to ensure they are correctly implemented, adding overhead and complexity.
The OAuth2 framework, specifically the Client Credentials Grant flow, provides a standardized and much more secure solution that we use at OneGround.
Here’s how it works:
POST request to our token endpoint (e.g., /oauth/token).client_id and client_secret that we provide to you.iss, aud, exp, etc.).RS256) with our private key. This key never leaves our server, which is a major security advantage over sharing a secret with you.Authorization header of your API requests. When it expires, you simply request a new one.Here’s what a token request and its usage look like in practice:
Request:
POST /token HTTP/1.1
Host: idp.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
&client_id=abc123xyz
&client_secret=very-secure-secret-here
Response:
{
"access_token": "eyJhbGciOi...",
"token_type": "Bearer",
"expires_in": 3600
}
Using the token:
GET /api/resources HTTP/1.1
Host: zgw-api.example.com
Authorization: Bearer eyJhbGciOi...
With this model, you don't have to worry about the internal structure of the JWT, signing algorithms, or claim management. You simply exchange your credentials for a ready-to-use token.
This model ensures that the responsibility for creating secure tokens remains with us, the API provider. As the issuer (iss), we have full control over the security of the token generation process.
We Maintain All Best Practices on Your Behalf
iss, aud, exp) are accurate, present, and standardized.RS256. Your application only needs to use the token, not understand its cryptographic implementation.Private Signing Key Stays Secret
client_secret we provide you is not a signing key, it functions more like a password for your application to authenticate itself when requesting a token. The risk of a leak is significantly lower:
client_secret can only request tokens, not create them. We can detect and rate-limit this activity.client_id or rotate your credentials without affecting the entire system's security. Revoking a client_secret is a straightforward, immediate action.It's Easier for Your Developers
POST request. This is a common task that any developer can implement in any language without specialized security knowledge.You Benefit from Better Security Controls
client_id immediately, cutting off all future token requests from that ID.At OneGround, we are committed to providing the most secure and reliable API experience. That's why we have implemented an OAuth2 Token Endpoint across our platform and we are giving possibility for all our customers to generate tokens through this standardized and secure approach.
By centralizing token creation, we ensure consistent security practices for all our customers, eliminate common vulnerabilities associated with client-side token generation, and simplify the integration process for your development teams. You no longer need to worry about JWT creation, signing algorithms, or claim validation. Instead, you can confidently and easily request a token from our endpoint to access our APIs.
Of course, we understand that this may require some adjustments on each customer application, so we are still supporting legacy integration during this transition period. Additionally, we are expecting that in the future (like ZGW 2.x) self-signed tokens will be deprecated in favor of centralized OAuth2 flows. Our approach not only enhances security today but also ensures your integration remains compliant and robust for years to come.
You can read more about our implementation and how to use the OAuth2 Token Endpoint in our ClientID Management and JWT Authentication in OneGround documentation.
JSON Web Tokens are an excellent standard for securely transmitting information in APIs. However, the creation of these tokens is a sensitive security function that should be managed by the API provider, not the customer.
By using a standard OAuth2 Token Endpoint, we provide a more secure and reliable authentication system. We handle the complexities of token generation, allowing you to focus on building your application. This approach simplifies development, reduces security risks, and ensures that best practices are followed consistently.
When a token expires, your application simply requests a new one. For the Client Credentials flow, this is a straightforward process that does not require complex refresh token logic, keeping your integration simple while maintaining a high level of security.
Digitally signing documents is a common task in the workflow of handling a case. Several software vendors offer solutions for this. This article describes a standardized pattern to handle the signing of documents within the ZGW API landscape. Most API-calls in this pattern are already part of the ZGW standard. The missing link was a way to initiate the signing transaction. In this article we propose a standardized trigger message for this purpose.
The basic flow for signing documents is as follows:
Below is a sequence diagram with a more detailed view of this flow.
Use this link to view the diagram in full size.
Most API requests in this sequence are standard requests already defined in the ZGW specification, with the following two remarks:
This article describes the interaction between the APIs of the TSA or ZAC, the signing software and the ZGW components (DRC, ZRC and NRC). The user interaction for selecting the documents and signers is out of scope, this is the responsibility of the TSA or ZAC. The same goes for the user interaction of the actual signing of the documents, that is the responsibility of the signing software.
The pattern described here is already implemented and used in production settings by the signing applications ValidSign and Zynyo and by the TSA Rx.Mission. These existing implementations use OneGround as the ZGW implementation, but as this pattern only uses standard behaviour of the ZGW components, it should work on any compliant ZGW implementation.
The requests to the ZGW components use the standard authentication for the ZGW API: a JWT generated based on a ClientId and secret. See the OneGround documentation or the general ZGW documentation for details.
The type of authentication on the trigger message is determined by the signing software and might vary between vendors. Acceptable authentication methods include usage of an api-key or a bearer token based on a ClientId and Secret. Note that in the case of ClientId and Secret, this will be a separate ClientId from the one used to access the ZGW API. The TSA or ZAC sending the trigger message should implement the authentication method for the signing software vendor(s) it integrates with.
Before the documents are presented to the signer, this person should authenticate themselves. Handling this authentication is the responsibility of the signing software, however the required type of authentication can be set in the trigger message send by the TSA or ZAC. See the details of the trigger message for more on this.
To initiate the signing process, the TSA or ZAC should post a trigger message to the signing software in this format:
{
"naam": "...", // Name of signing transaction, can be displayed to the signer(s) by the signing software
"zaak": {
"uuid": "2cc803a9-7404-4289-8f3d-f2300f666eac", // uuid of the case
"url": "$url" // ZRC url of the case
},
"bewijs_informatieobjecttype": "$url", // informationobjecttype for evidence document
"documenten": [ // list of documents to sign
{
"uuid": "f491249e-0651-4d22-a8a8-bb35885e9245", // uuid of the document
"url": "$url", // DRC url of the document
"titel": "Some document", // title of the document
"versie": 1, // version of the document to be signed
"bestandsnaam" : "some file.docx" // filename of the document;
}
],
"ondertekenaars": [ // list of signers
{
"voornaam": "John", // first name of signer
"achternaam": "Doe", // last name of signer
"identificatie": "Signer1", // string to identify the signer; can be used to relate this signer to specific signing areas or placeholders in the documents (not all signing software might need this property but supplying it should not cause errors)
"volgorde": 1, // order number used to determine in which order the signers should sign in case of multiple signers
"authenticatie": { // Method of authentication this signer should use to identify themselves
"methode": "sms",
"waarde": "+31655555555" // phone number of signer
}
}
]
}
Some details about properties that might not be clear from the description:
If the trigger message is received correctly, the signing software should respond with status code 200 and this message:
{
"transaction_id": "2opbDxqSB8BX3137ulqdw2q9_Mw=" // unique id of this signing transaction; format to be determined by the signing software
}
When the signing is complete and the signing software has uploaded the signed documents to the DRC, it should send a notification to the NRC. The TSA or ZAC can subscribe to these notifications, for instance to notify the user that initiated the signing request.
Note that signing documents is a very asynchronous sequence: it might be several hours, days, or even longer before all signers have signed all documents in the signing request. That means the time between sending the trigger message and receiving the NRC notification can be quite long. It is recommended that while the signing transaction is in progress, the TSA or ZAC indicates this to users in the UI where the documents are displayed so the user knows these documents are awaiting signing.
When signing is completed successfully, the request to the NRC should be:
{
"kanaal": "documentacties",
"hoofdObject": ""$url"", // ZRC url of the case
"resource": "zaak",
"resourceUrl": ""$url"", // ZRC url of the case
"actie": "OndertekenenVoltooid", // this value indicates this notification is about a successfully completed signing transaction
"aanmaakdatum": "2025-01-01T12:00:00Z", // date-time of this notification
"kenmerken": {
"transaction_id": "2opbDxqSB8BX3137ulqdw2q9_Mw=" // id of this signing transaction, same as in the response on the trigger message
}
}
When signing has failed, is cancelled or has otherwise not completed successfully, the request to the NRC should be:
{
"kanaal": "documentacties",
"hoofdObject": ""$url"", // ZRC url of the case
"resource": "zaak",
"resourceUrl": ""$url"", // ZRC url of the case
"actie": "OndertekenenAfgebroken", // this value indicates this notification is about a failed signing transaction
"aanmaakdatum": "2025-01-01T12:00:00Z", // date-time of this notification
"kenmerken": {
"reden": "reason", // string specifying why the transaction has not completed, for instance "refused by signer", "retracted" or "technical error ..." (preferably in Dutch, so it can be presented to the end user in the TSA or ZAC)
"transaction_id": "2opbDxqSB8BX3137ulqdw2q9_Mw=" // Id of this signing transaction, same as in the response on the trigger message
}
}
Some details about these notifications:
A JWT is a compact, URL-safe token format that encodes information in three parts:
When used appropriately, JWTs enable stateless authentication, eliminating the need for the server to store session data. The token itself holds all essential authentication information and expires after a defined period.
To maximize security, we recommend the following best practices:
JWT claims are key-value pairs encoded into the token's payload. While you can include custom claims based on your API's needs, several standard claims form the backbone of a well-constructed JWT:
iss (Issuer): Identifies the entity that issued the JWT. Always explicitly set the iss claim to indicate the origin of the token. Both the API and consuming applications should agree on the expected iss value in advance, and the application generating the JWT should include its name or link in this claim. Tokens with missing or incorrect iss values may be rejected by the API for security purposes.
exp (Expiration Time): Sets the token's expiration date and time using a Unix timestamp. This ensures the token cannot be used indefinitely, even if it is compromised.
aud (Audience): Defines the intended audience for the token. By specifying the aud claim, an API can ensure that the token is meant for its use and not an unrelated service.
iat (Issued At): The timestamp of when the token was created, which can help detect tokens created in the future by malicious actors.
exp)Long-lived tokens pose a significant risk if they are leaked or intercepted. To protect sensitive resources, set an appropriate expiration time (exp) when creating tokens (from 5 minutes to a few hours) and avoid using long expiration times (e.g., months or years) for tokens, especially those used in production.
So, using short-lived tokens reduces the window for potential misuse when the token is compromised. Even if an unauthorized party gains access to a token, the inherent time restriction ensures its usability is limited.
When generating JWTs signed with symmetric algorithms, a secret key is used to sign and validate the token. If this secret is exposed, attackers can create their valid tokens. To prevent this:
When implementing JWT functionality in your applications, selecting the correct library is crucial for ensuring security and reliability.
client_id)Each JWT token should be associated with a unique client ID for improved security and accountability, which must be dedicated to only one application.
JWT tokens are a powerful tool for securing APIs, but their effectiveness depends on proper configuration and usage. By following the best practices - such as setting appropriate claims, and limiting token lifetime — you can build a robust and secure authentication system for your APIs.
For detailed guidance on implementing JWT in your specific environment, you can find recommendations in JWT libraries and tools for your programming language or framework. Always stay informed about the latest security updates to evolve your implementation as threats and technologies change.