Test Report

0
0
83
16525

ID Title Duration (ms)
1 Client constructor throws if no state is given 11
2 Client request the url defaults to the serverUrl 350
3 Client request accepts url string 545
4 Client request accepts options object 470
5 Client request handles OperationOutcome errors 187
6 Client request handles other errors 47
7 Client request rejects invalid access tokens 183
8 Client request tries to use the refresh token on 401 253
9 Client refresh rejects with no state.tokenResponse 0
10 Client refresh rejects with no state.tokenResponse.refresh_token 1
11 Client refresh deletes invalid refresh tokens 124
12 Client refresh handles other errors 0
13 Client refresh works as expected 1003
14 express adapter getStorage returns null if session is empty 1
15 express adapter getStorage can be set as option 0
16 express adapter getStorage uses session storage by default 1
17 express adapter authorize using options.serverUrl and an open server 538
18 express adapter authorize using options.serverUrl and an protected server 521
19 express adapter authorize using query.serverUrl and an open server 536
20 express adapter authorize using query.serverUrl and an protected server 495
21 express adapter authorize using query.iss and query.launch and an open server 551
22 express adapter authorize using query.iss and query.launch and protected server 502
23 express adapter authorize calls next if not enough arguments 1
24 express adapter completeAuth ends with error and error_description if provided 1
25 express adapter completeAuth handles internal errors 1
26 express adapter completeAuth handles errors 128
27 express adapter completeAuth calls next if not enough arguments provided 0
28 express adapter completeAuth works as expected 883
29 express adapter refreshAuth ends with error message if no client can be constructed 1
30 express adapter refreshAuth handles errors 0
31 hapi adapter getStorage returns null if session is empty 0
32 hapi adapter getStorage can be set as option 1
33 hapi adapter getStorage uses session storage by default 0
34 hapi adapter authorize using options.serverUrl and an open server 525
35 hapi adapter authorize using options.serverUrl and an protected server 530
36 hapi adapter authorize using query.serverUrl and an open server 571
37 hapi adapter authorize using query.serverUrl and an protected server 578
38 hapi adapter authorize using query.iss and query.launch and an open server 534
39 hapi adapter authorize using query.iss and query.launch and protected server 615
40 hapi adapter authorize calls next if not enough arguments 0
41 hapi adapter completeAuth ends with error and error_description if provided 1
42 hapi adapter completeAuth handles internal errors 1
43 hapi adapter completeAuth calls next if not enough arguments provided 0
44 hapi adapter completeAuth works as expected 978
45 hapi adapter refreshAuth ends with error message if no client can be constructed 1
46 hapi adapter refreshAuth handles errors 1
47 lib getPath returns the first arg if no path 0
48 lib getPath returns the first arg for empty path 0
49 lib getPath works as expected 1
50 lib printf works as expected 1
51 lib getErrorText works as expected 1
52 lib HttpError constructor The message defaults to 'Unknown error' 1
53 lib HttpError constructor The httpCode defaults to 500 0
54 lib HttpError constructor The httpCode can be set 0
55 lib HttpError constructor Variable substitution works 0
56 lib getSecurityExtensions Works without arguments 3
57 lib getSecurityExtensions Works with HSPC 1228
58 lib getSecurityExtensions Works with R3 - Protected 500
59 lib getSecurityExtensions Works with R3 - Open 697
60 lib resolveUrl works as expected 3
61 lib buildAuthorizeUrl Rejects if no serverUrl can be found 1
62 lib buildAuthorizeUrl Works with open servers 2
63 lib buildAuthorizeUrl Works with query.fhirServiceUrl parameter 2
64 lib buildAuthorizeUrl Works with options.serverUrl parameter 1
65 lib buildAuthorizeUrl requires 'launch' param if 'iss' is used 1
66 lib buildAuthorizeUrl clears previous state 4
67 lib buildAuthorizeUrl Works with clientSecret 2
68 lib buildAuthorizeUrl Works with protected servers 494
69 lib buildAuthorizeUrl Works with protected servers standalone 500
70 lib authorize works as expected 498
71 lib buildTokenRequest rejects if no state is found 1
72 lib buildTokenRequest rejects if there is no redirectUri in state 14
73 lib buildTokenRequest rejects if there is no tokenUri in state 1
74 lib buildTokenRequest rejects if there is no clientId in state 1
75 lib buildTokenRequest works as expected 1
76 lib buildTokenRequest works with clientSecret 1
77 lib handleTokenError works as expected 1
78 lib handleTokenError message can be set 0
79 lib handleTokenError catches 'error' property in response payloads 1
80 lib handleTokenError catches 'error_description' property in response payloads 1
81 lib handleTokenError catches errors from string payloads 0
82 lib completeAuth handles errors 130
83 lib completeAuth works as expected 748

