From 45ef22f34af26cae754ab301083a74c8da65a3d8 Mon Sep 17 00:00:00 2001 From: spencerwooo Date: Fri, 31 Dec 2021 12:17:00 +0800 Subject: [PATCH] store access token and refresh token in the file system /tmp --- pages/api/index.ts | 38 ++++++++++++-------- pages/onedrive-vercel-index-oauth/step-3.tsx | 8 ++++- utils/accessTokenHandler.ts | 18 +++++----- utils/odAuthTokenStore.ts | 32 +++++++++++++++++ 4 files changed, 70 insertions(+), 26 deletions(-) create mode 100644 utils/odAuthTokenStore.ts diff --git a/pages/api/index.ts b/pages/api/index.ts index 77609a5..ea263c5 100644 --- a/pages/api/index.ts +++ b/pages/api/index.ts @@ -2,13 +2,12 @@ import { posix as pathPosix } from 'path' import type { NextApiRequest, NextApiResponse } from 'next' import axios from 'axios' -import Keyv from 'keyv' -import { KeyvFile } from 'keyv-file' import apiConfig from '../../config/api.json' import siteConfig from '../../config/site.json' import { revealObfuscatedToken } from '../../utils/accessTokenHandler' import { compareHashedToken } from '../../utils/protectedRouteHandler' +import { getOdAuthTokens, storeOdAuthTokens } from '../../utils/odAuthTokenStore' const basePath = pathPosix.resolve('/', apiConfig.base) const encodePath = (path: string) => { @@ -22,26 +21,27 @@ const encodePath = (path: string) => { const clientSecret = revealObfuscatedToken(apiConfig.obfuscatedClientSecret) -// Store access token in memory, cuz Vercel doesn't provide key-value storage natively -let _access_token = '' -let _refresh_token = '' -const getAccessToken = async () => { - if (_access_token) { - console.log('Fetch access token from memory.') - return _access_token +async function getAccessToken(): Promise { + const { accessToken, refreshToken } = await getOdAuthTokens() + + // Return in storage access token if it is still valid + if (typeof accessToken === 'string') { + console.log('Fetch access token from storage.') + return accessToken } - // Return if refresh_token is empty - if (!_refresh_token) { + // Return empty string if no refresh token is stored, which requires the application to be re-authenticated + if (typeof refreshToken !== 'string') { console.log('No refresh token, return empty access token.') return '' } + // Fetch new access token with in storage refresh token const body = new URLSearchParams() body.append('client_id', apiConfig.clientId) body.append('redirect_uri', apiConfig.redirectUri) body.append('client_secret', clientSecret) - body.append('refresh_token', _refresh_token) + body.append('refresh_token', refreshToken) body.append('grant_type', 'refresh_token') const resp = await axios.post(apiConfig.authApi, body, { @@ -50,10 +50,18 @@ const getAccessToken = async () => { }, }) - if (resp.data.access_token) { - _access_token = resp.data.access_token - return _access_token + if ('access_token' in resp.data && 'refresh_token' in resp.data) { + const { expires_in, access_token, refresh_token } = resp.data + await storeOdAuthTokens({ + accessToken: access_token, + accessTokenExpiry: parseInt(expires_in), + refreshToken: refresh_token, + }) + console.log('Fetch new access token with stored refresh token.') + return access_token } + + return '' } export default async function handler(req: NextApiRequest, res: NextApiResponse) { diff --git a/pages/onedrive-vercel-index-oauth/step-3.tsx b/pages/onedrive-vercel-index-oauth/step-3.tsx index 419645d..482b194 100644 --- a/pages/onedrive-vercel-index-oauth/step-3.tsx +++ b/pages/onedrive-vercel-index-oauth/step-3.tsx @@ -5,7 +5,9 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import siteConfig from '../../config/site.json' import Navbar from '../../components/Navbar' import Footer from '../../components/Footer' + import { requestTokenWithAuthCode } from '../../utils/accessTokenHandler' +import { storeOdAuthTokens } from '../../utils/odAuthTokenStore' export default function OAuthStep3({ accessToken, refreshToken, error, description, errorUri }) { return ( @@ -118,7 +120,7 @@ export async function getServerSideProps({ query }) { return { props: { error: 'No auth code present', - description: 'Where is the auth code??? Did you follow step 2 you silly donut?', + description: 'Where is the auth code? Did you follow step 2 you silly donut?', }, } } @@ -137,6 +139,10 @@ export async function getServerSideProps({ query }) { } const { expiryTime, accessToken, refreshToken } = response + + // We can safely leverage Vercel's /tmp directory, and persist the tokens with file-system based KV storage + await storeOdAuthTokens({ accessToken, accessTokenExpiry: parseInt(expiryTime), refreshToken }) + return { props: { error: null, diff --git a/utils/accessTokenHandler.ts b/utils/accessTokenHandler.ts index 5053f68..5a99fea 100644 --- a/utils/accessTokenHandler.ts +++ b/utils/accessTokenHandler.ts @@ -1,24 +1,22 @@ import axios from 'axios' import CryptoJS from 'crypto-js' -import Keyv from 'keyv' -import KeyvFile from 'keyv-file' import apiConfig from '../config/api.json' +// Just a disguise to obfuscate the client secret, used along with the following two functions const AES_SECRET_KEY = 'onedrive-vercel-index' - -export function obfuscateToken(token: string): string { +function obfuscateToken(token: string): string { // Encrypt token with AES const encrypted = CryptoJS.AES.encrypt(token, AES_SECRET_KEY) return encrypted.toString() } - export function revealObfuscatedToken(obfuscated: string): string { // Decrypt SHA256 obfuscated token const decrypted = CryptoJS.AES.decrypt(obfuscated, AES_SECRET_KEY) return decrypted.toString(CryptoJS.enc.Utf8) } +// Generate the Microsoft OAuth 2.0 authorization URL, used for requesting the authorisation code export function generateAuthorisationUrl(): string { const { clientId, redirectUri, authApi } = apiConfig const authUrl = authApi.replace('/token', '/authorize') @@ -34,6 +32,8 @@ export function generateAuthorisationUrl(): string { return `${authUrl}?${params.toString()}` } +// The code returned from the Microsoft OAuth 2.0 authorization URL is a request URL with hostname +// http://localhost and URL parameter code. This function extracts the code from the request URL export function extractAuthCodeFromRedirected(url: string): string { // Return empty string if the url is not the defined redirect uri if (!url.startsWith(apiConfig.redirectUri)) { @@ -45,6 +45,9 @@ export function extractAuthCodeFromRedirected(url: string): string { return params.get('code') || '' } +// After a successful authorisation, the code returned from the Microsoft OAuth 2.0 authorization URL +// will be used to request an access token. This function requests the access token with the authorisation code +// and returns the access token and refresh token on success. export async function requestTokenWithAuthCode( code: string ): Promise< @@ -78,8 +81,3 @@ export async function requestTokenWithAuthCode( return { error, errorDescription: error_description, errorUri: error_uri } }) } - -export function storeTokens(accessToken: string, refreshToken: string) { - // We can safely leverage Vercel's /tmp directory, and persist the tokens with file-system based KV storage - const kv = new Keyv({ store: new KeyvFile(), namespace: 'onedrive-vercel-index' }) -} diff --git a/utils/odAuthTokenStore.ts b/utils/odAuthTokenStore.ts new file mode 100644 index 0000000..462d701 --- /dev/null +++ b/utils/odAuthTokenStore.ts @@ -0,0 +1,32 @@ +// This should be only used on the server side, where the tokens are stored with KV store using +// a file system based storage. The tokens are stored in the file system as JSON at /tmp path. + +import Keyv from 'keyv' +import { KeyvFile } from 'keyv-file' + +const kv = new Keyv({ + store: new KeyvFile(), +}) + +export async function storeOdAuthTokens({ + accessToken, + accessTokenExpiry, + refreshToken, +}: { + accessToken: string + accessTokenExpiry: number + refreshToken: string +}): Promise { + await kv.set('access_token', accessToken, accessTokenExpiry) + await kv.set('refresh_token', refreshToken) +} + +export async function getOdAuthTokens(): Promise<{ accessToken: unknown; refreshToken: unknown }> { + const accessToken = await kv.get('access_token') + const refreshToken = await kv.get('refresh_token') + + return { + accessToken, + refreshToken, + } +}