Support authentication & authorization
HerbCaudill opened this issue · comments
What follows is a sketch of a proposed approach for supporting authentication & authorization within automerge-repo.
Why
Automerge Repo currently offers no way to authenticate a peer, and very little in the way of access control.
Our current security model is the "Rumplestiltskin rule": If you know a document's ID, you can read that document, and everyone else who knows that ID will accept your changes.
That model is good enough for a surprising number of situations — the ID serves as an unguessable secret "password" for the document — but it has limitations. Without a way to establish a peer's identity, we can't revoke access for an individual peer — say if someone leaves a team, or if a device is lost. And we can't distinguish between read and write permissions, or limit access to specific documents.
An application might implement authentication and authorization in any number of ways, so this should be pluggable — like the existing network and storage adapters.
So initializing a repo might look something like this:
import { SuperCoolAuthProvider } from 'supercool-auth-library'
const authOptions = {
// ...options specific to this type of authentication
}
const auth = new SuperCoolAuthProvider(options)
const repo = new Repo({ network, storage, auth })
API
An auth provider inherits from this abstract class:
export abstract class AuthProvider extends EventEmitter<AuthProviderEvents> {
/**
* Can this peer prove their identity? The provider implementation will
* use the web socket to communicate with the peer.
*/
abstract async authenticate(peerId: PeerId, socket?: WebSocket): Promise<true | Error> {}
// The following methods may be overriden by the provider and would replace
// the existing `sharePolicy` that we pass to a repo.
/** Should we tell this peer about the existence of this document? */
async okToAdvertise(peerId: PeerId, documentId: DocumentId): Promise<boolean> {
return false
}
/** Should we provide this document (and changes to it) to this peer when asked for it by ID? */
async okToSend(peerId: PeerId, documentId: DocumentId): Promise<boolean> {
return false
}
/** Should we accept changes to this document from this peer? */
async okToReceive(peerId: PeerId, documentId: DocumentId): Promise<boolean> {
return false
}
}
Authentication
The auth provider's authenticate
method is invoked when the network adapter emits a
peer-candidate
event.
// NetworkSubsystem.ts
networkAdapter.on('peer-candidate', async ({ peerId, channelId, socket }) => {
const { authenticationResult } = await authProvider.authenticate({ peerId, socket })
if (!authenticationResult.isValid) {
const { error } = authenticationResult
this.emit('peer-authentication-failed', { peerId, channelId, error })
} else {
// ...
this.emit('peer', { peerId, channelId })
}
})
Authorization
Advertising
TODO
Sending changes
TODO
Receiving changes
TODO
More substantive comments later, but for now: great stuff, and bravo for the excellent term "Rumpelstiltskin rule"!
haha can't take credit for that, it's @pvh 's term
See also localfirst/auth provider for Automerge Repo (pseudo-documentation for a hypothetical provider built around @localfirst/auth).