Code Coverage Report

100%
413
413
0

lib/client.js

100%
75
75
0
Line Lint Hits Source
1 "use strict";
2 1 Object.defineProperty(exports, "__esModule", { value: true });
3 1 const axios_1 = require("axios");
4 1 const querystring = require("querystring");
5 1 const _debug = require("debug");
6 1 const lib_1 = require("./lib");
7 1 const debug = _debug("smart");
8 /**
9 * A SMART Client instance will simplify some tasks for you. It will authorize
10 * requests automatically, use refresh tokens, handle errors and so on.
11 */
12 class Client {
13 constructor(state) {
14 // This might happen if the state have been lost (for example the server
15 // was restarted and a memory storage was used).
16 if (!state) {
17 1 throw new Error("No state provided to the client");
18 }
19 23 this.state = state;
20 }
21 /**
22 * Allows you to do the following:
23 * 1. Use relative URLs (treat them as relative to the "serverUrl" option)
24 * 2. Automatically authorize requests with your accessToken (if any)
25 * 3. Automatically re-authorize using the refreshToken (if available)
26 * 4. Automatically parse error operation outcomes and turn them into
27 * JavaScript Error objects with which the resulting promises are rejected
28 * @param {object|string} options URL or axios request options
29 */
30 async request(options = {}) {
31 if (typeof options == "string") {
32 6 options = { url: options };
33 }
34 9 options.baseURL = this.state.serverUrl.replace(/\/?$/, "/");
35 8 const accessToken = lib_1.getPath(this.state, "tokenResponse.access_token");
36 8 if (accessToken) {
37 3 options.headers = Object.assign({}, options.headers, { Authorization: `Bearer ${accessToken}` });
38 }
39 8 return axios_1.default(options).catch(error => {
40 // The request was made and the server responded with a
41 // status code that falls out of the range of 2xx
42 if (error.response) {
43 5 if (error.response.status == 401) {
44 3 debug("401 received");
45 3 if (lib_1.getPath(this.state, "tokenResponse.refresh_token")) {
46 1 debug("Refreshing using the refresh token");
47 1 return this.refresh().then(() => this.request(options));
48 }
49 }
50 4 const resourceType = lib_1.getPath(error, "response.data.resourceType");
51 4 const issues = lib_1.getPath(error, "response.data.issue");
52 4 const body = error.response.data;
53 4 if (resourceType == "OperationOutcome" && issues.length) {
54 2 debug("OperationOutcome error response detected");
55 2 const errors = body.issue.map((o) => `${o.severity} ${o.code} ${o.diagnostics}`);
56 2 throw new Error(errors.join("\n"));
57 }
58 2 throw error;
59 }
60 // The request was made but no response was received
61 // "error.request" is an instance of http.ClientRequest
62 1 throw new Error(lib_1.getErrorText("no_fhir_response"));
63 });
64 }
65 /**
66 * Use the refresh token to obtain new access token.
67 * If the refresh token is expired it will be deleted from the state, so
68 * that we don't enter into loops trying to re-authorize.
69 */
70 async refresh() {
71 if (!this.state.tokenResponse || !this.state.tokenResponse.refresh_token) {
72 4 throw new Error("Trying to refresh but there is no refresh token");
73 }
74 5 const refreshToken = lib_1.getPath(this.state, "tokenResponse.refresh_token");
75 5 return axios_1.default({
76 method: "POST",
77 url: this.state.tokenUri,
78 headers: {
79 "content-type": "application/x-www-form-urlencoded"
80 },
81 data: querystring.stringify({
82 grant_type: "refresh_token",
83 refresh_token: refreshToken
84 })
85 }).then(({ data }) => {
86 this.state.tokenResponse = Object.assign({}, this.state.tokenResponse, data);
87 3 return data;
88 })
89 .catch(error => {
90 if (error.response && error.response.status == 401) {
91 1 debug("401 received - refresh token expired or invalid");
92 1 debug("Deleting the expired or invalid refresh token");
93 1 delete this.state.tokenResponse.refresh_token;
94 }
95 2 throw error;
96 });
97 }
98 }
99 1 exports.default = Client;
100

