Skip to content

Commit

Permalink
feat: 🎸 Added events for instrumentation and APM (#32)
Browse files Browse the repository at this point in the history
Created a global event emitter for the client that sends instrumentation
events
  • Loading branch information
JohanObrink committed Oct 2, 2019
1 parent e702a50 commit 2e215be
Show file tree
Hide file tree
Showing 11 changed files with 314 additions and 103 deletions.
15 changes: 15 additions & 0 deletions __tests__/client.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const { createMemoryStore } = require('../lib/memoryStore')
const axios = require('axios')
const { generateKey } = require('../lib/crypto')
const { JWT } = require('@panva/jose')
const { CONNECT_TO_OPERATOR_START, CONNECT_TO_OPERATOR } = require('../lib/events')
jest.mock('axios')

describe('client', () => {
Expand Down Expand Up @@ -98,6 +99,20 @@ describe('client', () => {
expect(listener).toHaveBeenCalledTimes(1)
})

it('calls event.emit with CONNECT_TO_OPERATOR_START', async () => {
const listener = jest.fn()
client.events.on(CONNECT_TO_OPERATOR_START, listener)
await client.connect()
expect(listener).toHaveBeenCalledWith({ retry: 0 })
})

it('calls event.emit with CONNECT_TO_OPERATOR', async () => {
const listener = jest.fn()
client.events.on(CONNECT_TO_OPERATOR, listener)
await client.connect()
expect(listener).toHaveBeenCalled()
})

it('signs the payload', async () => {
await client.connect()
const jwt = axios.post.mock.calls[0][1]
Expand Down
12 changes: 6 additions & 6 deletions __tests__/data.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,11 @@ describe('data', () => {
describe('#read', () => {
let data
function createJWE (data) {
const signed = JWS.sign(JSON.stringify(data), JWK.importKey(signingKey), { kid: signingKey.kid })
const signed = JWS.sign(JSON.stringify(data), JWK.asKey(signingKey), { kid: signingKey.kid })
const encryptor = new JWE.Encrypt(signed)
encryptor.recipient(JWK.importKey(toPublicKey(accountEncryptionKey)),
encryptor.recipient(JWK.asKey(toPublicKey(accountEncryptionKey)),
{ kid: accountEncryptionKey.kid })
encryptor.recipient(JWK.importKey(toPublicKey(serviceEncryptionKey)),
encryptor.recipient(JWK.asKey(toPublicKey(serviceEncryptionKey)),
{ kid: serviceEncryptionKey.kid })
return encryptor.encrypt('general')
}
Expand Down Expand Up @@ -237,11 +237,11 @@ describe('data', () => {
describe('#read', () => {
let data
function createJWE (data) {
const signed = JWS.sign(JSON.stringify(data), JWK.importKey(signingKey), { kid: signingKey.kid })
const signed = JWS.sign(JSON.stringify(data), JWK.asKey(signingKey), { kid: signingKey.kid })
const encryptor = new JWE.Encrypt(signed)
encryptor.recipient(JWK.importKey(toPublicKey(accountEncryptionKey)),
encryptor.recipient(JWK.asKey(toPublicKey(accountEncryptionKey)),
{ kid: accountEncryptionKey.kid })
encryptor.recipient(JWK.importKey(toPublicKey(serviceEncryptionKey)),
encryptor.recipient(JWK.asKey(toPublicKey(serviceEncryptionKey)),
{ kid: serviceEncryptionKey.kid })
return encryptor.encrypt('general')
}
Expand Down
11 changes: 8 additions & 3 deletions __tests__/keyProvider.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ const jsonToBase64 = (obj) => Buffer.from(JSON.stringify(obj), 'utf8').toString(
const base64ToJson = (str) => JSON.parse(Buffer.from(str, 'base64').toString('utf8'))

describe('KeyProvider', () => {
let keyOptions, keyProvider, pemKey, clientKey, keyValueStore, domain, jwksURI
let keyOptions, pemKey, clientKey, domain, jwksURI
let keyProvider, keyValueStore
beforeEach(async () => {
keyValueStore = {
load: jest.fn().mockName('load').mockResolvedValue(''),
Expand All @@ -25,13 +26,17 @@ describe('KeyProvider', () => {
privateKeyEncoding: { type: 'pkcs1', format: 'pem' }
}).privateKey
clientKey = importPEM(pemKey, jwksURI, { use: 'sig', kid: `${jwksURI}/client_key` })
keyProvider = new KeyProvider({ clientKey: pemKey, keyValueStore, keyOptions, jwksURI })
keyProvider = new KeyProvider({
config: { clientKey: pemKey, keyValueStore, keyOptions, jwksURI }
})
})
it('works with a PEM client key', () => {
expect(keyProvider.clientKey).toEqual(clientKey)
})
it('works with a JWK client key', () => {
keyProvider = new KeyProvider({ clientKey, keyValueStore, keyOptions, jwksURI })
keyProvider = new KeyProvider({
config: { clientKey, keyValueStore, keyOptions, jwksURI }
})

expect(keyProvider.clientKey).toEqual(clientKey)
})
Expand Down
6 changes: 3 additions & 3 deletions __tests__/tokens.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -207,10 +207,10 @@ describe('tokens', () => {
domain = 'https://mycv.work'
area = 'edumacation'
data = ['Jag älskar hästar']
const signedData = await JWS.sign(JSON.stringify(data), JWK.importKey(serviceSigningKey), { kid: serviceSigningKey.kid })
const signedData = await JWS.sign(JSON.stringify(data), JWK.asKey(serviceSigningKey), { kid: serviceSigningKey.kid })
const encrypt = new JWE.Encrypt(signedData)
encrypt.recipient(JWK.importKey(accountEncryptionKey), { kid: accountEncryptionKey.kid })
encrypt.recipient(JWK.importKey(serviceEncryptionKey), { kid: serviceEncryptionKey.kid })
encrypt.recipient(JWK.asKey(accountEncryptionKey), { kid: accountEncryptionKey.kid })
encrypt.recipient(JWK.asKey(serviceEncryptionKey), { kid: serviceEncryptionKey.kid })
jwe = encrypt.encrypt('general')
})
it('creates a valid jwt', async () => {
Expand Down
14 changes: 11 additions & 3 deletions lib/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@ const axios = require('axios')
const routes = require('./routes')
const data = require('./data')
const KeyProvider = require('./keyProvider')
const { EventEmitter } = require('events')
const { configSchema } = require('./schemas')
const { v4 } = require('uuid')
const { createAuthenticationUrl } = require('./auth')
const tokens = require('./tokens')
const {
emitter,
CONNECT_TO_OPERATOR_START,
CONNECT_TO_OPERATOR,
CONNECT_TO_OPERATOR_ERROR
} = require('./events')

const defaults = {
jwksPath: '/jwks',
Expand All @@ -24,11 +29,11 @@ class Client {
this.connecting = false
this.config.jwksURI = `${config.clientId}${config.jwksPath}`
this.config.eventsURI = `${config.clientId}${config.eventsPath}`
this.keyProvider = new KeyProvider(this.config)
this.events = emitter
this.keyProvider = new KeyProvider(this)
this.routes = routes(this)
this.tokens = tokens(this)
this.data = data(this)
this.events = new EventEmitter()
this.keyValueStore = config.keyValueStore

this.connect = this.connect.bind(this)
Expand Down Expand Up @@ -67,12 +72,15 @@ class Client {

const serviceRegistration = await this.tokens.createServiceRegistration()
try {
this.events.emit(CONNECT_TO_OPERATOR_START, { retry })
this.events.emit('CONNECTING', retry)
const result = await axios.post(`${this.config.operator}/api`, serviceRegistration, { headers: { 'content-type': 'application/jwt' } })
this.connected = true
this.connecting = false
this.events.emit(CONNECT_TO_OPERATOR)
this.events.emit('CONNECTED', result)
} catch (err) {
this.events.emit(CONNECT_TO_OPERATOR_ERROR, err)
this.events.emit('CONNECTION ERROR', err)
const timeout = Math.min(1000 * Math.pow(2, retry++), 5000)
await new Promise((resolve) => setTimeout(resolve, timeout))
Expand Down
46 changes: 30 additions & 16 deletions lib/crypto.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ const {
} = require('crypto')
const { promisify } = require('util')
const pem2jwk = require('pem-jwk').pem2jwk
const {
emitter,
GENERATE_KEY_START,
GENERATE_KEY,
GENERATE_KEY_ERROR
} = require('./events')

function toBase64Url (base64) {
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '')
Expand All @@ -17,23 +23,31 @@ function thumbprint ({ e, kty, n }) {
}

async function generateKey (jwksURI, options = {}, modulusLength = 2048) {
if (!jwksURI && !options.kid) {
throw new Error('jwksURI must be passed in')
}
if (!options || !options.use) {
throw new Error('{ use } must be passed as an option')
}
const { privateKey } = await promisify(generateKeyPair)('rsa', {
modulusLength,
publicKeyEncoding: { type: 'pkcs1', format: 'pem' },
privateKeyEncoding: { type: 'pkcs1', format: 'pem' }
})
const key = pem2jwk(privateKey, options)
if (!key.kid) {
key.kid = `${jwksURI}/${await thumbprint(key)}`
}
emitter.emit(GENERATE_KEY_START, { modulusLength })

return key
try {
if (!jwksURI && !options.kid) {
throw new Error('jwksURI must be passed in')
}
if (!options || !options.use) {
throw new Error('{ use } must be passed as an option')
}
const { privateKey } = await promisify(generateKeyPair)('rsa', {
modulusLength,
publicKeyEncoding: { type: 'pkcs1', format: 'pem' },
privateKeyEncoding: { type: 'pkcs1', format: 'pem' }
})
const key = pem2jwk(privateKey, options)
if (!key.kid) {
key.kid = `${jwksURI}/${await thumbprint(key)}`
}
emitter.emit(GENERATE_KEY, { modulusLength: modulusLength })

return key
} catch (error) {
emitter.emit(GENERATE_KEY_ERROR, error)
throw error
}
}

function toPublicKey ({ e, kid, kty, n, use }) {
Expand Down
Loading

0 comments on commit 2e215be

Please sign in to comment.