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)
}
}