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

[do not merge] Add enforce active org guide #1331

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

royanger
Copy link
Member

@royanger royanger commented Jul 25, 2024

Added a guide to cover enforcing both organization selection and an active organization for all users in an application.

This PR:

@royanger royanger requested a review from a team as a code owner July 25, 2024 19:54
Copy link

Hey, here’s your docs preview: https://clerk.com/docs/pr/1331

@panteliselef
Copy link
Member

@royanger is this a replacement for a similar page we have ? Shall we drop the old one to avoid having 2 source of truth ?

@royanger
Copy link
Member Author

@royanger is this a replacement for a similar page we have ? Shall we drop the old one to avoid having 2 source of truth ?

Nope! The one you wrote has a bunch of other useful stuff, so the plan is that we will edit it. We'll get the two working together nicely.


# Enforce an active organization for users

Clerk's organization feature enables the users of an application to be members of more than one organization at a time. Each membership can have its own role, allowing a user to habe different privileges for each organization.
Copy link
Contributor

@kylemac kylemac Jul 30, 2024

Choose a reason for hiding this comment

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

Suggested change
Clerk's organization feature enables the users of an application to be members of more than one organization at a time. Each membership can have its own role, allowing a user to habe different privileges for each organization.
Clerk's organization feature enables users of an application to be members of more than one organization at a time. Each membership can have its own role, allowing a user to have different privileges for each organization.

@kylemac kylemac changed the title Add enforce active org guide [do not merge] Add enforce active org guide Jul 31, 2024
@kylemac
Copy link
Contributor

kylemac commented Jul 31, 2024

don’t merge this one yet as we wanna do a run through to have it match with our force-an-org example on clerk/orgs before going live with this.

@alexisintech
Copy link
Member

let's also edit the existing one asap so it doesn't get left in the dust

Comment on lines +8 to +12
Clerk's organization feature enables the users of an application to be members of more than one organization at a time. Each membership can have its own role, allowing a user to habe different privileges for each organization.

When performing an authorization check on a user, the user's current organization is used to determine what role permissions the the user. Adding to the this complexity a user will not have an organization initially. When a user first signs up they will be in their Personal Workspace.

