0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

Merge pull request #4847 from logto-io/gao-update-org-scopes

refactor: update organization scopes
This commit is contained in:
Gao Sun 2023-11-10 14:48:47 +08:00 committed by GitHub
commit a03c0dbe5e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 47 additions and 35 deletions

View file

@ -1,6 +1,6 @@
import type { UserClaim } from '@logto/core-kit';
import { idTokenClaims, userinfoClaims, UserScope } from '@logto/core-kit';
import type { OrganizationClaimItem, User } from '@logto/schemas';
import { type User } from '@logto/schemas';
import type { Nullable } from '@silverhand/essentials';
import type { ClaimsParameterMember } from 'oidc-provider';
@ -9,7 +9,10 @@ import type Queries from '#src/tenants/Queries.js';
const claimToUserKey: Readonly<
Record<
Exclude<UserClaim, 'email_verified' | 'phone_number_verified' | 'roles' | 'organizations'>,
Exclude<
UserClaim,
'email_verified' | 'phone_number_verified' | 'roles' | 'organizations' | 'organization_roles'
>,
keyof User
>
> = Object.freeze({
@ -43,15 +46,14 @@ export const getUserClaimData = async (
return roles.map(({ name }) => name);
}
if (claim === 'organizations') {
if (claim === 'organizations' || claim === 'organization_roles') {
const data = await organizationQueries.relations.users.getOrganizationsByUserId(user.id);
return data.map(
({ id, organizationRoles }) =>
({
id,
roles: organizationRoles.map(({ name }) => name),
}) satisfies OrganizationClaimItem
);
return claim === 'organizations'
? data.map(({ id }) => id)
: data.flatMap(({ id, organizationRoles }) =>
organizationRoles.map(({ name }) => `${id}:${name}`)
);
}
return user[claimToUserKey[claim]];

View file

@ -39,6 +39,7 @@ export default function organizationRoutes<T extends AuthedRouter>(...args: Rout
errorHandler,
searchFields: ['name'],
disabled: { get: true },
idLength: 12,
});
router.get(

View file

@ -54,6 +54,12 @@ type SchemaRouterConfig<Key extends string> = {
errorHandler?: (error: unknown) => void;
/** The fields that can be searched for the `GET /` route. */
searchFields: SearchOptions<Key>['fields'];
/**
* The length of the ID generated by `generateStandardId()`.
*
* @see {@link generateStandardId} for the default length.
*/
idLength?: number;
};
type RelationRoutesConfig = {
@ -118,7 +124,7 @@ export default class SchemaRouter<
});
}
const { disabled, searchFields } = this.config;
const { disabled, searchFields, idLength } = this.config;
if (!disabled.get) {
this.get(
@ -152,7 +158,7 @@ export default class SchemaRouter<
async (ctx, next) => {
// eslint-disable-next-line no-restricted-syntax -- `.omit()` doesn't play well with generics
ctx.body = await queries.insert({
id: generateStandardId(),
id: generateStandardId(idLength),
...ctx.guard.body,
} as CreateSchema);
ctx.status = 201;

View file

@ -102,6 +102,9 @@ const App = () => {
endpoint: window.location.origin,
appId: demoAppApplicationId,
prompt: Prompt.Login,
// Use enum values once JS SDK is updated
scopes: ['urn:logto:scope:organizations', 'urn:logto:scope:organization_roles'],
resources: ['urn:logto:resource:organizations'],
}}
>
<Main />

View file

@ -62,7 +62,7 @@ describe('OpenID Connect ID token', () => {
});
});
it('should be issued with `organizations` claim', async () => {
it('should be issued with `organizations` and `organization_roles` claims', async () => {
const [org1, org2] = await Promise.all([
organizationApi.create({ name: 'org1' }),
organizationApi.create({ name: 'org2' }),
@ -76,19 +76,23 @@ describe('OpenID Connect ID token', () => {
const role = await organizationApi.roleApi.create({ name: 'member' });
await organizationApi.addUserRoles(org1.id, userId, [role.id]);
// Organizations claim
const idToken = await fetchIdToken(['urn:logto:scope:organizations']);
// @ts-expect-error type definition needs to be updated
const organizations = idToken.organizations as unknown;
expect(organizations).toHaveLength(2);
expect(organizations).toContainEqual({
id: org1.id,
roles: ['member'],
});
expect(organizations).toContainEqual({
id: org2.id,
roles: [],
});
expect(organizations).toContainEqual(org1.id);
expect(organizations).toContainEqual(org2.id);
// Organization roles claim
const idToken2 = await fetchIdToken(['urn:logto:scope:organization_roles']);
// @ts-expect-error type definition needs to be updated
const organizationRoles = idToken2.organization_roles as unknown;
expect(organizationRoles).toHaveLength(1);
expect(organizationRoles).toContainEqual(`${org1.id}:${role.name}`);
});
});

View file

@ -81,16 +81,3 @@ export type OrganizationWithFeatured = Organization & {
usersCount?: number;
featuredUsers?: FeaturedUser[];
};
/**
* The item that describes a user's membership and roles in an organization. It is
* designed to be used in the `organizations` claim of the ID token.
*
* @see {@link https://github.com/logto-io/rfcs | RFC 0001} for more details.
*/
export type OrganizationClaimItem = {
/** The ID of the organization. */
id: string;
/** The role names of the user in the organization. */
roles: string[];
};

View file

@ -24,6 +24,7 @@ export type UserClaim =
| 'phone_number_verified'
| 'roles'
| 'organizations'
| 'organization_roles'
| 'custom_data'
| 'identities';
@ -68,11 +69,17 @@ export enum UserScope {
*/
Roles = 'roles',
/**
* Scope for user's organizations data per [RFC 0001](https://https://github.com/logto-io/rfcs).
* Scope for user's organization IDs and perform organization token grant per [RFC 0001](https://github.com/logto-io/rfcs).
*
* See {@link idTokenClaims} for mapped claims in ID Token and {@link userinfoClaims} for additional claims in Userinfo Endpoint.
*/
Organizations = 'urn:logto:scope:organizations',
/**
* Scope for user's organization roles per [RFC 0001](https://github.com/logto-io/rfcs).
*
* See {@link idTokenClaims} for mapped claims in ID Token and {@link userinfoClaims} for additional claims in Userinfo Endpoint.
*/
OrganizationRoles = 'urn:logto:scope:organization_roles',
}
/**
@ -84,6 +91,7 @@ export const idTokenClaims: Readonly<Record<UserScope, UserClaim[]>> = Object.fr
[UserScope.Phone]: ['phone_number', 'phone_number_verified'],
[UserScope.Roles]: ['roles'],
[UserScope.Organizations]: ['organizations'],
[UserScope.OrganizationRoles]: ['organization_roles'],
[UserScope.CustomData]: [],
[UserScope.Identities]: [],
});
@ -97,6 +105,7 @@ export const userinfoClaims: Readonly<Record<UserScope, UserClaim[]>> = Object.f
[UserScope.Phone]: [],
[UserScope.Roles]: [],
[UserScope.Organizations]: [],
[UserScope.OrganizationRoles]: [],
[UserScope.CustomData]: ['custom_data'],
[UserScope.Identities]: ['identities'],
});