From 72153cc8527a352fee97be1ce64703ee03735ca0 Mon Sep 17 00:00:00 2001 From: spencerwooo Date: Fri, 31 Dec 2021 15:17:39 +0800 Subject: [PATCH] store access token and refresh token in serverless mem --- config/api.json | 1 - config/site.json | 1 + pages/api/index.ts | 36 +++++++++--- pages/onedrive-vercel-index-oauth/step-1.tsx | 2 +- pages/onedrive-vercel-index-oauth/step-2.tsx | 22 ++++++-- pages/onedrive-vercel-index-oauth/step-3.tsx | 17 ++++-- ...{accessTokenHandler.ts => oAuthHandler.ts} | 5 +- utils/odAuthTokenStore.ts | 56 +++++++++++-------- 8 files changed, 95 insertions(+), 45 deletions(-) rename utils/{accessTokenHandler.ts => oAuthHandler.ts} (93%) diff --git a/config/api.json b/config/api.json index 5414703..2594f63 100644 --- a/config/api.json +++ b/config/api.json @@ -2,7 +2,6 @@ "clientId": "d87bcc39-1750-4ca0-ad54-f8d0efbb2735", "obfuscatedClientSecret": "U2FsdGVkX1830zo3/pFDqaBCVBb37iLw3WnBDWGF9GIB2f4apzv0roemp8Y+iIxI3Ih5ecyukqELQEGzZlYiWg==", "redirectUri": "http://localhost", - "base": "/Public", "authApi": "https://login.microsoftonline.com/common/oauth2/v2.0/token", "driveApi": "https://graph.microsoft.com/v1.0/me/drive", "scope": "Files.Read.All Files.ReadWrite.All offline_access" diff --git a/config/site.json b/config/site.json index 9a5db54..19f5912 100644 --- a/config/site.json +++ b/config/site.json @@ -1,6 +1,7 @@ { "icon": "/icons/128.png", "title": "Spencer's OneDrive", + "baseDirectory": "/Public", "maxItems": 100, "googleFontSans": "Inter", "googleFontMono": "Fira Mono", diff --git a/pages/api/index.ts b/pages/api/index.ts index ea263c5..5fb4226 100644 --- a/pages/api/index.ts +++ b/pages/api/index.ts @@ -5,11 +5,14 @@ import axios from 'axios' import apiConfig from '../../config/api.json' import siteConfig from '../../config/site.json' -import { revealObfuscatedToken } from '../../utils/accessTokenHandler' +import { revealObfuscatedToken } from '../../utils/oAuthHandler' import { compareHashedToken } from '../../utils/protectedRouteHandler' -import { getOdAuthTokens, storeOdAuthTokens } from '../../utils/odAuthTokenStore' +import TokenStore from '../../utils/odAuthTokenStore' + +const basePath = pathPosix.resolve('/', siteConfig.baseDirectory) +const clientSecret = revealObfuscatedToken(apiConfig.obfuscatedClientSecret) +const tokenStore = new TokenStore() -const basePath = pathPosix.resolve('/', apiConfig.base) const encodePath = (path: string) => { let encodedPath = pathPosix.join(basePath, pathPosix.resolve('/', path)) if (encodedPath === '/' || encodedPath === '') { @@ -19,10 +22,8 @@ const encodePath = (path: string) => { return `:${encodeURIComponent(encodedPath)}` } -const clientSecret = revealObfuscatedToken(apiConfig.obfuscatedClientSecret) - async function getAccessToken(): Promise { - const { accessToken, refreshToken } = await getOdAuthTokens() + const { accessToken, refreshToken } = await tokenStore.getOdAuthTokens() // Return in storage access token if it is still valid if (typeof accessToken === 'string') { @@ -52,7 +53,7 @@ async function getAccessToken(): Promise { if ('access_token' in resp.data && 'refresh_token' in resp.data) { const { expires_in, access_token, refresh_token } = resp.data - await storeOdAuthTokens({ + await tokenStore.storeOdAuthTokens({ accessToken: access_token, accessTokenExpiry: parseInt(expires_in), refreshToken: refresh_token, @@ -65,6 +66,27 @@ async function getAccessToken(): Promise { } export default async function handler(req: NextApiRequest, res: NextApiResponse) { + // If method is POST, then the API is called by the client to store acquired tokens + if (req.method === 'POST') { + const { obfuscatedAccessToken, accessTokenExpiry, obfuscatedRefreshToken } = req.body + const accessToken = revealObfuscatedToken(obfuscatedAccessToken) + const refreshToken = revealObfuscatedToken(obfuscatedRefreshToken) + + if (typeof accessToken !== 'string' || typeof refreshToken !== 'string') { + res.status(400).send('Invalid request body') + return + } + + await tokenStore.storeOdAuthTokens({ + accessToken, + accessTokenExpiry, + refreshToken, + }) + res.status(200).send('OK') + return + } + + // If method is GET, then the API is a normal request to the OneDrive API for files or folders const { path = '/', raw = false, next = '' } = req.query // Sometimes the path parameter is defaulted to '[...path]' which we need to handle diff --git a/pages/onedrive-vercel-index-oauth/step-1.tsx b/pages/onedrive-vercel-index-oauth/step-1.tsx index 7dc95b0..39b4ef0 100644 --- a/pages/onedrive-vercel-index-oauth/step-1.tsx +++ b/pages/onedrive-vercel-index-oauth/step-1.tsx @@ -28,7 +28,7 @@ export default function OAuthStep1() { present on this deployed instance.

-

Step 1: Preparations

+

Step 1/3: Preparations

Check the following configurations (especially client_id and{' '} client_secret (obfuscated)) and see if they match the official diff --git a/pages/onedrive-vercel-index-oauth/step-2.tsx b/pages/onedrive-vercel-index-oauth/step-2.tsx index 578f28d..a589b67 100644 --- a/pages/onedrive-vercel-index-oauth/step-2.tsx +++ b/pages/onedrive-vercel-index-oauth/step-2.tsx @@ -1,16 +1,19 @@ import Head from 'next/head' import router from 'next/router' import { useState } from 'react' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import siteConfig from '../../config/site.json' import Navbar from '../../components/Navbar' import Footer from '../../components/Footer' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { extractAuthCodeFromRedirected, generateAuthorisationUrl } from '../../utils/accessTokenHandler' +import { LoadingIcon } from '../../components/Loading' +import { extractAuthCodeFromRedirected, generateAuthorisationUrl } from '../../utils/oAuthHandler' export default function OAuthStep2() { const [oAuthRedirectedUrl, setOAuthRedirectedUrl] = useState('') const [authCode, setAuthCode] = useState('') + const [buttonLoading, setButtonLoading] = useState(false) + const oAuthUrl = generateAuthorisationUrl() return ( @@ -33,10 +36,10 @@ export default function OAuthStep2() { present on this deployed instance.

-

Step 2: Get authorisation code

+

Step 2/3: Get authorisation code

The OAuth URL has been generated for you:

{ window.open(oAuthUrl) }} @@ -87,10 +90,19 @@ export default function OAuthStep2() { className="text-white bg-gradient-to-br from-green-500 to-cyan-400 hover:bg-gradient-to-bl focus:ring-4 focus:ring-green-200 dark:focus:ring-green-800 font-medium rounded-lg text-sm px-4 py-2.5 text-center disabled:cursor-not-allowed disabled:grayscale" disabled={authCode === ''} onClick={() => { + setButtonLoading(true) router.push({ pathname: '/onedrive-vercel-index-oauth/step-3', query: { authCode } }) }} > - Get tokens + {buttonLoading ? ( + <> + Requesting tokens + + ) : ( + <> + Get tokens + + )}
diff --git a/pages/onedrive-vercel-index-oauth/step-3.tsx b/pages/onedrive-vercel-index-oauth/step-3.tsx index 482b194..80f0817 100644 --- a/pages/onedrive-vercel-index-oauth/step-3.tsx +++ b/pages/onedrive-vercel-index-oauth/step-3.tsx @@ -6,8 +6,8 @@ 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' +import { obfuscateToken, requestTokenWithAuthCode } from '../../utils/oAuthHandler' +import axios from 'axios' export default function OAuthStep3({ accessToken, refreshToken, error, description, errorUri }) { return ( @@ -30,7 +30,7 @@ export default function OAuthStep3({ accessToken, refreshToken, error, descripti present on this deployed instance.

-

Step 3: Get access and refresh tokens

+

Step 3/3: Get access and refresh tokens

{error ? (

@@ -140,8 +140,15 @@ 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 }) + // await tokenStore.storeOdAuthTokens({ accessToken, accessTokenExpiry: parseInt(expiryTime), refreshToken }) + + // We perform a POST request to the default API route to store the tokens inside the main route memory + // This is a bit of a hack, but it's the only way to get the tokens to the main route + await axios.post('/api', { + obfuscatedAccessToken: obfuscateToken(accessToken), + accessTokenExpiry: parseInt(expiryTime), + obfuscatedRefreshToken: obfuscateToken(refreshToken), + }) return { props: { diff --git a/utils/accessTokenHandler.ts b/utils/oAuthHandler.ts similarity index 93% rename from utils/accessTokenHandler.ts rename to utils/oAuthHandler.ts index 5a99fea..8b8202b 100644 --- a/utils/accessTokenHandler.ts +++ b/utils/oAuthHandler.ts @@ -3,9 +3,10 @@ import CryptoJS from 'crypto-js' import apiConfig from '../config/api.json' -// Just a disguise to obfuscate the client secret, used along with the following two functions +// Just a disguise to obfuscate required tokens (including but not limited to client secret, +// access tokens, and refresh tokens), used along with the following two functions const AES_SECRET_KEY = 'onedrive-vercel-index' -function obfuscateToken(token: string): string { +export function obfuscateToken(token: string): string { // Encrypt token with AES const encrypted = CryptoJS.AES.encrypt(token, AES_SECRET_KEY) return encrypted.toString() diff --git a/utils/odAuthTokenStore.ts b/utils/odAuthTokenStore.ts index 3e626b3..aaf2315 100644 --- a/utils/odAuthTokenStore.ts +++ b/utils/odAuthTokenStore.ts @@ -1,38 +1,46 @@ // 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 os from 'os' +// import os from 'os' import Keyv from 'keyv' // import { KeyvFile } from 'keyv-file' -console.log(`${os.tmpdir()}/od-auth-token.json`) +// console.log(`${os.tmpdir()}/od-auth-token.json`) -// const kv = new Keyv({ -// store: new KeyvFile({ -// filename: `${os.tmpdir()}/od-auth-token.json`, -// }), -// }) -const kv = new Keyv({}) +export default class TokenStore { + kv: Keyv -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) -} + constructor() { + this.kv = new Keyv() -export async function getOdAuthTokens(): Promise<{ accessToken: unknown; refreshToken: unknown }> { - const accessToken = await kv.get('access_token') - const refreshToken = await kv.get('refresh_token') + // this.kv = new Keyv({ + // store: new KeyvFile({ + // filename: `${os.tmpdir()}/od-auth-token.json`, + // }), + // }) + console.log('TokenStore constructor') + } - return { + async getOdAuthTokens(): Promise<{ accessToken: unknown; refreshToken: unknown }> { + const accessToken = await this.kv.get('access_token') + const refreshToken = await this.kv.get('refresh_token') + + return { + accessToken, + refreshToken, + } + } + + async storeOdAuthTokens({ accessToken, + accessTokenExpiry, refreshToken, + }: { + accessToken: string + accessTokenExpiry: number + refreshToken: string + }): Promise { + await this.kv.set('access_token', accessToken, accessTokenExpiry) + await this.kv.set('refresh_token', refreshToken) } }