Skip to content

Commit

Permalink
Improved error handling #51
Browse files Browse the repository at this point in the history
  • Loading branch information
vlad-ignatov committed Jun 15, 2021
1 parent 89224f8 commit 7950140
Show file tree
Hide file tree
Showing 18 changed files with 1,143 additions and 1,050 deletions.
35 changes: 15 additions & 20 deletions src/AuthorizeHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@
const jwt = require("jsonwebtoken");
const base64url = require("base64-url");
const Url = require("url");
const util = require("util")
const ScopeSet = require("./ScopeSet");
const config = require("./config");
const Codec = require("../static/codec.js");
const Lib = require("./lib");
const SMARTHandler = require("./SMARTHandler");
const errors = require("./errors");


class AuthorizeHandler extends SMARTHandler {

Expand Down Expand Up @@ -253,29 +256,24 @@ class AuthorizeHandler extends SMARTHandler {

const missingParam = Lib.getFirstMissingProperty(req.query, requiredParams);
if (missingParam) {
if (missingParam == "redirect_uri") {
Lib.replyWithError(res, "missing_parameter", 400, missingParam);
}
else {
Lib.redirectWithError(req, res, "missing_parameter", missingParam);
}
return false;

// If redirect_uri is the missing param reply with OAuth.
// Otherwise redirect and pass error params to the redirect uri.
throw new Lib.OAuthError(missingParam == "redirect_uri" ? 400 : 302, util.format("Missing %s parameter", missingParam), "invalid_request")
}

// bad_redirect_uri if we cannot parse it
let RedirectURL;
try {
RedirectURL = Url.parse(decodeURIComponent(req.query.redirect_uri), true);
} catch (ex) {
Lib.replyWithError(res, "bad_redirect_uri", 400, ex.message);
return false;
throw Lib.OAuthError.from(errors.authorization_code.bad_redirect_uri, ex.message)
}

// Relative redirect_uri like "whatever" will eventually result in wrong
// URLs like "/auth/whatever". We must only support full URLs.
if (!RedirectURL.protocol) {
Lib.replyWithError(res, "no_redirect_uri_protocol", 400, req.query.redirect_uri);
return false;
throw Lib.OAuthError.from(errors.authorization_code.no_redirect_uri_protocol, req.query.redirect_uri)
}

// The "aud" param must match the apiUrl (but can have different protocol)
Expand All @@ -284,8 +282,7 @@ class AuthorizeHandler extends SMARTHandler {
let a = Lib.normalizeUrl(req.query.aud).replace(/^https?/, "").replace(/^:\/\/localhost/, "://127.0.0.1");
let b = Lib.normalizeUrl(apiUrl ).replace(/^https?/, "").replace(/^:\/\/localhost/, "://127.0.0.1");
if (a != b) {
Lib.redirectWithError(req, res, "bad_audience");
return false;
throw new Lib.OAuthError(302, "Bad audience value", "invalid_request")
}
sim.aud_validated = "1";
}
Expand All @@ -309,28 +306,26 @@ class AuthorizeHandler extends SMARTHandler {

// User decided not to authorize the app launch
if (req.query.auth_success == "0") {
return Lib.redirectWithError(req, res, "unauthorized");
throw Lib.OAuthError.from(errors.authorization_code.unauthorized)
}

// Simulate auth_invalid_client_id error if requested
if (sim.auth_error == "auth_invalid_client_id") {
return Lib.redirectWithError(req, res, "sim_invalid_client_id");
throw Lib.OAuthError.from(errors.authorization_code.sim_invalid_client_id)
}

// Simulate auth_invalid_redirect_uri error if requested
if (sim.auth_error == "auth_invalid_redirect_uri") {
return Lib.redirectWithError(req, res, "sim_invalid_redirect_uri");
throw Lib.OAuthError.from(errors.authorization_code.sim_invalid_redirect_uri)
}

// Simulate auth_invalid_scope error if requested
if (sim.auth_error == "auth_invalid_scope") {
return Lib.redirectWithError(req, res, "sim_invalid_scope");
throw Lib.OAuthError.from(errors.authorization_code.sim_invalid_scope)
}

// Validate query parameters
if (!this.validateParams()) {
return;
}
this.validateParams();

// PATIENT LOGIN SCREEN
if (this.needToLoginAsPatient()) {
Expand Down
52 changes: 52 additions & 0 deletions src/OperationOutcome.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@

const RE_GT = />/g;
const RE_LT = /</g;
const RE_AMP = /&/g;
const RE_QUOT = /"/g;

function htmlEncode(html) {
return String(html)
.trim()
.replace(RE_AMP , "&amp;")
.replace(RE_LT , "&lt;")
.replace(RE_GT , "&gt;")
.replace(RE_QUOT, "&quot;");
}

class OperationOutcome
{
/**
*
* @param {string} message
* @param {"fatal" | "error" | "warning" | "information"|string} [severity]
* @param {string} [issueCode] see http://hl7.org/fhir/valueset-issue-type.html
*/
constructor(message, issueCode = "processing", severity = "error")
{
this.message = message
this.issueCode = issueCode
this.severity = severity
}

toJSON()
{
return {
"resourceType": "OperationOutcome",
"text": {
"status": "generated",
"div": '<div xmlns="http://www.w3.org/1999/xhtml"><h1>Operation Outcome</h1>' +
'<table border="0"><tr><td style="font-weight:bold;">ERROR</td><td>[]</td>' +
'<td><pre>' + htmlEncode(this.message) + '</pre></td></tr></table></div>'
},
"issue": [
{
"severity" : this.severity,
"code" : this.issueCode,
"diagnostics": this.message
}
]
}
}
}

module.exports = OperationOutcome
97 changes: 35 additions & 62 deletions src/RegistrationHandler.js
Original file line number Diff line number Diff line change
@@ -1,67 +1,40 @@
const jwt = require("jsonwebtoken");
const config = require("./config");
const SMARTHandler = require("./SMARTHandler");
const Lib = require("./lib");

class RegistrationHandler extends SMARTHandler {

static handleRequest(req, res) {
return new RegistrationHandler(req, res).handle();
const jwt = require("jsonwebtoken")
const config = require("./config")
const errors = require("./errors")

/** @type any */
const assert = require("./lib").assert

module.exports = function handleRegistration(req, res) {

// Require "application/x-www-form-urlencoded" POSTs
assert(req.is("application/x-www-form-urlencoded"), errors.form_content_type_required)

// parse and validate the "iss" parameter
let iss = String(req.body.iss || "").trim()
assert(iss, errors.registration.missing_param, "iss")

// parse and validate the "pub_key" parameter
let publicKey = String(req.body.pub_key || "").trim()
assert(publicKey, errors.registration.missing_param, "pub_key")

// parse and validate the "dur" parameter
let dur = parseInt(req.body.dur || "15", 10)
assert(!isNaN(dur) && isFinite(dur) && dur >= 0, errors.registration.invalid_param, "dur")

// Build the result token
let jwtToken = { pub_key: publicKey, iss }

// Note that if dur is 0 accessTokensExpireIn will not be included
if (dur) {
jwtToken.accessTokensExpireIn = dur
}

handle() {
const req = this.request;
const res = this.response;

// Require "application/x-www-form-urlencoded" POSTs
if (!req.headers["content-type"] || req.headers["content-type"].indexOf("application/x-www-form-urlencoded") !== 0) {
return Lib.replyWithError(res, "form_content_type_required", 401);
}

this.handleBackendServiceRegistration();
// Custom errors (if any)
if (req.body.auth_error) {
jwtToken.auth_error = req.body.auth_error
}

handleBackendServiceRegistration() {
const req = this.request;
const res = this.response;

// parse and validate the "iss" parameter
let iss = String(req.body.iss || "").trim();
if (!iss) {
return Lib.replyWithError(res, "missing_parameter", 400, "iss");
}

// parse and validate the "pub_key" parameter
let publicKey = String(req.body.pub_key || "").trim();
if (!publicKey) {
return Lib.replyWithError(res, "missing_parameter", 400, "pub_key");
}

// parse and validate the "dur" parameter
let dur = parseInt(req.body.dur || "15", 10);
if (isNaN(dur) || !isFinite(dur) || dur < 0) {
return Lib.replyWithError(res, "invalid_parameter", 400, "dur");
}

// Build the result token
let jwtToken = {
pub_key: publicKey,
iss
};

// Note that if dur is 0 accessTokensExpireIn will not be included
if (dur) {
jwtToken.accessTokensExpireIn = dur;
}

// Custom errors (if any)
if (req.body.auth_error) {
jwtToken.auth_error = req.body.auth_error;
}

// Reply with signed token as text
res.type("text").send(jwt.sign(jwtToken, config.jwtSecret));
}
// Reply with signed token as text
res.type("text").send(jwt.sign(jwtToken, config.jwtSecret))
}

module.exports = RegistrationHandler;
7 changes: 0 additions & 7 deletions src/ScopeSet.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
const config = require("./config");

/**
* This class tries to make it easier and cleaner to work with scopes (mostly by
* using the two major methods - "has" and "matches").
Expand Down Expand Up @@ -88,11 +86,6 @@ class ScopeSet
*/
static getInvalidSystemScopes(scopes) {
scopes = String(scopes || "").trim();

if (!scopes) {
return config.errors.missing_scope;
}

return scopes.split(/\s+/).find(s => !(
/^system\/(\*|[A-Z][a-zA-Z]+)(\.(read|write|\*))?$/.test(s)
)) || "";
Expand Down
Loading

0 comments on commit 7950140

Please sign in to comment.