JWT HTTP HMAC Spec
Features:
- Signs request method, path, query, preferred headers and request body
- Optionally supports token expiration and "Not Before" times (helps to prevent Replay attacks)
- Signs response status code, preferred headers and response body
- Reuses nonce from the request to ensure that response belongs to the request
- Supports HS256, HS384, HS512, RS256, RS384, RS512, ES256, ES384, ES512 singing algorithms
Overview of Request Header and Signature
The pseudocode below illustrates construction of the HTTP signature header:
RequestNonce = HexV4OfRandomUUID()
SignedHeaders: Array(n) = [
HTTP-Header-Value, for each RequestHeadersToSign. null if undefined.
]
DataToHash: Array(6) = [
RequestNonce
Uppercase( HTTPVerb )
PathWithQuery
SignedHeaders
RequestBody or null
]
HashedData = Base64( SHA256( JSONStringify( DataToHash ) ) )
Payload = {
jti = RequestNonce
sub = Subject
headers = RequestHeadersToSign
hash = HashedData
}
Token = JWTSign( Payload, Secret )
Singature = "jwt-http-hmac/1.0" + " " + Token
-
RequestNonce- UUID v4 -
RequestHeadersToSign- an array of header names client wishes to sign -
HTTPVerb- GET, POST, PUT etc. -
PathWithQuery- '/path/to/the/endpoint?param=one' -
SignedHeaders- an array of values of headers defined inRequestHeadersToSignarray in the exact order -
RequestBody- request body payload.nullfor requests with no request payload (ie GET).
Example
Predefined variables
For the sake of this example these variables will be predefined so that you can verify the correct implementation.
Nonce
62934be2-adab-4964-9d18-e4360fcb29de
Secret
WzQXBajnHpi1Q2li+gf/z4dKBGLyRd1/WpOWZIwGQydh7x/DFv4Rkt/uMe8USQIVSH9FD30SlqEWqUx99aTI4Q==
Signing process
In this example we will sign the GET request along with the Test header value.
GET /users?limit=5 HTTP/1.1
Host: splyt.com
Content-Type: text/plain
Test: one
Accept: */*
First thing to do would be to flatten the names of the headers we would like to sign into a one dimentional ordered array SignedHeaders:
["one"]
Then the value of DataToHash would be equal to:
[
"62934be2-adab-4964-9d18-e4360fcb29de",
"splyt.com",
"GET",
"/users?limit=5",
[ "one" ],
null
]
After Base64( SHA256( JSONStringify( DataToHash ) ) ) we shouldget back value m1fDgPOWWXXJtvgqjn3M1fonUvVQnuNtMOCexeCwXOI=
Having that we can build a JWT payload.
{
headers: [ 'test' ],
hash: 'm1fDgPOWWXXJtvgqjn3M1fonUvVQnuNtMOCexeCwXOI=',
sub: 'urn:partner',
jti: '62934be2-adab-4964-9d18-e4360fcb29de'
}
Using our predefined secret from this example we can sign the JWT payload using HS256 algorithm. We should get the following:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJoZWFkZXJzIjpbInRlc3QiXSwiaGFzaCI6Im0xZkRnUE9XV1hYSnR2Z3FqbjNNMWZvblV2VlFudU50TU9DZXhlQ3dYT0k9Iiwic3ViIjoidXJuOnBhcnRuZXIiLCJqdGkiOiI2MjkzNGJlMi1hZGFiLTQ5NjQtOWQxOC1lNDM2MGZjYjI5ZGUifQ.7WP66f3TneZfV_wY2vu6I0NweKE_fFCn9B8KbugP6No
Now the original request should be transformed to include signature header as following:
GET /users?limit=5 HTTP/1.1
Host: splyt.com
Content-Type: text/plain
Test: one
Accept: */*
X-JWT-Signature: jwt-http-hmac/1.0 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJoZWFkZXJzIjpbInRlc3QiXSwiaGFzaCI6Im0xZkRnUE9XV1hYSnR2Z3FqbjNNMWZvblV2VlFudU50TU9DZXhlQ3dYT0k9Iiwic3ViIjoidXJuOnBhcnRuZXIiLCJqdGkiOiI2MjkzNGJlMi1hZGFiLTQ5NjQtOWQxOC1lNDM2MGZjYjI5ZGUifQ.7WP66f3TneZfV_wY2vu6I0NweKE_fFCn9B8KbugP6No
Overview of Request Verification
The pseudocode below illustrates verification algorithm of the signed HTTP request:
Token = extracted token from the request header
Payload = JWTVerifyAndDecode( Token, Secret )
SignedHeaders: Array(n) = [
HTTP-Header-Value, for each Payload.headers. null if undefined.
]
DataToSign: Array(6) = [
Payload.jti # aka nonce
Uppercase( HTTPVerb )
PathWithQuery
SignedHeaders
RequestBody or null
]
HashedData = Base64( SHA256( JSONStringify( DataToSign ) ) )
SignatureValid = if HashedData == Payload.hash
Overview of Response Header and Signature
RequestNonce = extracted from request signature
SignedHeaders: Array(n) = [
HTTP-Header-Value, for each ResponseHeadersToSign. null if undefined.
]
DataToSign: Array(4) = [
RequestNonce
HTTPStatusCode
SignedHeaders
ResponseBody or null
]
HashedData = Base64( SHA256( JSONStringify( DataToSign ) ) )
Payload = {
jti = RequestNonce
headers = ResponseHeadersToSign
hash = HashedData
}
Signature = JWTSign( Payload, Secret )
Overview of Response Verification
Token = extracted token from the response header
Payload = JWTVerifyAndDecode( Token, Secret )
SignedHeaders: Array(n) = [
HTTP-Header-Value, for each Payload.headers in exact order. null if undefined.
]
DataToSign: Array(4) = [
data.jti
HTTPStatusCode
SignedHeaders
ResponseBody or null
]
HashedData = Base64( SHA256( JSONStringify( DataToSign ) ) )
SignatureValid = if HashedData == Payload.hash AND data.jti == RequestNonce
Glossary
-
Token- contents of aX-JWT-Signatureheader of the response. -
Secret- Secret, Private or Public key