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

Fix/13 improve auth security #14

Merged
merged 46 commits into from
Oct 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
c401b22
Support running a single test or multiple. Add JWT dependency.
Apr 3, 2022
216a828
Implement JWT authorization. Use a new consent message for signature …
Apr 3, 2022
3ef1235
Update permission tests to support JWT auth.
Apr 3, 2022
0b8f929
Implement JWT tests
Apr 3, 2022
6d44827
Add README instructions for enabling JWT auth in CouchDb
Apr 3, 2022
5844b0c
Merge branch 'main' into fix/13-improve-auth-security
Apr 4, 2022
9165368
Fix incorrect merge issues.
Apr 4, 2022
0a7583c
Add support for generating and verifying refresh token
Apr 29, 2022
821464d
More refresh token unit tests. Add access token support. Db refactor.
Apr 30, 2022
75d0069
Fix permission unit tests with new refactor.
May 2, 2022
286f0fb
Update server endpoints to use new auth. Create server integration te…
May 2, 2022
51d931c
Hash database name server side before creating
May 2, 2022
48267a3
Verify context name when verifying refresh token. Don't generate acce…
May 2, 2022
b613681
Confirm expected user is admin of database before deleting it.
May 2, 2022
e06e864
Ensure database names begin with "v"
May 2, 2022
f1ca758
Return an access token and host when requesting a refresh token.
May 2, 2022
39448ce
Support deleteDatabase endpoint
May 2, 2022
6f90e38
Expand unit tests
May 2, 2022
9361a8c
Split controller into auth and user.
May 5, 2022
f1a7480
Fix broken test
May 5, 2022
0dbfe07
Fix broken delete database calls
May 5, 2022
3adf44e
Remove JWT test no longer required
May 5, 2022
09db622
Support invalidating device ID
May 6, 2022
74d4847
Invalidating devices now working correctly with tests
May 7, 2022
3eaa41b
Implement garbage collection
May 7, 2022
38a1214
Fix building DSN for public credentials
Aug 23, 2022
d391e9b
Add CouchDB configuration note around basic auth
Aug 30, 2022
4d0f63b
Add more docs
Aug 30, 2022
137e795
Fix edge case issues identified during testing
Aug 31, 2022
a90966f
Merge branch 'develop' into fix/13-improve-auth-security
Aug 31, 2022
ae70c70
Fix yarn dependencies
Aug 31, 2022
84f4af1
Fix example .env files and remove duplicate.
Aug 31, 2022
ee6bddb
Describe how authentication works
Aug 31, 2022
2f9e190
Rename to authorization
Aug 31, 2022
46af155
Add missing account-node dev dependency
Aug 31, 2022
a89716b
Fix minor issues with tests. Better config docs in README.
Sep 1, 2022
d133fd7
Resolve feedback for further review.
Sep 1, 2022
069989d
Merge branch 'develop' into fix/13-improve-auth-security
Sep 1, 2022
f2b7e3a
Fix missed merge issue
Sep 1, 2022
fd7ff28
Support running single test
Sep 1, 2022
cbb988c
Add support for saving user databases and getting the info as an auth…
Sep 2, 2022
2bb753c
Add missing require for PouchDB
Sep 2, 2022
cf06c2f
Cleanup didsToUsernames
Sep 2, 2022
edefc67
Destructure some vars
Sep 2, 2022
63ec142
Add cors requirement to README
Sep 2, 2022
20c3519
Add isTokenValid endpoint to verify a refresh token is still valid an…
Sep 2, 2022
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
74 changes: 71 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,23 @@ Key features:
- Adding a second layer of security by managing per-database ACL validation rules
- Providing applications with user's database connection strings

## How Authorization Works

This is the login flow:

1. The Verida Account makes a request to the storage node API to obtain an auth JWT to be signed (`/auth/generateAuthJwt`). This prevents replay attacks.
2. The Verida Account signs a consent message using their private key. This consent message proves the user wants to unlock a specific application context
3. The Verida Account submits the signed authorization request (`/auth/authenticate`). Assuming the signed AuthJWT is valid, the storage node returns a refresh token and an access token
4. The Verida Account can then use the access token to either; 1) make storage node requests (ie: create database) or 2) directly access CouchDB as an authenticated user (using `Bearer` token auth)
5. When the access token expires, the Verida Account can use the refresh token to request a new access token (`/auth/connect`)
6. If a refresh token is close to expiry, the Verida Account can use the active refresh token to obtain a new refresh token (`/auth/regenerateRefreshToken`)

