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

feat(nextjs,shared,backend,clerk-react): Introduce Protect for authorization #2170

Merged
merged 26 commits into from
Dec 11, 2023

Conversation

panteliselef
Copy link
Member

@panteliselef panteliselef commented Nov 20, 2023

Description

This PR builds upon the Experimental__Gate and experimental__has there introduced recently.

  • Rename Gate to Protect
  • Support for permission checks. (Previously only roles could be used)
  • Remove the experimental tags and prefixes
  • Drop some from the has utility and Protect. Protect now accepts a condition prop where a function is expected with the has being exposed as the param.
  • Protect can now be used without required props. In this case behaves as <SignedIn>, if no authorization props are passed.
  • has will throw an error if neither permission or role is passed.
  • Introduce auth().protect() for App Router.

auth().protect()

Allow per page protection in app router. This utility will automatically throw a 404 error if user is not authorized or authenticated.
When auth().protect() is called

  • inside a page or layout file it will render the nearest not-found component set by the developer
  • inside a route handler it will return empty response body with a 404 status code

Examples

RSC in Nextjs

import { Protect } from '@clerk/nextjs';

<Protect permission="org:appointment:accept">
    <button>Accept appointment</button>
</Protect>

<Protect
  condition={has =>
    has({ permission: 'org:appointment:accept' }) || has({ permission: 'org:appointment:decline' })
  }>
     <button>Accept appointment</button>
     <button>Decline appointment</button>
</Protect>

Client component

"use client"
/**
 * As a client component
 * The client component is exposed also from `@clerk/clerk-react
 */
import { Protect } from '@clerk/nextjs'; 

<Protect permission="org:appointment:accept">
    <button>Accept appointment</button>
</Protect>

<Protect
  condition={has =>
    has({ permission: 'org:appointment:accept' }) || has({ permission: 'org:appointment:decline' })
  }>
     <button>Accept appointment</button>
     <button>Decline appointment</button>
</Protect>

has from the Auth object

import { auth } from '@clerk/nextjs';

const isAuthorized = auth().has({permission: "org:appointment:decline"})

import { getAuth } from '@clerk/remix';

const isAuthorized = getAuth().has({permission: "org:appointment:decline"})

protect from auth() only for App Router

import { auth } from '@clerk/nextjs';

const {userId, ...restAuthObj } = auth().protect()
// ^ userId is string

const {userId, ...restAuthObj } = auth().protect({permission: "org:appointment:decline"})
const {userId, ...restAuthObj } = auth().protect({role: "org:admin"})
const {userId, ...restAuthObj } = auth().protect(has => has({permission: "org:appointment:decline"}))

Type-safety for custom roles and permissions

Create a clerk.d.ts file and replace ClerkAuthorization with your own types

// clerk.d.ts

interface ClerkAuthorization {
  role: 'org:admin' | 'org:editor' | 'org:viewer';
  permission: 'org:article:create' | 'org:article:manage' | 'org:article:publish';
}

If the file is a module, you can do this

// clerk.d.ts

declare global {
  interface ClerkAuthorization {
    role: 'org:admin' | 'org:editor' | 'org:viewer';
    permission: 'org:article:create' | 'org:article:manage' | 'org:article:publish';
  }
}

export {};

Checklist

  • npm test runs as expected.
  • npm run build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

Packages affected

  • @clerk/backend
  • @clerk/chrome-extension
  • @clerk/clerk-js
  • @clerk/clerk-expo
  • @clerk/fastify
  • gatsby-plugin-clerk
  • @clerk/localizations
  • @clerk/nextjs
  • @clerk/clerk-react
  • @clerk/remix
  • @clerk/clerk-sdk-node
  • @clerk/shared
  • @clerk/themes
  • @clerk/types
  • build/tooling/chore

@panteliselef panteliselef self-assigned this Nov 20, 2023
Copy link

changeset-bot bot commented Nov 20, 2023

🦋 Changeset detected

