Skip to content

Commit

Permalink
feat!: rate limiting by ucan agent did (#2093)
Browse files Browse the repository at this point in the history
fixes #2092

Co-authored-by: Alan Shaw <alan.shaw@protocol.ai>
  • Loading branch information
Gozala and Alan Shaw authored Jul 29, 2022
1 parent ae799be commit b29d3d5
Show file tree
Hide file tree
Showing 8 changed files with 160 additions and 18 deletions.
2 changes: 1 addition & 1 deletion packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"nanoid": "^3.1.30",
"regexparam": "^2.0.0",
"toucan-js": "^2.4.1",
"ucan-storage": "^1.0.0",
"ucan-storage": "^1.3.0",
"uint8arrays": "^3.0.0"
},
"devDependencies": {
Expand Down
17 changes: 17 additions & 0 deletions packages/api/src/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -243,3 +243,20 @@ export class ErrorDIDNotFound extends HTTPError {
}
}
ErrorMaintenance.CODE = 'ERROR_DID_NOT_FOUND'

export class ErrorAgentDIDRequired extends HTTPError {
constructor(
msg = 'UCAN authorized request must be supplied with x-agent-did header set to the DID of the UCAN issuer',
status = 401
) {
super(msg, status)
this.name = 'ErrorAgentDIDRequired'
this.code = ErrorUnauthenticated.CODE
}
}