When a Verida Account authenticates, it can designate an `authenticate` requst to be linked to a particular device by specifying the `deviceId` in the request.

This allows a specific device to be linked to a refresh token. A call to `/auth/invalidateDeviceId` can be used to invalidate any refresh tokens linked to the specified `deviceId`. This allows the Verida Vault to remotely log out an application that previously logged in.

Note: This only invalidates the refresh token. The access token will remain valid until it expires. It's for this reason that access tokens are configured to have a short expiry (5 minutes by default). CouchDB does not support manually invalidating access tokens, so we have to take this timeout approach to invalidation.

## Usage

```bash
Expand Down Expand Up @@ -40,6 +57,10 @@ A `sample.env` is included. Copy this to `.env` and update the configuration:
- `DB_REJECT_UNAUTHORIZED_SSL`: Boolean indicating if unauthorized SSL certificates should be rejected (`true` or `false`). Defaults to `false` for development testing. Must be `true` for production environments otherwise SSL certificates won't be verified.
- `DB_PUBLIC_USER`: Alphanumeric string for a public database user. These credentials can be requested by anyone and provide access to all databases where the permissions have been set to `public`.
- `DB_PUBLIC_PASS`: Alphanumeric string for a public database password.
- `ACCESS_TOKEN_EXPIRY`: Number of seconds before an access token expires. The protocol will use the refresh token to obtain a new access token. CouchDB does not support a way to force the expiry of an issued token, so the access token expiry should always be set to 5 minutes (300)
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

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

That link doesn't point to a specific line.

Can you expand on your question, I don't follow.

Copy link
Contributor

Choose a reason for hiding this comment

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

In authManager.js there is:

process.env.REFRESH_JWT_SIGN_PK, {
            // expies in 1 minute
            expiresIn: 60
        })

It's not immediately clear what this token expiry is for and why it is a different value to ACCESS_TOKEN_EXPIRY

Copy link
Member Author

Choose a reason for hiding this comment

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

- `REFRESH_TOKEN_EXPIRY`: Number of seconds before a refresh token expires. Users will be forced to re-login once this time limit is reached. This should be set to 7 days (604800).
- `ACCESS_JWT_SIGN_PK`: The access token private key. The base64 version of this must be specified in the CouchDB configuration under `jwt_keys/hmac:_default`
- `REFRESH_JWT_SIGN_PK`: The refresh token private key

### Setting up environment variables on Windows

Expand All @@ -63,21 +84,58 @@ $env:DB_PUBLIC_PASS="784c2n780c9cn0789"
- CORS must be enabled so that database requests can come from any domain name
- A valid user must be enforced for security reasons

[Ensure `{chttpd_auth, jwt_authentication_handler}` is added to the list of the active `chttpd/authentication_handlers`](https://docs.couchdb.org/en/stable/api/server/authn.html?highlight=jwt#jwt-authentication)

DO NOT include ` {chttpd_auth, default_authentication_handler}` in the authentication handlers. This option is a default enable in CouchDB and causes web browsers to display a HTTP Basic Auth popup if authentication fails. This creates an awful UX and is unecessary as the protocol handles authentication issues automatically.

Learn more here: https://stackoverflow.com/questions/32670580/prevent-authentication-popup-401-with-couchdb-pouchdb

```
[httpd]
WWW-Authenticate = Basic realm="administrator"
enable_cors = true
Copy link
Contributor

Choose a reason for hiding this comment

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

Does it work alright without enabling cors?

Copy link
Member Author

@tahpot tahpot Sep 2, 2022

Choose a reason for hiding this comment

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

No. Web browsers will throw CORS errors when they access CouchDB directly.

This shouldn't have been removed, re-added to README.

[chttpd]
authentication_handlers = {chttpd_auth, jwt_authentication_handler}, {chttpd_auth, cookie_authentication_handler}
enable_cors = false

[chttpd_auth]
require_valid_user = true

[jwt_auth]
required_claims = exp

[jwt_keys]
hmac:_default = <base64 secret key>