Latest commit: 79fff5a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 11 packages
Name Type
@clerk/chrome-extension Minor
@clerk/clerk-js Minor
@clerk/backend Minor
@clerk/nextjs Minor
@clerk/clerk-react Minor
@clerk/types Minor
@clerk/clerk-expo Patch
@clerk/fastify Patch
gatsby-plugin-clerk Patch
@clerk/remix Patch
@clerk/clerk-sdk-node Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@panteliselef panteliselef requested review from BRKalow and a team November 24, 2023 12:21
@panteliselef panteliselef force-pushed the elef/core-810-gate-with-permissions branch from 91c3ba3 to 9dfe2b3 Compare November 27, 2023 11:51
@panteliselef
Copy link
Member Author

!snapshot

@clerk-cookie

This comment was marked as outdated.

@panteliselef panteliselef force-pushed the elef/core-810-gate-with-permissions branch 2 times, most recently from 91bf440 to bda5428 Compare December 3, 2023 15:01
@panteliselef
Copy link
Member Author

!snapshot

@clerk-cookie
Copy link
Collaborator

Hey @panteliselef - the snapshot version command generated the following package versions:

Package Version
@clerk/backend 1.0.1-snapshot.vbda5428
@clerk/chrome-extension 1.0.1-snapshot.vbda5428
@clerk/clerk-js 5.0.1-snapshot.vbda5428
@clerk/clerk-expo 1.0.1-snapshot.vbda5428
@clerk/fastify 1.0.1-snapshot.vbda5428
gatsby-plugin-clerk 5.0.1-snapshot.vbda5428
@clerk/localizations 2.0.1-snapshot.vbda5428
@clerk/nextjs 5.0.1-snapshot.vbda5428
@clerk/clerk-react 5.0.1-snapshot.vbda5428
@clerk/remix 4.0.1-snapshot.vbda5428
@clerk/clerk-sdk-node 5.0.1-snapshot.vbda5428
@clerk/shared 2.0.1-snapshot.vbda5428
@clerk/themes 2.0.1-snapshot.vbda5428
@clerk/types 4.0.1-snapshot.vbda5428

Tip: Use the snippet copy button below to quickly install the required packages.
@clerk/backend

npm i @clerk/[email protected] --save-exact

@clerk/chrome-extension

npm i @clerk/[email protected] --save-exact

@clerk/clerk-js

npm i @clerk/[email protected] --save-exact

@clerk/clerk-expo

npm i @clerk/[email protected] --save-exact

@clerk/fastify

npm i @clerk/[email protected] --save-exact

gatsby-plugin-clerk

npm i [email protected] --save-exact

@clerk/localizations

npm i @clerk/[email protected] --save-exact

@clerk/nextjs

npm i @clerk/[email protected] --save-exact

@clerk/clerk-react

npm i @clerk/[email protected] --save-exact

@clerk/remix

npm i @clerk/[email protected] --save-exact

@clerk/clerk-sdk-node

npm i @clerk/[email protected] --save-exact

@clerk/shared

npm i @clerk/[email protected] --save-exact

@clerk/themes

npm i @clerk/[email protected] --save-exact

@clerk/types

npm i @clerk/[email protected] --save-exact

@panteliselef panteliselef changed the title feat(nextjs,shared,backend,clerk-react): Support permissions in Gate feat(nextjs,shared,backend,clerk-react): Introduce Protect for authorization Dec 4, 2023
@panteliselef panteliselef force-pushed the elef/core-810-gate-with-permissions branch from be85aef to 636a20a Compare December 4, 2023 10:00
packages/nextjs/src/app-router/server/auth.ts Show resolved Hide resolved
debugLoggerName: 'auth()',
noAuthStatusMessage: authAuthHeaderMissing(),
})(buildRequestLike());

(authObject as AuthSignedIn).protect = params => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We talked about supporting redirectUrl as an optional parameter when doing authz checks, but I can't remember if we decided to omit that for the initial implementation 🤔

