diff --git a/docs/docs/recipes/organizations/README.mdx b/docs/docs/recipes/organizations/README.mdx index fb6b2ff556e..fb006488b06 100644 --- a/docs/docs/recipes/organizations/README.mdx +++ b/docs/docs/recipes/organizations/README.mdx @@ -6,11 +6,11 @@ import Availability from '@components/Availability'; # 🏢 Organizations (Multi-tenancy) - + -Organization is particularly effective in business-to-business (B2B) apps. In addtion to individual consumers, business clients can also consist of teams, organizations, or entire companies. Logto introduces the entity of an organization as a foundational element for authentication and authorization in B2B products, for example, SaaS. +Organization is particularly effective in business-to-business (B2B) apps. In addition to individual users, your clients can also consist of teams, organizations, or entire companies. Logto introduces Organizations as a foundational element for B2B authentication and authorization. -Even if your product is not B2B, organization can still be useful for collaboration features, such as sharing resources with other users. +Even if your product is consumer-facing, organization can still be useful for collaboration features, such as sharing resources with other users. With this fundamental element, you can build the must-have features for multi-tenancy apps, such as: diff --git a/docs/docs/recipes/organizations/configure-organizations.mdx b/docs/docs/recipes/organizations/configuration.mdx similarity index 100% rename from docs/docs/recipes/organizations/configure-organizations.mdx rename to docs/docs/recipes/organizations/configuration.mdx diff --git a/docs/docs/recipes/organizations/impact-on-end-users.mdx b/docs/docs/recipes/organizations/impact-on-end-users.mdx index d7bcdb5371c..e2997ae07ab 100644 --- a/docs/docs/recipes/organizations/impact-on-end-users.mdx +++ b/docs/docs/recipes/organizations/impact-on-end-users.mdx @@ -8,4 +8,4 @@ Currently, there's no impact to the end-user sign-in experience since Logto deco As the user signs in, they can get the access to all organizations with membership. For detailed information, please refer to Organization RBAC. -We may add advanced features for organizations that change the sign-in experience in the future. Please +We may add advanced features for organizations that change the sign-in experience in the future. Stay tuned! Join our [Discord community](https://discord.gg/UEPaF3j5e6) to share your thoughts and feedback. diff --git a/docs/docs/recipes/organizations/integration.mdx b/docs/docs/recipes/organizations/integration.mdx new file mode 100644 index 00000000000..77b5dcc949d --- /dev/null +++ b/docs/docs/recipes/organizations/integration.mdx @@ -0,0 +1,276 @@ +--- +sidebar_position: 4 +--- + +import TabItem from '@theme/TabItem'; +import Tabs from '@theme/Tabs'; + +# Integrate Organizations with your app + +:::note +This document assumes you have already [integrated Logto with your app](/docs/docs/recipes/integrate-logto/README.md). +::: + +In your app, you may want to show a list of organizations that the user is a member of, and perform actions in the context of an organization. Let's see how to do that. + +## Get organization IDs of the current user + +Logto extends the standard [OpenID Connect](https://openid.net/specs/openid-connect-core-1_0.html) protocol to allow your app to get the organization info from the user. There are two ways to do that: + +- If you are using a Logto SDK with Organizations support, you can add the `urn:logto:scope:organizations` scope to `scopes` parameter of the configuration object. Usually the SDK will have an enum for this scope, e.g. `UserScope.Organizations` in [Logto JS SDKs](https://github.com/logto-io/js). +- For other cases, you need to add the `urn:logto:scope:organizations` scope to the `scope` parameter of the SDK config (or auth request). + + + + + +```ts +const logto = new LogtoClient({ + // ... + scopes: [UserScope.Organizations], +}); +``` + + + + +```tsx + +``` + + + + + +```ts +const config = { + // ... + scope: 'openid offline_access urn:logto:scope:organizations', +}; +``` + + + + + +Once the user finishes the authentication flow, you can get the organization info from the `idToken`: + +```ts +// Use JavaScript as an example +const idToken = await logto.getIdTokenClaims(); + +console.log(idToken.organizations); // A string array of organization IDs +``` + +The `organizations` field (claim) will also be included in response from the [UserInfo endpoint](https://openid.net/specs/openid-connect-core-1_0.html#UserInfo). + +### Get organization roles + +To get all organization roles of the current user: + +- If you are using a Logto SDK with Organizations support, you can add the `urn:logto:scope:organization_roles` scope to `scopes` parameter of the configuration object. Usually the SDK will have an enum for this scope, e.g. `UserScope.OrganizationRoles` in [Logto JS SDKs](https://github.com/logto-io/js). +- For other cases, you need to add the `urn:logto:scope:organization_roles` scope to the `scope` parameter of the SDK config (or auth request). + +Then you can get the organization roles from the `idToken`: + +```ts +// Use JavaScript as an example +const idToken = await logto.getIdTokenClaims(); + +console.log(idToken.organization_roles); // A string array of organization roles +``` + +Each string in the array is in the format of `organization_id:role_id`, e.g. `org_123:admin` means the user has the `admin` role in the organization with ID `org_123`. + +The `organization_roles` field (claim) will also be included in response from the [UserInfo endpoint](https://openid.net/specs/openid-connect-core-1_0.html#UserInfo). + +## Fetch access token for an organization + +To perform actions in the context of an organization, the user needs to be granted an access token for that organization (organization token). The organization token is a JWT token that contains the organization ID and the user's permissions (scopes) in the organization. + +### Step 1: Add parameters to the authentication request + +- If you are using a Logto SDK with Organizations support, you can add the `urn:logto:scope:organization_token` scope to `scopes` parameter of the configuration object, the same way as [Get organization IDs of the current user](#get-organization-ids-of-the-current-user). + - Logto SDK with Organizations support will automatically handle the rest of the configuration. +- For other cases, you need to add the `offline_access` and `urn:logto:scope:organizations` scopes to the `scope` parameter and the `urn:logto:resource:organizations` resource to the `resource` parameter of the SDK config (or auth request). + - Note: `offline_access` is required to get the `refresh_token` that can be used to fetch organization tokens. + + + + + +```ts +const logto = new LogtoClient({ + // ... + scopes: [UserScope.Organizations], +}); +``` + + + + +```tsx + +``` + + + + + +```ts +const config = { + // ... + scope: 'openid offline_access urn:logto:scope:organizations', + resource: 'urn:logto:resource:organizations', +}; +``` + + + + + +:::info +The `urn:logto:resource:organizations` resource is a special resource that represents the organization template. +::: + +### Step 2: Fetch the organization token + +Logto extends the standard `refresh_token` grant type to allow your app to fetch organization tokens. + +- If you are using a Logto SDK with Organizations support, you can call the `getOrganizationToken()` method (or `getOrganizationTokenClaims()` method) of the SDK. +- For other cases, you need to call the token endpoint with the following parameters: + - `grant_type`: `refresh_token`. + - `client_id`: The app ID the user used to authenticate. + - `refresh_token`: The `refresh_token` you got from the authentication flow. + - `organization_id`: The ID of the organization you want to get the token for. + - `scope` (optional): The scopes you want to grant to the user in the organization. If not specified, the authorization server will try to grant the same scopes as the authentication flow. + + + + + +```ts +const token = await logto.getOrganizationToken('org_123'); +``` + + + + +```tsx +const App = () => { + const { getOrganizationToken } = useLogto(); + + const getToken = async () => { + const token = await getOrganizationToken('org_123'); + }; + + return ; +}; +``` + + + + + +```ts +// Use JavaScript as an example + +const params = new URLSearchParams(); + +params.append('grant_type', 'refresh_token'); +params.append('client_id', 'YOUR_CLIENT_ID'); +params.append('refresh_token', 'REFRESH_TOKEN'); +params.append('organization_id', 'org_123'); + +const response = await fetch('https://YOUR_LOGTO_ENDPOINT/oidc/token', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: params, +}); +``` + + + + + +The response will be in the same format as the [standard token endpoint](https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint), and the `access_token` is the organization token in JWT format. + +Besides regular claims of an access token, the organization token also contains the following claims: + +- `aud`: The audience of the organization token is `urn:logto:organization:{organization_id}`. +- `scope`: The scopes granted to the user in the organization with space as delimiter. + +### Example + +A good example can beat a thousand words. Assume our organization template has the following setup: + +- Permissions: `read:logs`, `write:logs`, `read:users`, `write:users`. +- Roles: `admin`, `member`. + - The `admin` role has all permissions. + - The `member` role has `read:logs` and `read:users` permissions. + +And the user has the following setup: + +- Organization IDs: `org_1`, `org_2`. +- Organization roles: `org_1:admin`, `org_2:member`. + +In the Logto SDK config (or auth request), we set up other things properly, and added the following scopes: + +- `urn:logto:scope:organizations` +- `openid` +- `offline_access` +- `read:logs` +- `write:logs` + +Now, when the user finishes the authentication flow, we can get the organization IDs from the `idToken`: + +```ts +// Use JavaScript as an example +const idToken = await logto.getIdTokenClaims(); + +console.log(idToken.organizations); // ['org_1', 'org_2'] +``` + +If we want to get the organization tokens: + +```ts +// Use JavaScript as an example +const org1Token = await logto.getOrganizationTokenClaims('org_1'); +const org2Token = await logto.getOrganizationTokenClaims('org_2'); + +console.log(org1Token.aud); // 'urn:logto:organization:org_1' +console.log(org1Token.scope); // 'read:logs write:logs' +console.log(org2Token.aud); // 'urn:logto:organization:org_2' +console.log(org2Token.scope); // 'read:logs' + +const org3Token = await logto.getOrganizationTokenClaims('org_3'); // Error: User is not a member of the organization +``` + +Explanation: + +- For `org_1`, the user has the `admin` role, so the organization token should have all available permissions (scopes). +- For `org_2`, the user has the `member` role, so the organization token should have `read:logs` and `read:users` permissions (scopes). + +Since we only requested `read:logs` and `write:logs` scopes in the authentication flow, the organization tokens have been "downscoped" accordingly, resulting in the intersection of the requested scopes and the available scopes. + +## Verify organization tokens + +Once the app gets an organization token, it can use the token in the same way as a regular access token, e.g. call the APIs with the token in the `Authorization` header in the format of `Bearer {token}`. + +In your API, you can verify the organization token which is similar to [Proctect your API](/docs/recipes/protect-your-api/#validate-the-authorization-token). Main differences: + +- Unlike access tokens for API resources, a user CANNOT get an organization token if the user is not a member of the organization. +- The audience of the organization token is `urn:logto:organization:{organization_id}`. +- For certain permissions (scopes), you need to check the `scope` claim of the organization token by splitting the string with space as delimiter. diff --git a/docs/docs/recipes/organizations/understand-how-it-works.mdx b/docs/docs/recipes/organizations/understand-how-it-works.mdx index 9756f00af30..5e3b69ffe52 100644 --- a/docs/docs/recipes/organizations/understand-how-it-works.mdx +++ b/docs/docs/recipes/organizations/understand-how-it-works.mdx @@ -19,7 +19,9 @@ The introduction of an organization as an entity is important, as it not only gr In Logto, a user who has the membership of an organization is referred to as an organization member (i.e. member) within that organization's context. -Organization member +
+ Organization member +
## Organization permission @@ -27,7 +29,9 @@ Organization permission refers to the authorization to perform an action in the For example, `edit:resource`. -Organization permission +
+ Organization permission +
Organization permissions are not meaningful without the context of an organization. For example, `edit:resource` in the context of organization `org1` is different from `edit:resource` in the context of organization `org2`. @@ -35,7 +39,9 @@ Organization permissions are not meaningful without the context of an organizati Organization role is a grouping of organization permissions that can be assigned to users. The permissions must come from the predefined organization permissions. -Organization role +
+ Organization role +
Organization roles are not meaningful without the context of an organization. For example, `admin` in the context of organization `org1` is different from `admin` in the context of organization `org2`.