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:
commit
a03c0dbe5e
7 changed files with 47 additions and 35 deletions
|
@ -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]];
|
||||
|
|
|
@ -39,6 +39,7 @@ export default function organizationRoutes<T extends AuthedRouter>(...args: Rout
|
|||
errorHandler,
|
||||
searchFields: ['name'],
|
||||
disabled: { get: true },
|
||||
idLength: 12,
|
||||
});
|
||||
|
||||
router.get(
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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}`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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[];
|
||||
};
|
||||
|
|
|
@ -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'],
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue