From 2f1fb659ef64d5fa3a21e7da4bfdf34538be8e37 Mon Sep 17 00:00:00 2001 From: simeng-li Date: Mon, 22 Jan 2024 10:29:32 +0800 Subject: [PATCH] refactor(core): update the consent apis (#5267) update the consent apis --- .../core/src/middleware/koa-auto-consent.ts | 33 +++++-- .../core/src/routes/interaction/consent.ts | 91 +++++++++++++++++-- packages/schemas/src/types/consent.ts | 4 +- 3 files changed, 110 insertions(+), 18 deletions(-) diff --git a/packages/core/src/middleware/koa-auto-consent.ts b/packages/core/src/middleware/koa-auto-consent.ts index 652557758..d465232f8 100644 --- a/packages/core/src/middleware/koa-auto-consent.ts +++ b/packages/core/src/middleware/koa-auto-consent.ts @@ -1,14 +1,18 @@ -import { trySafe } from '@silverhand/essentials'; +import { demoAppApplicationId } from '@logto/schemas'; import { type MiddlewareType } from 'koa'; import { type IRouterParamContext } from 'koa-router'; import type Provider from 'oidc-provider'; +import { errors } from 'oidc-provider'; +import { EnvSet } from '#src/env-set/index.js'; import { consent } from '#src/libraries/session.js'; import type Queries from '#src/tenants/Queries.js'; +import assertThat from '#src/utils/assert-that.js'; /** * Automatically consent for the first party apps. */ + export default function koaAutoConsent( provider: Provider, query: Queries @@ -17,18 +21,29 @@ export default function koaAutoConsent - query.applications.findApplicationById(String(clientId)) + const { + applications: { findApplicationById }, + } = query; + + assertThat( + clientId && typeof clientId === 'string', + new errors.InvalidClient('client must be available') ); - const shouldAutoConsent = !application?.isThirdParty; + // Demo app not in the database + const application = + clientId === demoAppApplicationId ? undefined : await findApplicationById(clientId); - if (!shouldAutoConsent) { - return next(); + // FIXME: @simeng-li remove this when the IdP is ready + const shouldAutoConsent = !EnvSet.values.isDevFeaturesEnabled || !application?.isThirdParty; + + if (shouldAutoConsent) { + const redirectTo = await consent(ctx, provider, query, interactionDetails); + + ctx.redirect(redirectTo); + return; } - const redirectTo = await consent(ctx, provider, query, interactionDetails); - - ctx.redirect(redirectTo); + return next(); }; } diff --git a/packages/core/src/routes/interaction/consent.ts b/packages/core/src/routes/interaction/consent.ts index 056895729..58d1236f9 100644 --- a/packages/core/src/routes/interaction/consent.ts +++ b/packages/core/src/routes/interaction/consent.ts @@ -1,4 +1,4 @@ -import { UserScope } from '@logto/core-kit'; +import { ReservedResource, UserScope } from '@logto/core-kit'; import { consentInfoResponseGuard, publicApplicationGuard, @@ -14,6 +14,7 @@ import { conditional } from '@silverhand/essentials'; import type Router from 'koa-router'; import { type IRouterParamContext } from 'koa-router'; import { errors } from 'oidc-provider'; +import { z } from 'zod'; import { EnvSet } from '#src/env-set/index.js'; import { consent, getMissingScopes } from '#src/libraries/session.js'; @@ -40,6 +41,30 @@ const parseMissingResourceScopesInfo = async ( const resourcesWithScopes = await Promise.all( Object.entries(missingResourceScopes).map(async ([resourceIndicator, scopeNames]) => { + // Organization resources are reserved resources, we don't need to find the resource details + if (resourceIndicator === ReservedResource.Organization) { + const [_, organizationScopes] = await queries.organizations.scopes.findAll(); + const scopes = scopeNames.map((scopeName) => { + const scope = organizationScopes.find((scope) => scope.name === scopeName); + + // Will be guarded by OIDC provider, should not happen + assertThat( + scope, + new InvalidTarget(`scope with name ${scopeName} not found for organization resource`) + ); + + return scope; + }); + + return { + resource: { + id: resourceIndicator, + name: resourceIndicator, + }, + scopes, + }; + } + const resource = await queries.resources.findResourceByIndicator(resourceIndicator); // Will be guarded by OIDC provider, should not happen @@ -74,19 +99,69 @@ const parseMissingResourceScopesInfo = async ( export default function consentRoutes( router: Router>, - { provider, queries }: TenantContext + { + provider, + queries, + libraries: { + applications: { validateUserConsentOrganizationMembership }, + }, + }: TenantContext ) { const consentPath = `${interactionPrefix}/consent`; - router.post(consentPath, async (ctx, next) => { - const { interactionDetails } = ctx; + router.post( + consentPath, + koaGuard({ + body: z.object({ + organizationIds: z.string().array().optional(), + }), + status: [200], + }), + async (ctx, next) => { + const { + interactionDetails, + guard: { + body: { organizationIds }, + }, + } = ctx; - const redirectTo = await consent(ctx, provider, queries, interactionDetails); + // FIXME: @simeng-li remove this when the IdP is ready - ctx.body = { redirectTo }; + // Grant the organizations to the application if the user has selected the organizations + if (organizationIds?.length && EnvSet.values.isDevFeaturesEnabled) { + const { + session, + params: { client_id: applicationId }, + } = interactionDetails; - return next(); - }); + assertThat(session, 'session.not_found'); + + assertThat( + applicationId && typeof applicationId === 'string', + new InvalidClient('client must be available') + ); + + const { accountId: userId } = session; + + // Assert that user is a member of all organizations + await validateUserConsentOrganizationMembership(userId, organizationIds); + + await queries.applications.userConsentOrganizations.insert( + ...organizationIds.map<[string, string, string]>((organizationId) => [ + applicationId, + userId, + organizationId, + ]) + ); + } + + const redirectTo = await consent(ctx, provider, queries, interactionDetails); + + ctx.body = { redirectTo }; + + return next(); + } + ); // FIXME: @simeng-li remove this when the IdP is ready if (!EnvSet.values.isDevFeaturesEnabled) { diff --git a/packages/schemas/src/types/consent.ts b/packages/schemas/src/types/consent.ts index 542299df0..250a6fe6c 100644 --- a/packages/schemas/src/types/consent.ts +++ b/packages/schemas/src/types/consent.ts @@ -45,7 +45,9 @@ export const publicOrganizationGuard = Organizations.guard.pick({ name: true, }); export const missingResourceScopesGuard = z.object({ - resource: Resources.guard.pick({ id: true, name: true }), + // The original resource id has a maximum length of 21 restriction. We need to make it compatible with the logto reserved organization name. + // use string here, as we do not care about the resource id length here. + resource: Resources.guard.pick({ name: true }).extend({ id: z.string() }), scopes: Scopes.guard.pick({ id: true, name: true, description: true }).array(), });