realm-object-server
Version:
294 lines • 12.4 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const crypto = require("crypto");
const moment = require("moment");
const jwt = require("jsonwebtoken");
const jsonpath = require("jsonpath");
const Token_1 = require("./Token");
const errors = require("../errors");
const realms_1 = require("../realms");
const util_1 = require("./util");
class TokenValidator {
constructor({ logger, publicKey, realmFactory, disableRevocation = false, refreshTokenValidators = [], }) {
this.logger = logger;
this.publicKey = publicKey;
this.realmFactory = realmFactory;
this.disableRevocation = disableRevocation;
this.refreshTokenValidators = refreshTokenValidators;
}
start() {
return __awaiter(this, void 0, void 0, function* () {
if (!this.disableRevocation) {
this.tokenRevocationRealm = yield this.realmFactory.open(realms_1.TokenRevocationRealm);
}
this.openAdminRealm().catch();
this.logger.detail("TokenValidator started");
this.hasStarted = true;
});
}
stop() {
if (this.tokenRevocationRealm) {
this.tokenRevocationRealm.close();
this.tokenRevocationRealm = null;
}
if (this.adminRealm) {
this.adminRealm.close();
this.adminRealm = null;
}
this.logger.detail("TokenValidator stopped");
}
parse(tokenData, options = {}) {
if (!tokenData || tokenData === "") {
throw new Error("The tokenString parameter was not provided or is blank");
}
let token;
if (this.isLegacyToken(tokenData)) {
token = this.parseLegacyToken(tokenData, options.ignoreExpiration);
}
else {
token = this.parseJWT(tokenData, options.ignoreExpiration);
}
if (options.mustBeAdmin && !this.isAdminToken(token)) {
throw new errors.realm.InvalidCredentials({
title: "Expected an admin"
});
}
if (util_1.getValueOrDefault(options.checkRevocation, true) && this.isTokenRevoked(token)) {
this.logger.detail(`Denying access for user with Id: ${token.identity}, because token '${token.getRevocationId()}' is revoked.`);
throw new errors.realm.AccessDenied({
title: "The token has been revoked"
});
}
return token;
}
isAdminToken(token) {
if (!token) {
return false;
}
if ("isAdminToken" in token) {
return token.isAdminToken();
}
if ("isAdmin" in token) {
const refreshToken = token;
if (this.adminRealm) {
const user = this.adminRealm.objectForPrimaryKey("User", refreshToken.identity);
return (user && user.isAdmin) || false;
}
return refreshToken.isAdmin;
}
return false;
}
isTokenRevoked(token) {
if (this.disableRevocation) {
return false;
}
if (!this.hasStarted) {
return false;
}
if (this.tokenRevocationRealm) {
if (this.tokenRevocationRealm.isClosed) {
this.logger.warn("TokenValidator couldn't check if token was revoked: The TokenRevocationRealm was closed. Trying to reopen.");
this.tokenRevocationRealm = null;
this.realmFactory.open(realms_1.TokenRevocationRealm).then(r => {
this.logger.warn("TokenValidator reopened the TokenRevocationRealm.");
this.tokenRevocationRealm = r;
}).catch((err) => {
this.logger.warn("TokenValidator couldn't open the TokenRevocationRealm:" + err, err);
this.tokenRevocationRealm = null;
});
}
else {
const revocation = this.tokenRevocationRealm.objectForPrimaryKey("TokenRevocation", token.getRevocationId());
return !!revocation;
}
}
if (!token.canSkipRevocationCheck) {
this.logger.warn("TokenValidator couldn't check if token was revoked: The TokenRevocationRealm wasn't opened.");
}
return false;
}
openAdminRealm() {
return __awaiter(this, void 0, void 0, function* () {
try {
this.adminRealm = yield this.realmFactory.open(realms_1.AdminRealm);
}
catch (err) {
this.logger.warn("Failed to open Admin Realm in token validator. Retrying...");
this.openAdminRealm().catch();
}
});
}
isLegacyToken(tokenString) {
return tokenString.split(":").length === 2;
}
parseLegacyToken(tokenString, ignoreExpiration) {
const parts = tokenString.split(":");
const tokenBase64 = parts[0];
const dataBuffer = Buffer.from(tokenBase64, "base64");
const signatureBase64 = parts[1];
const verify = crypto.createVerify("RSA-SHA256");
const signatureBuffer = Buffer.from(signatureBase64, "base64");
verify.update(dataBuffer);
if (!verify.verify(this.publicKey, signatureBuffer)) {
throw new Error("The token data could not be verified against its signature");
}
const tokenData = JSON.parse(dataBuffer.toString());
if (tokenData.expires) {
const expiresAt = moment.unix(tokenData.expires);
if (expiresAt.isBefore(moment()) && !ignoreExpiration) {
throw new Error("The token has expired");
}
}
const common = {
identity: tokenData.identity,
appId: tokenData.app_id,
expires: tokenData.expires,
access: tokenData.access || [],
salt: tokenData.salt,
canSkipRevocationCheck: tokenData.canSkipRevocationCheck || false,
tokenId: null,
};
if (common.access.indexOf("refresh") >= 0) {
return new Token_1.RefreshToken(Object.assign(common, {
isAdmin: (tokenData.is_admin === true),
isEmailConfirmed: (tokenData.isEmailConfirmed === true)
}));
}
return new Token_1.AccessToken(Object.assign(common, {
path: tokenData.path,
syncLabel: tokenData.sync_label,
}));
}
parseJWT(tokenString, ignoreExpiration) {
const decoded = jwt.decode(tokenString);
if (!decoded) {
throw new errors.realm.AccessDenied({
title: "Provided token is not a valid JWT."
});
}
const issuer = decoded.iss;
if (issuer === "realm") {
try {
return this.parseRealmToken(tokenString, ignoreExpiration);
}
catch (e) {
if (e instanceof jwt.TokenExpiredError && (decoded.access || []).indexOf("refresh") > 0) {
throw new errors.realm.ExpiredRefreshToken({ detail: e.message });
}
this.logger.debug("Error parsing realm token", { token: decoded, error: e });
throw new errors.realm.InvalidCredentials({ title: e.message });
}
}
const customValidator = this.refreshTokenValidators.find(v => v.issuer === issuer);
if (!customValidator) {
throw new errors.realm.InvalidCredentials({
title: "Invalid token issuer",
detail: `Token issued by unknown issuer: ${issuer}`
});
}
try {
return this.parseCustomToken(customValidator, tokenString, ignoreExpiration);
}
catch (e) {
if (e instanceof jwt.TokenExpiredError) {
throw new errors.realm.ExpiredRefreshToken({ detail: e.message });
}
this.logger.debug("Error parsing custom refresh token", { validator: customValidator, token: decoded, error: e });
throw new errors.realm.InvalidCredentials({ title: e.message });
}
}
parseRealmToken(tokenString, ignoreExpiration) {
const tokenData = jwt.verify(tokenString, this.publicKey, {
algorithms: ["RS256"],
ignoreExpiration,
audience: "realm",
issuer: "realm"
});
if (tokenData.stitch_data) {
tokenData.path = tokenData.path || tokenData.stitch_data.realm_path;
tokenData.access = tokenData.access || tokenData.stitch_data.realm_access;
tokenData.syncLabel = tokenData.syncLabel || tokenData.stitch_data.realm_sync_label;
}
const common = {
identity: tokenData.sub,
appId: tokenData.appId,
expires: tokenData.exp,
access: tokenData.access || [],
canSkipRevocationCheck: tokenData.canSkipRevocationCheck || false,
tokenId: tokenData.jti,
};
if (common.access.indexOf("refresh") >= 0) {
return new Token_1.RefreshToken(Object.assign(common, {
isAdmin: tokenData.isAdmin === true,
isEmailConfirmed: tokenData.isEmailConfirmed === true
}));
}
return new Token_1.AccessToken(Object.assign(common, {
path: tokenData.path,
syncLabel: tokenData.syncLabel,
}));
}
parseCustomToken(customValidator, tokenString, ignoreExpiration) {
const tokenData = jwt.verify(tokenString, customValidator.publicKey, {
algorithms: customValidator.algorithms,
ignoreExpiration,
audience: customValidator.audience,
issuer: customValidator.issuer,
});
if (customValidator.requiredClaims) {
for (const rc in customValidator.requiredClaims) {
if (jsonpath.value(tokenData, rc) !== customValidator.requiredClaims[rc]) {
throw new errors.realm.AccessDenied({
detail: "The payload does not contain the required claims",
});
}
}
}
const userId = tokenData[customValidator.userIdFieldName || "sub"];
const isAdmin = customValidator.isAdminQuery ? (jsonpath.value(tokenData, customValidator.isAdminQuery.path) === customValidator.isAdminQuery.value) : false;
this.ensureUserExists({
isAdmin, userId
});
return new Token_1.RefreshToken({
appId: "",
canSkipRevocationCheck: false,
expires: tokenData.exp,
identity: userId,
isAdmin,
tokenId: tokenData.jti,
});
}
ensureUserExists(user) {
const encodedUserId = encodeURIComponent(user.userId);
if (user.userId !== encodedUserId) {
throw new errors.realm.InvalidParameters({
name: "userId",
reason: "Realm requires that the userId does not require URI encoding."
});
}
if (this.adminRealm) {
let existing = this.adminRealm.objectForPrimaryKey("User", user.userId);
if (!existing) {
this.adminRealm.beginTransaction();
existing = this.adminRealm.objectForPrimaryKey("User", user.userId);
if (existing) {
this.adminRealm.cancelTransaction();
}
else {
this.adminRealm.create("User", user);
this.adminRealm.commitTransaction();
}
}
}
}
}
exports.TokenValidator = TokenValidator;
//# sourceMappingURL=TokenValidator.js.map