Making the user a member of an organization will not make them active in that organization. The user would need to use the [`<OrganizationList />`](/docs/components/organization/organization-list) or [`<OrganizationSwitcher />`](/docs/components/organization/organization-switcher) components to select an organization, or their active organization could be set programmatically with the [`setActive()`](/docs/references/javascript/clerk/session-methods#set-active) method from the [`useOrganizationList()`](/docs/references/react/use-organization-list) component. The components in their default configuration allow the user to switch back to their Personal Workspace.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
Clerk's organization feature enables the users of an application to be members of more than one organization at a time. Each membership can have its own role, allowing a user to habe different privileges for each organization.
When performing an authorization check on a user, the user's current organization is used to determine what role permissions the the user. Adding to the this complexity a user will not have an organization initially. When a user first signs up they will be in their Personal Workspace.
Making the user a member of an organization will not make them active in that organization. The user would need to use the [`<OrganizationList />`](/docs/components/organization/organization-list) or [`<OrganizationSwitcher />`](/docs/components/organization/organization-switcher) components to select an organization, or their active organization could be set programmatically with the [`setActive()`](/docs/references/javascript/clerk/session-methods#set-active) method from the [`useOrganizationList()`](/docs/references/react/use-organization-list) component. The components in their default configuration allow the user to switch back to their Personal Workspace.
When a user is a member of an organization, they can switch between their personal workspace and an organization workspace. By default, when a user initially signs in to a Clerk-powered application, they are signed in to their personal workspace and no active organization is set. Even if they are a member of only one organization, they must explicitly set it as active or the application can have logic to set this automatically. [Learn more about active organizations.](/docs/organizations/overview#active-organization)

Comment on lines +14 to +34
## How do I know if a user is active in an organization?

You can check the `orgId` value on the [Auth](docs/references/nextjs/auth-object#auth-object). If this value is `undefined` then the user is not active in an organization and is instead in their Personal Workspace.

If you check the returned Auth object from [auth()](/docs/references/nextjs/auth) and the user is not active in an organization, it would like this:

```json
{
"sessionId": "sess_2GaMqUCB3Sc1WNAkWuNzsnYVVEy",
"userId": "user_2F2u1wtUyUlxKgFkKqtJNtpJJWj",
"claims": {
"azp": "http://localhost:3000",
"exp": 1666622607,
"iat": 1666622547,
"iss": "https://clerk.quiet.muskox-85.lcl.dev",
"nbf": 1666622537,
"sid": "sess_2GaMqUCB3Sc1WNAkWuNzsnYVVEy",
"sub": "user_2F2u1wtUyUlxKgFkKqtJNtpJJWj"
}
}
```
Copy link
Member

Choose a reason for hiding this comment

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

I don't necessarily think we need a section just on this - we can provide this info in the first step

Comment on lines +41 to +65
Let's start with a Middleware and that many applications could use. This example will do the following:

1. Check if the user accessing a private route is signed in. If the user is not, they are redirected to the `/sign-in` page.
1. Check if the user accessing a private route is signed in and if the user is then allow them to access that route.
1. Any use who is not accessing a public route will get access to the prublic.

```typescript {{ filename: '/middleware.ts' }}
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
import { NextRequest, NextResponse } from 'next/server'

const isPublicRoute = createRouteMatcher(['/', '/sign-in', '/sign-up'])

export default clerkMiddleware((auth, req: NextRequest) => {
// If the user isn't signed in and the route is private, redirect to sign-in
if (!auth().userId && !isPublicRoute(req))
return auth().redirectToSignIn({ returnBackUrl: req.url })

// If the user is logged in and the route is protected, let them view.
if (auth().userId && !isPublicRoute(req)) return NextResponse.next()
})

export const config = {
matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
}
```
Copy link
Member

Choose a reason for hiding this comment

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

We don't need two separate examples, when the second example is the same code as the first with the addition of the orgId check. We can simply say something like:

Suggested change
Let's start with a Middleware and that many applications could use. This example will do the following:
1. Check if the user accessing a private route is signed in. If the user is not, they are redirected to the `/sign-in` page.
1. Check if the user accessing a private route is signed in and if the user is then allow them to access that route.
1. Any use who is not accessing a public route will get access to the prublic.
```typescript {{ filename: '/middleware.ts' }}
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
import { NextRequest, NextResponse } from 'next/server'
const isPublicRoute = createRouteMatcher(['/', '/sign-in', '/sign-up'])
export default clerkMiddleware((auth, req: NextRequest) => {
// If the user isn't signed in and the route is private, redirect to sign-in
if (!auth().userId && !isPublicRoute(req))
return auth().redirectToSignIn({ returnBackUrl: req.url })
// If the user is logged in and the route is protected, let them view.
if (auth().userId && !isPublicRoute(req)) return NextResponse.next()
})
export const config = {
matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
}
```
The `orgId` value on the `Auth` object can be used to check if a user has an active organization. If this value is `undefined`, then the user is not active in an organization and is in their personal workspace.
To protect your application from users that do not have an active organization, configure your Middleware to check the user's `orgId`.
The following example configures `clerkMiddleware()` to protect all routes except `/`, `/sign-in`, and `/sign-up`. It will handle both authentication _and_ authorization. If a user is not signed in, they will be redirected to the sign-in page. If a user does not have an active organization, they will be redirected to`/organization-selection`, which you will create in the next step.

## Enforcing an active organization

<Steps>
### Configuring Middleware
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
### Configuring Middleware
### Configure Middleware to protect pages

Comment on lines +99 to +101
The `/ogranization-selection` route will render the `<OrganizationList />` component. The `hidePeronsal` prop is passed, which will hide the option for a user to select their Personal Worksapce. This will mean that all users, even new users, will need to either create or join and organization.

The component will also check the `orgId` from `auth()`. This will be falsy when the user is first redirected to the route. When the user creates or joins an organization, the auth object will be refreshed and `orgId` will have a valid organization id. When that happens the user will be redirected to `/dashboard`.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
The `/ogranization-selection` route will render the `<OrganizationList />` component. The `hidePeronsal` prop is passed, which will hide the option for a user to select their Personal Worksapce. This will mean that all users, even new users, will need to either create or join and organization.
The component will also check the `orgId` from `auth()`. This will be falsy when the user is first redirected to the route. When the user creates or joins an organization, the auth object will be refreshed and `orgId` will have a valid organization id. When that happens the user will be redirected to `/dashboard`.
Create the `/organization-selection` route, which is where users who do not have an active organization will get redirected to.
The following example:
- Uses the `<OrganizationList />` component to list the user's organization memberships. The `hidePeronsal` prop is passed, which will hide the option for a user to select their personal workspace. This will mean that all users, even new users, will need to either create or join an organization.
- Checks the `orgId` from `auth()`. Initially, this will be falsy when the user is redirected to the route. Once the user creates or joins an organization, the `Auth` object is refreshed and `orgId` contains a valid organization ID. At this point, the user will be redirected to `/dashboard`.

Comment on lines +129 to +131
### Updating Middleware

Let's start with the changes to Middleware. The Middleware will now read the `req.nextUrl.href` and URL endcode it, and then add that as a `redirect_url` search parameter.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
### Updating Middleware
Let's start with the changes to Middleware. The Middleware will now read the `req.nextUrl.href` and URL endcode it, and then add that as a `redirect_url` search parameter.
#### Update the Middleware
Modify `clerkMiddleware()` to include the `redirect_url` search parameter in the URL when redirecting to the `/organization-selection` route. This will allow that route to consume the search parameter and use it to redirect the user back to the route they were trying to access once they have created or joined an organization.

}
```

### Updating `/organization-selection`
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
### Updating `/organization-selection`
#### Update `/organization-selection`

Comment on lines +164 to +168
With Middleware adding the `redirect_url` search parameter to the URL, the route can then consume that and use it once the user has selected or created an organization.

First, modify the component parameters to ready the search parameters. Additionally indicate that the parameter expected is `redirect_url` with a type of string.

Second, once `orgId` is truthy check `redirect_url` was passed as a search parameter. If a `redirect_url` was passed then use that value, after URL decoding it, for the redirect. If there is no `redirect_url` then use `/dashboard` as a fallback.
Copy link
Member

Choose a reason for hiding this comment

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

Removing repetition, and cleaning up the explanation here.

Suggested change
With Middleware adding the `redirect_url` search parameter to the URL, the route can then consume that and use it once the user has selected or created an organization.
First, modify the component parameters to ready the search parameters. Additionally indicate that the parameter expected is `redirect_url` with a type of string.
Second, once `orgId` is truthy check `redirect_url` was passed as a search parameter. If a `redirect_url` was passed then use that value, after URL decoding it, for the redirect. If there is no `redirect_url` then use `/dashboard` as a fallback.
Modify the `orgId` check to check if the `redirect_url` search parameter is present. If it is, redirect the user back to that route. If it is not present, redirect the user to a fallback. In this example, the fallback is `/dashboard`.


## Wrap up

This will now reliably detect any user without an active organization (`orgId`), whether that user is someone who has just signed up or someone who organization was deleted or who left an organization. Any user without an active organization will be redirected and forced to create or select and organization.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
This will now reliably detect any user without an active organization (`orgId`), whether that user is someone who has just signed up or someone who organization was deleted or who left an organization. Any user without an active organization will be redirected and forced to create or select and organization.
Your application will now reliably detect any user without an active organization (`orgId`), whether that user is someone who has just signed up or someone whose organization was deleted or who left an organization. Any user without an active organization will be redirected and forced to create or join an organization.


This will now reliably detect any user without an active organization (`orgId`), whether that user is someone who has just signed up or someone who organization was deleted or who left an organization. Any user without an active organization will be redirected and forced to create or select and organization.

The route can modified to fit the needs of your application. You could configure it so the user can join an organization based on [email domain](/docs/organizations/verified-domains), or require a user to subscribe to a plan in your application before creating an organization.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
The route can modified to fit the needs of your application. You could configure it so the user can join an organization based on [email domain](/docs/organizations/verified-domains), or require a user to subscribe to a plan in your application before creating an organization.
The route can modified to fit the needs of your application. You could configure it so that the user can join an organization based on an [email domain](/docs/organizations/verified-domains), or require a user to subscribe to a plan in your application before creating an organization.


export default clerkMiddleware((auth, req: NextRequest) => {
// Check if the user is signed in and does not have an auth().orgId
// If true, then redirect them to the organization selection route
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// If true, then redirect them to the organization selection route
// If they don't have an active org, redirect them to `/organization-selection`

export default clerkMiddleware((auth, req: NextRequest) => {
// Check if the user is signed in and does not have an auth().orgId
// If true, then redirect them to the organization selection route
// Include their current route as a redirect_url, which will be handled on the route
Copy link
Member

Choose a reason for hiding this comment

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

I don't think this is added yet

Suggested change
// Include their current route as a redirect_url, which will be handled on the route

return NextResponse.redirect(orgListUrl)
}

// If the user isn't signed in and the route is private, redirect to sign-in
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// If the user isn't signed in and the route is private, redirect to sign-in
// If the user isn't signed in and the route is protected, redirect them to sign-in

if (!auth().userId && !isPublicRoute(req))
return auth().redirectToSignIn({ returnBackUrl: req.url })

// If the user is logged in and the route is protected, let them view.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// If the user is logged in and the route is protected, let them view.
// If the user is signed in and the route is protected, let them view

Comment on lines +157 to +159
export const config = {
matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
}
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
export const config = {
matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
}
export const config = {
matcher: [
// Skip Next.js internals and all static files, unless found in search params
'/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
// Always run for API routes
'/(api|trpc)(.*)',
],
};

const { orgId } = auth()

if (orgId) {
// If the orgId is truthy, then redirect the user to the redirect_url if present
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// If the orgId is truthy, then redirect the user to the redirect_url if present
// If the user has an active org, redirect them to the redirect_url if present

@alexisintech
Copy link
Member

@royanger @kylemac where are we with this?

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.

5 participants