Signed requests




Introduction


Sometimes an application needs to call an endpoint that makes us lay awake at night thinking about what would happen if someone manage to steal an access_token and call this endpoint to either steal sensitive data or make changes to data. An example could be adding subscription data to a user. We don’t want the user to be able to do this by himself if he has managed to pick up his access_token from some insecure cookie or something like that.

The solution we have chosen for this problem is to use signed requests in these cases. Our implementation is very inspired by Instagram's secure requests. But also introduces a timestamp parameter that is required in order to protect against replay attacks (eg. performing old requests from a log).


How the signing works


First we’ll look into how the signing works in theory. Then we’ll look at an example.


Parameters

Title
Title
label
Description
sig
This is the signature, a signed request_token with your Application’s client_secret using the SHA256 hash algorithm.
timestamp
An ISO8601 timestamp of when the request was made from the application.

Creating the timestamp parameter is easy. In Ruby you could do it like this:

timestamp = Time.now.iso8601

The sig parameter is more difficult. First you need to create a request_token, a string representing the request. Then this request_token must be signed using SHa256 hash algorithm using the same client_secret that the application use for getting access_tokens during  OAuth Authorization .


Generating the request_token


The request_token is a string that represents the current request the application is trying to perform on one of aID’s service endpoints. It’s a combination of the URL, parameters and posted fields in the request.

Definition: request_token: API endpoint URL appended with a concatenation of all key/value pairs of your request parameters (or posted fields), including timestamp (but not sig, that would cause pain), sorted by key in ascending order. URL and each key/value pair are separated by the pipe character.

So if I want to perform a request like this:

curl 'https://www.aid.no/api/vespasian/v1/test?param1=a&param2=b' --data "field1=1&feild2=2&timestamp=2016-01-28T15%3A42%3A21%2B01%3A00"

I would generate the request_token like this:

require 'time'

request_token = 'https://www.aid.no/api/vespasian/v1/test'
timestamp = Time.now.iso8601params = {
'timestamp' => timestamp,
'param1' => 'a',
'param2' => 'b',
'field1' => '1',
'field2' => '2'}
params.sort.each do |k, v|
request_token << '|%s=%s' % [k, v]
end

This should result in a request_token like this:

https://www.aid.no/api/vespasian/v1/test|field1=1|field2=2|param1=a|param2=b|timestamp=2016-01-28T15:42:21+01:00

The same algorithm is performed on the server, so both the server and application will sign the same request_token with the same client_secret. So let’s have a look at how we do the signing.


Signing the request_token


require 'openssl'

def generate_sig(request_token, client_secret)
digest = OpenSSL::Digest.new('sha256')
return OpenSSL::HMAC.hexdigest(digest, client_secret, request_token)end


Using the above request_token and the client_secret “1c3b00d4”, you should get this signature:

496d8611926d1df9e486354da5df968e7255f3d502e51776b08994f46012f032

So this should now be sent as the parameter (or posted field) called sig in the request.


Putting it all together


So this is an example in Ruby that puts all the concepts together and results in a curl-command that could be used in a terminal to do the actual request:

require 'openssl'
require 'cgi'
require 'time'

def generate_request_token(endpoint, params, fields)
request_token = endpoint.clone
params.merge(fields).sort.each do |k, v|
request_token << '|%s=%s' % [k, v]
end
request_token
end

def generate_sig(request_token, client_secret)
digest = OpenSSL::Digest.new('sha256')
OpenSSL::HMAC.hexdigest(digest, client_secret, request_token)
end

def stringify_params(params)
params.map {|k,v| k+'='+CGI.escape(v)}.join('&')
end

client_id = 'c4feb4b3'
client_secret = '1c3b00d4'
access_token = 'd4bbad00'

timestamp = Time.now.iso8601
endpoint = 'https://www.aid.no/api/vespasian/v1/test'

params = {
'param1' => 'a',
'param2' => 'b'
}

fields = {
'field1' => '1',
'field2' => '2',
'timestamp' => timestamp
}

request_token = generate_request_token(endpoint, params, fields)

sig = generate_sig(request_token, client_secret)
fields['sig'] = sig

puts "curl -H \"Authorization: Bearer #{access_token}\" \"#{endpoint}?#{stringify_params(params)}\" --data \"#{stringify_params(fields)}\""

This should result in something like this:

curl -H "Authorization: Bearer d4bbad00" "https://www.aid.no/api/vespasian/v1/test?param1=a&param2=b" --data "field1=1&field2=2&timestamp=2016-01-28T15%3A42%3A21%2B01%3A00&sig=496d8611926d1df9e486354da5df968e7255f3d502e51776b08994f46012f032"

So this is the request the application should make to the service endpoint in aID. The endpoint will perform the same logic to generate and sign the request using the secret aID knows for the application owning the access_token in the request. If this succeeds, the request is performed. If not, the response will be an error response.

Error responses

Signature is invalid


If the provided sig parameter can not be recreated exactly by the server, the following response will be returned:

HTTP/1.1 403 Forbidden

{
"errors":[
{
"id":"53225f86-c5e3-49bb-a5d8-daa740445ebb",
"meta":{},
"code":"request.access.signature.invalid",
"status":"403",
"title":"Signature does not match request or secret",
"detail":"Provided signature does not match using the application secret and request URL with parameters (included posted fields)"
}
]
}


Timestamp format is invalid


If the server does not understand the timestamp parameter, the response will look like this:
HTTP/1.1 400 Bad Request

{
"errors":[
{
"id":"b64b96d1-a130-4b25-a466-d5d3bf1945bf",
"meta":{},
"code":"request.access.timestamp.invalid.format",
"status":"400",
"title":"Timestamp format is invalid",
"detail":"Timestamp must match ISO8601 format, like this: 2016-01-28T15:25:16+00:00"
}
]
}

The server will use the Time.iso8601 method in Ruby to decode the parameter, so the service will support whatever this method supports.


Timestamp is no longer valid


If the timestamp is not considered valid by the server, the response will look like this:

HTTP/1.1 403 Forbidden

{
"errors":[
{
"id":"b64b96d1-a130-4b25-a466-d5d3bf1945bf",
"meta":{},
"code":"request.access.timestamp.invalid",
"status":"403",
"title":"Timestamp not currently valid",
"detail":"Provided timestamp is not valid, current time on server is: 2016-01-28T15:25:16+00:00"
}
]
}

Exact time synchronization is virtually impossible, so there is some slack here. The detail in the error response will provide some helpful hint about what the server considers is the correct time. This should match the time on your server, if not someone needs to read up on NTP.


Missing parameter


If you forget to submit either timestamp or sig, you will get one of these responses:

HTTP/1.1 400 Bad Request

{
"errors":[
{
"id":"165b2923-5c1e-4535-9de2-cb1b58fb065e",
"meta":{},
"code":"request.parameter.missing",
"status":"400",
"title":"Required parameter missing in request",
"detail":"parameter=timestamp"
}
]
}

HTTP/1.1 400 Bad Request

{
"errors":[
{
"id":"32e76bc9-d352-4a18-9fdc-1c5b12d62766",
"meta":{},
"code":"request.parameter.missing",
"status":"400",
"title":"Required parameter missing in request",
"detail":"parameter=sig"
}
]
}