Skip to content
This repository has been archived by the owner on Feb 20, 2024. It is now read-only.

Commit

Permalink
Add AUTH_TYPE env (#45)
Browse files Browse the repository at this point in the history
* add AUTH_TYPE env

* made open api auth conditional

* refactor auth location

* use test.env

* conditional security handlers and auth envs

* helm auth ifs
  • Loading branch information
jonmattgray authored May 26, 2022
1 parent a393eed commit 822d899
Show file tree
Hide file tree
Showing 17 changed files with 181 additions and 125 deletions.
54 changes: 30 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,29 +52,35 @@ npm run test:integration

The following environment variables are used by `dscp-api` and can be configured. Entries marked as `required` are needed when running `dscp-api` in production mode.

| variable | required | default | description |
| :------------------------------ | :------: | :-------------------------------------------------: | :------------------------------------------------------------------------------------------- |
| PORT | N | `3001` | The port for the API to listen on |
| API_HOST | Y | - | The hostname of the `dscp-node` the API should connect to |
| API_PORT | N | `9944` | The port of the `dscp-node` the API should connect to |
| LOG_LEVEL | N | `info` | Logging level. Valid values are [`trace`, `debug`, `info`, `warn`, `error`, `fatal`] |
| USER_URI | Y | - | The Substrate `URI` representing the private key to use when making `dscp-node` transactions |
| IPFS_HOST | Y | - | Hostname of the `IPFS` node to use for metadata storage |
| IPFS_PORT | N | `15001` | Port of the `IPFS` node to use for metadata storage |
| AUTH_JWKS_URI | N | `https://inteli.eu.auth0.com/.well-known/jwks.json` | JSON Web Key Set containing public keys used by the Auth0 API |
| AUTH_AUDIENCE | N | `inteli-dev` | Identifier of the Auth0 API |
| AUTH_ISSUER | N | `https://inteli.eu.auth0.com/` | Domain of the Auth0 API ` |
| AUTH_TOKEN_URL | N | `https://inteli.eu.auth0.com/oauth/token` | Auth0 API endpoint that issues an Authorisation (Bearer) access token |
| METADATA_KEY_LENGTH | N | `32` | Fixed length of metadata keys |
| METADATA_VALUE_LITERAL_LENGTH | N | `32` | Fixed length of metadata LITERAL values |
| MAX_METADATA_COUNT | N | `16` | Maximum number of metadata items allowed per token |
| API_VERSION | N | `package.json version` | API version |
| API_MAJOR_VERSION | N | `v3` | API major version |
| FILE_UPLOAD_MAX_SIZE | N | `200 * 1024 * 1024` | The Maximum file upload size (bytes) |
| SUBSTRATE_STATUS_POLL_PERIOD_MS | N | `10 * 1000` | Number of ms between calls to check dscp-node status |
| SUBSTRATE_STATUS_TIMEOUT_MS | N | `2 * 1000` | Number of ms to wait for response to dscp-node health requests |
| IPFS_STATUS_POLL_PERIOD_MS | N | `10 * 1000` | Number of ms between calls to check ipfs status |
| IPFS_STATUS_TIMEOUT_MS | N | `2 * 1000` | Number of ms to wait for response to ipfs health requests |
| variable | required | default | description |
| :------------------------------ | :------: | :--------------------: | :------------------------------------------------------------------------------------------- |
| PORT | N | `3001` | The port for the API to listen on |
| API_HOST | Y | - | The hostname of the `dscp-node` the API should connect to |
| API_PORT | N | `9944` | The port of the `dscp-node` the API should connect to |
| LOG_LEVEL | N | `info` | Logging level. Valid values are [`trace`, `debug`, `info`, `warn`, `error`, `fatal`] |
| USER_URI | Y | - | The Substrate `URI` representing the private key to use when making `dscp-node` transactions |
| IPFS_HOST | Y | - | Hostname of the `IPFS` node to use for metadata storage |
| IPFS_PORT | N | `15001` | Port of the `IPFS` node to use for metadata storage |
| METADATA_KEY_LENGTH | N | `32` | Fixed length of metadata keys |
| METADATA_VALUE_LITERAL_LENGTH | N | `32` | Fixed length of metadata LITERAL values |
| MAX_METADATA_COUNT | N | `16` | Maximum number of metadata items allowed per token |
| API_VERSION | N | `package.json version` | API version |
| API_MAJOR_VERSION | N | `v3` | API major version |
| FILE_UPLOAD_MAX_SIZE | N | `200 * 1024 * 1024` | The Maximum file upload size (bytes) |
| SUBSTRATE_STATUS_POLL_PERIOD_MS | N | `10 * 1000` | Number of ms between calls to check dscp-node status |
| SUBSTRATE_STATUS_TIMEOUT_MS | N | `2 * 1000` | Number of ms to wait for response to dscp-node health requests |
| IPFS_STATUS_POLL_PERIOD_MS | N | `10 * 1000` | Number of ms between calls to check ipfs status |
| IPFS_STATUS_TIMEOUT_MS | N | `2 * 1000` | Number of ms to wait for response to ipfs health requests |
| AUTH_TYPE | N | `NONE` | Authentication type for routes on the service. Valid values: [`NONE`, `JWT`] |

The following environment variables are additionally used when `AUTH_TYPE : 'JWT'`

| variable | required | default | description |
| :------------- | :------: | :-------------------------------------------------: | :-------------------------------------------------------------------- |
| AUTH_JWKS_URI | N | `https://inteli.eu.auth0.com/.well-known/jwks.json` | JSON Web Key Set containing public keys used by the Auth0 API |
| AUTH_AUDIENCE | N | `inteli-dev` | Identifier of the Auth0 API |
| AUTH_ISSUER | N | `https://inteli.eu.auth0.com/` | Domain of the Auth0 API ` |
| AUTH_TOKEN_URL | N | `https://inteli.eu.auth0.com/oauth/token` | Auth0 API endpoint that issues an Authorisation (Bearer) access token |

## Running the API

Expand Down Expand Up @@ -111,7 +117,7 @@ This will return a JSON response (`Content-Type` `application/json`) of the form

### Authenticated endpoints

The rest of the endpoints in `dscp-api` require authentication in the form of a header `'Authorization: Bearer YOUR_ACCESS_TOKEN'`:
If `AUTH_TYPE` env is set to `JWT`, the rest of the endpoints in `dscp-api` require authentication in the form of a header `'Authorization: Bearer YOUR_ACCESS_TOKEN'`:

1. [GET /item/:id](#get-/item/:id)
2. [GET /item/:id/metadata/:metadataKey](#get-/item/:id/metadata/:metadataKey)
Expand Down
3 changes: 2 additions & 1 deletion app/api-v3/routes/item/{id}.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const { validateTokenId } = require('../../../util/appUtil')
const logger = require('../../../logger')
const { getReadableMetadataKeys } = require('../../../util/appUtil')
const { getDefaultSecurity } = require('../../../util/auth')

module.exports = function (apiService) {
const doc = {
Expand Down Expand Up @@ -82,7 +83,7 @@ module.exports = function (apiService) {
},
},
},
security: [{ bearerAuth: [] }],
security: getDefaultSecurity(),
tags: ['item'],
}

Expand Down
3 changes: 2 additions & 1 deletion app/api-v3/routes/item/{id}/metadata/{metadataKey}.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const { getMetadataResponse } = require('../../../../../util/appUtil')
const { getDefaultSecurity } = require('../../../../../util/auth')

module.exports = function () {
const doc = {
Expand Down Expand Up @@ -61,7 +62,7 @@ module.exports = function () {
},
},
},
security: [{ bearerAuth: [] }],
security: getDefaultSecurity(),
tags: ['item'],
}

Expand Down
3 changes: 2 additions & 1 deletion app/api-v3/routes/last-token.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const logger = require('../../logger')
const { getDefaultSecurity } = require('../../util/auth')

module.exports = function (apiService) {
const doc = {
Expand Down Expand Up @@ -51,7 +52,7 @@ module.exports = function (apiService) {
},
},
},
security: [{ bearerAuth: [] }],
security: getDefaultSecurity(),
tags: ['system'],
}

