UNPKG

realm-object-server

Version:

Realm Object Server

294 lines 12.4 kB
"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