Skip to content

Commit

Permalink
refactor and add some tests
Browse files Browse the repository at this point in the history
  • Loading branch information
bucko13 committed Jun 22, 2021
1 parent e0dc5c0 commit 2846a62
Show file tree
Hide file tree
Showing 10 changed files with 233 additions and 99 deletions.
10 changes: 10 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"html.format.unformatted": "",
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,13 +129,13 @@ function. `satisfyFinal` will test only the last caveat on a macaroon of the mat
and `satisfyPrevious` compares each caveat of the same condition against each other. This allows
more flexible attenuation where you can ensure, for example, that every "new" caveat is not less
restrictive than a previously added one. In the case of an expiration, you probably want to have a satisfier
that tests that a newer `expiration` is sooner than the first `expiration` added, otherwise, a client
could add their own expiration further into the future.
that tests that a newer `expiration` is sooner than the first `expiration` added, otherwise, a client could
add their own expiration further into the future.

The exported `Satisfier` interface described in the docs provides more details on creating
your own satisfiers

#### `verifyFirstPartyMacaroon`
#### `verifyMacaroonCaveats`

This can only be run by the creator of the macaroon since the signing secret is required to
verify the macaroon. This will run all necessary checks (requires satisfiers to be passed
Expand Down
73 changes: 2 additions & 71 deletions src/caveat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/
import assert from 'bsert'
import { CaveatOptions, Satisfier } from './types'
import { stringToBytes } from './helpers'

import * as Macaroon from 'macaroon'
import { MacaroonJSONV2 } from 'macaroon/src/macaroon'

Expand Down Expand Up @@ -157,11 +157,9 @@ export function hasCaveat(
*/
export function verifyCaveats(
caveats: Caveat[],
satisfiers: Satisfier | Satisfier[],
satisfiers?: Satisfier | Satisfier[],
options: object = {}
): boolean {
assert(satisfiers, 'Must have satisfiers in order to verify caveats')

// if there are no satisfiers then we can just assume everything is verified
if (!satisfiers) return true
else if (!Array.isArray(satisfiers)) satisfiers = [satisfiers]
Expand Down Expand Up @@ -208,70 +206,3 @@ export function verifyCaveats(
}
return true
}

/**
* @description verifyFirstPartyMacaroon will check if a macaroon is valid or
* not based on a set of satisfiers to pass as general caveat verifiers. This will also run
* against caveat.verifyCaveats to ensure that satisfyPrevious will validate
* @param {string} macaroon A raw macaroon to run a verifier against
* @param {String} secret The secret key used to sign the macaroon
* @param {(Satisfier | Satisfier[])} satisfiers a single satisfier or list of satisfiers used to verify caveats
* @param {Object} [options] An optional options object that will be passed to the satisfiers.
* In many circumstances this will be a request object, for example when this is used in a server
* @returns {boolean}
*/
export function verifyFirstPartyMacaroon(
rawMac: string,
secret: string,
satisfiers?: Satisfier | Satisfier[],
// eslint-disable-next-line @typescript-eslint/no-explicit-any
options: any = {}
): boolean {
// if given a raw macaroon string, convert to a Macaroon class
const macaroon = Macaroon.importMacaroon(rawMac)
const secretBytesArray = stringToBytes(secret)

const verify = function(rawCaveat: string): void | 'not satisfied' | null {
let verified = false;
const caveat = Caveat.decode(rawCaveat)
if (satisfiers) {
if (!Array.isArray(satisfiers)) satisfiers = [satisfiers]
for (const satisfier of satisfiers) {
if (satisfier.condition !== caveat.condition) continue
const valid = satisfier.satisfyFinal(caveat, options)
if (valid) {
verified = true
}
break;
}
}

if (!verified) return 'not satisfied'

// want to also do previous caveat check
// so need to collect all caveats and pass them with satisfiers to `verifyCaveats`
const caveats = []
const rawCaveats = macaroon._exportAsJSONObjectV2()?.c

// satisfy possibly undefined check by ts
if (!rawCaveats) return null;

for (const c of rawCaveats) {
if (!c.i) continue;
const caveat = Caveat.decode(c.i)
caveats.push(caveat)
}

if (Array.isArray(satisfiers)) {
if (!verifyCaveats(caveats, satisfiers, options)) return 'not satisfied';
}
return null
}

try {
macaroon.verify(secretBytesArray, verify)
} catch (e) {
return false
}
return true
}
6 changes: 4 additions & 2 deletions src/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import bolt11 from 'bolt11'
import assert from 'bsert'
import { MacaroonClass } from './types';
import * as Macaroon from 'macaroon'

let TextEncoder
if (typeof window !== 'undefined' && window && window.TextEncoder) {
Expand All @@ -11,8 +13,8 @@ if (typeof window !== 'undefined' && window && window.TextEncoder) {
}

export const utf8Encoder = new TextEncoder();
export const isValue = (x: string | null | undefined) => x !== undefined && x !== null;
export const stringToBytes = (s: string | null | undefined) => isValue(s) ? utf8Encoder.encode(s) : s;
export const isValue = (x: string | null | undefined): boolean => x !== undefined && x !== null;
export const stringToBytes = (s: string | null | undefined): Uint8Array => isValue(s) ? utf8Encoder.encode(s) : s;


/**
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from './caveat'
export * from './lsat'
export * from './types'
export { expirationSatisfier } from './satisfiers'
export * from './macaroon'
68 changes: 68 additions & 0 deletions src/macaroon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { Caveat, verifyCaveats } from "./caveat";
import { stringToBytes } from './helpers'
import * as Macaroon from 'macaroon'
import { MacaroonClass, Satisfier } from "./types";

/**
* @description utility function to get an array of caveat instances from
* a raw macaroon.
* @param {string} macaroon - raw macaroon to retrieve caveats from
* @returns {Caveat[]} array of caveats on the macaroon
*/
export function getCaveatsFromMacaroon(rawMac: string): Caveat[] {
const macaroon = Macaroon.importMacaroon(rawMac)
const caveats = []
const rawCaveats = macaroon._exportAsJSONObjectV2()?.c

if (rawCaveats) {
for (const c of rawCaveats) {
if (!c.i) continue;
const caveat = Caveat.decode(c.i)
caveats.push(caveat)
}
}
return caveats
}

/**
* @description verifyMacaroonCaveats will check if a macaroon is valid or
* not based on a set of satisfiers to pass as general caveat verifiers. This will also run
* against caveat.verifyCaveats to ensure that satisfyPrevious will validate
* @param {string} macaroon A raw macaroon to run a verifier against
* @param {String} secret The secret key used to sign the macaroon
* @param {(Satisfier | Satisfier[])} satisfiers a single satisfier or list of satisfiers used to verify caveats
* @param {Object} [options] An optional options object that will be passed to the satisfiers.
* In many circumstances this will be a request object, for example when this is used in a server
* @returns {boolean}
*/
export function verifyMacaroonCaveats(
rawMac: string,
secret: string,
satisfiers?: Satisfier | Satisfier[],
// eslint-disable-next-line @typescript-eslint/no-explicit-any
options: any = {}
): boolean {
try {
const macaroon = Macaroon.importMacaroon(rawMac)
const secretBytesArray = stringToBytes(secret)

// js-macaroon's verification takes a function as its second
// arg that runs a check against each caveat which is a less full-featured
// version of `verifyCaveats` used below since it doesn't support contextual
// checks like comparing w/ previous caveats for the same condition.
// we pass this stubbed function so signature checks can be done
// and satisfier checks will be done next if this passes.
macaroon.verify(secretBytesArray, () => null)

const caveats = getCaveatsFromMacaroon(rawMac)
if (!caveats.length) return true;
// check caveats against satisfiers, including previous caveat checks
return verifyCaveats(caveats, satisfiers, options)
} catch (e) {
return false
}
}

export function getRawMacaroon(mac: MacaroonClass): string {
return Macaroon.bytesToBase64(mac._exportBinaryV2())
}
15 changes: 15 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,17 @@
import { MacaroonJSONV2 } from 'macaroon'

export * from './lsat'
export * from './satisfier'

// js-macaroon doesn't export a type for its base class
// this throws off some of the ts linting when it wants a return type
/**
* @typedef {Object} MacaroonClass
*/
export interface MacaroonClass {
_exportAsJSONObjectV2(): MacaroonJSONV2
addFirstPartyCaveat(caveatIdBytes: Uint8Array | string): void
_exportBinaryV2(): Uint8Array
}

// could maybe do the above as -> typeof Macaroon.newMacaroon({...})
52 changes: 32 additions & 20 deletions tests/caveat.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { expect } from 'chai'
import * as Macaroon from 'macaroon'
import { Caveat, ErrInvalidCaveat, hasCaveat, verifyCaveats } from '../src'

import { Satisfier } from '../src/types'

describe('Caveats', () => {
Expand Down Expand Up @@ -55,8 +56,8 @@ describe('Caveats', () => {
version: 1,
rootKey: 'secret',
identifier: 'pubId',
location: 'location'
});
location: 'location',
})
macaroon.addFirstPartyCaveat(caveat.encode())

const macBin = macaroon._exportBinaryV2()
Expand Down Expand Up @@ -92,8 +93,8 @@ describe('Caveats', () => {
version: 1,
rootKey: 'secret',
identifier: 'pubId',
location: 'location'
});
location: 'location',
})

const macBin3 = macaroon._exportBinaryV2()
if (macBin3 == null) {
Expand All @@ -112,14 +113,13 @@ describe('Caveats', () => {
let caveat1: Caveat,
caveat2: Caveat,
caveat3: Caveat,
caveats: Caveat[],
satisfier: Satisfier
satisfier: Satisfier,
caveats: Caveat[]

beforeEach(() => {
caveat1 = new Caveat({ condition: '1', value: 'test' })
caveat2 = new Caveat({ condition: '1', value: 'test2' })
caveat3 = new Caveat({ condition: '3', value: 'foobar' })
caveats = [caveat1, caveat2, caveat3]

satisfier = {
condition: caveat1.condition,
Expand All @@ -129,11 +129,10 @@ describe('Caveats', () => {
cur.value.toString().includes('test'),
satisfyFinal: (): boolean => true,
}
caveats = [caveat1, caveat2, caveat3]
})

it('should verify caveats given a set of satisfiers', () => {
const validatesCaveats = (): boolean =>
verifyCaveats(caveats, satisfier)
const validatesCaveats = (): boolean => verifyCaveats(caveats, satisfier)

expect(validatesCaveats).to.not.throw()
expect(validatesCaveats()).to.be.true
Expand Down Expand Up @@ -163,29 +162,42 @@ describe('Caveats', () => {
})

it('should be able to use an options object for verification', () => {
const testCaveat = new Caveat({condition: 'middlename', value: 'danger'})
const testCaveat = new Caveat({
condition: 'middlename',
value: 'danger',
})
caveats.push(testCaveat)
satisfier = {
condition: testCaveat.condition,
// dummy satisfyPrevious function to test that it tests caveat lists correctly
satisfyPrevious: (prev, cur, options): boolean =>
prev.value.toString().includes('test') &&
cur.value.toString().includes('test') && options.body.middlename === testCaveat.value,
cur.value.toString().includes('test') &&
options.body.middlename === testCaveat.value,
satisfyFinal: (caveat, options): boolean => {
if (caveat.condition === testCaveat.condition && options?.body.middlename === testCaveat.value)
if (
caveat.condition === testCaveat.condition &&
options.body.middlename === testCaveat.value
)
return true

return false
},
}

let isValid = verifyCaveats(caveats, satisfier, {body: { middlename: 'bob' }})

expect(isValid, 'should fail when given an invalid options object').to.be.false

isValid = verifyCaveats(caveats, satisfier, {body: { middlename: testCaveat.value }})
let isValid = verifyCaveats(caveats, satisfier, {
body: { middlename: 'bob' },
})

expect(isValid, 'should fail when given an invalid options object').to.be
.false

isValid = verifyCaveats(caveats, satisfier, {
body: { middlename: testCaveat.value },
})

expect(isValid, 'should pass when given a valid options object').to.be.true
expect(isValid, 'should pass when given a valid options object').to.be
.true
})
})
})
Loading

0 comments on commit 2846a62

Please sign in to comment.