Expand Down
3 changes: 2 additions & 1 deletion app/api-v3/routes/members.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const logger = require('../../logger')
const { membershipReducer } = require('../../util/appUtil')
const { getDefaultSecurity } = require('../../util/auth')

module.exports = function (apiService) {
const doc = {
Expand Down Expand Up @@ -47,7 +48,7 @@ module.exports = function (apiService) {
},
},
},
security: [{ bearerAuth: [] }],
security: getDefaultSecurity(),
tags: ['system'],
}

Expand Down
3 changes: 2 additions & 1 deletion app/api-v3/routes/run-process.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const logger = require('../../logger')
const { validateInputIds, processRoles, processMetadata, validateProcess } = require('../../util/appUtil')
const { getDefaultSecurity } = require('../../util/auth')
const { PROCESS_IDENTIFIER_LENGTH } = require('../../env')

module.exports = function (apiService) {
Expand Down Expand Up @@ -169,7 +170,7 @@ module.exports = function (apiService) {
},
},
},
security: [{ bearerAuth: [] }],
security: getDefaultSecurity(),
tags: ['system'],
}

Expand Down
20 changes: 16 additions & 4 deletions app/env.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,29 @@ if (process.env.NODE_ENV === 'test') {
dotenv.config({ path: '.env' })
}