lib/errors.js

100%
17
17
0
Line Lint Hits Source
1 "use strict";
2 1 Object.defineProperty(exports, "__esModule", { value: true });
3 1 exports.default = {
4 "": "Unknown error",
5 unknown_error: "Unknown error",
6 missing_url_parameter: `Missing url parameter "%s"`,
7 missing_state_by_id: `No state found using the given id: "%s".`,
8 missing_state_redirectUri: "Missing state.redirectUri",
9 missing_state_tokenUri: "Missing state.tokenUri",
10 missing_state_clientId: "Missing state.clientId",
11 no_fhir_response: "No response received from the FHIR server",
12 no_server_url_provided: "Cannot detect which FHIR server to launch against. " +
13 "For EHR launch call your endpoint with 'launch' " +
14 "and 'iss' parameters. For standalone launch pass " +
15 "'fhirServiceUrl' parameter or set it as 'serverUrl'" +
16 "in your configuration."
17 };
18

lib/express.js

100%
49
49
0
Line Lint Hits Source
1 1 const smart = require("./lib");
2
3
4 1 module.exports = function(options) {
5 const cfg = { getStorage: smart.getSessionStorage, ...options };
6
7 // Return the Express-specific high-level API
8 17 return {
9
10 /**
11 * Given a request object this function will return a Promise that will
12 * eventually be resolved with a Fhir Client object or with `null` if
13 * there is a problem. It will lookup the storage for stored SMART state.
14 * If such state is found, it will create and return a Fhir Client
15 * instance that knows how to use that (automatically authorize any
16 * requests made with `client.request`, use clientSecret and refresh
17 * tokens etc.).
18 * @param {Object} request
19 * @returns {Promise<Client|null>}
20 */
21 async getClient(request) {
22 const storage = cfg.getStorage(request);
23 7 const id = await storage.get("smartId");
24 7 if (id) {
25 5 const state = await storage.get(id);
26 5 if (state) {
27 4 return new smart.Client(state);
28 }
29 }
30 3 return null;
31 },
32
33 /**
34 * If the fhir server base can be determined (from query.iss or
35 * query.fhirServiceUrl or options.serverUrl), then the user will be
36 * redirected to the authorization endpoint (or to your redirectUri if
37 * it happens to be an open server). Otherwise, the `next` callback
38 * will be called.
39 * @param {Object} req
40 * @param {Object} res
41 * @param {Function} next
42 */
43 authorize(req, res, next) {
44 let { launch, iss, fhirServiceUrl } = req.query;
45 8 if ((launch && iss) || fhirServiceUrl || cfg.serverUrl) {
46 7 return smart.authorize(req, res, cfg, cfg.getStorage(req));
47 }
48 1 next();
49 },
50
51 /**
52 * The `authorize` function above will redirect the browser to the
53 * authorization endpoint of the SMART server. Depending on the requested
54 * scopes, the user might be asked to select a patient or practitioner,
55 * authorize the app and so on. Eventually, the user will be redirected
56 * back to your site at your `options.redirectUri`. At that point you
57 * should have received a code that needs to be exchanged for an access
58 * token.
59 * @param {Object} req
60 * @param {Object} res
61 * @param {Function} next
62 */
63 completeAuth(req, res, next) {
64 let { code, state, error, error_description } = req.query;
65 6 if (error) {
66 2 let msg = error + (error_description ? ":\n" + error_description : "");
67 2 return res.end(msg);
68 }
69 4 if (code && state) {
70 3 return smart.completeAuth(req, cfg.getStorage(req))
71 .then(
72 () => res.redirect(cfg.redirectUri),
73 e => {
74 res.writeHead(e.httpCode || 500, { 'Content-Type': 'text/plain' });
75 2 res.end(e.message, "utf8");
76 }
77 );
78 }
79 1 next();
80 },
81
82 /**
83 * You don't typically need to use this because if you have a refresh
84 * token (which you should if your scope includes `offline_access`),
85 * then the request method will automatically use it when your access
86 * token expires.
87 * @param {Object} req
88 * @param {Object} res
89 * @param {Function} next
90 */
91 async refreshAuth(req, res, next) {
92 const client = await this.getClient(req);
93
94 // Perhaps the server was restarted or lost it's session for some other reason
95 3 if (!client) {
96 1 return res.end("No smart state found in session. Please re-authorize your app.")
97 }
98
99 2 return client.refresh().then(() => next(), error => res.end(error.message));
100 }
101 }
102 };
103

