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

App owned: account linking #335

Merged
merged 77 commits into from
Mar 17, 2022
Merged

App owned: account linking #335

merged 77 commits into from
Mar 17, 2022

Conversation

walkah
Copy link
Contributor

@walkah walkah commented Dec 10, 2021

@bgins here 👋. I took over this feature and submit the PR described below.


Summary

This PR brings the account linking functionality currently in the Fission Auth Lobby into webnative.

This PR implements the following features

  • Adds account linking
  • Adds an event emitter implementation
  • Adds console.warn to debug module
  • Adds a Result type to our collection of common types

This PR aims to make account linking available to any app that uses webnative. Before this PR, account linking was only available in the Fission Auth Lobby.

API surface

The API exposes account linking providers and requestors. A provider is an authenticated device with the capability to delegate account access. A requestor is an unauthenticated device that would like access.

Note: Internally, we refer to providers as producers and requestors as consumers. We rename these in the API with more intuitive names.

Follow-up note: We decided to use the names producer and consumer everywhere. See discussion below.

Providers are event emitters with a cancel function:

import { auth } from 'webnative'

const provider = await auth.createProvider({ username })

provider.on('challenge', ({ pin, confirmPin, rejectPin }) => {
  // Request pin confirmation, then call confirmPin or rejectPin
});

provider.on('link', ({ approved, username }) => {
  // Indicate to user whether linking was approved or not
});

provider.on("done", () => {
  // Clean up after account linking
});

// Cancel linking
provider.cancel();

A provider immediately listens for account linking requests when created and continues to listen until cancel is called. Providers can link multiple requestors without needing to be recreated.

Requestors are also event emitters with a cancel function:

import { auth } from 'webnative'

const requestor = await auth.createRequestor({ username })
  
requestor.on('challenge', ({ pin }) => {
  // Display pin
});

requestor.on('link', ({ approved, username }) => {
  // Indicate to user whether linking was approved or not
});

requestor.on("done", () => {
  // Clean up after account linking
});

// Cancel linking
requestor.cancel();

A requestor attempts to link on creation. Unlike providers, requestors are automatically terminated when they complete linking or receive a linking declined message.

Custom account semantics

By default, account linking sets a UCAN that delegates SUPER_USER access to the requestor. It expects that the provider is the root user (whose public key matches the DID registered with the server) or has been delegated SUPER_USER access by another device with sufficient capabilities.

Developers may want to use custom account semantics and they can by dependency injecting three functions:

https://github.com/fission-suite/webnative/blob/98ceb8f5264101481ba44e0bc85f27ed29d7823c/src/auth/implementation/types.ts#L12-L14

checkCapability checks that a provider can delegate an account for a username when createProvider is called. A provider calls delegateAccount to delegate credentials. A consumer calls linkDevice on receiving a delegation to process and store credentials. These last two functions are tightly coupled -- linkDevice receives whatever delegateAccount returns.

The default implementation and auth lobby implementation contain examples of these functions.

Account linking always sets the username in storage at webnative.auth_username. We rely on a username at this location in other parts of webnative, so we set this internally.

Custom channels

By default, account linking uses websockets to send account linking messages between the provider and requestor. Developers may want to use another transport and they can by dependency injecting createChannel:

https://github.com/fission-suite/webnative/blob/98ceb8f5264101481ba44e0bc85f27ed29d7823c/src/auth/implementation/types.ts#L11

A channel is responsible for sending messages and calling handleMessage when messages are received. A channel must be named for the DID associated with the username.

https://github.com/fission-suite/webnative/blob/f75f4d22cca84c4437cdf60957971aa37a0f93b6/src/auth/channel.ts#L7-L17

Test plan (required)

Unit tests and prop checks for the provider and requestor are in producer.test.ts and consumer.test.ts. Individual linking functions are tested, but the interaction between provider and requestor is not.

Integration tests to check the interaction between provider and requestor are in linking.node.test.ts. This test suite uses an event emitter for its account linking channel. It tracks when providers delegate and requestors link in separate objects. If account linking was successful, the objects should be the same at the end of the test.

Closing issues

Closes #305

After Merge

  • Does this change invalidate any docs or tutorials? No, but we will need new docs.
  • Does this change require a release to be made? Yes, we will do an alpha release first

Task list

There were several key pieces:

  • Add mechanisms for Opening / Closing and publishing data across channels
  • Make that system dependency injectable
  • Add a "state" machine for tracking progress in both consumer & producer
  • Implement each step as separate functions
  • Finalize API for interaction
  • Ensure that each step follows the current whitepaper
  • Handle errors / cancellations effectively

src/auth/linking.ts Outdated Show resolved Hide resolved
src/auth/linking.ts Outdated Show resolved Hide resolved
src/auth/linking.ts Outdated Show resolved Hide resolved
src/auth/linking.ts Outdated Show resolved Hide resolved
src/auth/linking.ts Outdated Show resolved Hide resolved
src/auth/linking.ts Outdated Show resolved Hide resolved
src/auth/linking.ts Outdated Show resolved Hide resolved
@matheus23
Copy link
Contributor

Writing down some thoughts that Brooke and I had late last year, not because I think that should be implemented in this PR, but because I think it could inform the design for the protocol state:

When there's more than 2 participants in a websocket channel for device linking, we should track the protocol state for all of them (maybe until a maximum of 10 or 100 or 1000) and simultaneously execute the AWAKE protocol with all of them.
When the step for "comparing the numbers on both devices" comes, instead of showing the user numbers, we show the user a number input and they have to enter the number that they see on the other device themselves.
This both serves as a check "are you connecting with the device you think you're connecting?" as well as a way to identify the device you want to connect with, if there's multiple devices that successfully went through every previous step of the AWAKE protocol.

