/* This file is part of the Notesnook project (https://notesnook.com/) Copyright (C) 2022 Streetwriters (Private) Limited This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ import http from "../utils/http"; import constants from "../utils/constants"; import { EV, EVENTS } from "../common"; import { withTimeout, Mutex } from "async-mutex"; import { logger } from "../logger"; const ENDPOINTS = { token: "/connect/token", revoke: "/connect/revocation", temporaryToken: "/account/token", logout: "/account/logout" }; class TokenManager { /** * * @param {import("../database/storage").default} storage */ constructor(storage) { this._storage = storage; this._refreshTokenMutex = withTimeout(new Mutex(), 10 * 1000); this.logger = logger.scope("TokenManager"); } async getToken(renew = true, forceRenew = false) { let token = await this._storage.read("token"); if (!token) return; this.logger.info("Access token requested", { accessToken: token.access_token.slice(0, 10) }); if (forceRenew || (renew && this._isTokenExpired(token))) { await this._refreshToken(forceRenew); return await this.getToken(); } return token; } _isTokenExpired(token) { const { t, expires_in } = token; const expiryMs = t + expires_in * 1000; return Date.now() >= expiryMs; } async getAccessToken(forceRenew = false) { return await getSafeToken(async () => { const token = await this.getToken(true, forceRenew); if (!token) return; return token.access_token; }, "Error getting access token:"); } async _refreshToken(forceRenew = false) { await this._refreshTokenMutex.runExclusive(async () => { this.logger.info("Refreshing access token"); const token = await this.getToken(false, false); if (!forceRenew && !this._isTokenExpired(token)) { return; } const { refresh_token, scope } = token; if (!refresh_token || !scope) return; const refreshTokenResponse = await await http.post( `${constants.AUTH_HOST}${ENDPOINTS.token}`, { refresh_token, grant_type: "refresh_token", scope: scope, client_id: "notesnook" } ); await this.saveToken(refreshTokenResponse); EV.publish(EVENTS.tokenRefreshed); }); } async revokeToken() { const token = await this.getToken(); if (!token) return; const { access_token } = token; await http.post( `${constants.AUTH_HOST}${ENDPOINTS.logout}`, null, access_token ); } saveToken(tokenResponse) { let token = { ...tokenResponse, t: Date.now() }; return this._storage.write("token", token); } async getAccessTokenFromAuthorizationCode(userId, authCode) { return await this.saveToken( await http.post(`${constants.AUTH_HOST}${ENDPOINTS.temporaryToken}`, { authorization_code: authCode, user_id: userId, client_id: "notesnook" }) ); } } export default TokenManager; async function getSafeToken(action, errorMessage) { try { return await action(); } catch (e) { console.error(errorMessage, e); if (e.message === "invalid_grant" || e.message === "invalid_client") { EV.publish(EVENTS.userSessionExpired); } throw e; } }