-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Parabol Icebreaker Zoom App (#12)
* feat: migrate the /auth and /install route logic * fix: rename function * feat: add the verifyzoom.html * feat: add the zoom & github entries * fix: fix the lint-staged lint issue * fix: update Missing OWASP Secure Headers * fix: add 'unsafe-inline' style-src CSP * fix: allow all CSP policy
- Loading branch information
Showing
13 changed files
with
596 additions
and
28 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,6 +18,7 @@ | |
# misc | ||
.DS_Store | ||
*.pem | ||
.idea/ | ||
|
||
# debug | ||
npm-debug.log* | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
interface Props { | ||
className?: string; | ||
} | ||
|
||
export const GitHub = ({ className }: Props) => { | ||
return ( | ||
<svg | ||
xmlns="http://www.w3.org/2000/svg" | ||
className={className} | ||
viewBox="0 0 496 512" | ||
fill="#fff" | ||
> | ||
<path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z" /> | ||
</svg> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
interface Props { | ||
className?: string; | ||
} | ||
|
||
export const Zoom = ({ className }: Props) => { | ||
return ( | ||
<svg | ||
xmlns="http://www.w3.org/2000/svg" | ||
className={className} | ||
viewBox="0 0 576 512" | ||
fill="#fff" | ||
> | ||
<path d="M0 128C0 92.7 28.7 64 64 64H320c35.3 0 64 28.7 64 64V384c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V128zM559.1 99.8c10.4 5.6 16.9 16.4 16.9 28.2V384c0 11.8-6.5 22.6-16.9 28.2s-23 5-32.9-1.6l-96-64L416 337.1V320 192 174.9l14.2-9.5 96-64c9.8-6.5 22.4-7.2 32.9-1.6z" /> | ||
</svg> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
export const zoomApp = { | ||
host: process.env.ZM_HOST || 'https://zoom.us', | ||
clientId: process.env.ZM_CLIENT_ID || '', | ||
clientSecret: process.env.ZM_CLIENT_SECRET || '', | ||
redirectUrl: process.env.ZM_REDIRECT_URL || '', | ||
sessionSecret: process.env.SESSION_SECRET || '', | ||
}; | ||
|
||
//{ | ||
// name: 'session', | ||
// httpOnly: true, | ||
// keys: [zoomApp.sessionSecret], | ||
// maxAge: 24 * 60 * 60 * 1000, | ||
// secure: process.env.NODE_ENV === 'production', | ||
// } | ||
export const sessionOptions = { | ||
cookieName: "parabol_session", | ||
password: zoomApp.sessionSecret, | ||
// secure: true should be used in production (HTTPS) but can't be used in development (HTTP) | ||
cookieOptions: { | ||
secure: process.env.NODE_ENV === "production", | ||
}, | ||
}; | ||
|
||
// Zoom App Info | ||
export const appName = process.env.APP_NAME || 'zoom-app'; | ||
export const redirectUri = zoomApp.redirectUrl; | ||
|
||
// HTTP | ||
export const port = process.env.PORT || '3000'; | ||
|
||
// require secrets are explicitly imported | ||
export default { | ||
appName, | ||
redirectUri, | ||
port, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import { withIronSessionApiRoute, withIronSessionSsr } from "iron-session/next"; | ||
import { | ||
GetServerSidePropsContext, | ||
GetServerSidePropsResult, | ||
NextApiHandler, | ||
} from "next"; | ||
import { sessionOptions } from '../config' | ||
|
||
declare module "iron-session" { | ||
interface IronSessionData { | ||
state: string | null | ||
verifier: string | null | ||
} | ||
} | ||
|
||
export function withSessionRoute(handler: NextApiHandler) { | ||
return withIronSessionApiRoute(handler, sessionOptions); | ||
} | ||
|
||
// Theses types are compatible with InferGetStaticPropsType https://nextjs.org/docs/basic-features/data-fetching#typescript-use-getstaticprops | ||
export function withSessionSsr< | ||
P extends { [key: string]: unknown } = { [key: string]: unknown }, | ||
>( | ||
handler: ( | ||
context: GetServerSidePropsContext, | ||
) => GetServerSidePropsResult<P> | Promise<GetServerSidePropsResult<P>>, | ||
) { | ||
return withIronSessionSsr(handler, sessionOptions); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
// @ts-nocheck | ||
import axios from "axios"; | ||
import { URL } from "url"; | ||
import { zoomApp } from "../config"; | ||
import crypto from "crypto"; | ||
|
||
// returns a base64 encoded url | ||
const base64URL = (s) => | ||
s | ||
.toString("base64") | ||
.replace(/\+/g, "-") | ||
.replace(/\//g, "_") | ||
.replace(/=/g, ""); | ||
|
||
// returns a random string of format fmt | ||
const rand = (fmt, depth = 32) => crypto.randomBytes(depth).toString(fmt); | ||
|
||
// Get Zoom API URL from Zoom Host value | ||
const host = new URL(zoomApp.host); | ||
host.hostname = `api.${host.hostname}`; | ||
|
||
const baseURL = host.href; | ||
|
||
/** | ||
* Generic function for getting access or refresh tokens | ||
* @param {string} [id=''] - Username for Basic Auth | ||
* @param {string} [secret=''] - Password for Basic Auth | ||
* @param {Object} params - Request parameters (form-urlencoded) | ||
*/ | ||
function tokenRequest(params, id = "", secret = "") { | ||
const username = id || zoomApp.clientId; | ||
const password = secret || zoomApp.clientSecret; | ||
|
||
return axios({ | ||
data: new URLSearchParams(params).toString(), | ||
baseURL: zoomApp.host, | ||
url: "/oauth/token", | ||
method: "POST", | ||
headers: { | ||
"Content-Type": "application/x-www-form-urlencoded", | ||
}, | ||
auth: { | ||
username, | ||
password, | ||
}, | ||
}).then(({ data }) => Promise.resolve(data)); | ||
} | ||
|
||
/** | ||
* Generic function for making requests to the Zoom API | ||
* @param {string} method - Request method | ||
* @param {string | URL} endpoint - Zoom API Endpoint | ||
* @param {string} token - Access Token | ||
* @param {object} [data=null] - Request data | ||
*/ | ||
function apiRequest(method, endpoint, token, data = null) { | ||
return axios({ | ||
data, | ||
method, | ||
baseURL, | ||
url: `/v2${endpoint}`, | ||
headers: { | ||
Authorization: `Bearer ${token}`, | ||
}, | ||
}).then(({ data }) => Promise.resolve(data)); | ||
} | ||
|
||
/** | ||
* Return the url, state and verifier for the Zoom App Install | ||
* @return {{verifier: string, state: string, url: module:url.URL}} | ||
*/ | ||
export function getInstallURL() { | ||
const state = rand("base64"); | ||
const verifier = rand("ascii"); | ||
|
||
const digest = crypto | ||
.createHash("sha256") | ||
.update(verifier) | ||
.digest("base64") | ||
.toString(); | ||
|
||
const challenge = base64URL(digest); | ||
|
||
const url = new URL("/oauth/authorize", zoomApp.host); | ||
|
||
url.searchParams.set("response_type", "code"); | ||
url.searchParams.set("client_id", zoomApp.clientId); | ||
url.searchParams.set("redirect_uri", zoomApp.redirectUrl); | ||
url.searchParams.set("code_challenge", challenge); | ||
url.searchParams.set("code_challenge_method", "S256"); | ||
url.searchParams.set("state", state); | ||
|
||
return { url, state, verifier }; | ||
} | ||
|
||
/** | ||
* Obtains an OAuth access token from Zoom | ||
* @param {string} code - Authorization code from user authorization | ||
* @param verifier - code_verifier for PKCE | ||
* @return {Promise} Promise resolving to the access token object | ||
*/ | ||
export async function getToken(code, verifier) { | ||
if (!code || typeof code !== "string") | ||
throw new Error("authorization code must be a valid string"); | ||
|
||
if (!verifier || typeof verifier !== "string") | ||
throw new Error("code verifier code must be a valid string"); | ||
|
||
return tokenRequest({ | ||
code, | ||
code_verifier: verifier, | ||
redirect_uri: zoomApp.redirectUrl, | ||
grant_type: "authorization_code", | ||
}); | ||
} | ||
|
||
/** | ||
* Obtain a new Access Token from a Zoom Refresh Token | ||
* @param {string} token - Refresh token to use | ||
* @return {Promise<void>} | ||
*/ | ||
export async function refreshToken(token) { | ||
if (!token || typeof token !== "string") | ||
throw new Error("refresh token must be a valid string"); | ||
|
||
return tokenRequest({ | ||
refresh_token: token, | ||
grant_type: "refresh_token", | ||
}); | ||
} | ||
|
||
/** | ||
* Use the Zoom API to get a Zoom User | ||
* @param {string} uid - User ID to query on | ||
* @param {string} token Zoom App Access Token | ||
*/ | ||
export function getZoomUser(uid, token) { | ||
return apiRequest("GET", `/users/${uid}`, token); | ||
} | ||
|
||
/** | ||
* Return the DeepLink for opening Zoom | ||
* @param {string} token - Zoom App Access Token | ||
* @return {Promise} | ||
*/ | ||
export function getDeeplink(token) { | ||
// @ts-ignore | ||
return apiRequest("POST", "/zoomapp/deeplink", token, { | ||
action: JSON.stringify({ | ||
url: "/", | ||
role_name: "Owner", | ||
verified: 1, | ||
role_id: 0, | ||
}), | ||
}).then((data) => Promise.resolve(data.deeplink)); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import { NextApiRequest, NextApiResponse } from "next"; | ||
import { withSessionRoute } from "../../../lib/zoom/with-session"; | ||
import { getDeeplink, getToken } from "../../../lib/zoom/zoom-api"; | ||
|
||
type Data = { | ||
name?: string; | ||
error?: string; | ||
}; | ||
|
||
export default withSessionRoute(authRoute); | ||
|
||
async function authRoute(req: NextApiRequest, res: NextApiResponse<Data>) { | ||
req.session.state = null; | ||
|
||
console.log('========auth req.session========', req.session) | ||
try { | ||
const code = req.query.code; | ||
const verifier = req.session.verifier; | ||
|
||
req.session.verifier = null; | ||
|
||
// get Access Token from Zoom | ||
const { access_token: accessToken } = await getToken(code, verifier); | ||
|
||
// fetch deeplink from Zoom API | ||
const deeplink = await getDeeplink(accessToken); | ||
|
||
await req.session.save(); | ||
// redirect the user to the Zoom Client | ||
res.redirect(deeplink); | ||
} catch (e: any) { | ||
res.status(500).send({ error: e.message }); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import { NextApiRequest, NextApiResponse } from "next"; | ||
import { withSessionRoute } from "../../../lib/zoom/with-session"; | ||
import { getInstallURL } from "../../../lib/zoom/zoom-api"; | ||
|
||
export default withSessionRoute(installRoute); | ||
|
||
interface Data {} | ||
|
||
async function installRoute(req: NextApiRequest, res: NextApiResponse<Data>) { | ||
const { url, state, verifier } = getInstallURL(); | ||
|
||
req.session.state = state; | ||
req.session.verifier = verifier; | ||
await req.session.save(); | ||
|
||
res.redirect(url.href); | ||
} |
Oops, something went wrong.
159e511
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Successfully deployed to the following URLs:
icebreakers – ./
icebreakers-parabol.vercel.app
parabol-icebreakers.vercel.app
icebreakers.parabol.co
icebreakers-git-main-parabol.vercel.app