0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-01-27 21:39:16 -05:00

refactor(core): update the consent apis (#5267)

update the consent apis
This commit is contained in:
simeng-li 2024-01-22 10:29:32 +08:00 committed by GitHub
parent cc8b9d88e3
commit 2f1fb659ef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 110 additions and 18 deletions

View file

@ -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<StateT, ContextT extends IRouterParamContext, ResponseBodyT>(
provider: Provider,
query: Queries
@ -17,18 +21,29 @@ export default function koaAutoConsent<StateT, ContextT extends IRouterParamCont
const interactionDetails = await provider.interactionDetails(ctx.req, ctx.res);
const { client_id: clientId } = interactionDetails.params;
const application = await trySafe(async () =>
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();
};
}

View file

@ -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<T extends IRouterParamContext>(
router: Router<unknown, WithInteractionDetailsContext<T>>,
{ 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) {

View file

@ -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(),
});