auth().protect({ role: 'admin' }, { redirectUrl: '/home' })

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we are skipping this. Especially with something like notAuthorized we wouldn't need to support redirectUrl

packages/react/src/errors.ts Outdated Show resolved Hide resolved
packages/types/src/organizationMembership.ts Outdated Show resolved Hide resolved
packages/types/src/organizationMembership.ts Outdated Show resolved Hide resolved
packages/backend/src/tokens/authObjects.ts Outdated Show resolved Hide resolved
.changeset/chatty-beds-doubt.md Outdated Show resolved Hide resolved
Copy link
Member

@LekoArts LekoArts left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Public API-wise I'd like to talk about this before we merge it (hence the request changes to block it):

<Protect condition={has => has({"org:appointment:accept"}) || has({"org:appointment:decline"}) }>

This feels inconsistent. It would need to be either of those two things:

  1. Like the <Protect permission="foobar"> prop so without the {}
  2. Like the rest of the has functions so { permission: "foobar" }

I'd prefer 2) as then has would be consistent everywhere

@LekoArts LekoArts dismissed their stale review December 6, 2023 07:38

PR description was incorrect, implementation is like 2)

Copy link
Member

@LekoArts LekoArts left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good work 👍

packages/nextjs/src/app-router/server/auth.ts Outdated Show resolved Hide resolved
packages/react/src/components/controlComponents.tsx Outdated Show resolved Hide resolved
packages/react/src/components/controlComponents.tsx Outdated Show resolved Hide resolved
packages/react/src/components/controlComponents.tsx Outdated Show resolved Hide resolved
packages/react/src/components/controlComponents.tsx Outdated Show resolved Hide resolved
@panteliselef panteliselef force-pushed the elef/core-810-gate-with-permissions branch from d4a99be to e07c709 Compare December 6, 2023 11:36
@panteliselef panteliselef force-pushed the elef/core-810-gate-with-permissions branch from 24efeec to 79fff5a Compare December 11, 2023 17:30
Copy link
Contributor

⚠️ Changes detected under the ClerkJS ui directory!

