UNPKG

passport-telegram-official

Version:

Telegram (not official, just the name) authentication strategy for Passport (https://core.telegram.org/widgets/login)

213 lines (176 loc) 5.53 kB
var crypto = require('crypto'); var passportStrategy = require('passport-strategy'); function _interopNamespace(e) { if (e && e.__esModule) return e; var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n['default'] = e; return n; } var crypto__namespace = /*#__PURE__*/_interopNamespace(crypto); function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } function deferPromise() { let resolve; let reject; const promise = new Promise((_resolve, _reject) => { resolve = _resolve; reject = _reject; }); return { then: f => promise.then(f), callback: (err, ...data) => err ? reject(err) : resolve(data), promise }; } function normalizeProfile(profile) { const normalizedProfile = _extends({}, profile, { provider: 'telegram', displayName: profile.username, name: { givenName: profile.first_name, familyName: profile.last_name }, photos: profile.photo_url ? [{ value: profile.photo_url }] : [] }); return normalizedProfile; } const defaultOptions = { queryExpiration: 86400, passReqToCallback: false }; const whitelistParams = ['id', 'first_name', 'last_name', 'username', 'photo_url', 'auth_date']; /** * `TelegramStrategy` constructor. * * The Telegram authentication strategy authenticates requests by delegating to * Telegram using their protocol: https://core.telegram.org/widgets/login * * Applications must supply a `verify` callback which accepts an `account` object, * and then calls `done` callback sypplying a `user`, which should be set to `false` if the * credentials are not valid. If an exception occurred, `error` should be set. * * More info here: https://core.telegram.org/widgets/login * * @param {Object} options * @param {Function} verify * @example * passport.use(new TelegramStrategy({ * botId: 12434151 * }), (user) => { * User.findOrCreate({telegramId: user.id}, done); * }); */ class TelegramStrategy extends passportStrategy.Strategy { constructor(options, verify) { super(); this.name = 'telegram'; this.options = void 0; this.verify = void 0; this.hashedBotToken = void 0; if (!options.botToken) { throw new TypeError('options.botToken is required in TelegramStrategy'); } if (!verify) { throw new TypeError('LocalStrategy requires a verify callback'); } this.options = _extends({}, defaultOptions, options); this.verify = verify; this.hashedBotToken = this.getBotToken(); } // eslint-disable-next-line consistent-return authenticate(req, options) { const query = req.method === 'GET' ? req.query : req.body; try { const validationResult = this.validateQuery(req); if (validationResult !== true) { return validationResult; } const profile = normalizeProfile(query); const promise = deferPromise(); if (this.options.passReqToCallback) { this.verify(req, profile, promise.callback); } else { this.verify(profile, promise.callback); } promise.then(([user, info]) => { if (!user) { return this.fail(info); } return this.success(user, info); }).catch(err => { return this.error(err); }); } catch (e) { return this.error(e); } } /** * Function to check if provided date in callback is outdated * @returns {number} */ getTimestamp() { return Math.floor(Date.now() / 1000); } // We have to hash botToken too getBotToken() { return crypto__namespace.createHash('sha256').update(this.options.botToken).digest(); } /** * Used to validate if fields like telegram must send are exists * @param {e.Request} req * @returns {any} */ validateQuery(req) { const query = req.method === 'GET' ? req.query : req.body; if (!query.auth_date || !query.hash || !query.id) { return this.fail({ message: 'Missing some important data' }, 400); } const authDate = Math.floor(Number(query.auth_date)); if (this.options.queryExpiration !== -1 && (Number.isNaN(authDate) || this.getTimestamp() - authDate > this.options.queryExpiration)) { return this.fail({ message: 'Data is outdated' }, 400); } const sorted = Object.keys(query).sort(); const mapped = sorted // Only whitelisted query parameters must be mapped .filter(d => whitelistParams.includes(d)).map(key => `${key}=${query[key]}`); const hashString = mapped.join('\n'); const hash = crypto__namespace.createHmac('sha256', this.hashedBotToken).update(hashString).digest('hex'); if (hash !== query.hash) { return this.fail({ message: 'Hash validation failed' }, 403); } return true; } } exports.TelegramStrategy = TelegramStrategy; exports['default'] = TelegramStrategy; //# sourceMappingURL=index.js.map