tokien
v1.0.0
Published
Internal library for the Salling Group API team to issue and verify access tokens for the API Gateway.
Downloads
2
Readme
Tokien
Internal library for the Salling Group API team to issue and verify access tokens for the API Gateway.
How to use
Ensure you're running at least node 12 before proceeding. The library does not work with node versions before 12.
Install the library from NPMJS.com:
npm i @dansk-supermarked/tokien
Require the library in your code:
const { 'v1': { issue, verify } } = require('@dansk-supermarked/tokien');
Issue or verify tokens as needed.
const { deepStrictEqual } = require('assert'); const payload = { ctx: { id1: '123', id2: '234', }, env: [ 'v1-test', 'v1-dev'], exp: new Date().getTime() + 3600000, id: 'my-project', sub: '345', }; const myKey = Buffer.from('a super secret key from env vars'); const token = issue(payload, myKey); const verifiedDecryption = verify(token, myKey); deepStrictEqual(payload, verifiedDecryption); console.log(token);
encrypt
The encrypt(plaintext, key)
function is a lower-level function that encrypts any given plaintext string without performing any validation of what's in the string. This is a building block used for the issue
function. The encrypt
function does not add the "sg.v1."
prefix to its output, but returns just the Base64URL-encoded ciphertext.
decrypt
The decrypt(ciphertext, key)
function is a lower-level function that attempts to decrypt any given ciphertext string without parsing the result as JSON or performing any validation of what's in the resulting string. This is a building block for the verify
function. The decrypt
function expects just the Base64URL-encoded ciphertext without the "sg.v1."
prefix as input and returns the original plaintext as-is.
issue
The issue(payload, key)
function takes two arguments:
Payload (Object)
The payload must be a regular Object with the following keys. Additional keys are allowed and will be included in the token as well as in the decryption of the token.
Key | Type | Examples | Description
-- | -- | -- | --
env
| Array of Strings | ["prod"]
, ["test","dev"]
| The environments that the customer is allowed to access. Typically takes the form prod
, test
, etc. Note that the environment is not the version. E.g. "v1-test"
is both a version and an environment. This field is used for the environment. Each access token may grant access to many different versions of APIs without too much of a security risk, but it's very important that we can pin the environment.
exp
| Number | new Date().getTime() + 3600000
| The timestamp (in milliseconds since epoch) that the token expires. Can NOT be in the past. Can NOT be more than 366 days ahead. In general you probably want to issue short-lived tokens with a lifespan of about one hour.
id
| String | "foetex-hd"
| The ID of the access rights template that this token grants the customer access to. The templates live elsewhere, in our api-products service, and it's expected that whoever consumes the access token has access to look up the template details from that service.
sub
| String | "b51fa1b52ebe44afb2c18fd6bcf060d7"
| The DS ID of the customer that this token is valid for. Since this token is on behalf of a customer, we must know which customer, and the canonical ID is the DS ID / Gigya UID / SAP Customer Cloud UID (all the same thing).
ctx
| (Optional) Object | {"hdfot":"123"}
| Any additional information that should be made available to the route template, like additional IDs. May be omitted. If present, this must be an Object with String values.
Key (Buffer)
The key must be an instance of Buffer and must have a length of exactly 32 bytes.
verify
The verify(token, key)
function takes two arguments:
Token (String)
A string returned by the encrypt
function.
Key (Buffer)
The key must be an instance of Buffer and must have a length of exactly 32 bytes.
Token Format
The resulting encrypted token has the following format:
{SALLING GROUP PREFIX}.{TOKEN VERSION}.{ENCODED CIPHER}
where
{SALLING GROUP PREFIX}
is alwayssg
.{TOKEN VERSION}
is currentlyv1
as the only option. Others may follow.{ENCODED CIPHER}
is a Base64URL encoding of the following bytes:12 bytes initialization vector.
16 bytes authentication tag.
Varying number of bytes of block cipher output.
In order to decrypt a token, then, the library simply has to do the following:
- Decode the Base64URL-encoded string into a byte buffer.
- Pick bytes [0, 12] as the initialization vector.
- Pick bytes [12, 28] as the authentication tag.
- Pick bytes [28, ...] as the cipher text.
- Feed these to the native crypto library.
Technical Details
A number of tehnical decisions have been made up-front to ensure there are no footguns when using the library.
Most interestingly:
ChaCha20-Poly1305
ChaCha20-Poly1305 was chosen as the core of the library, since it offers some very appealing traits:
The payload is confidential, meaning that no one is able to learn anything about what the token grants access to.
It wouldn't be a big problem if they did, but it's better that they don't. The token will include details like the IDs of the user the token was issued for, but it does not contain any details that could be used to elevate access rights.
The payload is authentic in the sense that the party that encrypted it held the same secret key as the party that decrypts it. Our team plays both roles in the use of this library.
The token is checked for integrity, meaning that it can not be altered by third parties without becoming invalid.
All of this is achieved with one layer of encryption, which is nice compared to the previous token library.
XChaCha-Poly1305 would have been better, but is not natively available in node. When or if it is, we could consider making a version 2 of this library.
Compressed Payload
Deflate compression is applied to the stringified payload before encrypting it, typically resulting in worthwhile size reductions. A lot of experimentation was done with deflate, gzip, and brotli, and the results boil down to:
- Deflate has great performance and great compression for these JSON payloads.
- GZip has great performance and slightly worse compression for these JSON payloads.
- Brotli has terrible performance (a factor 20 worse, often) and even better compression.
Of the three deflate seems a very good compromise between speed and compression.