Skip to content
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!: implement new account-based multi-device flow #433

Merged
merged 59 commits into from
Mar 17, 2023
Merged
Show file tree
Hide file tree
Changes from 51 commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
ced0e0d
feat: wip implement access/authorize in agent
Feb 9, 2023
ddd3d0c
Merge branch 'main' into feat/implement-access-authorize-in-agent
travis Mar 8, 2023
6b1d820
fix: updates for recent ucanto changes
travis Mar 8, 2023
daace23
wip: break various auth steps out into separate functions
travis Mar 10, 2023
ba1ce34
Merge branch 'main' into feat/implement-access-authorize-in-agent
gobengo Mar 13, 2023
941bb4d
adjust access-client until lint passes
gobengo Mar 13, 2023
92f7caa
remove unnecessary '?' from agent data sessionProof check
gobengo Mar 13, 2023
75e243f
claimDelegations uses invokeAndExecute
gobengo Mar 13, 2023
4ceb10b
feat: add access-api + access-client agent test and improve types (#533)
gobengo Mar 14, 2023
7df2133
rename access-client-agent test
gobengo Mar 14, 2023
86d77a1
feat: get space creation working-ish
travis Mar 14, 2023
0471e89
feat: define `access/confirm` handler and use it in ucanto-test-utils…
gobengo Mar 14, 2023
098d488
fix tsc
gobengo Mar 14, 2023
78cb8bf
feat: fix canDelegateCapability et al
travis Mar 14, 2023
f4ad371
Revert "feat: fix canDelegateCapability et al"
travis Mar 14, 2023
95a1c4a
feat: test access-client-agent authorize (#535)
gobengo Mar 14, 2023
5c2bc71
feat: @web3-storage/capabilities depends on latest ucanto (#541)
gobengo Mar 15, 2023
2156f23
feat: use new ucanto allows api for `canDelegateCapability` and imple…
travis Mar 15, 2023
6e89c42
fix: upgrade to ucanto/transport@5.1.1 (#544)
gobengo Mar 15, 2023
dfe264d
feat: expose session principal as account
travis Mar 15, 2023
459aa6f
chore: 433 minimize public api (#545)
gobengo Mar 15, 2023
1c79d0b
Merge branch 'main' into feat/implement-access-authorize-in-agent
gobengo Mar 15, 2023
e5e9a43
fix: rm mention of 'sessionPrincipal' from agent and agent-data (#546)
gobengo Mar 15, 2023
0cc4e47
@web3-storage/access/agent exports createDidMailtoFromEmail
gobengo Mar 15, 2023
6d942f1
upgrade everything to multiformats 11.0.2
gobengo Mar 15, 2023
1ea805e
createIssuerSaysAccountCanAdminSpace has capabilities as param, and d…
gobengo Mar 15, 2023
50402a4
access-client agent can pass opts.capabilities
gobengo Mar 15, 2023
53c63b8
feat: restore registerSpace and only use newRegisterSpace if a provid…
travis Mar 15, 2023
c0672ca
test: test access/authorize, access/confirm, access/claim (#548)
gobengo Mar 16, 2023
9aa3a26
rm console.logs in test
gobengo Mar 16, 2023
36628f3
lint
gobengo Mar 16, 2023
a637d04
Merge branch 'main' into feat/implement-access-authorize-in-agent
gobengo Mar 16, 2023
08db79b
feat: Agent#requestAuthorization (#550)
gobengo Mar 16, 2023
7aa68d4
assert which delegations are claimed in access-client-agent.test
gobengo Mar 16, 2023
48903aa
chore: test registerSpace (#553)
gobengo Mar 16, 2023
3d70dd9
fix: update `proofs` function so that it adds all necessary session p…
travis Mar 16, 2023
14390af
chore: test access-client-agent same agent, multiple accounts, addPro…
gobengo Mar 16, 2023
ea385ba
chore: 433 test multidevice (#555)
gobengo Mar 16, 2023
e28f993
fix: authorize claims as account, not only this.issuer (#556)
gobengo Mar 16, 2023
50960f0
fix: 433 rm todo (#557)
gobengo Mar 16, 2023
a66a294
Merge branch 'main' into feat/implement-access-authorize-in-agent
gobengo Mar 16, 2023
6885743
feat: store spaces after claiming delegations (#558)
travis Mar 16, 2023
1e9e70f
fix: 433 avoid new agent method (#559)
gobengo Mar 17, 2023
0432309
start skipped test invoking authorize method
gobengo Mar 17, 2023
97669c5
test can poll access/claim to find out about confirmation
gobengo Mar 17, 2023
0b2e268
fix: Agent#authorize races websocket and polling access/claim (#560)
gobengo Mar 17, 2023
00d125c
be sure to request authorizzation to space/*
travis Mar 17, 2023
5d5f577
fix: access-client authorize and waitForDelegation use cases (#563)
gobengo Mar 17, 2023
8ebf0b9
remove unnecessary second claim in authorize
gobengo Mar 17, 2023
c0e1bb5
feat: get rid of Promise.race in authorize. (#565)
gobengo Mar 17, 2023
0d8dd30
remove unnecessary proofs
gobengo Mar 17, 2023
4e74674
fix: don't let space delegation proofs expire (#564)
travis Mar 17, 2023
3af8816
Update packages/access-client/src/agent-use-cases.js
gobengo Mar 17, 2023
2658b44
fix: add checkAudience back to addProof
travis Mar 17, 2023
9ca0df7
Revert "fix: add checkAudience back to addProof"
travis Mar 17, 2023
571713d
simpler change to proofs logic, thx @gozala
travis Mar 17, 2023
d1ea1b0
chore: 433 bengo review comments (#567)
gobengo Mar 17, 2023
70b663d
re-introduce checkAudience in agent addProof/validate
gobengo Mar 17, 2023
943458d
fix: only use spaces with key dids (#568)
travis Mar 17, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@
"docs:markdown": "pnpm run build && docusaurus generate-typedoc"
},
"devDependencies": {
"@docusaurus/core": "^2.2.0",
"@docusaurus/core": "^2.3.1",
"docusaurus-plugin-typedoc": "^0.18.0",
"lint-staged": "^13.1.0",
"lint-staged": "^13.2.0",
"prettier": "2.8.3",
"simple-git-hooks": "^2.8.1",
"typedoc-plugin-markdown": "^3.14.0",
"typescript": "4.9.5",
"wrangler": "^2.8.0"
"wrangler": "^2.12.3"
},
"simple-git-hooks": {
"pre-commit": "npx lint-staged"
Expand All @@ -44,7 +44,7 @@
},
"dependencies": {
"depcheck": "^1.4.3",
"typedoc": "^0.23.22",
"typedoc": "^0.23.26",
"typedoc-plugin-missing-exports": "^1.0.0"
},
"packageManager": "pnpm@7.24.3",
Expand Down
13 changes: 7 additions & 6 deletions packages/access-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,18 @@
"license": "(Apache-2.0 OR MIT)",
"dependencies": {
"@ipld/dag-ucan": "^3.2.0",
"@ucanto/core": "^5.1.0",
"@ucanto/interface": "^6.0.0",
"@ucanto/core": "^5.2.0",
"@ucanto/interface": "^6.2.0",
"@ucanto/principal": "^5.1.0",
"@ucanto/server": "^6.0.0",
"@ucanto/transport": "^5.1.0",
"@ucanto/validator": "^6.0.0",
"@ucanto/server": "^6.1.0",
"@ucanto/transport": "^5.1.1",
"@ucanto/validator": "^6.1.0",
"@web3-storage/access": "workspace:^",
"@web3-storage/capabilities": "workspace:^",
"@web3-storage/worker-utils": "0.4.3-dev",
"kysely": "^0.23.4",
"kysely-d1": "^0.1.0",
"multiformats": "^11.0.1",
"multiformats": "^11.0.2",
"p-retry": "^5.1.2",
"preact": "^10.11.3",
"preact-render-to-string": "^5.2.6",
Expand Down Expand Up @@ -94,6 +94,7 @@
"error",
{
"definedTypes": [
"AsyncIterable",
"AsyncIterableIterator",
"Awaited",
"D1Database",
Expand Down
8 changes: 5 additions & 3 deletions packages/access-api/src/routes/validate-email.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ async function authorize(req, env) {

if (confirmation.error) {
throw new Error(`unable to validate access session: ${confirmation}`, {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe something more descriptive here like:

"can not authorize this session, probably link in the email has expired ${confirmation}"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cause: confirmation.error,
cause: confirmation,
})
}

Expand All @@ -168,12 +168,12 @@ async function authorize(req, env) {
}
)
const confirmResult = await confirm(request, {
id: env.signer.verifier,
id: env.signer,
principal: Verifier,
})
if (confirmResult.error) {
throw new Error('error confirming', {
cause: confirmResult.error,
cause: confirmResult,
})
}
const { account, agent } = accessConfirm.parse(request)
Expand All @@ -193,6 +193,8 @@ async function authorize(req, env) {
)
)
} catch (error) {
// eslint-disable-next-line no-console
console.warn('error in validate-email', error)
const err = /** @type {Error} */ (error)
env.log.error(err)
return new HtmlResponse(
Expand Down
6 changes: 3 additions & 3 deletions packages/access-api/src/service/access-authorize.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import * as Server from '@ucanto/server'
import * as Access from '@web3-storage/capabilities/access'
import * as Mailto from '../utils/did-mailto.js'
import * as DID from '@ipld/dag-ucan/did'
import { delegationToString } from '@web3-storage/access/encoding'

/**
Expand All @@ -25,7 +24,7 @@ export function accessAuthorizeProvider(ctx) {
const confirmation = await Access.confirm
.invoke({
issuer: ctx.signer,
audience: DID.parse(capability.nb.iss),
audience: ctx.signer,
// Because with is set to our DID no other actor will be able to issue
// this delegation without our private key.
with: ctx.signer.did(),
Expand All @@ -35,8 +34,9 @@ export function accessAuthorizeProvider(ctx) {
nb: {
// we copy request details and set the `aud` field to the agent DID
// that requested the authorization.
...capability.nb,
iss: capability.nb.iss,
aud: capability.with,
att: capability.nb.att,
},
})
.delegate()
Expand Down
72 changes: 51 additions & 21 deletions packages/access-api/src/service/access-confirm.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import * as Ucanto from '@ucanto/interface'
import * as ucanto from '@ucanto/core'
import { Verifier, Absentee } from '@ucanto/principal'
import { Absentee } from '@ucanto/principal'
import { collect } from 'streaming-iterables'
import * as Access from '@web3-storage/capabilities/access'
import { delegationsToString } from '@web3-storage/access/encoding'
import * as delegationsResponse from '../utils/delegations-response.js'
import * as validator from '@ucanto/validator'

/**
* @typedef {import('@web3-storage/capabilities/types').AccessConfirmSuccess} AccessConfirmSuccess
Expand All @@ -18,7 +19,9 @@ export function parse(invocation) {
const capability = invocation.capabilities[0]
// Create a absentee signer for the account that authorized the delegation
const account = Absentee.from({ id: capability.nb.iss })
const agent = Verifier.parse(capability.nb.aud)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I liked the former version better. It's also more immune to the changes in ucanto if say one day we change principal interface for whatever reason. If you do want to do method validation you could still do it using DID.match... before passing to verifier.

Also I'm not sure we do need to care if agent uses did:key or not. Why do we care what did they use if they are able to proof ownership.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was to appease the fact that I wanted to make opts.agent on createSessionProofs of type DID<'key'> since that's all I ever expect it to be. I relaxed that to DID and so changed this back to what it was too.

const agent = {
did: () => validator.DID.match({ method: 'key' }).from(capability.nb.aud),
}
return {
account,
agent,
Expand Down Expand Up @@ -50,31 +53,21 @@ export async function handleAccessConfirm(invocation, ctx) {
}))
)

// create an delegation on behalf of the account with an absent signature.
const delegation = await ucanto.delegate({
issuer: account,
audience: agent,
const [delegation, attestation] = await createSessionProofs(
ctx.signer,
account,
agent,
capabilities,
expiration: Infinity,
// We include all the delegations to the account so that the agent will
// have delegation chains to all the delegated resources.
// We should actually filter out only delegations that support delegated
// capabilities, but for now we just include all of them since we only
// implement sudo access anyway.
proofs: await collect(
ctx.models.delegations.find({
audience: account.did(),
})
),
})

const attestation = await Access.session.delegate({
issuer: ctx.signer,
audience: agent,
with: ctx.signer.did(),
nb: { proof: delegation.cid },
expiration: Infinity,
})
ctx.models.delegations.find({
audience: account.did(),
}),
Infinity
)

// Store the delegations so that they can be pulled with access/claim
// The fact that we're storing proofs chains that we pulled from the
Expand All @@ -89,3 +82,40 @@ export async function handleAccessConfirm(invocation, ctx) {
delegations: delegationsResponse.encode([delegation, attestation]),
}
}

/**
* @param {Ucanto.Signer} service
* @param {Ucanto.Principal<Ucanto.DID<'mailto'>>} account
* @param {Ucanto.Principal<Ucanto.DID<'key'>>} agent
* @param {Ucanto.Capabilities} capabilities
* @param {AsyncIterable<Ucanto.Delegation>} delegationProofs
* @param {number} expiration
* @returns {Promise<[delegation: Ucanto.Delegation, attestation: Ucanto.Delegation]>}
*/
export async function createSessionProofs(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I think named parameters are better especially when your function takes more than 3 params.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

service,
account,
agent,
capabilities,
delegationProofs,
expiration
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally I think we want attestations to be non expiring because otherwise all the delegations issued by account will be time bound as well. I would suggest removing this parameter and perhaps adding a code comment in the delegations below explaining rational for non expiring delegation.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

) {
// create an delegation on behalf of the account with an absent signature.
const delegation = await ucanto.delegate({
issuer: Absentee.from({ id: account.did() }),
audience: agent,
capabilities,
expiration,
proofs: [...(await collect(delegationProofs))],
})

const attestation = await Access.session.delegate({
issuer: service,
audience: agent,
with: service.did(),
nb: { proof: delegation.cid },
expiration,
})

return [delegation, attestation]
}
8 changes: 8 additions & 0 deletions packages/access-api/src/types/ucanto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import * as Ucanto from '@ucanto/interface'

export type ServiceInvoke<
Service extends Record<string, any>,
InvocationCapabilities extends Ucanto.Capability = Ucanto.Capability
> = <Capability extends InvocationCapabilities>(
invocation: Ucanto.ServiceInvocation<Capability>
) => Promise<Ucanto.InferServiceInvocationReturn<Capability, Service>>
5 changes: 3 additions & 2 deletions packages/access-api/test/access-authorize.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ describe('access/authorize', function () {
)
const delegation = stringToDelegation(encoded)
t.deepEqual(delegation.issuer.did(), service.did())
t.deepEqual(delegation.audience.did(), accountDID)
t.deepEqual(delegation.audience.did(), service.did())
t.deepEqual(delegation.capabilities, [
{
with: conn.id.did(),
Expand Down Expand Up @@ -122,8 +122,9 @@ describe('access/authorize', function () {

const url = new URL(email.url)
const rsp = await mf.dispatchFetch(url, { method: 'POST' })
const html = await rsp.text()
assert.deepEqual(rsp.status, 200)

const html = await rsp.text()
assert(html.includes('Email Validated'))
assert(html.includes(toEmail(accountDID)))
assert(html.includes(issuer.did()))
Expand Down
Loading