-
Notifications
You must be signed in to change notification settings - Fork 19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: access/authorize confirmation email click results in a delegation back to the issuer did:key so that access/claim works #460
Changes from 11 commits
6db052a
24f9266
94b1577
b92fad2
1735d15
c951907
a8cab05
1be596f
013ac0d
722abde
83141c4
685af65
48f4744
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
/* eslint-disable no-unused-vars */ | ||
import { stringToDelegation } from '@web3-storage/access/encoding' | ||
import * as Access from '@web3-storage/capabilities/access' | ||
import QRCode from 'qrcode' | ||
import { toEmail } from '../utils/did-mailto.js' | ||
import { | ||
|
@@ -8,6 +9,10 @@ import { | |
ValidateEmailError, | ||
PendingValidateEmail, | ||
} from '../utils/html.js' | ||
import * as ucanto from '@ucanto/core' | ||
import * as validator from '@ucanto/validator' | ||
import { Verifier } from '@ucanto/principal/ed25519' | ||
import { ed25519 } from '@ucanto/principal' | ||
|
||
/** | ||
* @param {import('@web3-storage/worker-utils/router').ParsedRequest} req | ||
|
@@ -134,6 +139,93 @@ async function session(req, env) { | |
req.query.ucan, | ||
delegation.capabilities[0].nb.key | ||
) | ||
if (req.method.toLowerCase() === 'post') { | ||
const accessSessionResult = await validator.access(delegation, { | ||
capability: Access.session, | ||
principal: Verifier, | ||
authority: env.signer, | ||
}) | ||
if (accessSessionResult.error) { | ||
throw new Error( | ||
`unable to validate access session: ${accessSessionResult.error}` | ||
) | ||
} | ||
const accountDID = accessSessionResult.audience.did() | ||
// @todo: use new ucanto `Account` instead | ||
const accountOneOffKey = await ed25519.generate() | ||
const account = accountOneOffKey.withDID(accountDID) | ||
|
||
const agentPubkey = accessSessionResult.capability.nb.key | ||
|
||
const update = await ucanto.delegate({ | ||
issuer: env.signer, | ||
audience: account, | ||
capabilities: [ | ||
{ | ||
with: env.signer.did(), | ||
can: './update', | ||
nb: { | ||
key: agentPubkey, | ||
}, | ||
}, | ||
], | ||
}) | ||
const attestKeyAuthorizesAccount = await ucanto.delegate({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Comment here describing what it's meant to do would also help, because it does not do what spec or issue suggests and it's not clear to me what the intentions are. |
||
issuer: env.signer, | ||
audience: { did: () => agentPubkey }, | ||
capabilities: [ | ||
{ | ||
with: env.signer.did(), | ||
can: 'ucan/attest', | ||
nb: { | ||
proof: update.cid, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be the CID for the (unsigned) delegation from an account to the agent. This delegation should be replacement for the That is the core change in the spec, if previously we were attesting to key authorization which implied sudo privileges, in a new version we attest to the specific delegation authorization. Which means it could be sudo privileges or a very specific capabilities. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reading further down I think you could just remove this delegation. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is what's needed to make this work https://github.com/web3-storage/w3protocol/pull/460/files#diff-0cebcdde20e55de2911e90a385f368240bd9109fc54b074b89759628b695e88bR202 |
||
}, | ||
}, | ||
], | ||
proofs: [update], | ||
}) | ||
|
||
// create delegations that should be claimable | ||
const delegationAccountToKey = await ucanto.delegate({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is somewhat correct, except it will have a signature from |
||
issuer: account, | ||
audience: { | ||
did() { | ||
return agentPubkey | ||
}, | ||
}, | ||
capabilities: [ | ||
{ | ||
with: 'ucan:*', | ||
can: '*', | ||
}, | ||
], | ||
}) | ||
// generate a delegation to the key that we can save in | ||
// models.delegations to be found by subsequent access/claim | ||
// invocations invoked by the did:key | ||
const delegateToKey = await ucanto.delegate({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably should be renamed and update comments as it does a different thing now. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. renamed/recommented in 685af65 |
||
issuer: env.signer, | ||
audience: { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Audience of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok. I updated to not use |
||
did() { | ||
return agentPubkey | ||
}, | ||
}, | ||
proofs: [delegationAccountToKey], | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure what is this for ? This could either contain delegation from |
||
capabilities: [ | ||
{ | ||
can: 'ucan/attest', | ||
with: env.signer.did(), | ||
nb: { | ||
proof: delegationAccountToKey.cid, | ||
}, | ||
}, | ||
], | ||
}) | ||
await env.models.delegations.putMany( | ||
delegateToKey, | ||
attestKeyAuthorizesAccount | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you need to put |
||
) | ||
} | ||
|
||
try { | ||
return new HtmlResponse( | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,7 @@ | ||
import { stringToDelegation } from '@web3-storage/access/encoding' | ||
import { | ||
stringToDelegation, | ||
bytesToDelegations, | ||
} from '@web3-storage/access/encoding' | ||
import * as Access from '@web3-storage/capabilities/access' | ||
import assert from 'assert' | ||
import pWaitFor from 'p-wait-for' | ||
|
@@ -7,6 +10,7 @@ import { context } from './helpers/context.js' | |
// @ts-ignore | ||
import isSubset from 'is-subset' | ||
import { toEmail } from '../src/utils/did-mailto.js' | ||
import { warnOnErrorResult } from './helpers/ucanto-test-utils.js' | ||
|
||
/** @type {typeof assert} */ | ||
const t = assert | ||
|
@@ -95,6 +99,129 @@ describe('access/authorize', function () { | |
assert(html.includes(toEmail(accountDID))) | ||
}) | ||
|
||
it('should send confirmation email with link that, when clicked, allows for access/claim', async function () { | ||
const { issuer, service, conn, mf } = ctx | ||
const accountDID = 'did:mailto:dag.house:email' | ||
|
||
const inv = await Access.authorize | ||
.invoke({ | ||
issuer, | ||
audience: service, | ||
with: issuer.did(), | ||
nb: { | ||
as: accountDID, | ||
}, | ||
}) | ||
.execute(conn) | ||
|
||
// @todo - this only returns string when ENV==='test'. Remove that env-specific behavior | ||
assert.ok(typeof inv === 'string', 'invocation result is a string') | ||
|
||
const confirmEmailPostUrl = new URL(inv) | ||
const confirmEmailPostResponse = await mf.dispatchFetch( | ||
confirmEmailPostUrl, | ||
{ method: 'POST' } | ||
) | ||
assert.deepEqual( | ||
confirmEmailPostResponse.status, | ||
200, | ||
'confirmEmailPostResponse status is 200' | ||
) | ||
|
||
const claim = Access.claim.invoke({ | ||
issuer, | ||
audience: conn.id, | ||
with: issuer.did(), | ||
}) | ||
const claimResult = await claim.execute(conn) | ||
assert.ok( | ||
'delegations' in claimResult, | ||
'claimResult should have delegations property' | ||
) | ||
const claimedDelegations = Object.values(claimResult.delegations).flatMap( | ||
(bytes) => { | ||
return bytesToDelegations( | ||
/** @type {import('@web3-storage/access/src/types.js').BytesDelegation} */ ( | ||
bytes | ||
) | ||
) | ||
} | ||
) | ||
assert.deepEqual( | ||
claimedDelegations.length, | ||
2, | ||
'should have claimed delegation(s)' | ||
) | ||
/** | ||
* narrow Ucanto.Proof to Ucanto.Delegation | ||
* | ||
* @param {import('@ucanto/interface').Proof} proof | ||
*/ | ||
// eslint-disable-next-line unicorn/consistent-function-scoping | ||
const proofDelegation = (proof) => { | ||
if (!('cid' in proof)) { | ||
throw new Error('proof must be delegation') | ||
} | ||
return proof | ||
} | ||
const attest = claimedDelegations.find( | ||
(d) => proofDelegation(d.proofs[0])?.issuer.did() === accountDID | ||
) | ||
assert.ok( | ||
attest, | ||
'should claim ucan/attest delegation with proof.iss=accountDID' | ||
) | ||
assert.deepEqual(attest.issuer.did(), service.did()) | ||
assert.deepEqual(attest.audience.did(), issuer.did()) | ||
assert.deepEqual(attest.capabilities[0].can, 'ucan/attest') | ||
assert.deepEqual(attest.capabilities[0].with, service.did()) | ||
|
||
// ucan/attest nb.proof can be decoded into a delegation | ||
const claimedNb = attest.capabilities[0].nb | ||
assert.ok( | ||
claimedNb && typeof claimedNb === 'object' && 'proof' in claimedNb, | ||
'should have nb.proof' | ||
) | ||
const expectAccountToKey = proofDelegation(attest.proofs[0]) | ||
assert.ok(expectAccountToKey, 'expect proofs to contain delegation') | ||
assert.deepEqual(expectAccountToKey.issuer.did(), accountDID) | ||
assert.deepEqual(expectAccountToKey.audience.did(), issuer.did()) | ||
assert.deepEqual(expectAccountToKey.capabilities, [ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. remove this whole delegation if it's not needed for this test There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done in 685af65 |
||
{ can: '*', with: 'ucan:*' }, | ||
]) | ||
|
||
const attestIssService = claimedDelegations.find( | ||
(d) => proofDelegation(d.proofs[0])?.issuer.did() === service.did() | ||
) | ||
assert.ok( | ||
attestIssService, | ||
'should claim ucan/attest with proof.iss=service' | ||
) | ||
const accountAuthorization = attestIssService.proofs[0] | ||
const account = issuer.withDID(accountDID) | ||
const claimAsAccount = Access.claim.invoke({ | ||
issuer: account, | ||
audience: service, | ||
with: account.did(), | ||
proofs: [accountAuthorization], | ||
}) | ||
const claimAsAccountResult = await claimAsAccount.execute(conn) | ||
warnOnErrorResult(claimAsAccountResult) | ||
assert.notDeepEqual( | ||
claimAsAccountResult.error, | ||
true, | ||
'claimAsAccountResult should not error' | ||
) | ||
assert.ok( | ||
'delegations' in claimAsAccountResult, | ||
'claimAsAccountResult should have delegations property' | ||
) | ||
const claimedAsAccountDelegations = Object.values( | ||
claimAsAccountResult.delegations | ||
) | ||
assert.deepEqual(claimedAsAccountDelegations.length, 0) | ||
}) | ||
|
||
it('should receive delegation in the ws', async function () { | ||
const { issuer, service, conn, mf } = ctx | ||
const accountDID = 'did:mailto:dag.house:email' | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would really help to add comment above explaining what this delegation intends to do. In the version that I think this implements this delegation is attestation from us that account has authorized agent key to be able to sign ucans where issuer is account did.