const AUTH_ENVS = {
NONE: {},
JWT: {
AUTH_JWKS_URI: envalid.url({ devDefault: 'https://inteli.eu.auth0.com/.well-known/jwks.json' }),
AUTH_AUDIENCE: envalid.str({ devDefault: 'inteli-dev' }),
AUTH_ISSUER: envalid.url({ devDefault: 'https://inteli.eu.auth0.com/' }),
AUTH_TOKEN_URL: envalid.url({ devDefault: 'https://inteli.eu.auth0.com/oauth/token' }),
},
}

const { AUTH_TYPE } = envalid.cleanEnv(process.env, {
AUTH_TYPE: envalid.str({ default: 'NONE', choices: ['NONE', 'JWT'] }),
})

const vars = envalid.cleanEnv(process.env, {
...AUTH_ENVS[AUTH_TYPE],
PORT: envalid.port({ default: 3001 }),
API_HOST: envalid.host({ devDefault: 'localhost' }),
API_PORT: envalid.port({ default: 9944 }),
LOG_LEVEL: envalid.str({ default: 'info', devDefault: 'debug' }),
USER_URI: envalid.str({ devDefault: '//Alice' }),
IPFS_HOST: envalid.host({ devDefault: 'localhost' }),
IPFS_PORT: envalid.port({ devDefault: 5001, default: 15001 }),
AUTH_JWKS_URI: envalid.url({ devDefault: 'https://inteli.eu.auth0.com/.well-known/jwks.json' }),
AUTH_AUDIENCE: envalid.str({ devDefault: 'inteli-dev' }),
AUTH_ISSUER: envalid.url({ devDefault: 'https://inteli.eu.auth0.com/' }),
AUTH_TOKEN_URL: envalid.url({ devDefault: 'https://inteli.eu.auth0.com/oauth/token' }),
METADATA_KEY_LENGTH: envalid.num({ default: 32 }),
METADATA_VALUE_LITERAL_LENGTH: envalid.num({ default: 32 }),
PROCESS_IDENTIFIER_LENGTH: envalid.num({ default: 32 }),
Expand All @@ -36,4 +47,5 @@ const vars = envalid.cleanEnv(process.env, {

module.exports = {
...vars,
AUTH_TYPE,
}
19 changes: 12 additions & 7 deletions app/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ const multer = require('multer')
const path = require('path')
const bodyParser = require('body-parser')
const compression = require('compression')
const { PORT, API_VERSION, API_MAJOR_VERSION } = require('./env')
const { PORT, API_VERSION, API_MAJOR_VERSION, AUTH_TYPE } = require('./env')
const logger = require('./logger')
const apiDoc = require('./api-v3/api-doc')
const apiService = require('./api-v3/services/apiService')
const { startStatusHandlers } = require('./serviceStatus')
const { serviceState } = require('./util/statusPoll')
const { verifyJwks } = require('./util/appUtil')
const { verifyJwks } = require('./util/auth')

async function createHttpServer() {
const requestLogger = pinoHttp({ logger })
Expand Down Expand Up @@ -57,6 +57,15 @@ async function createHttpServer() {
})

const multerStorage = multer.diskStorage({})
const securityHandlers =
AUTH_TYPE === 'JWT'
? {
bearerAuth: (req) => {
return verifyJwks(req.headers['authorization'])
},
}
: {}

initialize({
app,
apiDoc: apiDoc,
Expand All @@ -68,11 +77,7 @@ async function createHttpServer() {
})
},
},
securityHandlers: {
bearerAuth: (req) => {
return verifyJwks(req.headers['authorization'])
},
},
securityHandlers: securityHandlers,
dependencies: {
apiService: apiService,
},
Expand Down
50 changes: 1 addition & 49 deletions app/util/appUtil.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ const BASE58 = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
const bs58 = require('base-x')(BASE58)
const fetch = require('node-fetch')
const FormData = require('form-data')
const jwksRsa = require('jwks-rsa')
const jwt = require('jsonwebtoken')

const {
types: {
Role: { _enum: rolesEnum },
Expand All @@ -20,9 +19,6 @@ const {
METADATA_KEY_LENGTH,
METADATA_VALUE_LITERAL_LENGTH,
MAX_METADATA_COUNT,
AUTH_AUDIENCE,
AUTH_JWKS_URI,
AUTH_ISSUER,
PROCESS_IDENTIFIER_LENGTH,
} = require('../env')
const logger = require('../logger')
Expand Down Expand Up @@ -456,49 +452,6 @@ const getMetadataResponse = async (tokenId, metadataKey, res) => {
return
}

const client = jwksRsa({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
jwksUri: AUTH_JWKS_URI,
})

async function getKey(header, cb) {
client.getSigningKey(header.kid, (err, key) => {
if (err) {
logger.warn(`An error occurred getting jwks key ${err}`)
cb(err, null)
} else if (key) {
const signingKey = key.publicKey || key.rsaPublicKey
cb(null, signingKey)
}
})
}

const verifyJwks = async (authHeader) => {
const authToken = authHeader ? authHeader.replace('Bearer ', '') : ''

const verifyOptions = {
audience: AUTH_AUDIENCE,
issuer: [AUTH_ISSUER],
algorithms: ['RS256'],
header: authToken,
}

return new Promise((resolve, reject) => {
jwt.verify(authToken, getKey, verifyOptions, (err, decoded) => {
if (err) {
resolve(false)
} else if (decoded) {
resolve(true)
} else {
logger.warn(`Error verifying jwks`)
reject({ message: 'An error occurred during jwks verification' })
}
})
})
}

module.exports = {
runProcess,
getMembers,
Expand All @@ -517,6 +470,5 @@ module.exports = {
rolesEnum,
containsInvalidMembershipRoles,
getMetadataResponse,
verifyJwks,
validateProcess,
}
64 changes: 64 additions & 0 deletions app/util/auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
const jwksRsa = require('jwks-rsa')
const jwt = require('jsonwebtoken')

const { AUTH_JWKS_URI, AUTH_AUDIENCE, AUTH_ISSUER, AUTH_TYPE } = require('../env')
const logger = require('../logger')

const client = jwksRsa({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
jwksUri: AUTH_JWKS_URI,
})

async function getKey(header, cb) {
client.getSigningKey(header.kid, (err, key) => {
if (err) {
logger.warn(`An error occurred getting jwks key ${err}`)
cb(err, null)
} else if (key) {
const signingKey = key.publicKey || key.rsaPublicKey
cb(null, signingKey)
}
})
}

const verifyJwks = async (authHeader) => {
const authToken = authHeader ? authHeader.replace('Bearer ', '') : ''

const verifyOptions = {
audience: AUTH_AUDIENCE,
issuer: [AUTH_ISSUER],
algorithms: ['RS256'],
header: authToken,
}

return new Promise((resolve, reject) => {
jwt.verify(authToken, getKey, verifyOptions, (err, decoded) => {
if (err) {
resolve(false)
} else if (decoded) {
resolve(true)
} else {
logger.warn(`Error verifying jwks`)
reject({ message: 'An error occurred during jwks verification' })
}
})
})
}

const getDefaultSecurity = () => {
switch (AUTH_TYPE) {
case 'NONE':
return []
case 'JWT':
return [{ bearerAuth: [] }]
default:
return []
}
}

module.exports = {
verifyJwks,
getDefaultSecurity,
}
4 changes: 2 additions & 2 deletions helm/dscp-api/Chart.yaml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
apiVersion: v2
name: dscp-api
appVersion: '4.1.0'
appVersion: '4.2.0'
description: A Helm chart for dscp-api
version: '4.1.0'
version: '4.2.0'
type: application
dependencies:
- name: dscp-node
Expand Down
3 changes: 3 additions & 0 deletions helm/dscp-api/templates/configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@ data:
{{- else if .Values.dscpIpfs.enabled }}
ipfsPort: {{ template "dscp-ipfs.ipfsApiPort" .Subcharts.dscpIpfs }}
{{- end }}
{{- if eq .Values.config.auth.type "JWT" }}
authJwksUri: {{ .Values.config.auth.jwksUri }}
authAudience: {{ .Values.config.auth.audience }}
authIssuer: {{ .Values.config.auth.issuer }}
authTokenUrl: {{ .Values.config.auth.tokenUrl }}
{{- end }}
substrateStatusPollPeriodMs: {{ .Values.config.substrateStatusPollPeriodMs | quote }}
substrateStatusTimeoutMs: {{ .Values.config.substrateStatusTimeoutMs | quote }}
ipfsStatusPollPeriodMs: {{ .Values.config.ipfsStatusPollPeriodMs | quote }}
ipfsStatusTimeoutMs: {{ .Values.config.ipfsStatusTimeoutMs | quote }}
authType: {{ .Values.config.auth.type }}
Loading

0 comments on commit 822d899

Please sign in to comment.