Skip to content

Commit

Permalink
Move auth logic from api gateway lambda to cas codebase (#1234)
Browse files Browse the repository at this point in the history
* wip

* wip

* wip

* drop network

* get auth lambda

* wip

* Metrics

* Metrics

* parse allowed dids list

* default dids

* reasons to disallow

* relaxed label

* Update src/auth/auth.middleware.ts

Co-authored-by: Mohsin Zaidi <[email protected]>

---------

Co-authored-by: Mohsin Zaidi <[email protected]>
  • Loading branch information
ukstv and smrz2001 authored Jul 6, 2024
1 parent f9678e6 commit a4495be
Show file tree
Hide file tree
Showing 9 changed files with 433 additions and 72 deletions.
6 changes: 5 additions & 1 deletion config/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
"merkleDepthLimit": 0,
"minStreamCount": 1024,
"readyRetryIntervalMS": 300000,
"requireAuth": false,
"schedulerIntervalMS": 300000,
"schedulerStopAfterNoOp": false,
"pubsubResponderWindowMs": 8035200000,
Expand Down Expand Up @@ -101,5 +100,10 @@
"s3Endpoint": "",
"maxTimeToHoldMessageSec": 21600,
"waitTimeForMessageSec": 0
},
"auth": {
"required": false,
"dids": "",
"relaxed": true
}
}
6 changes: 5 additions & 1 deletion config/env/dev.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
"merkleDepthLimit": "@@MERKLE_DEPTH_LIMIT",
"minStreamCount": "@@MIN_STREAM_COUNT",
"readyRetryIntervalMS": "@@READY_RETRY_INTERVAL_MS",
"requireAuth": "@@REQUIRE_AUTH",
"schedulerIntervalMS": "@@SCHEDULER_INTERVAL_MS",
"schedulerStopAfterNoOp": "@@SCHEDULER_STOP_AFTER_NO_OP",
"pubsubResponderWindowMs": "@@PUBSUB_RESPONDER_WINDOW_MS",
Expand Down Expand Up @@ -93,5 +92,10 @@
"s3Endpoint": "@@S3_ENDPOINT",
"maxTimeToHoldMessageSec": "@@MAX_TIME_TO_HOLD_MESSAGE_SEC",
"waitTimeForMessageSec": "@@WAIT_TIME_FOR_MESSAGE_SEC"
},
"auth": {
"required": "@@REQUIRE_AUTH",
"dids": "@@AUTH_DIDS_ALLOWED",
"relaxed": "@@AUTH_RELAXED"
}
}
6 changes: 5 additions & 1 deletion config/env/prod.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
"merkleDepthLimit": "@@MERKLE_DEPTH_LIMIT",
"minStreamCount": "@@MIN_STREAM_COUNT",
"readyRetryIntervalMS": "@@READY_RETRY_INTERVAL_MS",
"requireAuth": "@@REQUIRE_AUTH",
"schedulerIntervalMS": "@@SCHEDULER_INTERVAL_MS",
"schedulerStopAfterNoOp": "@@SCHEDULER_STOP_AFTER_NO_OP",
"pubsubResponderWindowMs": "@@PUBSUB_RESPONDER_WINDOW_MS",
Expand Down Expand Up @@ -93,5 +92,10 @@
"s3Endpoint": "@@S3_ENDPOINT",
"maxTimeToHoldMessageSec": "@@MAX_TIME_TO_HOLD_MESSAGE_SEC",
"waitTimeForMessageSec": "@@WAIT_TIME_FOR_MESSAGE_SEC"
},
"auth": {
"required": "@@REQUIRE_AUTH",
"dids": "@@AUTH_DIDS_ALLOWED",
"relaxed": "@@AUTH_RELAXED"
}
}
6 changes: 5 additions & 1 deletion config/env/test.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
"expirationPeriod": 0,
"loadStreamTimeoutMs": 1000,
"readyRetryIntervalMS": 10000,
"requireAuth": false,
"schedulerIntervalMS": 10000,
"carStorage": {
"mode": "s3",
Expand Down Expand Up @@ -74,5 +73,10 @@
"s3BucketName": "ceramic-tnet-cas",
"maxTimeToHoldMessageSec": 10800,
"waitTimeForMessageSec": 10
},
"auth": {
"required": false,
"dids": "",
"relaxed": true
}
}
246 changes: 246 additions & 0 deletions src/auth/__tests__/auth.middleware.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
import { test, describe, expect, beforeAll } from '@jest/globals'
import express, { Express } from 'express'
import { auth } from '../auth.middleware.js'
import supertest from 'supertest'
import { DID } from 'dids'
import { Ed25519Provider } from 'key-did-provider-ed25519'
import KeyDIDResolver from 'key-did-resolver'
import { CARFactory } from 'cartonne'
import bodyParser from 'body-parser'
import { logger } from '../../logger/index.js'

const carFactory = new CARFactory()

async function createDidKey(
seed: Uint8Array = crypto.getRandomValues(new Uint8Array(32))
): Promise<DID> {
const did = new DID({
provider: new Ed25519Provider(seed),
resolver: KeyDIDResolver.getResolver(),
})
await did.authenticate()
return did
}

async function makeJWS(did: DID, payload: object): Promise<string> {
const dagJWS = await did.createJWS(payload)
const signature = dagJWS.signatures[0]
if (!signature) throw new Error(`No signature`)
return `${signature.protected}.${dagJWS.payload}.${signature.signature}`
}

describe('Authorization header: strict', () => {
let app: Express
let did: DID
let disallowedDID: DID

beforeAll(async () => {
did = await createDidKey()
disallowedDID = await createDidKey()
app = express().use(express.json())
app.use(bodyParser.raw({ inflate: true, type: 'application/vnd.ipld.car', limit: '1mb' }))
app.use(bodyParser.json({ type: 'application/json' }))
app.use(bodyParser.urlencoded({ extended: true, type: 'application/x-www-form-urlencoded' }))
app.use(
auth({
allowedDIDs: new Set([did.id]),
isRelaxed: false,
logger: logger,
})
)
app.post('/', (req, res) => {
res.json({ hello: 'world' })
})
})

test('allowed DID, valid digest', async () => {
const carFile = carFactory.build()
const cid = carFile.put({ hello: 'world' }, { isRoot: true })
const jws = await makeJWS(did, { nonce: '1234567890', digest: cid.toString() })
const response = await supertest(app)
.post('/')
.set('Content-Type', 'application/vnd.ipld.car')
.set('Authorization', `Bearer ${jws}`)
.send(Buffer.from(carFile.bytes)) // Supertest quirk
expect(response.status).toBe(200)
})
test('allowed DID, invalid digest', async () => {
const carFile = carFactory.build()
const jws = await makeJWS(did, { nonce: '1234567890', digest: `Invalid` })
const response = await supertest(app)
.post('/')
.set('Content-Type', 'application/vnd.ipld.car')
.set('Authorization', `Bearer ${jws}`)
.send(Buffer.from(carFile.bytes)) // Supertest quirk
expect(response.status).toBe(403)
})
test('disallowed DID, valid digest', async () => {
const carFile = carFactory.build()
const cid = carFile.put({ hello: 'world' }, { isRoot: true })
const jws = await makeJWS(disallowedDID, { nonce: '1234567890', digest: cid.toString() })
const response = await supertest(app)
.post('/')
.set('Content-Type', 'application/vnd.ipld.car')
.set('Authorization', `Bearer ${jws}`)
.send(Buffer.from(carFile.bytes)) // Supertest quirk
expect(response.status).toBe(403)
})
test('disallowed DID, invalid digest', async () => {
const carFile = carFactory.build()
const jws = await makeJWS(disallowedDID, { nonce: '1234567890', digest: `Invalid` })
const response = await supertest(app)
.post('/')
.set('Content-Type', 'application/vnd.ipld.car')
.set('Authorization', `Bearer ${jws}`)
.send(Buffer.from(carFile.bytes)) // Supertest quirk
expect(response.status).toBe(403)
})
})

