UNPKG

tesjs

Version:

A module to streamline the use of Twitch EventSub in Node.js and Web applications

140 lines (124 loc) 5.31 kB
// Copyright (c) 2022 Mitchell Adair // // This software is released under the MIT License. // https://opensource.org/licenses/MIT const _browser = typeof global !== "undefined" ? global : typeof window !== "undefined" ? window : {}; const fetch = _browser.fetch || require("node-fetch"); const logger = require("./logger"); const AUTH_API_URL = "https://id.twitch.tv/oauth2"; class AuthManager { constructor({ clientID, clientSecret, onAuthFailure, initialToken, refreshToken }) { this._isWebClient = typeof window !== "undefined"; if (!onAuthFailure) { if (!this._isWebClient && (!clientID || !clientSecret)) { throw new Error("AuthManager config must contain client ID and secret if onAuthFailure not defined"); } } if (AuthManager._instance) { return AuthManager._instance; } AuthManager._instance = this; this._clientID = clientID; this._clientSecret = clientSecret; this._validationInterval; this._customRefresh = onAuthFailure; this._refreshToken = refreshToken; if (initialToken) { this._authToken = initialToken; this._resetValidationInterval(); } else { this.refreshToken(); } } static getInstance() { return AuthManager._instance; } /** * Gets the current authentication token. This will wait until the * auth token exists before returning. The auth token will be undefined * in the cases of app startup (until initial fetch/refresh) and token * refresh. If getting the token takes longer than 1000 seconds, * something catastrophic is up and it will reject. * * @returns a promise that resolves the current token */ getToken() { return new Promise((resolve, reject) => { const start = new Date(); const retry = () => { if (this._authToken) { resolve(this._authToken); } else if (new Date() - start > 1000000) { const message = "Timed out trying to get token"; logger.error(`${message}. Something catastrophic has happened!`); reject(message); } else { setTimeout(retry); } }; retry(); }); } /** * Refreshes the authentication token */ async refreshToken() { logger.debug("Getting new app access token"); try { this._authToken = undefined; // set current token undefined to prevent API calls from using stale token // if we have a custom refresh function passed through onAuthenticationFailure, we will use that if (this._customRefresh) { // Support webclients that have custom refreshes (must have a phone home server) // we want to wait for this function to finish executing before continuing // as if a getToken is executing near the same time, this._authToken will be the value of a promise wrapper // and thus getToken will resolve improperly. const tempTokenSave = await this._customRefresh(); this._authToken = tempTokenSave; } else if (this._isWebClient) { // If a custom refresh is not provided, throw an error. throw new Error("cannot refresh access token on web client"); } else { let refreshSnippet = ""; let grantType = "client_credentials"; if (this._refreshToken) { grantType = "refresh_token"; refreshSnippet = `&refresh_token=${this._refreshToken}`; } const res = await fetch( `${AUTH_API_URL}/token?client_id=${this._clientID}&client_secret=${this._clientSecret}&grant_type=${grantType}${refreshSnippet}`, { method: "POST" } ); if (res.ok) { const { access_token, refresh_token } = await res.json(); this._authToken = access_token; this._refreshToken = refresh_token; this._resetValidationInterval(); } else { const { message } = await res.json(); throw new Error(message); } } } catch (err) { logger.error(`Error refreshing app access token: ${err.message}`); throw err; } } async _validateToken() { logger.debug("Validating app access token"); const headers = { Authorization: `Bearer ${this._authToken}`, }; const res = await fetch(`${AUTH_API_URL}/validate`, { headers }); if (res.status === 401) { logger.debug("Access token not valid, refreshing..."); this.refreshToken(); } } _resetValidationInterval() { clearInterval(this._validationInterval); if (!this._isWebClient) { this._validationInterval = setInterval(this._validateToken.bind(this), 3600000); } } } module.exports = AuthManager;