lib/hapi.js

100%
43
43
0
Line Lint Hits Source
1 1 const smart = require("./lib");
2
3
4 1 module.exports = function(options) {
5 const cfg = { getStorage: smart.getSessionStorage, ...options };
6
7 // Return the HAPI-specific high-level API
8 16 return {
9
10 /**
11 * Given a request object this function will return a Promise that will
12 * eventually be resolved with a Fhir Client object or with `null` if
13 * there is a problem. It will lookup the storage for stored SMART state.
14 * If such state is found, it will create and return a Fhir Client
15 * instance that knows how to use that (automatically authorize any
16 * requests made with `client.request`, use clientSecret and refresh
17 * tokens etc.).
18 * @param {Object} request
19 * @returns {Promise<Client|null>}
20 */
21 async getClient(request) {
22 const storage = cfg.getStorage(request);
23 7 const id = await storage.get("smartId");
24 7 if (id) {
25 5 const state = await storage.get(id);
26 5 if (state) {
27 4 return new smart.Client(state);
28 }
29 }
30 3 return null;
31 },
32
33 /**
34 * If the fhir server base can be determined (from query.iss or
35 * query.fhirServiceUrl or options.serverUrl), then the user will be
36 * redirected to the authorization endpoint (or to your redirectUri if
37 * it happens to be an open server). Otherwise, the `next` callback
38 * will be called.
39 * @param {Object} req
40 * @param {Object} h
41 * @returns {Promise<any>|Symbol}
42 */
43 authorize(req, h) {
44 let { launch, iss, fhirServiceUrl } = req.query;
45 8 if ((launch && iss) || fhirServiceUrl || cfg.serverUrl) {
46 7 return smart.buildAuthorizeUrl(req.raw.req, cfg, cfg.getStorage(req))
47 .then(url => h.redirect(url));
48 }
49 1 return h.continue;
50 },
51
52 /**
53 * The `authorize` function above will redirect the browser to the
54 * authorization endpoint of the SMART server. Depending on the requested
55 * scopes, the user might be asked to select a patient or practitioner,
56 * authorize the app and so on. Eventually, the user will be redirected
57 * back to your site at your `options.redirectUri`. At that point you
58 * should have received a code that needs to be exchanged for an access
59 * token.
60 * @param {Object} req
61 * @param {Object} h
62 * @returns {Promise<any>|Symbol}
63 */
64 completeAuth(req, h) {
65 let { code, state, error, error_description } = req.query;
66 5 if (error) {
67 2 throw new Error(error + (error_description ? ":\n" + error_description : ""));
68 }
69 3 if (code && state) {
70 2 return smart.completeAuth(req.raw.req, cfg.getStorage(req))
71 .then(() => h.redirect(cfg.redirectUri));
72 }
73 1 return h.continue;
74 },
75
76 /**
77 * You don't typically need to use this because if you have a refresh
78 * token (which you should if your scope includes `offline_access`),
79 * then the request method will automatically use it when your access
80 * token expires.
81 * @param {Object} request
82 * @param {Object} h
83 */
84 async refreshAuth(request, h) {
85 const client = await this.getClient(request);
86
87 // Perhaps the server was restarted or lost it's session for some other reason
88 3 if (!client) {
89 1 throw new Error("No smart state found in session. Please re-authorize your app.");
90 }
91
92 2 return client.refresh().then(() => h.continue, error => { throw error });
93 }
94 };
95 }
96
97

lib/lib.js