(I thought I had written up an issue for that, but I can't find it :/ )

Again, just to inform the design code-wise. I personally find it helpful seeing ways that code might evolve in the future.

@bgins
Copy link
Member

bgins commented Jan 11, 2022

When there's more than 2 participants in a websocket channel for device linking, we should track the protocol state for all of them (maybe until a maximum of 10 or 100 or 1000) and simultaneously execute the AWAKE protocol with all of them. When the step for "comparing the numbers on both devices" comes, instead of showing the user numbers, we show the user a number input and they have to enter the number that they see on the other device themselves. This both serves as a check "are you connecting with the device you think you're connecting?" as well as a way to identify the device you want to connect with, if there's multiple devices that successfully went through every previous step of the AWAKE protocol.

Thanks for bringing it up! Good to think about this now while we are working on this for the app owned version.

Do we think we should replace this original pin interaction with this new flow? Or allow for both?

I see that @expede opened an issue to discuss AWAKE improvements. I have some questions about how the new flow would work that I'll ask over there.

@matheus23
Copy link
Contributor

Do we think we should replace this original pin interaction with this new flow? Or allow for both?

I (personally) think we should replace it at some point. But not this PR anyway, so that's a discussion for another day 😄

walkah and others added 23 commits February 28, 2022 09:40
Add onCompletion to producer. The producer implicitly knows it is done
after the challenge, but in the future the producer may stay active for
multiple account link requests for consumers. The producer onCompletion
will be useful then as a signal that all link requests have been
handled.
These are the paired functions that developers will implement.
delegateAccount returns a record and linkDevice takes a record. It will
be up to developers to make sure they the records match.
Returning the username is mostly a convenience for developers, but
moving it to the link state might prove useful for multiple accounts at
some point in the future.

Also, a small change in the consuer to reset the username on reseting
the link state. It's not clear yet whether this should also happen in
the producer, and in general whether the producer should keep listening
after a consumer has been linked.
Instead of expecting callbacks to handle events, the event emitter lets
a developer add event listeners. This interface is more flexible for
developers and for our internal implementation -- developers can
subscribe to events multiple times and we can add events in the future
if we want to.
At the moment, this is incomplete and we need to handle timeouts and
think about cancellation semantics. Various edge cases should also be
considered.
The consumer should be informed when linking was declined. A message was
added when a proder declines, and an approved field was added to the
link event of both the consumer and producer.
We want to avoid module global state because it may modules may be
re-instantiated in some cases, which can produce unpleasant and hard to
debug situations. This refactor also removes channel and user
interaction side effects from many of the account linking functions,
which should make them easier to test.
Lastly, the dependency injected channel functions are reduced to a
single createChannel function. It generates send, receive, and close
functions. At the moment, the receive function is unused and maybe won't
be needed.
An event is only created when we have at least one listener. This change
prevents dispatch for events when no listeners have been added.
The functions are easier to test if they do not depend on the entire
linking state. In addition, this will make it easier to re-use them for
other AWAKE use cases.
We only want these functions to be called once and only one or the other
should be called.
Copy link
Contributor

@matheus23 matheus23 left a comment

Choose a reason for hiding this comment

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

Super happy with this & the tests! 🎉 Great work.

src/auth/linking/producer.ts Show resolved Hide resolved
src/auth/channel.ts Outdated Show resolved Hide resolved
src/auth/channel.ts Outdated Show resolved Hide resolved
src/auth/index.ts Outdated Show resolved Hide resolved
src/auth/linking/consumer.test.ts Show resolved Hide resolved
src/common/event-emitter.ts Outdated Show resolved Hide resolved
tests/auth/linking.node.test.ts Show resolved Hide resolved
@matheus23
Copy link
Contributor

CI note: we should either increase the timeout or decrease the numRuns for one of the symlink tests that keeps timing out occasionally.

Copy link
Contributor

@icidasset icidasset left a comment

Choose a reason for hiding this comment

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

Couple of minor notes/questions.
Awesome work!

src/auth/channel.ts Show resolved Hide resolved
src/auth/linking/producer.ts Outdated Show resolved Hide resolved
src/auth/lobby.ts Show resolved Hide resolved
@bgins
Copy link
Member

bgins commented Mar 3, 2022

CI note: we should either increase the timeout or decrease the numRuns for one of the symlink tests that keeps timing out occasionally.

Sounds good. I decreased the numRuns from 50 down to 25 and that gets them running through in time.

Destructuring the implemenation and exporting its functions does not
work because it only happens when the module evaluates. Instead, we can
export wrapped versions of the implementation functions.
Rename provider to producer and requestor to consumer. Export them at
the top level under account instead of auth.
@matheus23
Copy link
Contributor

matheus23 commented Mar 16, 2022

This is looking good!
Last thing I'd love to see is backwards compatibility with the auth lobby. My impression was that this PR shouldn't change the protocol, so it seems weird to me that the auth lobby wouldn't link against the demo app (or the other way around?) 🤔 My feeling is this might expose an issue. We should check what it is just to be sure.

Copy link
Contributor

@matheus23 matheus23 left a comment

Choose a reason for hiding this comment

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

We can go ahead and merge this. We're not 100% sure yet whether we'll need/want backwards-compat on the AWAKE protocol, so we wouldn't want to cut a release with this PR immediately.
However, we can decide that still after merging, so let's go ahead! 🎉

yes

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Move account linking to webnative
6 participants