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
JavaScript
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