From 6a113a6292aadafc1e7064814622339e978a66f2 Mon Sep 17 00:00:00 2001 From: Eunjae Lee Date: Wed, 18 Sep 2024 10:18:21 +0200 Subject: [PATCH] fix: incomplete implemention using Next.js middleware --- space-plugins/nextjs-starter/src/auth.ts | 1 + .../nextjs-starter/src/components/Test.tsx | 15 +++--- space-plugins/nextjs-starter/src/config.ts | 11 ++++ .../nextjs-starter/src/{utils => }/const.ts | 0 .../nextjs-starter/src/hooks/useAppBridge.ts | 32 +++++------ .../nextjs-starter/src/middleware.ts | 54 +++++++++++++++++++ .../nextjs-starter/src/pages/401.tsx | 13 ----- .../nextjs-starter/src/pages/api/test.ts | 1 + .../nextjs-starter/src/pages/index.tsx | 2 +- .../src/utils/server/appBridge.ts | 2 +- 10 files changed, 90 insertions(+), 41 deletions(-) create mode 100644 space-plugins/nextjs-starter/src/config.ts rename space-plugins/nextjs-starter/src/{utils => }/const.ts (100%) create mode 100644 space-plugins/nextjs-starter/src/middleware.ts delete mode 100644 space-plugins/nextjs-starter/src/pages/401.tsx diff --git a/space-plugins/nextjs-starter/src/auth.ts b/space-plugins/nextjs-starter/src/auth.ts index 99ccf30..256ee15 100644 --- a/space-plugins/nextjs-starter/src/auth.ts +++ b/space-plugins/nextjs-starter/src/auth.ts @@ -3,6 +3,7 @@ import { AuthHandlerParams, getSessionStore, } from '@storyblok/app-extension-auth'; + ['CLIENT_ID', 'CLIENT_SECRET', 'BASE_URL'].forEach((key) => { if (!process.env[key]) { throw new Error(`Environment variable "${key}" is missing.`); diff --git a/space-plugins/nextjs-starter/src/components/Test.tsx b/space-plugins/nextjs-starter/src/components/Test.tsx index 93d0559..52a1c0e 100644 --- a/space-plugins/nextjs-starter/src/components/Test.tsx +++ b/space-plugins/nextjs-starter/src/components/Test.tsx @@ -1,10 +1,8 @@ -import { APP_BRIDGE_TOKEN_HEADER_KEY, KEY_TOKEN } from '@/utils/const'; +import { APP_BRIDGE_TOKEN_HEADER_KEY, KEY_TOKEN } from '@/const'; import { useEffect, useState } from 'react'; export default function Test() { - const [testInfo, setTestInfo] = useState<{ verified: boolean }>({ - verified: false, - }); + const [response, setResponse] = useState(null); useEffect(() => { const fetchTestInfo = async () => { const response = await fetch('/api/test', { @@ -14,14 +12,15 @@ export default function Test() { }, }); const json = await response.json(); - setTestInfo(json); + setResponse(json); }; fetchTestInfo(); }, []); return ( -
-			App Bridge session is {testInfo?.verified ? 'verified' : 'not verified'}
-		
+
+

Response from /api/test:

+
{response}
+
); } diff --git a/space-plugins/nextjs-starter/src/config.ts b/space-plugins/nextjs-starter/src/config.ts new file mode 100644 index 0000000..c9d63e6 --- /dev/null +++ b/space-plugins/nextjs-starter/src/config.ts @@ -0,0 +1,11 @@ +type Config = { + type: 'space-plugin' | 'tool-plugin'; + oauth: boolean; +}; + +const config: Config = { + type: 'space-plugin', + oauth: true, +}; + +export default config; diff --git a/space-plugins/nextjs-starter/src/utils/const.ts b/space-plugins/nextjs-starter/src/const.ts similarity index 100% rename from space-plugins/nextjs-starter/src/utils/const.ts rename to space-plugins/nextjs-starter/src/const.ts diff --git a/space-plugins/nextjs-starter/src/hooks/useAppBridge.ts b/space-plugins/nextjs-starter/src/hooks/useAppBridge.ts index 8658113..c244322 100644 --- a/space-plugins/nextjs-starter/src/hooks/useAppBridge.ts +++ b/space-plugins/nextjs-starter/src/hooks/useAppBridge.ts @@ -13,7 +13,8 @@ import { KEY_SLUG, KEY_TOKEN, KEY_VALIDATED_PAYLOAD, -} from '@/utils/const'; +} from '@/const'; +import config from '@/config'; import { useState, useEffect } from 'react'; const getPostMessageAction = (type: PluginType): PostMessageAction => { @@ -55,10 +56,8 @@ const postMessageToParent = (payload: unknown) => { }; const useAppBridgeAuth = ({ - type, authenticated, }: { - type: PluginType; authenticated: () => Promise; }) => { const [status, setStatus] = useState< @@ -104,7 +103,7 @@ const useAppBridgeAuth = ({ const slug = getSlug(); try { - const payload = createValidateMessagePayload({ type, slug }); + const payload = createValidateMessagePayload({ type: config.type, slug }); postMessageToParent(payload); sessionStorage.setItem(KEY_PARENT_HOST, host); @@ -183,7 +182,7 @@ const useAppBridgeAuth = ({ return { status, init, error }; }; -const useOAuth = ({ type }: { type: PluginType }) => { +const useOAuth = () => { const [status, setStatus] = useState< 'init' | 'authenticating' | 'authenticated' >('init'); @@ -215,7 +214,11 @@ const useOAuth = ({ type }: { type: PluginType }) => { const sendBeginOAuthMessageToParent = (redirectTo: string) => { const slug = getSlug(); - const payload = createOAuthInitMessagePayload({ type, slug, redirectTo }); + const payload = createOAuthInitMessagePayload({ + type: config.type, + slug, + redirectTo, + }); postMessageToParent(payload); }; @@ -240,32 +243,25 @@ const useOAuth = ({ type }: { type: PluginType }) => { return { init, status }; }; -export const useAppBridge = ({ - type, - oauth, -}: { - type: PluginType; - oauth: boolean; -}) => { - const { init: initOAuth, status: oauthStatus } = useOAuth({ type }); +export const useAppBridge = () => { + const { init: initOAuth, status: oauthStatus } = useOAuth(); const { init: initAppBridgeAuth, status: appBridgeAuthStatus } = useAppBridgeAuth({ - type, authenticated: async () => { - if (oauth) { + if (config.oauth) { await initOAuth(); } }, }); - const completed = oauth + const completed = config.oauth ? appBridgeAuthStatus === 'authenticated' && oauthStatus === 'authenticated' : appBridgeAuthStatus === 'authenticated'; useEffect(() => { initAppBridgeAuth(); - }, [type, oauth]); + }, []); return { completed, diff --git a/space-plugins/nextjs-starter/src/middleware.ts b/space-plugins/nextjs-starter/src/middleware.ts new file mode 100644 index 0000000..0670b5d --- /dev/null +++ b/space-plugins/nextjs-starter/src/middleware.ts @@ -0,0 +1,54 @@ +import { NextResponse } from 'next/server'; +import type { NextRequest } from 'next/server'; +import appConfig from '@/config'; +import { initOauthFlowUrl, authParams } from '@/auth'; +import { APP_BRIDGE_TOKEN_HEADER_KEY } from '@/const'; +import { verifyAppBridgeToken } from '@/utils/server'; + +// Limit the middleware to paths starting with `/api/` +export const config = { + // matcher: '/api/:function*', +}; + +const SKIP_AUTH_FOR = ['/api/_app_bridge', '/api/_oauth']; + +export async function middleware(request: NextRequest) { + // handle 401 error after the initial oauth flow + if ( + request.nextUrl.pathname === '/401' && + request.headers.get('Referer') === 'https://app.storyblok.com/' + ) { + return NextResponse.redirect( + new URL( + appConfig.type === 'tool-plugin' + ? 'https://app.storyblok.com/oauth/tool_redirect' + : 'https://app.storyblok.com/oauth/app_redirect', + ), + ); + } + + // verify App Bridge token for all API routes + const pathname = request.nextUrl.pathname; + if (!pathname.startsWith('/api/')) { + return NextResponse.next(); + } + if (SKIP_AUTH_FOR.includes(pathname)) { + return NextResponse.next(); + } + if ( + [initOauthFlowUrl, `${authParams.endpointPrefix}/callback`].includes( + pathname, + ) + ) { + return NextResponse.next(); + } + const token = request.headers.get(APP_BRIDGE_TOKEN_HEADER_KEY); + const result = await verifyAppBridgeToken(token || ''); + if (result.ok) { + return NextResponse.next(); + } else { + return new NextResponse(null, { + status: 401, + }); + } +} diff --git a/space-plugins/nextjs-starter/src/pages/401.tsx b/space-plugins/nextjs-starter/src/pages/401.tsx deleted file mode 100644 index 371bbd2..0000000 --- a/space-plugins/nextjs-starter/src/pages/401.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { useEffect } from 'react'; - -export default function Error401() { - useEffect(() => { - /** When initially approving the Tool having access to storyblok, the user is navigated outside Storyblok. - This piece of code redirects the user back to the Storyblok Application. **/ - if (typeof window !== 'undefined' && window.top === window.self) { - window.location.assign('https://app.storyblok.com/oauth/app_redirect'); - } - }, []); - - return
; -} diff --git a/space-plugins/nextjs-starter/src/pages/api/test.ts b/space-plugins/nextjs-starter/src/pages/api/test.ts index eed1304..2cac83c 100644 --- a/space-plugins/nextjs-starter/src/pages/api/test.ts +++ b/space-plugins/nextjs-starter/src/pages/api/test.ts @@ -5,6 +5,7 @@ export default async function handler( req: NextApiRequest, res: NextApiResponse, ) { + console.log('💡 /api/test'); const verified = await verifyAppBridgeHeader(req); if (verified.ok) { diff --git a/space-plugins/nextjs-starter/src/pages/index.tsx b/space-plugins/nextjs-starter/src/pages/index.tsx index 12d4206..a45f47f 100644 --- a/space-plugins/nextjs-starter/src/pages/index.tsx +++ b/space-plugins/nextjs-starter/src/pages/index.tsx @@ -13,7 +13,7 @@ type UserInfo = { }; export default function Home() { - const { completed } = useAppBridge({ type: 'space-plugin', oauth: true }); + const { completed } = useAppBridge(); return ( <> diff --git a/space-plugins/nextjs-starter/src/utils/server/appBridge.ts b/space-plugins/nextjs-starter/src/utils/server/appBridge.ts index fac20c9..fbd264e 100644 --- a/space-plugins/nextjs-starter/src/utils/server/appBridge.ts +++ b/space-plugins/nextjs-starter/src/utils/server/appBridge.ts @@ -1,7 +1,7 @@ import jwt, { type VerifyCallback } from 'jsonwebtoken'; import { AppBridgeSession, VerifyResponse } from '@/types'; import { NextApiRequest } from 'next'; -import { APP_BRIDGE_TOKEN_HEADER_KEY } from '../const'; +import { APP_BRIDGE_TOKEN_HEADER_KEY } from '../../const'; export const verifyAppBridgeHeader = async (req: NextApiRequest) => { const token = req.headers[APP_BRIDGE_TOKEN_HEADER_KEY];