Some API integrations require work with callback URLs for proper data exchange. In this post we’ll review some techniques that allow to use this integration feature securely. This is important for projects containing sensitive information such as personal, medical or financial records.
Callback usage scenario
Let’s say we are building an API or application that need a 3rd party service to perform some task. This task involves some heavy processing (that’s why you outsourced it, right?) so the API provided to you is asynchronous.
Basically it means any task you submit to this service will be performed later in the background. And as soon as it’s done with processing the result will be sent back to you via callback URL. The diagram below shows how it works.
Our application/API has two components:
- Sender: creates the task and send it to external service, the callback URL is part of the task
- POST /results: API endpoint for receiving result, it’ll process result and save into our database
External service has two components as well:
- POST /tasks: API endpoint that accepts new task for processing, it places the task into internal queue
- Processor: performs actual task processing and submit result back to us via callback URL (POST /results)
Important feature of this scenario is ability to specify callback URL dynamically as part of the task submitted to the external service. Techniques described below depend on this ability, so they may not be suitable for each API in the reality.
Callback without token
Building callback URL is a straightforward task, we can use something like:
https://api.myapp.io/v1/results/{id}
where https://api.myapp.io/v1
is an API root URL, /results
is our callback
endpoint, and {id}
is a result identifier we are expecting.
This approach works but impose an obvious threat: an attacker who knows about
this callback URL can easily rewrite all results we have by replacing
{id}
and sending requests to us. It means this approach is not suitable for
projects where security is an important factor.
Callback with static token
Let’s introduce {token}
component as part of the callback URL:
https://api.myapp.io/v1/results/{id}/{token}
Token in it’s simplest form is a random string associated with an application
user
, so for every request coming in we can find appropriate user in our
database and authorize access to our resources.
Assuming that token
is accessible to legitimate users only we can say that
this approach is safe enough for some projects.
However this is not enough is we consider another potential threat: data breach of the external service. If this happens an attacker can retrieve our complete callback URLs from the database or log files. This, again, grants unrestricted access to our results records and, potentially, to all resources of users exposed to the attack.
Callback with JWT token
The improvement to our callback URLs security is possible if we change the way
of token
generation. JSON Web Token (JWT) is an open,
industry standard method for representing claims securely between two parties.
Just to be clear, callback URL structure remains the same:
https://api.myapp.io/v1/results/{id}/{jwt-token}
The main advantage of JWT in our case is ability to securely restrict permissions granted to external service with the token. This significantly reduces the attack surface as opposed to user-based or static tokens (associated with user only).
Tokens limited by path and resource
When we generate a JWT based token following information must be included:
_id
: user identifierpath
: allowed API endpoint (or path)res_id
: allowed resource identifier
Each request to the callback URL must be accompanied with JWT based token which we need to validate to make sure requester is allowed to send us result data.
Now in case of 3rd party service data breach the amount of resources exposed to attacker will be much smaller.
Tokens limited by time
To further reduce abilities of potential attacker we need to use another dimension (not shown on the attack surface diagram) which is time. We can limit lifetime of each JWT token, this is a standard feature supported by JWT libraries. It means that after certain period of time token becomes invalid and can not be used by anyone.
The concrete time-out value need to be carefully chosen depending on characteristics of the external service. Token lifetime must exceed normal processing time guaranteed by the service. Otherwise there is a risk of denying legitimate requests with valid results if processing took more time than allowed by our JWT token.
That’s it! Hope it was useful.