Don't forget to apply the same changes under the /ui.retheme directory:
packages/clerk-js/src/ui/** ➡️ packages/clerk-js/src/ui.retheme/**

Also, you may need to update the following files:

  • packages/localizations/src/en-US.retheme.ts
  • packages/localizations/src/index.retheme.ts
  • packages/types/src/appearance.retheme.ts
  • packages/types/src/clerk.retheme.ts
  • packages/types/src/index.retheme.ts
  • packages/types/src/localization.retheme.ts

@panteliselef
Copy link
Member Author

!snapshot

@clerk-cookie
Copy link
Collaborator

Hey @panteliselef - the snapshot version command generated the following package versions:

Package Version
@clerk/backend 1.0.1-snapshot.v79fff5a
@clerk/chrome-extension 1.0.1-snapshot.v79fff5a
@clerk/clerk-js 5.0.1-snapshot.v79fff5a
@clerk/clerk-expo 1.0.1-snapshot.v79fff5a
@clerk/fastify 1.0.1-snapshot.v79fff5a
gatsby-plugin-clerk 5.0.1-snapshot.v79fff5a
@clerk/localizations 2.0.1-snapshot.v79fff5a
@clerk/nextjs 5.0.1-snapshot.v79fff5a
@clerk/clerk-react 5.0.1-snapshot.v79fff5a
@clerk/remix 4.0.1-snapshot.v79fff5a
@clerk/clerk-sdk-node 5.0.1-snapshot.v79fff5a
@clerk/shared 2.0.1-snapshot.v79fff5a
@clerk/themes 2.0.1-snapshot.v79fff5a
@clerk/types 4.0.1-snapshot.v79fff5a

Tip: Use the snippet copy button below to quickly install the required packages.
@clerk/backend

npm i @clerk/[email protected] --save-exact

@clerk/chrome-extension

npm i @clerk/[email protected] --save-exact

@clerk/clerk-js

npm i @clerk/[email protected] --save-exact

@clerk/clerk-expo

npm i @clerk/[email protected] --save-exact

@clerk/fastify

npm i @clerk/[email protected] --save-exact

gatsby-plugin-clerk

npm i [email protected] --save-exact

@clerk/localizations

npm i @clerk/[email protected] --save-exact

@clerk/nextjs

npm i @clerk/[email protected] --save-exact

@clerk/clerk-react

npm i @clerk/[email protected] --save-exact

@clerk/remix

npm i @clerk/[email protected] --save-exact

@clerk/clerk-sdk-node

npm i @clerk/[email protected] --save-exact

@clerk/shared

npm i @clerk/[email protected] --save-exact

@clerk/themes

npm i @clerk/[email protected] --save-exact

@clerk/types

npm i @clerk/[email protected] --save-exact

@panteliselef panteliselef added this pull request to the merge queue Dec 11, 2023
Merged via the queue into main with commit 46040a2 Dec 11, 2023
10 checks passed
@panteliselef panteliselef deleted the elef/core-810-gate-with-permissions branch December 11, 2023 17:52
github-merge-queue bot pushed a commit that referenced this pull request Dec 12, 2023
…ization (#2170) (#2309)

* feat(nextjs,shared,backend,clerk-react): Introduce Protect for authorization (#2170)

* fix(types): Avoid using `ts-expect-error` as it may fail for hosting apps

* fix(types,nextjs): Improve complex type

* fix(types): Typescript v5 cannot infer types correctly

* fix(types): Update MembershipRole and OrganizationPermissionKey to not resolve to `any`
octoper pushed a commit that referenced this pull request Dec 13, 2023
…ization (#2170)

* feat(nextjs,shared,backend,clerk-react): Support permissions in Gate

* chore(types,backend,clerk-react): Create type for OrganizationCustomPermissions

* chore(types,backend,clerk-react): Create type for custom roles

* chore(types,backend,clerk-react): Add changeset

* chore(types,backend,clerk-react): Add comments

* chore(types,nextjs): Remove custom types

* fix(clerk-react): Missing `some` support for has in useAuth

* chore(types,clerk-react): Use OrganizationCustomPermission for permissions in ssr

* chore(nextjs): Drop redirect from RSC `<Gate/>`

* feat(types,nextjs,clerk-react,backend): Rename Gate to Protect

- Drop `some` from the `has` utility and Protect. Protect now accepts a `condition` prop where a function is expected with the `has` being exposed as the param.
- Protect can now be used without required props. In this chae behaves as `<SignedIn>` if no authorization props are passed.
- `has` will throw an error if neither `permission` or `role` is passed.

* feat(nextjs): Introduce `auth().protect()` for App Router

Allow per page protection in app router. This utility will automatically throw a 404 error if user is not authorized or authenticated.
When `auth().protect()` is called
- inside a page or layout file it will render the nearest `not-found` component set by the developer
- inside a route handler it will return empty response body with a 404 status code

* chore(types): Add `Key` prefix to OrganizationCustomPermission

* chore(nextjs): Remove duplicate types

* chore(nextjs): Minor improvements in readability

* chore(nextjs): Mark protect utility as experimental for Nextjs

* chore(nextjs): Minor improvements

* fix(nextjs,clerk-react,backend): Utility `has` is undefined when user is signed out

* fix(clerk-react): Utility `has` returns false when user isLoaded is true and no user or org

* chore(clerk-react,nextjs): Improve comments

* fix(clerk-react): Eliminate flickering of fallback for CSR applications

* feat(types): Allow overriding of types for custom roles and permissions

* chore(repo): Update changeset file

* fix(types): `MembershipRole` will include custom roles if applicable

* chore(nextjs): Improve readability of conditionals

* Revert "fix(nextjs,clerk-react,backend): Utility `has` is undefined when user is signed out"

This reverts commit cf736cc

* fix(clerk-js,types): Remove `experimental` from checkAuthorization
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants