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 13 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
104 changes: 33 additions & 71 deletions packages/access-api/src/routes/validate-email.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {
stringToDelegation,
delegationsToString,
stringToDelegation,
} from '@web3-storage/access/encoding'
import * as Access from '@web3-storage/capabilities/access'
import QRCode from 'qrcode'
Expand All @@ -11,10 +11,12 @@ import {
ValidateEmailError,
PendingValidateEmail,
} from '../utils/html.js'
import * as ucanto from '@ucanto/core'
import * as validator from '@ucanto/validator'
import { Verifier, Absentee } from '@ucanto/principal'
import { collect } from 'streaming-iterables'
import { Verifier } from '@ucanto/principal'
import * as delegationsResponse from '../utils/delegations-response.js'
import * as accessConfirm from '../service/access-confirm.js'
import { provide } from '@ucanto/server'
import * as Ucanto from '@ucanto/interface'

/**
* @param {import('@web3-storage/worker-utils/router').ParsedRequest} req
Expand Down Expand Up @@ -149,62 +151,35 @@ async function authorize(req, env) {
})

if (confirmation.error) {
throw new Error(`unable to validate access session: ${confirmation}`)
}
if (confirmation.capability.with !== env.signer.did()) {
throw new Error(`Not a valid access/confirm delegation`)
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,
})
}

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

// 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} */
(
confirmation.capability.nb.att.map(({ can }) => ({
can,
with: /** @type {ucanto.UCAN.Resource} */ ('ucan:*'),
}))
)

// create an delegation on behalf of the account with an absent signature.
const delegation = await ucanto.delegate({
issuer: account,
audience: 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(
env.models.delegations.find({
audience: account.did(),
})
),
})

const attestation = await Access.session.delegate({
issuer: env.signer,
audience: agent,
with: env.signer.did(),
nb: { proof: delegation.cid },
expiration: Infinity,
const confirm = provide(
Access.confirm,
async ({ capability, invocation }) => {
return accessConfirm.handleAccessConfirm(
/** @type {Ucanto.Invocation<import('@web3-storage/access/types').AccessConfirm>} */ (
invocation
),
env
)
}
)
const confirmResult = await confirm(request, {
gobengo marked this conversation as resolved.
Show resolved Hide resolved
id: env.signer.verifier,
principal: Verifier,
})

// 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
// database is not great, but it's a tradeoff we're making for now.
await env.models.delegations.putMany(delegation, attestation)

const authorization = delegationsToString([delegation, attestation])
// Send delegations to the client through a websocket
await env.models.validations.putSession(authorization, agent.did())
if (confirmResult.error) {
throw new Error('error confirming', {
cause: confirmResult.error,
})
}
const { account, agent } = accessConfirm.parse(request)
const confirmDelegations = [
...delegationsResponse.decode(confirmResult.delegations),
]

// We render HTML page explaining to the user what has happened and providing
// a QR code in the details if they want to drill down.
Expand All @@ -213,20 +188,7 @@ async function authorize(req, env) {
<ValidateEmail
email={toEmail(account.did())}
audience={agent.did()}
ucan={authorization}
qrcode={await QRCode.toString(authorization, {
type: 'svg',
errorCorrectionLevel: 'M',
margin: 10,
}).catch((error) => {
if (/too big to be stored in a qr/i.test(error.message)) {
env.log.error(error)
// It's not important to have the QR code
// eslint-disable-next-line unicorn/no-useless-undefined
return undefined
}
throw error
})}
ucan={delegationsToString(confirmDelegations)}
/>
)
)
Expand Down
91 changes: 91 additions & 0 deletions packages/access-api/src/service/access-confirm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import * as Ucanto from '@ucanto/interface'
import * as ucanto from '@ucanto/core'
import { Verifier, 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'

/**
* @typedef {import('@web3-storage/capabilities/types').AccessConfirmSuccess} AccessConfirmSuccess
* @typedef {import('@web3-storage/capabilities/types').AccessConfirmFailure} AccessConfirmFailure
*/

/**
* @param {Ucanto.Invocation<import('@web3-storage/capabilities/src/types').AccessConfirm>} invocation
*/
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)
return {
account,
agent,
}
}

