store access token and refresh token in the file system /tmp

This commit is contained in:
spencerwooo 2021-12-31 12:17:00 +08:00
parent 38d24146fa
commit 45ef22f34a
No known key found for this signature in database
GPG key ID: 24CD550268849CA0
4 changed files with 70 additions and 26 deletions

View file

@ -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<any> {
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) {

View file

@ -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,

View file

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

32
utils/odAuthTokenStore.ts Normal file
View file

@ -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<void> {
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,
}
}