
Writing Javascript that runs in a browser is great, but there is a serious downside, which is that anyone can view the source of your code, and there is a myriad of tools available to help people dig right in to even the most obfuscated stuff. That’s not a problem if you are careful to keep actual secrets out of client-facing code, but every so often you’ll hit a scenario where some sort of secret needs to be exposed in userland. To do this correctly you need to ensure that secret is encrypted. Not just one-way hashed like a password, but two-way encrypted so you can actually use it again.
A common scenario.
To service your site’s user your server needs to interact with one or more 3rd party APIs. Those APIs expect some sort of auth token to be passed in via the Authorization
header and they send you this token the first time you authenticate with them on behalf of your user. The token the API needs is specific to each of your users and can’t be shared between users. So your server talks to theirs, gets back a Json Web Token (aka a JWT
), and now, because your server is totally stateless†, the only place for you to keep that token is back in the user’s own browser.

So what’s a JWT
?
JWT
s have emerged as the most popular way to encode data for exchange between API servers. They are a simple JSON
structure with built in properties that describe the issuer, audience, incept date and expiry time, as well as a subject that can be any kind of JSON
data you like. JWT
s are signed on creation using an arbitrary string as a signing secret, and that secret gets shared with whomsoever the creator whishes to be able to verify the validity of the JWT
. It’s simple and quick, and it’s relatively secure so long as the secret is only shared between trusted parties, the API calls are all made over HTTPS
and the JWT
itself is never exposed. So great for API to API authentication. Not so great for browser to API authentication.
Making a JWT
is trivial.
const jwt = require('jwt-simple')const secret = 'some super shared secret'
const token = jwt.encode({ sub: { someData: 'goes here' } }, secret)
However any JWT
string can be decoded, even without knowing the secret. The secret only lets you verify that the JWT
was correctly signed. JWT
s are only encoded, not encrypted. And this makes them utterly unsuitable for exposing within a website, or, worse, a URL
.
Enter JOSE.
JOSE
, the JSON
Object Signing and Encryption standard, solves this issue by giving you a formal mechanism to create two-way encrypted tokens. The main JOSE
library for Node is made by Cisco and is called node-jose
.
The Node-Jose library is also quite simple to use, but the docs assume you’ve digested the entire JOSE
spec first.
Let’s assume what you really want is something that works like a JWT
, but is actually secure. So you need an encrypt
function that takes in some sort of arbitrary data, and returns a base64
encoded string, and you want a decrypt
function that takes in a base64
encoded string and returns the decrypted data again.

Base64 Encoding
JOSE
doesn’t have anything to say about base64
encoding so let’s manage that first.
Here’s a general purpose base64
encoder/decoder I use a lot in my code.
// src/utils/base64.jsconst encodeBuffer = buffer => buffer.toString('base64')
const encodeString = string => encodeBuffer(Buffer.from(string))
const encodeData = data => encodeString(JSON.stringify(data))const encode = (data) => {
if (Buffer.isBuffer(data)) return encodeBuffer(data)
if (typeof data === 'string') return encodeString(data) return encodeData(data)
}const decode = (string) => {
const decoded = Buffer.from(string, 'base64').toString() try {
return JSON.parse(decoded)
} catch (e) {
return decoded
}
}module.exports = { encode, decode }
It’s tested via this:
// test/unit/utils/base64_spec.jsconst { expect } = require('chai')
const faker = require('faker')
const { encode, decode } = require('../../../src/utils/base64')describe('base64', () => {
describe('encode', () => {
describe('given a string', () => { const raw = faker.lorem.words()
const original = `${raw}` it('encodes without altering the original string', () => {
expect(encode(raw)).to.exist
expect(raw).to.equal(original)
})
}) describe('given a buffer', () => { const raw = Buffer.from(faker.lorem.words()) it('encodes', () => {
expect(encode(raw)).to.exist
})
}) describe('given an object', () => { const raw = { test: faker.lorem.words() } it('encodes', () => {
expect(encode(raw)).to.exist
})
})
}) describe('decode', () => {
describe('given an encoded string', () => { const raw = { test: faker.lorem.words() }
const encoded = encode(raw)
const original = `${encoded}` it('decodes without altering the original string', () => {
expect(decode(encoded)).to.eql(raw)
expect(encoded).to.equal(original)
})
})
})
})
Encrypting / decrypting.
Now we’ve got the underlying base64
encoding out of the way, the actual JOSE
part is simply this
// src/utils/jose.jsconst { JWE } = require('node-jose')
const { encode, decode } = require('./base64')const jose = (privateKey, publicKey) => { async function encrypt(raw) {
if (!raw) throw new Error('Missing raw data.')
const buffer = Buffer.from(JSON.stringify(raw))
const encrypted = await JWE.createEncrypt(publicKey)
.update(buffer).final() return encode(encrypted)
} async function decrypt(encrypted) {
if (!encrypted) throw new Error('Missing encrypted data.')
const decoded = decode(encrypted)
const { payload } = await JWE.createDecrypt(privateKey)
.decrypt(decoded) return JSON.parse(payload)
} return { encrypt, decrypt }
}module.exports = jose
The encrypt
function JSON.stringify
s the raw data then uses the publicKey
provided to then encrypt it via node-jose
’sJWE
, and then base64
encodes the result.
The decrypt
function base64
decodes the incoming data and then uses the privateKey
to decrypt it, then parses the returned JSON
result back into an object.
Test this as follows
// test/unit/utils/jose_spec.jsconst { expect } = require('chai')
const faker = require('faker')
const keygen = require('generate-rsa-keypair')
const { JWK } = require('node-jose')const jose = require('../../../src/utils/jose')const makeKey = pem => JWK.asKey(pem, 'pem')describe('jose-simple', () => {
const raw = {
iss: 'test',
exp: faker.date.future().getTime(),
sub: { test: faker.lorem.words() }
} let encrypted
let decrypted const keys = keygen() before(async () => {
const jwKeys = await Promise.all([
makeKey(keys.private),
makeKey(keys.public)
])
const { encrypt, decrypt } = jose(...jwKeys)
encrypted = await encrypt(raw)
decrypted = await decrypt(encrypted)
}) it('encrypts', () => {
expect(encrypted).to.exist
expect(encrypted).to.be.a('string')
}) it('decrypts', () => {
expect(decrypted).to.exist
expect(decrypted).to.be.an('object')
}) it('decrypted version of encrypted is raw', () => {
expect(decrypted).to.eql(raw)
})
})
So now in your code you can still use 3rd party JWT
s but then wrap them in a properly encrypted token that only someone with access to the correct private key can decrypt. You can use whatever kind of keys you like (In my test I create an RSA
key pair).
Encryption ought to be simple, and widespread.
I wrote this because I found the Node Jose docs confusing, there is a lack of JOSE
code examples online, and very few people seem to use it, instead mistakenly assuming that JWT
s are actually secure. This is a terrible situation I wish to rectify.
The code I have provided is of course fairly trivial but if you wish to improve it, I have wrapped all this up into an actual npm
library called jose-simple
( Sourcecode in GitHub at github.com/davesag/jose-simple
.
Update: 2018–05–10
I’ve tidyied up the example code a bit and updated the jose-simple
package to version 1.0.1
to support Node 10+
. The update has also been published to npm
.
Update: 2018–06–04
Updated a number of dependencies and released version 1.0.2
to npm
.
☛
† Why a stateless server? That’s a topic for a whole other article, but it’s valid and increasingly common.
—
Like this but not a subscriber? You can support the author by joining via davesag.medium.com.