[cors]
origins = *
credentials = true
methods = GET, PUT, POST, HEAD, DELETE
headers = accept, authorization, content-type, origin, referer, x-csrf-token
```

The `hmac:_default` key is a base64 encoded representation of the

## Generating JWT key

Note: A secret key (string) suitable for `jwt_keys` can be base64 encoded with the following:

```
const secretKey = 'secretKey'
const encodedKey = Buffer.from(secretKey).toString('base64')
```

This can be tested via curl:

```
curl -H "Host: localhost:5984" \
-H "accept: application/json, text/plain, */*" \
-H "authorization: Bearer <bearer_token>" \
"http://localhost:5984/_session"
```

Where:

- `bearer_token` - A bearer token generated via the `test/jwt` unit test
- `localhost` - Replace this with the hostname of the server being tested

## Lambda deployment

We use [Claudia.js](https://claudiajs.com/) to turn our Express app into an Express-on-Lambda app.
Expand All @@ -87,3 +145,13 @@ See the [Claudia Docs for information](https://claudiajs.com/news/2016/11/24/cla

Verida staff can see the [internal Verida repo]( https://github.com/verida/infrastructure/blob/develop/storage_node.md) for docs on this.

## Tests

Run tests with `yarn run tests`

Note: The tests in `server.js` require the server to be running locally. The other tests operate fine without the server running.

Common issues when running tests:

1. `Bad key`: The key in CouchDB configuration for `jwt_keys/hmac:_default` is not a valid Base64 encoded key
2. `HMAC error`: The key in CouchDB configuration for `jwt_keys/hmac:_default` does not match `ACCESS_JWT_SIGN_PK` in `.env`
12 changes: 9 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"main": "dist/server.js",
"scripts": {
"clean": "rm -rf dist",
"test": "mocha -mocha --require @babel/polyfill --require @babel/register './test/**/*.js' --timeout 30000",
"tests": "mocha -mocha --require @babel/polyfill --require @babel/register './test/**/*.js' --timeout 30000",
"test": "mocha -mocha --require @babel/polyfill --require @babel/register --timeout 30000",
"dev": "nodemon --exec babel-node src/server.js",
"build": "yarn run clean && babel src -d dist --extensions .js",
"serve": "node dist/server.js",
Expand Down Expand Up @@ -52,17 +53,20 @@
"homepage": "https://github.com/verida/storage-node/README.md",
"dependencies": {
"@babel/runtime": "^7.16.7",
"@verida/did-client": "^0.1.2",
"@verida/did-client": "^0.1.8",
"@verida/encryption-utils": "^1.1.3",
"aws-serverless-express": "^3.4.0",
"cors": "^2.8.5",
"did-resolver": "^3.1.0",
"dotenv": "^8.2.0",
"ethers": "^4.0.42",
"express": "^4.17.1",
"express-basic-auth": "git+https://github.com/Mozzler/express-basic-auth.git",
"jsonwebtoken": "^8.5.1",
"lodash": "^4.17.21",
"memory-cache": "^0.2.0",
"nano": "^8.1.0"
"nano": "^8.1.0",
"rand-token": "^1.0.1"
},
"devDependencies": {
"@babel/cli": "^7.15.7",
Expand All @@ -71,6 +75,8 @@
"@babel/plugin-transform-runtime": "^7.16.7",
"@babel/polyfill": "^7.8.0",
"@babel/preset-env": "^7.7.7",
"@verida/account-node": "^1.1.9",
"axios": "^0.27.2",
"babel-core": "^6.26.3",
"babel-loader": "^8.2.3",
"babel-polyfill": "^6.26.0",
Expand Down
14 changes: 13 additions & 1 deletion sample.env
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,20 @@ DB_USER="admin"
DB_PASS="admin"
DB_HOST="localhost"
DB_PORT=5984
DB_REJECT_UNAUTHORIZED_SSL=false
DB_REJECT_UNAUTHORIZED_SSL=true
ACCESS_JWT_SIGN_PK="insert-random-access-symmetric-key"
# 5 Minutes
ACCESS_TOKEN_EXPIRY=300
REFRESH_JWT_SIGN_PK="insert-random-refresh-symmetric-key"
# 30 Days
REFRESH_TOKEN_EXPIRY=2592000
DB_REFRESH_TOKENS="verida_refresh_tokens"
DB_DB_INFO="verida_db_info"
# How often garbage collection runs (1=100%, 0.5 = 50%)
GC_PERCENT=0.1

// alpha numeric only
DB_PUBLIC_USER="784c2n780c9cn0789"
DB_PUBLIC_PASS="784c2n780c9cn0789"

PORT=5151
Loading