From d801898790d89aa14960a14cee553998fc4e3942 Mon Sep 17 00:00:00 2001 From: Dominic Saadi Date: Thu, 22 Jun 2023 16:31:51 -0700 Subject: [PATCH] fix(clerk): add alternative decoder (#8642) * fix(clerk) add alternative decoder * Update packages/auth-providers/clerk/api/src/decoder.ts Co-authored-by: Charlie Ray * Update packages/auth-providers/clerk/api/src/decoder.ts Co-authored-by: Charlie Ray * add `id` for backwards compatibility * fix prettier error * extend type for jwt payload * simplify decoder * duplicate test for new decoder * fix set up notes * remove `parseJWT`; only return `id` in the template * lint fix * update docs * fix typos, add link * rename to clerkAuthDecoder --------- Co-authored-by: Charlie Ray --- docs/docs/auth/clerk.md | 56 +++++++++++++++---- .../clerk/api/src/__tests__/clerk.test.ts | 32 ++++++++--- .../auth-providers/clerk/api/src/decoder.ts | 36 ++++++++++++ .../auth-providers/clerk/api/src/index.ts | 2 +- .../clerk/setup/src/setupHandler.ts | 9 +-- .../src/templates/api/lib/auth.ts.template | 19 ++----- 6 files changed, 115 insertions(+), 39 deletions(-) diff --git a/docs/docs/auth/clerk.md b/docs/docs/auth/clerk.md index f81dfd2fdfc1..ed71df136f8e 100644 --- a/docs/docs/auth/clerk.md +++ b/docs/docs/auth/clerk.md @@ -4,6 +4,16 @@ sidebar_label: Clerk # Clerk Authentication +:::caution Did you set up Clerk a while ago? + +If you set up Clerk a while ago, you may be using a deprecated `authDecoder` that's subject to rate limiting. +This decoder will be removed in the next major. +There's a new decoder you can use right now! +See the [migration guide](https://github.com/redwoodjs/redwood/releases/tag/v5.3.2) for how to upgrade. + +::: + + To get started, run the setup command: ```text @@ -12,7 +22,7 @@ yarn rw setup auth clerk This installs all the packages, writes all the files, and makes all the code modifications you need. For a detailed explanation of all the api- and web-side changes that aren't exclusive to Clerk, see the top-level [Authentication](../authentication.md) doc. -There's one Clerk-specific thing we'll get to, but for now, let's focus on Clerk's side of things. +But for now, let's focus on Clerk's side of things. If you don't have a Clerk account yet, now's the time to make one: navigate to https://clerk.dev, sign up, and create an application. The defaults are good enough to get us going, but feel free to configure things as you wish. @@ -27,9 +37,8 @@ How you get your API keys to production depends on your deploy provider. ::: -We're looking for two API keys. -Head over to the "Developers" section in the nav on the left and click "API Keys". Finally select RedwoodJS in the Framework dropdown in the Quick Copy section. -Do as it says and copy the two keys into your project's `.env` file: +After you create the application, you should be redirected to its dashboard where you should see the RedwoodJS logo. +Click on it and copy the two API keys it shows into your project's `.env` file: ```bash title=".env" CLERK_PUBLISHABLE_KEY="..." @@ -41,7 +50,9 @@ Lastly, in your project's `redwood.toml` file, include `CLERK_PUBLISHABLE_KEY` i ```toml title="redwood.toml" [web] # ... - includeEnvironmentVariables = ["CLERK_PUBLISHABLE_KEY"] + includeEnvironmentVariables = [ + "CLERK_PUBLISHABLE_KEY", + ] ``` That should be enough; now, things should just work. @@ -71,15 +82,28 @@ Clicking sign up should open a sign-up box: After you sign up, you should see `{"isAuthenticated":true}` on the page. -## Deep dive: the `ClerkStatusUpdater` component +## Customizing the session token + +There's not a lot to the default session token. +Besides the standard claims, the only thing it really has is the user's `id`. +Eventually, you'll want to customize it so that you can get back more information from Clerk. +You can do so by navigating to the "Sessions" section in the nav on the left, then clicking on "Edit" in the "Customize session token" box: -At the start of this doc, we said that there's one Clerk-specific thing worth noting. -We'll discuss it here, but feel free to skip this section if you'd like—this is all extracurricular. +![clerk_customize_session_token](https://github.com/redwoodjs/redwood/assets/32992335/6d30c616-b4d2-4b44-971b-8addf3b79e5a) -Clerk is a bit unlike the other auth providers Redwood integrates with in that it puts an instance of its client SDK on the browser's `window` object. -That means we have to wait for it to be ready. -With other providers, we instantiate their client SDK in `web/src/auth.ts`, then pass it to `createAuth`. -Not so with Clerk—instead we use special Clerk components and hooks, like `ClerkLoaded` and `useUser`, to update Redwood's auth context with the client when it's ready. +As long as you're using the `clerkJwtDecoder` +all the properties you add will be available to the `getCurrentUser` function: + +```ts title="api/src/lib/auth.ts" +export const getCurrentUser = async ( + decoded, // 👈 All the claims you add will be available on the `decoded` object + // ... +) => { + decoded.myClaim... + + // ... +} +```` ## Avoiding feature duplication @@ -87,3 +111,11 @@ Redwood's Clerk integration is based on [Clerk's React SDK](https://clerk.dev/do This means that there's some duplication between the features in the SDK and the ones in `@redwoodjs/auth-clerk-web`. For example, the SDK ha a `SignedOut` component that redirects a user away from a private page—very much like wrapping a route with Redwood's `Private` component. We recommend you use Redwood's way of doing things as much as possible since it's much more likely to get along with the rest of the framework. + +## Deep dive: the `ClerkStatusUpdater` component + +With Clerk, there's a bit more going on in the `web/src/auth.tsx` file than other auth providers. +This is because Clerk is a bit unlike the other auth providers Redwood integrates with in that it puts an instance of its client SDK on the browser's `window` object. +That means Redwood has to wait for it to be ready. +With other providers, Redwood instantiates their client SDK in `web/src/auth.ts{x}`, then passes it to `createAuth`. +With Clerk, instead Redwood uses Clerk components and hooks, like `ClerkLoaded` and `useUser`, to update Redwood's auth context with the client when it's ready. diff --git a/packages/auth-providers/clerk/api/src/__tests__/clerk.test.ts b/packages/auth-providers/clerk/api/src/__tests__/clerk.test.ts index 50643326c471..d80fe627d223 100644 --- a/packages/auth-providers/clerk/api/src/__tests__/clerk.test.ts +++ b/packages/auth-providers/clerk/api/src/__tests__/clerk.test.ts @@ -1,6 +1,6 @@ import type { APIGatewayProxyEvent, Context as LambdaContext } from 'aws-lambda' -import { authDecoder } from '../decoder' +import { authDecoder, clerkAuthDecoder } from '../decoder' const req = { event: {} as APIGatewayProxyEvent, @@ -18,14 +18,32 @@ afterAll(() => { console.error = consoleError }) -test('returns null for unsupported type', async () => { - const decoded = await authDecoder('token', 'netlify', req) +describe('deprecated authDecoder', () => { + test('returns null for unsupported type', async () => { + const decoded = await authDecoder('token', 'netlify', req) - expect(decoded).toBe(null) + expect(decoded).toBe(null) + }) + + test('rejects when the token is invalid', async () => { + process.env.CLERK_JWT_KEY = 'jwt-key' + + await expect(authDecoder('invalid-token', 'clerk', req)).rejects.toThrow() + }) }) -test('rejects when the token is invalid', async () => { - process.env.CLERK_JWT_KEY = 'jwt-key' +describe('clerkAuthDecoder', () => { + test('returns null for unsupported type', async () => { + const decoded = await clerkAuthDecoder('token', 'netlify', req) + + expect(decoded).toBe(null) + }) + + test('rejects when the token is invalid', async () => { + process.env.CLERK_JWT_KEY = 'jwt-key' - await expect(authDecoder('invalid-token', 'clerk', req)).rejects.toThrow() + await expect( + clerkAuthDecoder('invalid-token', 'clerk', req) + ).rejects.toThrow() + }) }) diff --git a/packages/auth-providers/clerk/api/src/decoder.ts b/packages/auth-providers/clerk/api/src/decoder.ts index 8335fd25be66..cb329c89a487 100644 --- a/packages/auth-providers/clerk/api/src/decoder.ts +++ b/packages/auth-providers/clerk/api/src/decoder.ts @@ -1,5 +1,8 @@ import { Decoder } from '@redwoodjs/api' +/** + * @deprecated This function will be removed; it uses a rate-limited API. Use `clerkAuthDecoder` instead. + */ export const authDecoder: Decoder = async (token: string, type: string) => { if (type !== 'clerk') { return null @@ -34,3 +37,36 @@ export const authDecoder: Decoder = async (token: string, type: string) => { return Promise.reject(error) } } + +export const clerkAuthDecoder: Decoder = async (token: string, type: string) => { + if (type !== 'clerk') { + return null + } + + const { verifyToken } = await import('@clerk/clerk-sdk-node') + + try { + const issuer = (iss: string) => + iss.startsWith('https://clerk.') || iss.includes('.clerk.accounts') + + const jwtPayload = await verifyToken(token, { + issuer, + apiUrl: process.env.CLERK_API_URL || 'https://api.clerk.dev', + jwtKey: process.env.CLERK_JWT_KEY, + apiKey: process.env.CLERK_API_KEY, + secretKey: process.env.CLERK_SECRET_KEY, + }) + + if (!jwtPayload.sub) { + return Promise.reject(new Error('Session invalid')) + } + + return { + ...jwtPayload, + id: jwtPayload.sub, + } + } catch (error) { + console.error(error) + return Promise.reject(error) + } +} diff --git a/packages/auth-providers/clerk/api/src/index.ts b/packages/auth-providers/clerk/api/src/index.ts index ead5bdde8676..6d1498a92969 100644 --- a/packages/auth-providers/clerk/api/src/index.ts +++ b/packages/auth-providers/clerk/api/src/index.ts @@ -1 +1 @@ -export { authDecoder } from './decoder' +export { authDecoder, clerkAuthDecoder } from './decoder' diff --git a/packages/auth-providers/clerk/setup/src/setupHandler.ts b/packages/auth-providers/clerk/setup/src/setupHandler.ts index ba42638f863e..f08d9c1406b4 100644 --- a/packages/auth-providers/clerk/setup/src/setupHandler.ts +++ b/packages/auth-providers/clerk/setup/src/setupHandler.ts @@ -13,7 +13,7 @@ export const handler = async ({ force: forceArg }: Args) => { standardAuthHandler({ basedir: __dirname, forceArg, - authDecoderImport: `import { authDecoder } from '@redwoodjs/auth-clerk-api'`, + authDecoderImport: `import { clerkAuthDecoder as authDecoder } from '@redwoodjs/auth-clerk-api'`, provider: 'clerk', webPackages: [ '@clerk/clerk-react@^4', @@ -21,14 +21,11 @@ export const handler = async ({ force: forceArg }: Args) => { ], apiPackages: [`@redwoodjs/auth-clerk-api@${version}`], notes: [ - "You'll need to add three env vars to your .env file:", + "You'll need to add two env vars to your .env file:", '', '```title=".env"', 'CLERK_PUBLISHABLE_KEY="..."', 'CLERK_SECRET_KEY="..."', - 'CLERK_JWT_KEY="-----BEGIN PUBLIC KEY-----', - '...', - '-----END PUBLIC KEY-----"', '```', '', `You can find their values under "API Keys" on your Clerk app's dashboard.`, @@ -36,7 +33,7 @@ export const handler = async ({ force: forceArg }: Args) => { '', '```toml title="redwood.toml"', 'includeEnvironmentVariables = [', - ' "CLERK_PUBLISHABLE_KEY"', + ' "CLERK_PUBLISHABLE_KEY,"', ']', '```', '', diff --git a/packages/auth-providers/clerk/setup/src/templates/api/lib/auth.ts.template b/packages/auth-providers/clerk/setup/src/templates/api/lib/auth.ts.template index 92f1ee761481..10339861cb1b 100644 --- a/packages/auth-providers/clerk/setup/src/templates/api/lib/auth.ts.template +++ b/packages/auth-providers/clerk/setup/src/templates/api/lib/auth.ts.template @@ -1,12 +1,11 @@ -import { parseJWT } from '@redwoodjs/api' import { AuthenticationError, ForbiddenError } from '@redwoodjs/graphql-server' import { logger } from 'src/lib/logger' /** - * getCurrentUser returns the user information together with - * an optional collection of roles used by requireAuth() to check - * if the user is authenticated or has role-based access + * getCurrentUser returns the user information. + * Once you're ready you can also return a collection of roles + * for `requireAuth` and the Router to use. * * @param decoded - The decoded access token containing user info and JWT claims like `sub`. Note could be null. * @param { token, SupportedAuthTypes type } - The access token itself as well as the auth provider type @@ -27,16 +26,10 @@ export const getCurrentUser = async ( return null } - const { roles } = parseJWT({ decoded }) + const { id, ..._rest } = decoded - // Remove privateMetadata property from CurrentUser as it should not be accessible on the web - const { privateMetadata, ...userWithoutPrivateMetadata } = decoded - - if (roles) { - return { ...userWithoutPrivateMetadata, roles } - } - - return { ...userWithoutPrivateMetadata } + // Be careful to only return information that should be accessible on the web side. + return { id } } /**