export class ErrorInvalidRoute extends HTTPError {
constructor(msg = 'Invalid route for the request', status = 400) {
super(msg, status)
}
}
8 changes: 8 additions & 0 deletions packages/api/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,14 @@ r.add('get', '/:cid', withAuth(withMode(nftGet, RO)), [postCors])
r.add(
'post',
'/upload',
withAuth(withMode(nftUpload, RW), {
checkHasAccountRestriction,
}),
[postCors]
)
r.add(
'post',
'/ucan-upload',
withAuth(withMode(nftUpload, RW), {
checkHasAccountRestriction,
checkUcan,
Expand Down
40 changes: 29 additions & 11 deletions packages/api/src/utils/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
ErrorTokenNotFound,
ErrorUnauthenticated,
ErrorTokenBlocked,
ErrorAgentDIDRequired,
ErrorInvalidRoute,
} from '../errors.js'
import { parseJWT, verifyJWT } from './jwt.js'
import * as Ucan from 'ucan-storage/ucan-storage'
Expand Down Expand Up @@ -37,19 +39,35 @@ export async function validate(event, { log, db, ucanService }, options) {
const auth = event.request.headers.get('Authorization') || ''
const token = magic.utils.parseAuthorizationHeader(auth)

if (options?.checkUcan && Ucan.isUcan(token)) {
const { root, cap } = await ucanService.validateFromCaps(token)
const user = await db.getUser(root.audience())
if (user) {
log.setUser({ id: user.id })
return {
user: filterDeletedKeys(user),
db,
ucan: { token, root: root._decoded.payload, cap },
type: 'ucan',
if (Ucan.isUcan(token)) {
if (options?.checkUcan) {
const agentDID = event.request.headers.get('x-agent-did') || ''
if (!agentDID.startsWith('did:key:')) {
throw new ErrorAgentDIDRequired()
}

const { root, cap, issuer } = await ucanService.validateFromCaps(token)
if (issuer !== agentDID) {
throw new ErrorAgentDIDRequired(
`Expected x-agent-did to be UCAN issuer DID: ${issuer}, instead got ${agentDID}`
)
}
const user = await db.getUser(root.audience())
if (user) {
log.setUser({ id: user.id })
return {
user: filterDeletedKeys(user),
db,
ucan: { token, root: root._decoded.payload, cap },
type: 'ucan',
}
} else {
throw new ErrorTokenNotFound()
}
} else {
throw new ErrorTokenNotFound()
throw new ErrorInvalidRoute(
`Invalid route, UCAN authorized requests must be directed to /ucan-upload instead`
)
}
}

Expand Down
50 changes: 48 additions & 2 deletions packages/api/test/nfts-upload.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -488,11 +488,57 @@ describe('NFT Upload ', () => {
const file = new Blob(['hello world!'], { type: 'application/text' })
// expected CID for the above data
const cid = 'bafkreidvbhs33ighmljlvr7zbv2ywwzcmp5adtf4kqvlly67cy56bdtmve'
const res = await fetch('upload', {
{
const res = await fetch('upload', {
method: 'POST',
headers: { Authorization: `Bearer ${opUcan}` },
body: file,
})

assert.equal(res.status, 400)
const { ok, error } = await res.json()
assert.equal(ok, false)
assert.ok(error.message.match(/Invalid route/))
}

{
const res = await fetch('ucan-upload', {
method: 'POST',
headers: { Authorization: `Bearer ${opUcan}` },
body: file,
})

assert.equal(res.status, 401)
const { ok, error } = await res.json()
assert.equal(ok, false)
assert.ok(error.message.match(/x-agent-did/))
}

{
const badkp = await KeyPair.create()
const res = await fetch('ucan-upload', {
method: 'POST',
headers: {
Authorization: `Bearer ${opUcan}`,
'x-agent-did': badkp.did(),
},
body: file,
})

assert.equal(res.status, 401)
const { ok, error } = await res.json()
assert.equal(ok, false)
assert.ok(
error.message.match(/Expected x-agent-did to be UCAN issuer DID/)
)
}

const res = await fetch('ucan-upload', {
method: 'POST',
headers: { Authorization: `Bearer ${opUcan}` },
headers: { Authorization: `Bearer ${opUcan}`, 'x-agent-did': kp.did() },
body: file,
})

const { ok, value } = await res.json()
assert(ok, 'Server response payload has `ok` property')

Expand Down
4 changes: 4 additions & 0 deletions packages/website/pages/docs/how-to/ucan.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,10 @@ Send a `GET` request to `https://api.nft.storage/did`, which should return a JSO

The `value` field contains the service DID, which is used when [creating request tokens][ucan-storage-typedoc-creating-a-request-token].

## Sending requests

You can send upload requests with UCAN auth token to `https://api.nft.storage/ucan-upload` endpoint. Requests to that endpoint must have additional `x-agent-did` HTTP header with a value set to a DID token was issued/signed by.

## Getting help

Use of UCANs to delegate upload permissions in NFT.Storage is currently a Preview Feature. If you find issues with the integration, need help with tooling, or have suggestions for improving the API for your use cases, please leave feedback in [this Github Discussion](https://github.com/nftstorage/nft.storage/discussions/1591). We're excited to see what you'll build!
Expand Down
49 changes: 49 additions & 0 deletions packages/website/public/schema.yml
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,55 @@ paths:
$ref: '#/components/responses/forbidden'
'500':
$ref: '#/components/responses/internalServerError'
/ucan-upload:
post:
tags:
- NFT Storage
summary: Store a file
description: |
Just like `/upload` endpoint but [using UCAN authorization tokens](https://nft.storage/docs/how-to/ucan/#using-ucans-with-nftstorage).
You must set `x-agent-did` header to a DID that signed the token.
operationId: ucan-upload
requestBody:
required: true
content:
image/*:
schema:
type: string
format: binary
application/car:
schema:
type: string
format: binary
multipart/form-data:
schema:
type: object
properties:
file:
type: array
items:
type: string
format: binary
'*/*':
schema:
type: string
format: binary
responses:
'200':
description: OK
content:
'application/json':
schema:
$ref: '#/components/schemas/UploadResponse'
'400':
$ref: '#/components/responses/badRequest'
'401':
$ref: '#/components/responses/unauthorized'
'403':
$ref: '#/components/responses/forbidden'
'500':
$ref: '#/components/responses/internalServerError'
/:
get:
tags:
Expand Down
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -19758,10 +19758,10 @@ ua-parser-js@^1.0.2:
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.2.tgz#e2976c34dbfb30b15d2c300b2a53eac87c57a775"
integrity sha512-00y/AXhx0/SsnI51fTc0rLRmafiGOM4/O+ny10Ps7f+j/b8p/ZY11ytMgznXkOVo4GQ+KwQG5UQLkLGirsACRg==

ucan-storage@^1.0.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/ucan-storage/-/ucan-storage-1.2.0.tgz#fa3899e86842cd6d832436303254512ee782a791"
integrity sha512-h3lDsuDVrKhgL8DBEMb1Fub+jvqp7jlOXJUaOTaKE5eXCG5IQZ1eol0YWOScSpjxOvpRwoYGNhEIZR0sb6DJaw==
"ucan-storage@^ 1.3.0":
version "1.3.0"
resolved "https://registry.yarnpkg.com/ucan-storage/-/ucan-storage-1.3.0.tgz#b9f3e29fa77da22a636ba5d917f4e747da0a89c8"
integrity sha512-C1PvShqWTg6JzcBAuWDeCsaL6AggwsGWqbvKZ8XdN9csAukQVnA5/kerddhdPrpeoCGnJFfSkvBcPklZzdJ+OQ==
dependencies:
"@noble/ed25519" "^1.5.2"
base-x "^4.0.0"
Expand Down

0 comments on commit b29d3d5

Please sign in to comment.