Skip to content

Commit

Permalink
feat(jwt): enable JWT support for client and server (#20)
Browse files Browse the repository at this point in the history
  • Loading branch information
christianmat committed Jan 24, 2024
1 parent a9e15ef commit 154af9a
Show file tree
Hide file tree
Showing 8 changed files with 149 additions and 13 deletions.
2 changes: 2 additions & 0 deletions apps/remote-storage-server/.env.example
Expand Up @@ -5,3 +5,5 @@ DATA_STORE=sqlite
NODE_ENV=development
REMOTE_STORAGE_SERVER_PORT=4000
REMOTE_STORAGE_SERVER_USE_HTTPS=false
# Set this value to your JWT secret if you wish to use JWTs for authentication
JWT_SECRET=
20 changes: 20 additions & 0 deletions apps/remote-storage-server/README.md
Expand Up @@ -60,6 +60,26 @@ HTTP/1.1 200 OK
{"foo":"bar"}
```

## Enabling authentication with JSON Web Tokens
We highly recommend using JSON Web Tokens (JWT) to authenticate requests to the server. This can be done by setting the `JWT_SECRET` environment variable in `.env` to your JWT secret.

To learn more about JWTs and you can generate them in your application, see [this guide](https://jwt.io/introduction/).

After setting up JWT authentication make sure to pass your JWTs to the remoteStorage client:

```js
const remoteStorage = new RemoteStorage({
serverAddress: 'http://localhost:4000',
userId: '123e4567-e89b-12d3-a456-426614174000'
})

remoteStorage.getItem('my-item', {
headers: {
Authorization: `Bearer ${jwt}`,
},
})
```

## Developing the server

The server is built using [NestJS](https://nestjs.com/). You can find the documentation for NestJS [here](https://docs.nestjs.com/).
Expand Down
1 change: 1 addition & 0 deletions apps/remote-storage-server/package.json
Expand Up @@ -30,6 +30,7 @@
"@nestjs/platform-fastify": "^9.3.8",
"@nestjs/schedule": "^3.0.2",
"@nestjs/swagger": "^6.1.4",
"jsonwebtoken": "^9.0.2",
"redis": "^4.6.12",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
Expand Down
3 changes: 2 additions & 1 deletion apps/remote-storage-server/src/app.module.ts
Expand Up @@ -2,9 +2,10 @@ import { Module } from '@nestjs/common'
import { AppController } from './app.controller'
import { AppService } from './app.service'
import { EntitiesModule } from './entities/entities.module'
import { ConfigModule } from '@nestjs/config'

@Module({
imports: [EntitiesModule],
imports: [ConfigModule.forRoot(), EntitiesModule],
controllers: [AppController],
providers: [AppService],
})
Expand Down
24 changes: 24 additions & 0 deletions apps/remote-storage-server/src/entities/entities.controller.ts
Expand Up @@ -16,6 +16,8 @@ const MAX_KEY_LENGTH = 255
@ApiTags('entities')
@Controller()
export class EntitiesController {
private jwt = require('jsonwebtoken')

constructor(private readonly entitiesService: EntitiesService) {}

@ApiOperation({ summary: 'Get a entity by key' })
Expand Down Expand Up @@ -78,6 +80,28 @@ export class EntitiesController {
throw new BadRequestException(`userId cannot be longer than ${MAX_KEY_LENGTH} characters`)
}

// Check if JWT is enabled in feature flags. JWT automatically gets turned on when a secret is set.
const jwtSecret = process.env.JWT_SECRET
if (jwtSecret) {
// Check if authorization header is present
if (!headers['authorization']) {
throw new BadRequestException(
'No authorization header provided. When JWT is enabled, you must provide an authorization header.'
)
}
const token = headers['authorization'].split(' ')[1]
if (!token) {
throw new BadRequestException(
'No authorization token provided. When JWT is enabled, you must provide an authorization token.'
)
}
try {
this.jwt.verify(token, jwtSecret)
} catch (e) {
throw new BadRequestException('Invalid authorization token')
}
}

return {
instanceId,
userId,
Expand Down
9 changes: 4 additions & 5 deletions packages/js-client/README.md
Expand Up @@ -29,6 +29,7 @@ That's where remoteStorage comes in. Using the same API as localStorage, remoteS
## Features

- ✨ Simple API (same as localStorage)
- 🔐 Secure (built-in JWT support)
- 👌 Works with all Javascript frameworks
- 📦 Lightweight (~1 kB minified)
- 🔓 Open source server and client (MIT license)
Expand Down Expand Up @@ -114,12 +115,10 @@ remoteStorage should only be used for non-sensitive data. We recommend using it

localStorage is a browser API that allows you to store data in the browser. The data is stored locally on the user's device and is not shared across devices or browsers. remoteStorage is a library that combines the localStorage API with a remote server to persist data across browsers and devices.

#### Can't anyone just guess a user ID and access someone else's data?

You can secure your calls to remote-storage by using a secret unique UUID generated with a package such as [uuid](https://www.npmjs.com/package/uuid) as your User ID. It is not recommended to use a sequential numeric ID or a user's email address as this makes it possible to easily guess other user IDs and access their data.

Alternatively, you can create a simple wrapper/proxy API around remoteStorage that uses your own authentication method to verify the user's identity before allowing them to access the data. Then, you can pick a secure and secret Instance ID that is not publicly available to ensure that only your application can access the data.
#### How do I authenticate requests to remoteStorage?

remoteStorage can be used without any authentication, but we highly recommend using JSON Web Tokens (JWT) to authenticate requests to the server. This can be done by setting the `JWT_SECRET` environment variable in `.env` to your JWT secret for the server.
See the [server documentation](/apps/remote-storage-server/README.md) for more information.

## Contributing

Expand Down
31 changes: 24 additions & 7 deletions packages/js-client/src/core/remote-storage.ts
Expand Up @@ -32,8 +32,13 @@ export class RemoteStorage {
this.userId = userId ?? this.getUserId()
}

async getItem<T>(key: string): Promise<T> {
const response = await this.call('GET', `${apiPrefix}${key}`, null)
/**
* Get an item from remote storage
* @param key the key that corresponds to the item to get
* @param fetchOptions optional fetch options to pass to the underlying fetch call. Currently only headers for authorization are supported.
*/
async getItem<T>(key: string, fetchOptions?: any): Promise<T> {
const response = await this.call('GET', `${apiPrefix}${key}`, fetchOptions, null)
// Check for 404 and return null if so
if (response.status === 404) {
return null
Expand All @@ -56,21 +61,33 @@ export class RemoteStorage {
return JSON.parse(data) as T
}

async setItem<T>(key: string, value: T): Promise<void> {
await this.call('PUT', `${apiPrefix}${key}`, value)
/**
* Set an item in remote storage
* @param key the key that corresponds to the item to set
* @param value the value to set
* @param fetchOptions optional fetch options to pass to the underlying fetch call. Currently only headers for authorization are supported.
*/
async setItem<T>(key: string, value: T, fetchOptions?: any): Promise<void> {
await this.call('PUT', `${apiPrefix}${key}`, fetchOptions, value)
}

async removeItem(key: string): Promise<void> {
await this.call('DELETE', `${apiPrefix}${key}`, null)
/**
* Remove an item from remote storage
* @param key the key that corresponds to the item to remove
* @param fetchOptions optional fetch options to pass to the underlying fetch call. Currently only headers for authorization are supported.
*/
async removeItem(key: string, fetchOptions?: any): Promise<void> {
await this.call('DELETE', `${apiPrefix}${key}`, fetchOptions, null)
}

async call(method: string, path: string, data?: any) {
async call(method: string, path: string, options?: any, data?: any) {
return fetch(new URL(path, this.serverAddress).toString(), {
method: method,
headers: {
'Content-Type': 'application/json',
[HEADER_REMOTE_STORAGE_INSTANCE_ID]: this.instanceId,
[HEADER_REMOTE_STORAGE_USER_ID]: this.userId,
...options?.headers,
},
body: data ? JSON.stringify(data) : undefined,
})
Expand Down
72 changes: 72 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 154af9a

Please sign in to comment.