Skip to content

Commit

Permalink
fix(clerk): add alternative decoder (#8642)
Browse files Browse the repository at this point in the history
* fix(clerk) add alternative decoder

* Update packages/auth-providers/clerk/api/src/decoder.ts

Co-authored-by: Charlie Ray <charlie@dsrecordings.com>

* Update packages/auth-providers/clerk/api/src/decoder.ts

Co-authored-by: Charlie Ray <charlie@dsrecordings.com>

* 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 <charlie@dsrecordings.com>
  • Loading branch information
jtoar and o0charlie0o authored Jun 22, 2023
1 parent a375046 commit f63678f
Show file tree
Hide file tree
Showing 6 changed files with 115 additions and 39 deletions.
56 changes: 44 additions & 12 deletions docs/docs/auth/clerk.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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="..."
Expand All @@ -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.
Expand Down Expand Up @@ -71,19 +82,40 @@ 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

Redwood's Clerk integration is based on [Clerk's React SDK](https://clerk.dev/docs/reference/clerk-react/installation).
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 pagevery 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.
32 changes: 25 additions & 7 deletions packages/auth-providers/clerk/api/src/__tests__/clerk.test.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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()
})
})
36 changes: 36 additions & 0 deletions packages/auth-providers/clerk/api/src/decoder.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
}
}
2 changes: 1 addition & 1 deletion packages/auth-providers/clerk/api/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { authDecoder } from './decoder'
export { authDecoder, clerkAuthDecoder } from './decoder'
9 changes: 3 additions & 6 deletions packages/auth-providers/clerk/setup/src/setupHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,30 +13,27 @@ 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',
`@redwoodjs/auth-clerk-web@${version}`,
],
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.`,
'Be sure to include `CLERK_PUBLISHABLE_KEY` in the `includeEnvironmentVariables` array in redwood.toml.',
'',
'```toml title="redwood.toml"',
'includeEnvironmentVariables = [',
' "CLERK_PUBLISHABLE_KEY"',
' "CLERK_PUBLISHABLE_KEY,"',
']',
'```',
'',
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 }
}

/**
Expand Down

0 comments on commit f63678f

Please sign in to comment.