/**
* @param {Ucanto.Invocation<import('@web3-storage/capabilities/src/types').AccessConfirm>} invocation
* @param {import('../bindings').RouteContext} ctx
* @returns {Promise<Ucanto.Result<AccessConfirmSuccess, AccessConfirmFailure>>}
*/
export async function handleAccessConfirm(invocation, ctx) {
const capability = invocation.capabilities[0]
if (capability.with !== ctx.signer.did()) {
throw new Error(`Not a valid access/confirm delegation`)
}

const { account, agent } = parse(invocation)

// 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} */
(
capability.nb.att.map(({ can }) => ({
can,
with: /** @type {ucanto.UCAN.Resource} */ ('ucan:*'),
}))
)

// create an delegation on behalf of the account with an absent signature.
const delegation = await ucanto.delegate({
issuer: account,
audience: 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,
})

// 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
// database is not great, but it's a tradeoff we're making for now.
await ctx.models.delegations.putMany(delegation, attestation)

const authorization = delegationsToString([delegation, attestation])
// Save delegations for the validation process
await ctx.models.validations.putSession(authorization, agent.did())

return {
delegations: delegationsResponse.encode([delegation, attestation]),
}
}
17 changes: 17 additions & 0 deletions packages/access-api/src/service/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as Server from '@ucanto/server'
import * as validator from '@ucanto/validator'
import { Failure } from '@ucanto/server'
import * as Space from '@web3-storage/capabilities/space'
import * as Access from '@web3-storage/capabilities/access'
import { top } from '@web3-storage/capabilities/top'
import {
delegationToString,
Expand All @@ -17,6 +18,7 @@ import { accessDelegateProvider } from './access-delegate.js'
import { accessClaimProvider } from './access-claim.js'
import { providerAddProvider } from './provider-add.js'
import { Spaces } from '../models/spaces.js'
import { handleAccessConfirm } from './access-confirm.js'

/**
* @param {import('../bindings').RouteContext} ctx
Expand Down Expand Up @@ -45,6 +47,21 @@ export function service(ctx) {
config: ctx.config,
})(...args)
},
confirm: Server.provide(
Access.confirm,
async ({ capability, invocation }) => {
// only needed in tests
if (ctx.config.ENV !== 'test') {
throw new Error(`access/confirm is disabled`)
}
return handleAccessConfirm(
/** @type {Ucanto.Invocation<import('@web3-storage/access/types').AccessConfirm>} */ (
invocation
),
ctx
)
}
),
delegate: (...args) => {
// disable until hardened in test/staging
if (ctx.config.ENV === 'production') {
Expand Down
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>>
19 changes: 18 additions & 1 deletion packages/access-api/test/access-authorize.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
} from './helpers/ucanto-test-utils.js'
import { ed25519, Absentee } from '@ucanto/principal'
import { delegate } from '@ucanto/core'
import { Space } from '@web3-storage/capabilities'

/** @type {typeof assert} */
const t = assert
Expand Down Expand Up @@ -312,8 +313,12 @@ describe('access/authorize', function () {
const space = await ed25519.generate()
const w3 = ctx.service

await registerSpaces([space], ctx)
const account = Absentee.from({ id: 'did:mailto:dag.house:test' })
await registerSpaces([space], {
...ctx,
agent: ctx.issuer,
account,
})

// delegate all space capabilities to the account
const delegation = await delegate({
Expand Down Expand Up @@ -342,6 +347,7 @@ describe('access/authorize', function () {
})
.execute(ctx.conn)

warnOnErrorResult(delegateResult)
assert.equal(delegateResult.error, undefined, 'delegation succeeded')

// Now generate an agent and try to authorize with the account
Expand Down Expand Up @@ -424,5 +430,16 @@ describe('access/authorize', function () {
delegation.cid,
'delegation to an account is included'
)

// use these delegations to do something on the space
const info = await Space.info
.invoke({
issuer: agent,
audience: w3,
with: space.did(),
proofs: [authorization, attestation],
})
.execute(ctx.conn)
assert.notDeepEqual(info.error, true, 'space/info did not error')
})
})
1 change: 1 addition & 0 deletions packages/access-api/test/access-claim.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ for (const handlerVariant of /** @type {const} */ ([
spaceWithStorageProvider,
...createTesterFromContext(() => context(), {
registerSpaces: [spaceWithStorageProvider],
account: { did: () => /** @type {const} */ ('did:mailto:foo') },
}),
}
})(),
Expand Down
35 changes: 35 additions & 0 deletions packages/access-api/test/access-client-agent.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { context } from './helpers/context.js'
import { createTesterFromContext } from './helpers/ucanto-test-utils.js'
import * as principal from '@ucanto/principal'
import { Agent as AccessAgent } from '@web3-storage/access/agent'
import * as assert from 'assert'

for (const accessApiVariant of /** @type {const} */ ([
{
name: 'using access-api in miniflare',
...(() => {
const account = {
did: () => /** @type {const} */ ('did:mailto:dag.house:foo'),
}
const spaceWithStorageProvider = principal.ed25519.generate()
return {
spaceWithStorageProvider,
...createTesterFromContext(context, {
account,
registerSpaces: [spaceWithStorageProvider],
}),
}
})(),
},
])) {
describe(`access-client-agent ${accessApiVariant.name}`, () => {
it('can createSpace', async () => {
const accessAgent = await AccessAgent.create(undefined, {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Do not yet know AccessAgent.create but undefined parameter looks funny here probably should be an optional option in the second param.

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree but didn't want to change API here

connection: await accessApiVariant.connection,
})
const space = await accessAgent.createSpace('test-add')
const delegations = accessAgent.proofs()
assert.equal(space.proof.cid, delegations[0].cid)
})
})
}
Loading