Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(clerk): add alternative decoder #8642

Merged
merged 18 commits into from
Jun 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
jtoar marked this conversation as resolved.
Show resolved Hide resolved
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-----"',
jtoar marked this conversation as resolved.
Show resolved Hide resolved
'```',
'',
`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