100%
229
229
0
Line Lint Hits Source
1 "use strict";
2 1 Object.defineProperty(exports, "__esModule", { value: true });
3 1 const axios_1 = require("axios");
4 1 const Crypto = require("crypto");
5 1 const Url = require("url");
6 1 const qs = require("querystring");
7 1 const _debug = require("debug");
8 1 const errors_1 = require("./errors");
9 1 const client_1 = require("./client");
10 1 exports.Client = client_1.default;
11 1 const debug = _debug("smart");
12 /**
13 * Walks thru an object (or array) and returns the value found at the
14 * provided path. This function is very simple so it intentionally does not
15 * support any argument polymorphism, meaning that the path can only be a
16 * dot-separated string. If the path is invalid returns undefined.
17 * @param {Object} obj The object (or Array) to walk through
18 * @param {String} path The path (eg. "a.b.4.c")
19 * @returns {*} Whatever is found in the path or undefined
20 */
21 function getPath(obj, path = "") {
22 63 path = path.trim();
23 63 if (!path) {
24 2 return obj;
25 }
26 61 return path.split(".").reduce((out, key) => out ? out[key] : undefined, obj);
27 }
28 1 exports.getPath = getPath;
29 /**
30 * Simplified version of printf. Just replaces all the occurrences of "%s" with
31 * whatever is supplied in the rest of the arguments. If no argument is supplied
32 * the "%s" token is replaced with an empty string.
33 * @param {String} s The string to format
34 * @param {*} ... The rest of the arguments are used for the replacements
35 * @return {String}
36 */
37 function printf(s, ...args) {
38 27 const l = args.length;
39 27 let i = 0;
40 27 return String(s || "").replace(/(%s)/g, () => i >= l ? "" : args[i++]);
41 }
42 1 exports.printf = printf;
43 /**
44 * Returns the error message for the given error name, replacing any "%s"
45 * occurrences with the rest of the given arguments
46 * @param name The error name
47 * @param rest Any other arguments passed to printf
48 */
49 function getErrorText(name = "", ...rest) {
50 22 return printf(errors_1.default[name], ...rest);
51 }
52 1 exports.getErrorText = getErrorText;
53 /**
54 * For custom error objects that also have an httpCode property
55 */
56 class HttpError extends Error {
57 constructor(name = "", httpCode = 500, ...params) {
58 super(getErrorText(name, ...params) || name);
59 15 this.httpCode = httpCode;
60 }
61 }
62 1 exports.HttpError = HttpError;
63 /**
64 * Given a fhir server returns an object with it's Oauth security endpoints
65 * @param baseUrl Fhir server base URL
66 */
67 async function getSecurityExtensions(baseUrl) {
68 28 const url = String(baseUrl || "").replace(/\/*$/, "/") + "metadata";
69 28 let metadata;
70 28 try {
71 28 metadata = await axios_1.default(url);
72 }
73 catch (ex) {
74 6 metadata = {};
75 }
76 28 const nsUri = "http://fhir-registry.smarthealthit.org/StructureDefinition/oauth-uris";
77 28 const extensions = (getPath(metadata, "data.rest.0.security.extension") || [])
78 .filter((e) => e.url === nsUri)
79 .map((o) => o.extension)[0];
80 28 const out = {
81 registrationUri: "",
82 authorizeUri: "",
83 tokenUri: ""
84 };
85 28 if (extensions) {
86 15 extensions.forEach((ext) => {
87 if (ext.url === "register") {
88 1 out.registrationUri = ext.valueUri;
89 }
90 31 if (ext.url === "authorize") {
91 15 out.authorizeUri = ext.valueUri;
92 }
93 31 if (ext.url === "token") {
94 15 out.tokenUri = ext.valueUri;
95 }
96 });
97 }
98 28 return out;
99 }
100 1 exports.getSecurityExtensions = getSecurityExtensions;
101 /**
102 * Converts the given relative URL to absolute based on the request origin
103 * @param req The http request object
104 * @param url The url to convert
105 */
106 function resolveUrl(req, url) {
107 48 const protocol = req.connection.encrypted ? "https" : "http";
108 48 return Url.resolve(protocol + "://" + req.headers.host, url);
109 }
110 1 exports.resolveUrl = resolveUrl;
111 /**
112 * First discovers the fhir server base URL from query.iis or query.fhirServiceUrl
113 * or options.serverUrl. Then compiles the proper authorization URL for that server.
114 * For open server that URL is the options.redirectUri so that we can skip the
115 * authorization part.
116 */
117 async function buildAuthorizeUrl(req, options, storage) {
118 26 const url = Url.parse(req.url, true);
119 26 const { launch, iss, fhirServiceUrl } = url.query;
120 26 const serverUrl = iss || fhirServiceUrl || options.serverUrl || "";
121 26 if (iss && !launch) {
122 1 throw new Error(getErrorText("missing_url_parameter", "launch"));
123 }
124 25 if (!serverUrl) {
125 1 debug("No serverUrl found. It must be specified as query.iss or " +
126 "query.fhirServiceUrl or options.serverUrl (in that order)");
127 1 throw new Error(getErrorText("no_server_url_provided"));
128 }
129 24 debug(`Looking up the authorization endpoint for "${serverUrl}"`);
130 24 const extensions = await getSecurityExtensions(serverUrl);
131 24 debug(`Found security extensions: `, extensions);
132 // Prepare the object that will be stored in the session
133 24 const state = Object.assign({ serverUrl: serverUrl, clientId: options.clientId, redirectUri: resolveUrl(req, options.redirectUri), scope: options.scope || "" }, extensions);
134 24 if (options.clientSecret) {
135 1 debug(`Adding clientSecret to the state`);
136 1 state.clientSecret = options.clientSecret;
137 }
138 // Create an unique key. We could use the session ID for that but we
139 // don't want it to be visible in the browser URL bar (as the state
140 // parameter) while redirecting.
141 24 const id = "smart-" + Crypto.randomBytes(8).toString("hex");
142 24 const oldId = await storage.get("smartId");
143 24 if (oldId) {
144 15 debug(`Deleting previous state by id ("${oldId}")`);
145 15 await storage.unset(oldId);
146 }
147 24 debug(`Saving new state by id ("${id}")`);
148 24 await storage.set(id, state);
149 24 await storage.set("smartId", id);
150 24 let redirectUrl = state.redirectUri;
151 24 if (state.authorizeUri) {
152 13 debug(`authorizeUri: ${state.authorizeUri}`);
153 13 const params = [
154 "response_type=code",
155 "client_id=" + encodeURIComponent(state.clientId),
156 "scope=" + encodeURIComponent(state.scope),
157 "redirect_uri=" + encodeURIComponent(state.redirectUri),
158 "aud=" + encodeURIComponent(state.serverUrl),
159 "state=" + id
160 ];
161 // also pass this in case of EHR launch
162 13 if (launch) {
163 6 params.push("launch=" + encodeURIComponent(launch));
164 }
165 13 redirectUrl = state.authorizeUri + "?" + params.join("&");
166 }
167 24 debug(`Making authorize redirect to ${redirectUrl}`);
168 24 return redirectUrl;
169 }
170 1 exports.buildAuthorizeUrl = buildAuthorizeUrl;
171 /**
172 * Calls the buildAuthorizeUrl function to construct the redirect URL and then
173 * just redirects to it.
174 */
175 async function authorize(req, res, options, storage) {
176 8 debug(`Authorizing...`);
177 8 const location = await buildAuthorizeUrl(req, options, storage);
178 8 debug(`Making authorize redirect to ${location}`);
179 8 res.writeHead(302, { location });
180 8 res.end();
181 }
182 1 exports.authorize = authorize;
183 /**
184 * Builds the token request options for axios. Does not make the request, just
185 * creates it's configuration and returns it in a Promise.
186 * NOTE that this function has side effects because it modifies the storage
187 * contents.
188 * @param req
189 * @param storage
190 */
191 async function buildTokenRequest(req, storage) {
192 12 const { state, code } = Url.parse(req.url, true).query;
193 12 const cached = await storage.get(state);
194 // First make sure we have such state and it contains everything we need
195 12 if (!cached) {
196 1 debug(`No state found by the given id (${state})`);
197 1 throw new HttpError("missing_state_by_id", 400, state);
198 }
199 11 if (!cached.redirectUri) {
200 1 debug(`Missing state.redirectUri`);
201 1 throw new HttpError("missing_state_redirectUri", 400);
202 }
203 10 if (!cached.tokenUri) {
204 1 debug(`Missing state.tokenUri`);
205 1 throw new HttpError("missing_state_tokenUri", 400);
206 }
207 9 if (!cached.clientId) {
208 1 debug(`Missing state.clientId`);
209 1 throw new HttpError("missing_state_clientId", 400);
210 }
211 8 const requestOptions = {
212 method: "POST",
213 url: cached.tokenUri,
214 headers: {
215 "content-type": "application/x-www-form-urlencoded"
216 },
217 data: {
218 code,
219 grant_type: "authorization_code",
220 redirect_uri: cached.redirectUri
221 }
222 };
223 // For public apps, authentication is not possible (and thus not
224 // required), since the app cannot be trusted to protect a secret.
225 // For confidential apps, an Authorization header using HTTP Basic
226 // authentication is required, where the username is the app’s client_id
227 // and the password is the app’s client_secret
228 8 if (cached.clientSecret) {
229 1 requestOptions.headers.Authorization = "Basic " + Buffer.from(cached.clientId + ":" + cached.clientSecret).toString("base64");
230 1 debug(`Using state.clientSecret to construct the authorization header: "${requestOptions.headers.Authorization}"`);
231 }
232 else {
233 7 debug(`No clientSecret found in state. Adding client_id to the POST body`);
234 7 requestOptions.data.client_id = cached.clientId;
235 }
236 8 return requestOptions;
237 }
238 1 exports.buildTokenRequest = buildTokenRequest;
239 /**
240 * Creates and returns an HttpError that should explain why the token request
241 * have failed
242 */
243 function handleTokenError(result) {
244 7 let msg = result.message;
245 // Some auth servers will respond with a payload that contains "error" and
246 // optionally an "error_description" properties
247 7 const body = getPath(result, "response.data");
248 7 if (body) {
249 5 if (body.error) {
250 2 msg += "\n" + body.error;
251 2 if (body.error_description) {
252 1 msg += ": " + body.error_description;
253 }
254 }
255 else {
256 3 msg += "\n" + body;
257 }
258 }
259 7 debug(msg + " - " + result.stack);
260 7 return new HttpError(msg, result.status);
261 }
262 1 exports.handleTokenError = handleTokenError;
263 /**
264 * After successful authorization we have received a code and state parameters.
265 * Use this function to exchange that code for an access token and complete the
266 * authorization flow.
267 */
268 async function completeAuth(req, storage) {
269 7 debug("Completing the code flow");
270 7 const { state } = Url.parse(req.url, true).query;
271 6 const cached = await storage.get(state);
272 6 const requestOptions = await buildTokenRequest(req, storage);
273 6 requestOptions.data = qs.stringify(requestOptions.data);
274 // The EHR authorization server SHALL return a JSON structure that
275 // includes an access token or a message indicating that the
276 // authorization request has been denied.
277 6 return axios_1.default(requestOptions)
278 .then(async ({ data }) => {
279 debug(`Received tokenResponse. Saving it to the state...`);
280 4 cached.tokenResponse = data;
281 4 return storage.set(state, cached);
282 })
283 .then(() => new client_1.default(cached))
284 .catch(result => {
285 throw handleTokenError(result);
286 });
287 }
288 1 exports.completeAuth = completeAuth;
289 /**
290 * The default storage factory assumes that the request object has a session
291 * property which is an object that we are free to manipulate. This happens to
292 * be the case for Express using `express-session` and for Hapi using
293 * hapi-server-session`.
294 */
295 function getSessionStorage(req) {
296 30 return {
297 /**
298 * Sets (adds or updates) a value at the given key
299 * @param {String} key
300 * @param {any} value
301 * @returns {Promise<any>} A promise resolved with the stored value
302 */
303 async set(key, value) {
304 req.session[key] = value;
305 30 return value;
306 },
307 /**
308 * Reads the value at the given key
309 * @param {String} key
310 * @returns {Promise<any>} A promise resolved with the stored value or undefined
311 */
312 async get(key) {
313 return req.session[key];
314 },
315 /**
316 * Deletes the value at the given key (if one exists)
317 * @param {String} key
318 * @returns {Promise<Boolean>} A promise resolved with true if the value
319 * was removed or with false otherwise
320 */
321 async unset(key) {
322 if (req.session.hasOwnProperty(key)) {
323 12 delete req.session[key];
324 12 return true;
325 }
326 2 return false;
327 }
328 };
329 }
330 1 exports.getSessionStorage = getSessionStorage;
331

Linting Report

Nothing to show here, linting is disabled.