SolarNet API authentication scheme V2 - SolarNetwork/solarnetwork GitHub Wiki
SolarNet uses a custom HTTP authentication scheme respectively adapted from the Amazon S3 web API V4 scheme. Clients of the API must use a security token pair to authenticate each request using a HMAC+SHA256 digest of specific request data.
The V1 version is still supported, but clients should migrate to this version.
See https://go.solarnetwork.net/dev/api/ for a JavaScript client implementation of this authentication scheme designed to showcase how the API can be used.
The request must include a HTTP date header, either using the standard HTTP Date
header or the custom X-SN-Date
header. If X-SN-Date
is provided, it will be used in preference to the Date
header. The exact date included in HTTP header is used in the signature message digest and the key used to sign the message so its value must be known by the client. As many browser AJAX fameworks set the Date
header automatically, the X-SN-Date
can be easier to use.
The value of the request date must match the current date on SolarNetwork at the time the request is made, within a small tolerance value. If the difference between the HTTP header date and the SolarNetwork system date is too large, a HTTP 401 error will be returned with a message along the lines of date skew too large.
The request must include a standard HTTP Authorization
header using SNWS2
as the authorization scheme. The authorization value is in the form Credential=tokenId,SignedHeaders=headerList,Signature=hash
where tokenId
is a SolarUser generated authorization token, headerList
is a semicolon-delimited list of HTTP headers included in the signature, and hash
is the HMAC+SHA256 digest using a signing key derived from the token secret and signing message derived from the request details, encoded as a hexidecimal string.
An example Authorization
HTTP header using this scheme looks like this:
Authorization: SNWS2 Credential=_tA{l51G2c08^icCXMyC,SignedHeaders=host;x-sn-date,Signature=854ed87aa277436d6771cd28fa4fb400ccccc7befe6cad8814b1f2e2bad66d32
The following table describes some terms used throughout this document.
tokenId |
The SolarUser generated token value. This is the value shown in the SolarUser token management screen. |
tokenSecret |
The SolarUser generated token secret. This value is shown only once in the SolarUser token management screen, when the token is first created. |
newline character \n
|
The newline character is the ASCII character 0x0A , or \n in most programming languages. |
UriEncode() |
An encoding function that accepts a UTF-8 string argument and returns a copy where all characters other than A-Z, a-z, 0-9, _ , - , ~ , and . are replaced by %X , where X is the upper case hexidecimal number of the character's code point (see RFC 3986). For example, UriEncode("Hello, world.") results in Hello%2C%20world. Note these rules are stricter than what JavaScript's encodeURIComponent() method performs. See the _encodeURIComponent() example JavaScript function that performs the required encoding. |
Trim() |
A function that accepts a string argument and returns a copy where all leading and trailing whitespace characters have been removed. |
Hex() |
A function that accepts an arbitrary array of bytes and returns an ASCII-encoded string where all bytes have been treated as unsigned values (0-255) and converted to lower case hexidecimal string values (00 - ff ). |
SHA256() |
A SHA256 encoding function that accepts a string argument and produces a 32-byte SHA256 digest. |
HMAC_SHA256() |
A HMAC+SHA256 encoding function that accepts a secret key and message data as arguments, e.g. HMAC(key, message) and produces a 32-byte HMAC signed SHA256 digest. |
A set of 6 items from the request is used to form a canonical request message string. The string is formed by concatenating the following items with the \n
newline character:
HTTP verb | The uppercase HTTP verb used in the request, for example GET or POST . |
Canonical URI | This is the path of the request URL, starting at / , without any query parameters, for example /solarquery/api/v1/sec/range/sources . |
Canonical query parameters | This is a string of all query parameters, either provided as URL query parameters or via a application/x-www-form-urlencoded encoded request body on POST requests, for example nodeId=1&sourceId=Foo . See Canonical query parameters for more information. |
Canonical headers | This is a newline-delimited string of all headers and associated values used to sign the request. The header names that appear here must match the signed header names list described next. See Canonical headers for more information. |
Signed header names | This is a semicolon-delimited string of all header names used to sign the request. The names must be in lowercase and sorted. The header names that appear here must match those that also appear in the canonical headers list described previously. See Signed header names for more information. |
Body content SHA256 digest | This is a Hex(SHA256()) digest of the request body content. If no body content is included then the Hex(SHA256()) digest of an empty string must be used. For example a JSON body of {"m":{"foo":"BAR"}} would result in 3fb055786e256de47c267183d53d67337afe7aed40e200a7ad798a256688782b
|
To summarise more succinctly in pseudo code, the canonical request message is composed of lines like this:
HTTPVerb + '\n'
CanonicalUri + '\n'
CanonicalQueryParameters + '\n'
CanonicalHeaderList + '\n'
SignedHeaderNames + '\n'
BodyContentSHA256
For example, a GET
request might result in a canonical request message like this:
GET
/solarquery/api/v1/sec/datum/meta/50
sourceId=Foo
host:data.solarnetwork.net
x-sn-date:Fri, 03 Mar 2017 04:36:28 GMT
host;x-sn-date
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
Here's an example of a POST
request:
POST
/solarquery/api/v1/sec/datum/meta/50
sourceId=Foo
content-type:application/json; charset=UTF-8
digest:SHA-256=P7BVeG4lbeR8JnGD1T1nM3r+eu1A4gCnrXmKJWaIeCs=
host:data.solarnetwork.net
x-sn-date:Fri, 03 Mar 2017 04:29:07 GMT
content-type;digest;host;x-sn-date
3fb055786e256de47c267183d53d67337afe7aed40e200a7ad798a256688782b
The canonical query parameters string is formed via the following algorithm.
Keep in mind and any POST
request with a Content-Type
of
application/x-www-form-urlencoded
the body content is treated like query
parameters.
- Sort all query parameter keys
- For each sorted
key
+ associatedvalue
pair: - If this is not the first key, append
&
to the result - Append
UriEncode(key)
,=
, andUriEncode(value)
to the result
For example, if a request URL includes query parameters like
/solarquery/api/v1/sec/range/interval?nodeId=1&sourceId=/foo/bar
then the
canonical query parameters string is:
nodeId=1&sourceId=%2Ffoo%2Fbar
The canonical headers string is formed via this algorithm:
- Lowercase all HTTP headers that will be included in the signature data
- Sort the lowercase header names
- For each sorted, lowercase header
name
andvalue
pair: - If this is not the first name, append
\n
to the result - Append
Trim(name)
,:
, andTrim(value)
to the result
For example, if a request includes a Host
header value data.solarnetwork.net
and a X-SN-Date
header value Fri, 03 Mar 2017 04:00:23 GMT
then the canonical headers string is:
host:data.solarnetwork.net
x-sn-date:Fri, 03 Mar 2017 04:00:23 GMT
The request must at a minimum sign the following HTTP header names:
Host
-
Date
(unlessX-SN-Date
is provided) -
Content-Type
(if body content is included in the request) - Any header starting with
X-SN-
.
The signed header names string is formed via this algorithm:
- Lowercase all HTTP headers that will be included in the signature data
- Sort the lowercase header names
- For each sorted, lowercase header
name
: - If this is not the first name, append
;
to the result - Append
Trim(name)
to the result
For example, if the request includes the Host
and X-SN-Date
headers, the signed header names string is:
host;x-sn-date
If the HTTP request includes body content then a Digest
or Content-MD5
header should be included. The RFC 5843 Digest
header using the SHA-256
algorithm is preferred. Examples headers are Digest: SHA-256=P7BVeG4lbeR8JnGD1T1nM3r+eu1A4gCnrXmKJWaIeCs=
and Content-MD5: /o1mwr8CitmYCfPTCeZp4A==
.
Another example, for a request including body content and the additional Digest
header, would result in a signed header names string of:
content-type;digest;host;x-sn-date
When running behind a proxy, the SolarNet server needs to know the original request's Host
value. SolarNet expects the Host
HTTP header value to be set to the original request host, not the proxied host. If the Host
header value does not contain a port (i.e. the proxy server does not pass this), a X-Forwarded-Port
header can be provided to pass the requesting port number. If X-Forwarded-Port
is not provided, a X-Forwarded-Proto
header can be provided; if that value is https
then a port of 443
will be assumed.
For example, given the following request headers:
Host: data.solarnetwork.net
X-Forwarded-Proto: https
the canonical Host
value will be treated as data.solarnetwork.net:443
. Similarly, given the following request headers:
Host: data.solarnetwork.net
X-Forwarded-Port: 443
the canonical Host
value will also be treated as data.solarnetwork.net:443
.
Here is an example Nginx configuration snippet that sets these headers appropriately:
proxy_pass https://data.solarnetwork.net:443;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
The SolarUser token secret is not used directly to sign the final HMAC message. Instead a 32-byte key derived from the SolarUser token secret value is used. The algorithm to derive this key is defined as:
HMAC_SHA256(HMAC_SHA256("SNWS2"+tokenSecret, utcDate), "snws2_request")
The utcDate
value is the signing date formatted as
YYYYMMDD
. A signing key is valid for up to 7 days from the date
it is signed. For some security-sensitive applications this can be useful so the
actual token secret does not have to be held in memory, just the derived signing
key does. The signing key can be refreshed before
it expires, allowing long-running apps to work without needing to know the token
secret. After the signing key expires, the token secret would then need to be
used again to generate a new signing key.
For example, if the token secret is ABC123
and the UTC date is
January 1, 2017, the result is derived like:
HMAC_SHA256(HMAC_SHA256("SNWS2ABC123", "20170101"), "snws2_request")
which results in a hex-encoded key of:
1f96b28b651285e49d06989aebaee169fa67a5f6a07fb72a8325fce83b425ad6
Note that when signing the message, use the actual output of the
HMAC_SHA256()
function, not the hex encoded version of it.
The final message to sign is 3 lines of data delimited by a newline character:
- The literal string
SNWS2-HMAC-SHA256
- The request date, formatted as an UTC ISO8601 timestamp like
YYYYMMDD'T'HHmmss'Z'
- The
Hex(SHA256(CanonicalRequestMessage))
whereCanonicalRequestMessage
is the canonical request message string as described in the previous section.
For example, the signing message for a request might look like:
SNWS2-HMAC-SHA256
20170303T043628Z
8f732085380ed6dc18d8556a96c58c820b0148852a61b3c828cb9cfd233ae05f
The final signature value is calculated as Hex(HMAC_SHA256(signingKey,signingMessage))
, where signingKey
is the signing key and signingMessage
is the signing message as described previously.