describe('Authorization header: relaxed', () => {
let app: Express
let disallowedDID: DID
let did: DID

beforeAll(async () => {
disallowedDID = await createDidKey()
did = await createDidKey()
app = express().use(express.json())
app.use(bodyParser.raw({ inflate: true, type: 'application/vnd.ipld.car', limit: '1mb' }))
app.use(bodyParser.json({ type: 'application/json' }))
app.use(bodyParser.urlencoded({ extended: true, type: 'application/x-www-form-urlencoded' }))
app.use(
auth({
allowedDIDs: new Set([did.id]),
isRelaxed: true,
logger: logger,
})
)
app.post('/', (req, res) => {
res.json({ hello: 'world' })
})
})

test('disallowed DID, valid digest', async () => {
const carFile = carFactory.build()
const cid = carFile.put({ hello: 'world' }, { isRoot: true })
const jws = await makeJWS(disallowedDID, { nonce: '1234567890', digest: cid.toString() })
const response = await supertest(app)
.post('/')
.set('Content-Type', 'application/vnd.ipld.car')
.set('Authorization', `Bearer ${jws}`)
.send(Buffer.from(carFile.bytes)) // Supertest quirk
expect(response.status).toBe(200)
})
test('disallowed DID, invalid digest', async () => {
const carFile = carFactory.build()
const jws = await makeJWS(disallowedDID, { nonce: '1234567890', digest: `Invalid` })
const response = await supertest(app)
.post('/')
.set('Content-Type', 'application/vnd.ipld.car')
.set('Authorization', `Bearer ${jws}`)
.send(Buffer.from(carFile.bytes)) // Supertest quirk
expect(response.status).toBe(403)
})
})

describe('Auth lambda', () => {
let app: Express
let did: DID

beforeAll(async () => {
did = await createDidKey()
app = express().use(express.json())
app.use(bodyParser.raw({ inflate: true, type: 'application/vnd.ipld.car', limit: '1mb' }))
app.use(bodyParser.json({ type: 'application/json' }))
app.use(bodyParser.urlencoded({ extended: true, type: 'application/x-www-form-urlencoded' }))
app.use(
auth({
allowedDIDs: new Set(),
isRelaxed: false,
logger: logger,
})
)
app.post('/', (req, res) => {
res.json({ hello: 'world' })
})
})

test('valid digest', async () => {
const carFile = carFactory.build()
const cid = carFile.put({ hello: 'world' }, { isRoot: true })
const response = await supertest(app)
.post('/')
.set('Content-Type', 'application/vnd.ipld.car')
.set('did', did.id)
.set('digest', cid.toString())
.send(Buffer.from(carFile.bytes)) // Supertest quirk
expect(response.status).toBe(200)
})
test('invalid digest', async () => {
const carFile = carFactory.build()
const response = await supertest(app)
.post('/')
.set('Content-Type', 'application/vnd.ipld.car')
.set('did', did.id)
.set('digest', 'INVALID')
.send(Buffer.from(carFile.bytes)) // Supertest quirk
expect(response.status).toBe(403)
})
})

describe('empty allowed dids list', () => {
let app: Express
let did: DID

beforeAll(async () => {
did = await createDidKey()
app = express().use(express.json())
app.use(bodyParser.raw({ inflate: true, type: 'application/vnd.ipld.car', limit: '1mb' }))
app.use(bodyParser.json({ type: 'application/json' }))
app.use(bodyParser.urlencoded({ extended: true, type: 'application/x-www-form-urlencoded' }))
app.use(
auth({
allowedDIDs: new Set(),
isRelaxed: false,
logger: logger,
})
)
app.post('/', (req, res) => {
res.json({ hello: 'world' })
})
})

test('pass Authorization header check', async () => {
const carFile = carFactory.build()
const cid = carFile.put({ hello: 'world' }, { isRoot: true })
const jws = await makeJWS(did, { nonce: '1234567890', digest: cid.toString() })
const response = await supertest(app)
.post('/')
.set('Content-Type', 'application/vnd.ipld.car')
.set('Authorization', `Bearer ${jws}`)
.send(Buffer.from(carFile.bytes)) // Supertest quirk
expect(response.status).toBe(200)
})
test('use Auth Lambda check: ok', async () => {
const carFile = carFactory.build()
const cid = carFile.put({ hello: 'world' }, { isRoot: true })
const response = await supertest(app)
.post('/')
.set('Content-Type', 'application/vnd.ipld.car')
.set('did', did.id)
.set('digest', cid.toString())
.send(Buffer.from(carFile.bytes)) // Supertest quirk
expect(response.status).toBe(200)
})
test('use Auth Lambda check: invalid digest', async () => {
const carFile = carFactory.build()
const response = await supertest(app)
.post('/')
.set('Content-Type', 'application/vnd.ipld.car')
.set('did', did.id)
.set('digest', 'INVALID')
.send(Buffer.from(carFile.bytes)) // Supertest quirk
expect(response.status).toBe(403)
})
})
Loading

0 comments on commit a4495be

Please sign in to comment.