diff --git a/README.md b/README.md index c3ae124..3831c5e 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ Version `1.0.0` is the first officially released and supported implementation of - [Quick Start](#quick-start) * [onVerify / Verifier](#onverify--verifier) +- [Encrypting session](#encrypting-session) - [Passing state](#passing-state) - [Error handling](#error-handling) - [Authentication process](#authentication-process) @@ -97,6 +98,39 @@ const verifier = async (tokenData, req, done) => { passport.use("veracity", new VIDPOpenIDCStrategy(strategySettings, verifier)) ``` +## Encrypting session +When configuring authentication you need to provide a place to store session data. This is done through the `store` configuration for `express-session`. In the samples we use a MemoryStore instance that keeps the data in memory, but this is not suitable to for production as it does not scale. For such systems you would probably go with a database or cache of some kind such as MySQL or Redis. + +Once you set up such a session storage mechanism, however there are some considerations you need to take into account. Since the access tokens for individual users are stored as session data it means that anyone with access to the session storage database can extract any token for a currently logged in user and use it themselves. Since the token is the only key needed to perform actions on behalf of the user it is considered sensitive information and must therefore be protected accordingly. + +This library comes with a helper function to deal with just this scenario called `createEncryptedSessionStore`. This function uses the **AES-256-CBC** algorithm to encrypt and decrypt a subset of session data on-the-fly preventing someone with access to the store from seeing the plain access tokens. They will only see an encrypted blob of text. + +The way `createEncryptedSessionStore` works is that it replaces the read and write functions of an `express-session` compatible store with augmented versions that decrypt and encrypt a set of specified properties (if present on the session object) respectively. This means that you can still use any of the compatible store connectors and simply pass it through the helper function to get a version that provides encryption. + +Using the Redis connector you can configure an encrypted session like this: +```javascript +const session = require("express-session") +const { createEncryptedSessionStore } = require("@veracity/node-auth") +const redisStore = require("connect-redis")(session) + +// You should NOT hard-code the encryption key. It should be served from a secure store such as Azure KeyVault or similar +const encryptedRedisStore = createEncryptedSessionStore("encryption key")(redisStore) + +// We can now use the encryptedRedisStore in place of a regular store to configure authentication +setupWebAppAuth({ + app, + strategy: { + clientId: "", + clientSecret: "", + replyUrl: "" + }, + session: { + secret: "ce4dd9d9-cac3-4728-a7d7-d3e6157a06d9", + store: encryptedRedisStore // Use encrypted version of redis store + } +}) +``` + ## Passing state Sometimes it is useful to be able to pass data from before the login begins all the way through the authentication process until control is returned back to your code. This library supports this in two ways for web and native applications (the bearer token validation strategy does not support this): diff --git a/package-lock.json b/package-lock.json index 6fbb72a..e1c6984 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@veracity/node-auth", - "version": "0.4.0-beta", + "version": "1.0.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 603b688..9d8e0b8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@veracity/node-auth", - "version": "1.0.0", + "version": "1.0.1", "description": "A library for authenticating with Veracity and retrieving one or more access tokens for communicating with APIs.", "scripts": { "build:copy-files": "ts-node -T scripts/copy-files.ts", diff --git a/samples/101-JS-Auth/README.md b/samples/101-JS-Auth/README.md index d3a9de0..eadfaf2 100644 --- a/samples/101-JS-Auth/README.md +++ b/samples/101-JS-Auth/README.md @@ -1,7 +1,7 @@ # JS Helper example This example implements Veracity authentication using the `setupWebAppAuth` helper function. For details see the `start.js` file. -You need to fill in application credentials on line 14-16 in `start.js` before this sample will run. Visit the [Veracity for Developers project portal](https://developer.veracity.com/projects) to create them. +You need to fill in application credentials on line 23-25 in `start.js` before this sample will run. Visit the [Veracity for Developers project portal](https://developer.veracity.com/projects) to create them. To run the sample: ```javascript diff --git a/samples/101-JS-Auth/package.json b/samples/101-JS-Auth/package.json index 6eb9cdc..9a56ee4 100644 --- a/samples/101-JS-Auth/package.json +++ b/samples/101-JS-Auth/package.json @@ -23,6 +23,7 @@ }, "homepage": "https://developer.veracity.com", "dependencies": { + "@veracity/node-auth": "^1.0.0", "body-parser": "^1.19.0", "express": "^4.17.1", "express-session": "^1.16.2", diff --git a/samples/102-TS-Auth/README.md b/samples/102-TS-Auth/README.md new file mode 100644 index 0000000..850676f --- /dev/null +++ b/samples/102-TS-Auth/README.md @@ -0,0 +1,20 @@ +# TypeScript Helper example +This example implements Veracity authentication using the `setupWebAppAuth` helper function and TypeScript. For details see the `start.ts` file. + +You need to fill in application credentials on line 23-25 in `start.ts` before this sample will run. Visit the [Veracity for Developers project portal](https://developer.veracity.com/projects) to create them. + +To run the sample: +```javascript +npm i +npm start +``` + +## HTTPS +This sample uses `node-forge` along with the `generateCertificate` utility to create a self-signed certificate for local development. This is **not** suitable for production and should be replaced with a more secure certificate signed by a trusted third-party. For example: [https://letsencrypt.org/](https://letsencrypt.org/) + +## Dependencies +- `@veracity/node-auth` +- `express` +- `express-session` +- `passport` +- `node-forge` \ No newline at end of file diff --git a/samples/102-TS-Auth/package.json b/samples/102-TS-Auth/package.json index 817d373..57cbda5 100644 --- a/samples/102-TS-Auth/package.json +++ b/samples/102-TS-Auth/package.json @@ -1,5 +1,5 @@ { - "name": "@veracity/node-auth-js-helper-example", + "name": "@veracity/node-auth-ts-helper-example", "private": true, "version": "1.0.0", "description": "", @@ -29,6 +29,7 @@ "typescript": "^3.6.3" }, "dependencies": { + "@veracity/node-auth": "^1.0.0", "body-parser": "^1.19.0", "express": "^4.17.1", "express-session": "^1.16.2", diff --git a/samples/103-JS-Passport/README.md b/samples/103-JS-Passport/README.md new file mode 100644 index 0000000..c52f933 --- /dev/null +++ b/samples/103-JS-Passport/README.md @@ -0,0 +1,20 @@ +# JS Passport example +This example implements Veracity authentication using the `VIDPWebAppStrategy` passport strategy directly. This is intended for more advanced scenarios where your code or structure makes it hard or impossible to use the simpler helper function. This sample ends up with the same features as the ones using the helper, but with more code as we have to implement everything ourselves. + +You need to fill in application credentials on line 33-35 in `start.js` before this sample will run. Visit the [Veracity for Developers project portal](https://developer.veracity.com/projects) to create them. + +To run the sample: +```javascript +npm i +npm start +``` + +## HTTPS +This sample uses `node-forge` along with the `generateCertificate` utility to create a self-signed certificate for local development. This is **not** suitable for production and should be replaced with a more secure certificate signed by a trusted third-party. For example: [https://letsencrypt.org/](https://letsencrypt.org/) + +## Dependencies +- `@veracity/node-auth` +- `express` +- `express-session` +- `passport` +- `node-forge` \ No newline at end of file diff --git a/samples/103-JS-Passport/package.json b/samples/103-JS-Passport/package.json new file mode 100644 index 0000000..f77d348 --- /dev/null +++ b/samples/103-JS-Passport/package.json @@ -0,0 +1,33 @@ +{ + "name": "@veracity/node-strategy-js-helper-example", + "private": true, + "version": "1.0.0", + "description": "", + "scripts": { + "start": "node start.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/veracity/node-auth.git" + }, + "keywords": [ + "veracity", + "authentication", + "openid", + "typescript" + ], + "author": "Veracity", + "license": "MIT", + "bugs": { + "url": "https://github.com/veracity/node-auth/issues" + }, + "homepage": "https://developer.veracity.com", + "dependencies": { + "@veracity/node-auth": "^1.0.0", + "body-parser": "^1.19.0", + "express": "^4.17.1", + "express-session": "^1.16.2", + "node-forge": "^0.9.1", + "passport": "^0.4.0" + } +} diff --git a/samples/103-JS-Passport/public/index.html b/samples/103-JS-Passport/public/index.html new file mode 100644 index 0000000..5abb387 --- /dev/null +++ b/samples/103-JS-Passport/public/index.html @@ -0,0 +1,30 @@ + + +
+ + + ++ This example demonstrates how to set up basic authentication with Veracity and viewing the response. +
+ + + \ No newline at end of file diff --git a/samples/103-JS-Passport/start.js b/samples/103-JS-Passport/start.js new file mode 100644 index 0000000..17ccbbf --- /dev/null +++ b/samples/103-JS-Passport/start.js @@ -0,0 +1,125 @@ +// Import the dependencies we need +const express = require("express") +const https = require("https") +const session = require("express-session") +const passport = require("passport") +const bodyParser = require("body-parser") + +const { + VIDPWebAppStrategy, + makeSessionConfigObject, + generateCertificate, + createEncryptedSessionStore, + createRefreshTokenMiddleware, + VERACITY_API_SCOPES, + VERACITY_LOGOUT_URL, + VERACITY_METADATA_ENDPOINT +} = require("@veracity/node-auth") + +// Create our express instance +const app = express() + +// Create an encrypted version of the memory store to ensure tokens are encrypted at rest. +// This is an optional, but recommeded step. +const encryptedSessionStorage = createEncryptedSessionStore("encryptionKey")(new session.MemoryStore()) + +// Set up session and passport +app.use(session(makeSessionConfigObject({ + secret: "ce4dd9d9-cac3-4728-a7d7-d3e6157a06d9", // Replace this with your own secret + store: encryptedSessionStorage +}))) + +// Initialize and configure passport +app.use(passport.initialize()) +app.use(passport.session()) +const strategySettings = { // Equivalent to the strategy property + clientId: "", + clientSecret: "", + replyUrl: "", + apiScopes: [VERACITY_API_SCOPES.services] +} +const strategy = new VIDPWebAppStrategy( + strategySettings, + (data, req, done) => { // Our verifier function (equivalent to onVerify) + done(null, data) + } +) +const strategyName = "veracity_oidc" // The name of our strategy +passport.use(strategyName, strategy) +passport.serializeUser((user, done) => { done(null, user) }) +passport.deserializeUser((id, done) => { done(null, id) }) + +// Create our login endpoint +app.get("/login", + (req, res, next) => { // This is equivalent to onBeforeLogin and can be removed if not used + next() + }, + passport.authenticate(strategyName), // Begin authenticating with passport + (req, res, next) => { // This handler will never be called in normal operation, but we log an error if it does + next(new Error("Login handler reached, this should not happen.")) + } +) + +// Create our return endpoint when the Veracity IDP has completed authentication +app.post("/auth/oidc/loginreturn", + bodyParser.urlencoded({extended: true}), // Decode body of request + passport.authenticate(strategyName), // Continue authentication by exchanging auth code for access token and optionally redirecting to another login + (req, res, next) => { // Equivalent to onLoginComplete. This is called once all tokens have been retrieved + res.redirect(req.query.returnTo || "/") // Redirect back to root or to the returnTo param sent to "/login" + } +) + +// Set up our logout path +app.get("/logout", + (req, res) => { + req.logout() // Destroy our internal session + res.redirect(VERACITY_LOGOUT_URL) // Redirect to central logout for all Veracity Services + } +) + +// The last feature we need to configure is the refresh middleware. +const refreshTokenMiddleware = createRefreshTokenMiddleware( + strategySettings, + (tokenData, req) => { + Object.assign(req.user.accessTokens, { + [tokenData.scope]: tokenData + }) + }, + VERACITY_METADATA_ENDPOINT +) + +// Now we can continue with our normal handlers as in the other samples + +// This endpoint will return our user data so we can inspect it. +app.get("/user", (req, res) => { + if (req.isAuthenticated()) { + res.send(req.user) + return + } + res.status(401).send("Unauthorized") +}) + +// Create an endpoint where we can refresh the services token. +// By default this will refresh it when it has less than 5 minutes until it expires. +app.get("/refresh", refreshTokenMiddleware(VERACITY_API_SCOPES.services), (req, res) => { + res.send({ + updated: Date.now(), + user: req.user + }) +}) + + +// Serve static content from the public folder so we can display the index.html page +app.use(express.static("public")) + +// Set up the HTTPS development server +const server = https.createServer({ + ...generateCertificate() // Generate self-signed certificates for development +}, app) +server.on("error", (error) => { // If an error occurs halt the application + console.error(error) + process.exit(1) +}) +server.listen(3000, () => { // Begin listening for connections + console.log("Listening for connections on port 3000") +}) \ No newline at end of file diff --git a/samples/104-TS-Passport/README.md b/samples/104-TS-Passport/README.md new file mode 100644 index 0000000..78ed4f0 --- /dev/null +++ b/samples/104-TS-Passport/README.md @@ -0,0 +1,20 @@ +# TS Passport example +This example implements Veracity authentication using the `VIDPWebAppStrategy` passport strategy directly in TypeScript. This is intended for more advanced scenarios where your code or structure makes it hard or impossible to use the simpler helper function. This sample ends up with the same features as the ones using the helper, but with more code as we have to implement everything ourselves. + +You need to fill in application credentials on line 38-40 in `start.ts` before this sample will run. Visit the [Veracity for Developers project portal](https://developer.veracity.com/projects) to create them. + +To run the sample: +```javascript +npm i +npm start +``` + +## HTTPS +This sample uses `node-forge` along with the `generateCertificate` utility to create a self-signed certificate for local development. This is **not** suitable for production and should be replaced with a more secure certificate signed by a trusted third-party. For example: [https://letsencrypt.org/](https://letsencrypt.org/) + +## Dependencies +- `@veracity/node-auth` +- `express` +- `express-session` +- `passport` +- `node-forge` \ No newline at end of file diff --git a/samples/104-TS-Passport/package.json b/samples/104-TS-Passport/package.json new file mode 100644 index 0000000..db351f4 --- /dev/null +++ b/samples/104-TS-Passport/package.json @@ -0,0 +1,40 @@ +{ + "name": "@veracity/node-strategy-ts-helper-example", + "private": true, + "version": "1.0.0", + "description": "", + "scripts": { + "start": "ts-node -P tsconfig.json start.ts" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/veracity/node-auth.git" + }, + "keywords": [ + "veracity", + "authentication", + "openid", + "typescript" + ], + "author": "Veracity", + "license": "MIT", + "bugs": { + "url": "https://github.com/veracity/node-auth/issues" + }, + "devDependencies": { + "@types/body-parser": "^1.17.1", + "@types/express": "^4.17.1", + "@types/express-session": "^1.15.14", + "ts-node": "^8.4.1", + "typescript": "^3.6.3" + }, + "homepage": "https://developer.veracity.com", + "dependencies": { + "@veracity/node-auth": "^1.0.0", + "body-parser": "^1.19.0", + "express": "^4.17.1", + "express-session": "^1.16.2", + "node-forge": "^0.9.1", + "passport": "^0.4.0" + } +} diff --git a/samples/104-TS-Passport/public/index.html b/samples/104-TS-Passport/public/index.html new file mode 100644 index 0000000..5abb387 --- /dev/null +++ b/samples/104-TS-Passport/public/index.html @@ -0,0 +1,30 @@ + + + + + + ++ This example demonstrates how to set up basic authentication with Veracity and viewing the response. +
+ + + \ No newline at end of file diff --git a/samples/104-TS-Passport/start.ts b/samples/104-TS-Passport/start.ts new file mode 100644 index 0000000..c1a62af --- /dev/null +++ b/samples/104-TS-Passport/start.ts @@ -0,0 +1,127 @@ +// tslint:disable: max-line-length + +// Import the dependencies we need +import { + createEncryptedSessionStore, + createRefreshTokenMiddleware, + generateCertificate, + makeSessionConfigObject, + VERACITY_API_SCOPES, + VERACITY_LOGOUT_URL, + VERACITY_METADATA_ENDPOINT, + VIDPWebAppStrategy +} from "@veracity/node-auth" +import { IVIDPWebAppStrategySettings } from "@veracity/node-auth/interfaces" +import bodyParser from "body-parser" +import express from "express" +import session, { MemoryStore } from "express-session" +import https from "https" +import passport from "passport" + +// Create our express instance +const app = express() + +// Create an encrypted version of the memory store to ensure tokens are encrypted at rest. +// This is an optional, but recommeded step. +const encryptedSessionStorage = createEncryptedSessionStore("encryptionKey")(new MemoryStore()) + +// Set up session and passport +app.use(session(makeSessionConfigObject({ + secret: "ce4dd9d9-cac3-4728-a7d7-d3e6157a06d9", // Replace this with your own secret + store: encryptedSessionStorage +}))) + +// Initialize and configure passport +app.use(passport.initialize()) +app.use(passport.session()) +const strategySettings: IVIDPWebAppStrategySettings = { // Equivalent to the strategy property + clientId: "", + clientSecret: "", + replyUrl: "", + apiScopes: [VERACITY_API_SCOPES.services] +} +const strategy = new VIDPWebAppStrategy( + strategySettings, + (data, req, done) => { // Our verifier function (equivalent to onVerify) + done(null, data) + } +) +const strategyName = "veracity_oidc" // The name of our strategy +passport.use(strategyName, strategy) +passport.serializeUser((user, done) => { done(null, user) }) +passport.deserializeUser((id, done) => { done(null, id) }) + +// Create our login endpoint +app.get("/login", + (req, res, next) => { // This is equivalent to onBeforeLogin and can be removed if not used + next() + }, + passport.authenticate(strategyName), // Begin authenticating with passport + (req, res, next) => { // This handler will never be called in normal operation, but we log an error if it does + next(new Error("Login handler reached, this should not happen.")) + } +) + +// Create our return endpoint when the Veracity IDP has completed authentication +app.post("/auth/oidc/loginreturn", + bodyParser.urlencoded({extended: true}), // Decode body of request + passport.authenticate(strategyName), // Continue authentication by exchanging auth code for access token and optionally redirecting to another login + (req, res, next) => { // Equivalent to onLoginComplete. This is called once all tokens have been retrieved + res.redirect(req.query.returnTo || "/") // Redirect back to root or to the returnTo param sent to "/login" + } +) + +// Set up our logout path +app.get("/logout", + (req, res) => { + req.logout() // Destroy our internal session + res.redirect(VERACITY_LOGOUT_URL) // Redirect to central logout for all Veracity Services + } +) + +// The last feature we need to configure is the refresh middleware. +const refreshTokenMiddleware = createRefreshTokenMiddleware( + strategySettings, + (tokenData, req) => { + const anyReq: any = req + Object.assign(anyReq.user.accessTokens, { + [tokenData.scope]: tokenData + }) + }, + VERACITY_METADATA_ENDPOINT +) + +// Now we can continue with our normal handlers as in the other samples + +// This endpoint will return our user data so we can inspect it. +app.get("/user", (req, res) => { + if (req.isAuthenticated()) { + res.send(req.user) + return + } + res.status(401).send("Unauthorized") +}) + +// Create an endpoint where we can refresh the services token. +// By default this will refresh it when it has less than 5 minutes until it expires. +app.get("/refresh", refreshTokenMiddleware(VERACITY_API_SCOPES.services), (req, res) => { + res.send({ + updated: Date.now(), + user: req.user + }) +}) + +// Serve static content from the public folder so we can display the index.html page +app.use(express.static("public")) + +// Set up the HTTPS development server +const server = https.createServer({ + ...generateCertificate() // Generate self-signed certificates for development +}, app) +server.on("error", (error) => { // If an error occurs halt the application + console.error(error) + process.exit(1) +}) +server.listen(3000, () => { // Begin listening for connections + console.log("Listening for connections on port 3000") +}) diff --git a/samples/104-TS-Passport/tsconfig.json b/samples/104-TS-Passport/tsconfig.json new file mode 100644 index 0000000..2c68433 --- /dev/null +++ b/samples/104-TS-Passport/tsconfig.json @@ -0,0 +1,66 @@ +{ + "compilerOptions": { + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "es2018", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ + "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ + // "lib": [], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + "outDir": "./dist", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true, /* Enable all strict type-checking options. */ + "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + + /* Module Resolution Options */ + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + }, + "exclude": [ + "node_modules" + ] +}