store access token and refresh token in the file system /tmp
This commit is contained in:
parent
38d24146fa
commit
45ef22f34a
4 changed files with 70 additions and 26 deletions
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
32
utils/odAuthTokenStore.ts
Normal 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,
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue