Skip to content

Commit

Permalink
feat: Message Authentication (#163)
Browse files Browse the repository at this point in the history
* Hacky, working, first pass

* wip

* refactor to use y-protocols approach with binary messages

* docs

* sp

* undo unused changes

* More docs

* Hook up custom callbacks

* tests: test the new onAuthenticate hook and the onAuthenticated and onAuthenticationFailed callbacks

* authentication -> token

* consts -> enum

* cleanup

* fix: Add missing object to authentication payload

Co-authored-by: Hans Pagel <[email protected]>
  • Loading branch information
tommoor and hanspagel authored Aug 19, 2021
1 parent a43411e commit a1e68d5
Show file tree
Hide file tree
Showing 27 changed files with 530 additions and 86 deletions.
6 changes: 6 additions & 0 deletions demos/backend/src/minimal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 7,12 @@ const server = Server.configure({
new Logger(),
],

async onAuthenticate(data) {
if (data.token !== 'my-access-token') {
throw new Error('Incorrect access token')
}
},

// Test error handling
// async onConnect(data) {
// throw new Error('CRASH')
Expand Down
7 changes: 7 additions & 0 deletions demos/frontend/src/pages/Index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 75,16 @@ export default {
url: 'ws://127.0.0.1:1234',
name: 'hocuspocus-demo',
document: this.ydoc,
token: 'my-access-token',
onConnect: () => {
console.log('connected')
},
onAuthenticated: () => {
console.log('authenticated')
},
onAuthenticationFailed: () => {
console.log('authentication failed')
},
onMessage: ({ event, message }) => {
console.log(`[message] ◀️ ${message.name}`, event)
},
Expand Down
2 changes: 1 addition & 1 deletion docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 46,4 @@
"sass-loader": "^10.1.1",
"style-resources-loader": "^1.4.1"
}
}
}
2 changes: 1 addition & 1 deletion docs/src/docPages/api/extensions/webhook.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 80,7 @@ When a new user connects to the server, the onConnect webhook will be triggered
}
```

You can use this to authorize your users. By responding with a 403 status code the user is not authorized and the connection wil be terminated. You can respond with a JSON payload that will be set as context throughout the rest of the application. For example:
You can use this to authorize your users. By responding with a 403 status code the user is not authorized and the connection will be terminated. You can respond with a JSON payload that will be set as context throughout the rest of the application. For example:

```typescript
// authorize the user by the request parameters or headers
Expand Down
6 changes: 3 additions & 3 deletions docs/src/docPages/api/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 12,7 @@ If a user isn’t allowed to connect: Just send `reject()` in the `onConnect()`
| Hook | Description | Link |
| ------------------ | ----------------------------------------- | ------------------------------------ |
| `onConnect` | When a connection is established | [Read more](/api/on-connect) |
| `onAuthenticate` | When authentication is passed | [Read more](/api/on-authenticate) |
| `onCreateDocument` | When a new document is created | [Read more](/api/on-create-document) |
| `onChange` | When a document has changed | [Read more](/api/on-change) |
| `onDisconnect` | When a connection was closed | [Read more](/api/on-disconnect) |
Expand All @@ -27,15 28,14 @@ If a user isn’t allowed to connect: Just send `reject()` in the `onConnect()`
import { Server } from '@hocuspocus/server'

