Integration Documentation
API Gateway (SSO)
packages/api-gateway/src/index.ts
import dotenv from 'dotenv'
import http from 'http'
import express from 'express'
import proxy from 'express-http-proxy'
dotenv.config()
// @todo: add username creation
interface User {
id: number
[field: string]: any
}
const getCurrentUserId = async (): Promise<User> => {
// for this simple example, hardcode the records usually loaded from a local database...
const users = [
{
id: 1,
email: 'nodietidaccount@gmail.com',
firstName: 'Jane',
lastName: 'Doe',
dietIdUserId: null
},
{
id: 42,
email: 'hasdietidaccount@gmail.com',
firstName: 'John',
lastName: 'Doe',
dietIdUserId: 72
}
]
// ...and the id of the signed in user
return users.find(user => user.id === 42)
}
interface DietIdConfig {
server: string
clientId: string
clientSecret: string
}
const dietId: DietIdConfig = {
server: process.env.DIET_ID_SERVER,
clientId: process.env.DIET_ID_CLIENT_ID,
clientSecret: process.env.DIET_ID_CLIENT_SECRET
}
const app = express()
const port = process.env.PORT || 3500
app.use(
proxy(dietId.server, {
proxyReqOptDecorator: async proxyReqOpts => {
const sensitiveHeaders = ['Authorization', 'X-DietID-ClientId', 'X-DietID-PartnerUserId'].map(name =>
name.toLowerCase()
)
// clear out any sensitive headers, preventing them from originating from the frontend.
// "X-DietID-PartnerUserId" in particular is very sensitive because it scopes the request
// at the user level, preventing access to other users' records.
Object.keys(proxyReqOpts.headers).forEach(header => {
if (sensitiveHeaders.includes(header.toLowerCase())) {
proxyReqOpts.headers[header] = ''
}
})
// Grab the record for the user that is currently logged in
const user = await getCurrentUserId()
// Only forward requests to Diet ID if there's a currently logged in user
if (!user) {
return Promise.reject('Please log in and try again.')
}
// "Authorization" and "X-DietID-ClientId" tell Diet ID who we are (partner-level access control).
// "X-DietID-PartnerUserId" tells Diet ID who the user making the request is (user-level access control).
proxyReqOpts.headers['Authorization'] = `Bearer ${dietId.clientSecret}`
proxyReqOpts.headers['X-DietID-ClientId'] = dietId.clientId
proxyReqOpts.headers['X-DietID-PartnerUserId'] = user.id
return proxyReqOpts
}
})
)
http.createServer(app).listen(port, () => console.log(`Proxying requests from port ${port} to ${dietId.server}`))
packages/api-gateway/src/sso.ts
// @ts-nocheck
import CryptoJS from 'crypto-js'
export const encryptData = (data: object, secret: string): string => {
const text = JSON.stringify(data)
const key = CryptoJS.enc.Hex.parse(secret)
const iv = CryptoJS.lib.WordArray.random(128 / 8)
const result = CryptoJS.AES.encrypt(text, key, { iv })
return [CryptoJS.enc.Base64.stringify(result.iv), CryptoJS.enc.Base64.stringify(result.ciphertext)].join('$')
}
export const decryptData = (string: string, secret: string): object => {
const text = string.split('$')[1]
const key = CryptoJS.enc.Hex.parse(secret)
const iv = CryptoJS.enc.Base64.parse(string.split('$')[0])
const result = CryptoJS.AES.decrypt(text, key, { iv })
return JSON.parse(result.toString(CryptoJS.enc.Utf8))
}
/**
* Returns a url the frontend can use to embed a Diet ID assessment in an iframe, webview, etc.
* The user is automatically signed in and prompted to complete the assessment.
*
* @param user The required fields for finding or creating a Diet ID user (id, email, first name, last name)
* @returns A url that signs in a user and prompts them to complete an assessment.
*
*/
export const getSSO = (user: DietID.CreateUserParams, clientId: string, clientSecret: string): string =>
`https://${clientId}.dietid.com/#/u/${encodeURIComponent(encryptData(user, clientSecret))}`
packages/api-gateway/.env.example
DIET_ID_SERVER=https://api.staging.dietid.com
DIET_ID_CLIENT_ID=ADD_CLIENT_ID_HERE
DIET_ID_CLIENT_SECRET=ADD_CLIENT_SECRET_HERE
packages/api-gateway/package.json
{
"name": "@dietid/api-gateway",
"version": "0.1.0",
"description": "Backend that authenticates and proxies requests originating from the frontend to the Diet ID API",
"homepage": "https://github.com/dqpn/diet-id-api-client",
"module": "./src/index.js",
"main": "./dist/index.js",
"scripts": {
"build": "tsc",
"watch": "concurrently -k -p \"[{name}]\" -n \"TypeScript,Node\" -c \"yellow.bold,green.bold\" \"npm run watch:ts\" \"npm run watch:node\"",
"watch:ts": "tsc -w",
"watch:node": "nodemon dist/index.js"
},
"dependencies": {
"crypto-js": "^4.0.0",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"express-http-proxy": "^1.6.0"
},
"devDependencies": {
"@types/express": "^4.17.3",
"@types/express-http-proxy": "^1.5.12",
"@types/node": "^13.7.6",
"concurrently": "^5.1.0",
"nodemon": "^2.0.2",
"typescript": "~3.7.5"
}
}
packages/api-gateway/tsconfig.json
{
"extends": "../../tsconfig.base",
"compilerOptions": {
"lib": ["esnext"],
"module": "commonjs",
"outDir": "dist",
"skipLibCheck": true
},
"include": ["src/**/*"]
}