Skip to content

Commit

Permalink
feat!: upgrade capabilities to latest ucanto (#463)
Browse files Browse the repository at this point in the history
  • Loading branch information
Gozala authored Mar 1, 2023
1 parent fc62691 commit e375ae4
Show file tree
Hide file tree
Showing 21 changed files with 809 additions and 713 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"prettier": "2.8.3",
"simple-git-hooks": "^2.8.1",
"typedoc-plugin-markdown": "^3.14.0",
"typescript": "4.9.4",
"typescript": "4.9.5",
"wrangler": "^2.8.0"
},
"simple-git-hooks": {
Expand Down
17 changes: 9 additions & 8 deletions packages/access-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,20 @@
"lint": "tsc --build && eslint '**/*.{js,ts}' && prettier --check '**/*.{js,ts,yml,json}' --ignore-path ../../.gitignore",
"dev": "scripts/cli.js dev",
"build": "scripts/cli.js build",
"check": "tsc --build",
"test": "pnpm build && mocha --bail --timeout 10s -n no-warnings -n experimental-vm-modules",
"test-watch": "pnpm build && mocha --bail --timeout 10s --watch --parallel -n no-warnings -n experimental-vm-modules --watch-files src,test"
},
"author": "Hugo Dias <[email protected]> (hugodias.me)",
"license": "(Apache-2.0 OR MIT)",
"dependencies": {
"@ipld/dag-ucan": "^3.2.0",
"@ucanto/core": "^4.2.3",
"@ucanto/interface": "^4.2.3",
"@ucanto/principal": "^4.2.3",
"@ucanto/server": "^4.2.3",
"@ucanto/transport": "^4.2.3",
"@ucanto/validator": "^4.2.3",
"@ucanto/core": "^5.0.0",
"@ucanto/interface": "^5.0.0",
"@ucanto/principal": "^5.0.0",
"@ucanto/server": "^5.0.0",
"@ucanto/transport": "^5.0.0",
"@ucanto/validator": "^5.0.0",
"@web3-storage/access": "workspace:^",
"@web3-storage/capabilities": "workspace:^",
"@web3-storage/worker-utils": "0.4.3-dev",
Expand All @@ -45,7 +46,7 @@
"@types/mocha": "^10.0.1",
"@types/node": "^18.11.18",
"@types/qrcode": "^1.5.0",
"@ucanto/client": "^4.2.3",
"@ucanto/client": "^5.0.0",
"better-sqlite3": "8.0.1",
"buffer": "^6.0.3",
"dotenv": "^16.0.3",
Expand All @@ -59,7 +60,7 @@
"process": "^0.11.10",
"readable-stream": "^4.2.0",
"sade": "^1.8.1",
"typescript": "4.9.4",
"typescript": "4.9.5",
"wrangler": "^2.8.0"
},
"eslintConfig": {
Expand Down
92 changes: 63 additions & 29 deletions packages/access-api/src/routes/validate-email.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
/* eslint-disable no-unused-vars */
import { stringToDelegation } from '@web3-storage/access/encoding'
import {
stringToDelegation,
delegationsToString,
} from '@web3-storage/access/encoding'
import * as Access from '@web3-storage/capabilities/access'
import QRCode from 'qrcode'
import { toEmail } from '../utils/did-mailto.js'
Expand All @@ -11,7 +14,7 @@ import {
} from '../utils/html.js'
import * as ucanto from '@ucanto/core'
import * as validator from '@ucanto/validator'
import { Verifier } from '@ucanto/principal/ed25519'
import { Verifier, Absentee } from '@ucanto/principal'

/**
* @param {import('@web3-storage/worker-utils/router').ParsedRequest} req
Expand Down Expand Up @@ -132,53 +135,84 @@ async function recover(req, env) {
* @param {import('../bindings.js').RouteContext} env
*/
async function session(req, env) {
/** @type {import('@ucanto/interface').Delegation<[import('@web3-storage/capabilities/src/types.js').AccessSession]>} */
/** @type {import('@ucanto/interface').Delegation<[import('@web3-storage/capabilities/src/types.js').AccessAuthorize]>} */
const delegation = stringToDelegation(req.query.ucan)
await env.models.validations.putSession(
req.query.ucan,
delegation.capabilities[0].nb.key
)

// ⚠️ This is not an ideal solution but we do need to ensure that attacker
// cannot simply send a valid `access/authorize` delegation to the service
// and get an attested session.
if (delegation.issuer.did() !== env.signer.did()) {
throw new Error('Delegation MUST be issued by the service')
}

// TODO: Figure when do we go through a post vs get request. WebSocket message
// was send regardless of the method, but delegations were only stored on post
// requests.
if (req.method.toLowerCase() === 'post') {
const accessSessionResult = await validator.access(delegation, {
capability: Access.session,
capability: Access.authorize,
principal: Verifier,
authority: env.signer,
})

if (accessSessionResult.error) {
throw new Error(
`unable to validate access session: ${accessSessionResult.error}`
)
}
const account = accessSessionResult.audience
const agentPubkey = accessSessionResult.capability.nb.key
const wrappedKeyCanAsignForAccount = await ucanto.delegate({

// Create a absentee signer for the account that authorized the delegation
const account = Absentee.from({ id: accessSessionResult.capability.nb.iss })
const agent = Verifier.parse(accessSessionResult.capability.with)

// It the future we should instead render a page and allow a user to select
// which delegations they wish to re-delegate. Right now we just re-delegate
// everything that was requested for all of the resources.
const capabilities =
/** @type {ucanto.UCAN.Capabilities} */
(
accessSessionResult.capability.nb.att.map(({ can }) => ({
can,
with: /** @type {ucanto.UCAN.Resource} */ ('ucan:*'),
}))
)

// create an authorization on behalf of the account with an absent
// signature.
const authorization = await ucanto.delegate({
issuer: account,
audience: agent,
capabilities,
expiration: Infinity,
// We should also include proofs with all the delegations we have for
// the account.
})

const attestation = await ucanto.delegate({
issuer: env.signer,
audience: { did: () => agentPubkey },
audience: agent,
capabilities: [
{
with: env.signer.did(),
can: 'access-api/delegation',
can: 'ucan/attest',
nb: { proof: authorization.cid },
},
],
proofs: [
await ucanto.delegate({
issuer: env.signer,
audience: account,
capabilities: [
{
with: env.signer.did(),
can: './update',
nb: {
key: agentPubkey,
},
},
],
}),
],
expiration: Infinity,
})
await env.models.delegations.putMany(wrappedKeyCanAsignForAccount)

// Store the delegations so that they can be pulled with access/claim
await env.models.delegations.putMany(authorization, attestation)

// Send delegations to the client through a websocket
await env.models.validations.putSession(
delegationsToString([authorization, attestation]),
agent.did()
)
}

// TODO: We clearly should not render that access/delegate in the QR code, but
// I'm not sure what this QR code is used for.
try {
return new HtmlResponse(
(
Expand Down
26 changes: 16 additions & 10 deletions packages/access-api/src/service/access-authorize.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,27 @@ export function accessAuthorizeProvider(ctx) {
return Server.provide(
Access.authorize,
async ({ capability, invocation }) => {
const session = await Access.session
/**
* We re-delegate the capability to the account DID and limit it's
* lifetime to 15 minutes which should be enough time for the user to
* complete the authorization. We don't want to allow authorization for
* long time because it could be used by an attacker to gain authorization
* by sending second request misleading a user to click a wrong one.
*/
const authorization = await Access.authorize
.invoke({
issuer: ctx.signer,
audience: DID.parse(capability.nb.as),
with: ctx.signer.did(),
lifetimeInSeconds: 86_400 * 7, // 7 days
nb: {
key: capability.with,
},
audience: DID.parse(capability.nb.iss),
with: capability.with,
lifetimeInSeconds: 60 * 15, // 15 minutes
nb: capability.nb,
proofs: [invocation],
})
.delegate()

const encoded = delegationToString(session)
const encoded = delegationToString(authorization)

await ctx.models.accounts.create(capability.nb.as)
await ctx.models.accounts.create(capability.nb.iss)

const url = `${ctx.url.protocol}//${ctx.url.host}/validate-email?ucan=${encoded}&mode=session`
// For testing
Expand All @@ -37,7 +43,7 @@ export function accessAuthorizeProvider(ctx) {
}

await ctx.email.sendValidation({
to: Mailto.toEmail(capability.nb.as),
to: Mailto.toEmail(capability.nb.iss),
url,
})
}
Expand Down
3 changes: 2 additions & 1 deletion packages/access-api/src/service/access-claim.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ export function accessClaimProvider(ctx) {
return Server.provide(claim, async ({ invocation }) => {
// disable until hardened in test/staging
if (ctx.config.ENV === 'production') {
throw new Error(`acccess/claim invocation handling is not enabled`)
throw new Error(`access/claim invocation handling is not enabled`)
}

return handleClaimInvocation(invocation)
})
}
Expand Down
Loading

0 comments on commit e375ae4

Please sign in to comment.