const server = Server.configure({
async onConnect({ documentName, requestParameters }) {
async onAuthenticate({ documentName, token }) {

// Could be an API call, DB query or whatever …
return axios.get('/user', {
headers: {
Authorization: `Bearer ${requestParameters.get('token')}}`
Authorization: `Bearer ${token}`
}
})

},
})

Expand Down
64 changes: 64 additions & 0 deletions docs/src/docPages/api/hooks/on-authenticate.md
Original file line number Diff line number Diff line change
@@ -0,0 1,64 @@
# onAuthenticate

## toc

## Introduction

The `onAuthenticate` hook will be called when the server receives an authentication request from the client provider. It should return a Promise. Throwing an exception or rejecting the Promise will terminate the connection.

## Hook payload

The `data` passed to the `onAuthenticate` hook has the following attributes:

```typescript
import { IncomingHttpHeaders } from 'http'
import { URLSearchParams } from 'url'
import { Doc } from 'yjs'

const data = {
clientsCount: number,
document: Doc,
documentName: string,
requestHeaders: IncomingHttpHeaders,
requestParameters: URLSearchParams,
socketId: string,
token: string,
connection: {
readOnly: boolean,
},
}
```

## Example

```typescript
import { Server } from '@hocuspocus/server'

const hocuspocus = Server.configure({
async onAuthenticate(data) {
const { token } = data

// Example test if a user is authenticated using a
// request parameter
if (token !== 'super-secret-token') {
throw new Error('Not authorized!')
}

// Example to set a document to read only for the current user
// thus changes will not be accepted and synced to other clients
if (someCondition === true) {
data.connection.readOnly = true
}

// You can set contextual data to use it in other hooks
return {
user: {
id: 1234,
name: 'John',
},
}
},
})

hocuspocus.listen()
```
27 changes: 4 additions & 23 deletions docs/src/docPages/api/hooks/on-connect.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,31 33,12 @@ const data = {
```typescript
import { Server } from '@hocuspocus/server'

const server = Server.configure({
const hocuspocus = Server.configure({
async onConnect(data) {
const { requestParameters } = data

// Example test if a user is authenticated using a
// request parameter
if (requestParameters.get('access_token') !== 'super-secret-token') {
throw new Error('Not authorized!')
}

// Example to set a document to read only for the current user
// thus changes will not be accepted and synced to other clients
if(someCondition === true) {
data.connection.readOnly = true
}

// You can set contextual data to use it in other hooks
return {
user: {
id: 1234,
name: 'John',
},
}
// Output some information
process.stdout.write(`New websocket connection`)
},
})

server.listen()
hocuspocus.listen()
```
26 changes: 18 additions & 8 deletions docs/src/docPages/guide/authentication-and-authorization.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 4,23 @@

## Introduction

With the `onConnect` hook you can check if a client is authenticated and authorized to view the current document. In a real world application this would probably be a request to an API, a database query or something else.
With the `onAuthenticate` hook you can check if a client is authenticated and authorized to view the current document. In a real world application this would probably be a request to an API, a database query or something else.

## Example

When throwing an error (or rejecting the returned Promise), the connection to the client will be terminated. If the client is authorized and authenticated you can also return contextual data which will be accessible in other hooks. But you don't need to.
When throwing an error (or rejecting the returned Promise), the connection to the client will be terminated. If the client is authorized and authenticated you can also return contextual data such as a user id which will be accessible in other hooks. But you dont need to.

For more information on the hook and it's payload checkout it's [API section](/api/on-connect).
For more information on the hook and it's payload checkout it's [API section](/api/on-authenticate).

```typescript
import { Server } from '@hocuspocus/server'

const server = Server.configure({
async onConnect(data) {
const { requestParameters } = data
async onAuthenticate(data) {
const { token } = data

// Example test if a user is authenticated using a
// request parameter
if (requestParameters.get('access_token') !== 'super-secret-token') {
// Example test if a user is authenticated with a token passed from the client
if (token !== 'super-secret-token') {
throw new Error('Not authorized!')
}

Expand All @@ -37,3 36,14 @@ const server = Server.configure({

server.listen()
```

On the client you would pass the "token" parameter as one of the Hocuspocus options, like so:

```typescript
new HocuspocusProvider({
url: 'ws://127.0.0.1:1234',
name: 'example-document',
document: ydoc,
token: 'super-secret-token',
})
```
6 changes: 3 additions & 3 deletions docs/src/docPages/guide/documents.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 84,7 @@ const hocuspocus = Server.configure({

// Maybe you want to store the user who changed the document?
// Guess what, you have access to your custom context from the
// onConnect hook here. See authorization & authentication for more
// onAuthenticate hook here. See authorization & authentication for more
// details
console.log(`Document ${data.documentName} changed by ${data.context.user.name}`)
}
Expand Down Expand Up @@ -211,7 211,7 @@ hocuspocus.listen()
## Read only mode

If you want to restrict the current user only to read the document and it's updates but not apply
updates him- or herself, you can use the `connection` property in the `onConnect` hooks payload:
updates him- or herself, you can use the `connection` property in the `onAuthenticate` hooks payload:

```typescript
import { Server } from '@hocuspocus/server'
Expand All @@ -221,7 221,7 @@ const usersWithWriteAccess = [
]

const hocuspocus = Server.configure({
async onConnect(data): Doc {
async onAuthenticate(data): Doc {

// Example code to check if the current user has write access by a
// request parameter. In a real world application you would probably
Expand Down
7 changes: 7 additions & 0 deletions docs/src/docPages/guide/extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 35,7 @@ import {
Extension,
onChangePayload,
onConnectPayload,
onAuthenticatePayload,
onCreateDocumentPayload,
onDisconnectPayload,
} from '@hocuspocus/server'
Expand All @@ -47,6 48,8 @@ export class MyHocuspocusExtension implements Extension {

async onConnect(data: onConnectPayload): Promise<void> {}

async onAuthenticate(data: onAuthenticatePayload): Promise<void> {}

async onDisconnect(data: onDisconnectPayload): Promise<void> {}

async onRequest(data: onRequestPayload): Promise<void> {}
Expand All @@ -73,6 76,7 @@ import {
Extension,
onChangePayload,
onConnectPayload,
onAuthenticatePayload,
onCreateDocumentPayload,
onDisconnectPayload,
} from '@hocuspocus/server'
Expand Down Expand Up @@ -102,6 106,9 @@ export class MyHocuspocusExtension implements Extension {
// eslint-disable-next-line @typescript-eslint/no-empty-function
async onConnect(data: onConnectPayload): Promise<void> {}

// eslint-disable-next-line @typescript-eslint/no-empty-function
async onAuthenticate(data: onAuthenticatePayload): Promise<void> {}

// eslint-disable-next-line @typescript-eslint/no-empty-function
async onDisconnect(data: onDisconnectPayload): Promise<void> {}

Expand Down
1 change: 1 addition & 0 deletions docs/src/docPages/provider/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 28,7 @@ There is definitely more to configure. Find the full list of all available setti
| parameters | `{}` | Parameters will be added to the server URL and passed to the server. |
| name | `''` | The name of the document. |
| document | `''` | The actual Y.js document. |
| token | `''` | An authentication token that will be passed to the server. |
| awareness | `new Awareness()` | Awareness object, by default attached to the passed Y.js document. |
| connect | `true` | Whether to connect to the server after intialization. |
| broadcast | `true` | By default changes are synced between browser tabs through broadcasting. |
Expand Down
31 changes: 20 additions & 11 deletions docs/src/docPages/provider/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 19,12 @@ const provider = new HocuspocusProvider({
onConnect: () => {
//
},
onAuthenticated: () => {
//
},
onAuthenticationFailed: ({ reason }) => {
//
},
onStatus: ({ status }) => {
//
},
Expand Down Expand Up @@ -74,14 80,17 @@ provider.off('onMessage', onMessage)

## List of events

| Name | Description |
| --------------- | ---------------------------------------------------------- |
| open | When the WebSocket connection is created. |
| connect | When the provider has succesfully connected to the server. |
| status | When the connections status changes. |
| message | When a message is incoming. |
| outgoingMessage | When a message will be sent. |
| synced | When the Y.js document is successfully synced. |
| close | When the WebSocket connection is closed. |
| disconnect | When the provider disconnects. |
| destroy | When the provider will be destroyed. |
| Name | Description |
|----------------------|------------------------------------------------------------|
| open | When the WebSocket connection is created. |
| connect | When the provider has succesfully connected to the server. |
| authenticated | When the client has successfully authenticated. |
| authenticationFailed | When the client authentication was not successful. |
| status | When the connections status changes. |
| message | When a message is incoming. |
| outgoingMessage | When a message will be sent. |
| synced | When the Y.js document is successfully synced. |
| close | When the WebSocket connection is closed. |
| disconnect | When the provider disconnects. |
| destroy | When the provider will be destroyed. |

2 changes: 2 additions & 0 deletions docs/src/links.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 73,8 @@
items:
- title: onConnect
link: /api/hooks/on-connect
- title: onAuthenticate
link: /api/hooks/on-authenticate
- title: onCreateDocument
link: /api/hooks/on-create-document
- title: onChange
Expand Down
Loading

0 comments on commit a1e68d5

Please sign in to comment.