Securing Tokens with help from JOSE (2024)

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?

JWTs 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. JWTs 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. JWTs 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.stringifys 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 JWTs 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 JWTs 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.

Securing  Tokens with help from JOSE (2024)
Top Articles
Latest Posts
Article information

Author: Madonna Wisozk

Last Updated:

Views: 5839

Rating: 4.8 / 5 (48 voted)

Reviews: 95% of readers found this page helpful

Author information

Name: Madonna Wisozk

Birthday: 2001-02-23

Address: 656 Gerhold Summit, Sidneyberg, FL 78179-2512

Phone: +6742282696652

Job: Customer Banking Liaison

Hobby: Flower arranging, Yo-yoing, Tai chi, Rowing, Macrame, Urban exploration, Knife making

Introduction: My name is Madonna Wisozk, I am a attractive, healthy, thoughtful, faithful, open, vivacious, zany person who loves writing and wants to share my knowledge and understanding with you.