diff --git a/packages/core/src/routes/session/index.ts b/packages/core/src/routes/session/index.ts index 3c6202fa4..9cde41dce 100644 --- a/packages/core/src/routes/session/index.ts +++ b/packages/core/src/routes/session/index.ts @@ -10,11 +10,15 @@ import { assignInteractionResults, saveUserFirstConsentedAppId } from '@/lib/ses import assertThat from '@/utils/assert-that'; import { AnonymousRouter } from '../types'; +import koaGuardSessionAction from './middleware/koa-guard-session-action'; import passwordlessRoutes from './passwordless'; import socialRoutes from './social'; import usernamePasswordRoutes from './username-password'; export default function sessionRoutes(router: T, provider: Provider) { + router.use('/session/sign-in', koaGuardSessionAction(provider, 'sign-in')); + router.use('/session/register', koaGuardSessionAction(provider, 'register')); + router.post('/session', async (ctx, next) => { const { prompt: { name }, diff --git a/packages/core/src/routes/session/middleware/koa-guard-session-action.ts b/packages/core/src/routes/session/middleware/koa-guard-session-action.ts new file mode 100644 index 000000000..ee756e134 --- /dev/null +++ b/packages/core/src/routes/session/middleware/koa-guard-session-action.ts @@ -0,0 +1,48 @@ +import { SignInMode } from '@logto/schemas'; +import { adminConsoleApplicationId } from '@logto/schemas/lib/seeds'; +import { MiddlewareType } from 'koa'; +import { Provider, errors } from 'oidc-provider'; + +import RequestError from '@/errors/RequestError'; +import { findDefaultSignInExperience } from '@/queries/sign-in-experience'; +import assertThat from '@/utils/assert-that'; + +export default function KoaGuardSessionAction( + provider: Provider, + forType: 'sign-in' | 'register' +): MiddlewareType { + const forbiddenError = new RequestError({ code: 'auth.forbidden', status: 403 }); + + return async (ctx, next) => { + const interaction = await provider + .interactionDetails(ctx.req, ctx.res) + .catch((error: unknown) => { + // Should not block if interaction is not found + if (error instanceof errors.SessionNotFound) { + return null; + } + + throw error; + }); + + /** + * We don't guard admin console in API for now since logically there's no need. + * Update to honor the config if we're implementing per-app SIE. + */ + if (interaction?.params.client_id === adminConsoleApplicationId) { + return next(); + } + + const { signInMode } = await findDefaultSignInExperience(); + + if (forType === 'sign-in') { + assertThat(signInMode !== SignInMode.Register, forbiddenError); + } + + if (forType === 'register') { + assertThat(signInMode !== SignInMode.SignIn, forbiddenError); + } + + return next(); + }; +} diff --git a/packages/core/src/routes/session/username-password.test.ts b/packages/core/src/routes/session/username-password.test.ts index cf5e5ddb2..f32efcd66 100644 --- a/packages/core/src/routes/session/username-password.test.ts +++ b/packages/core/src/routes/session/username-password.test.ts @@ -6,7 +6,7 @@ import { mockUser } from '@/__mocks__'; import RequestError from '@/errors/RequestError'; import { createRequester } from '@/utils/test-utils'; -import sessionRoutes from '.'; +import sessionRoutes from './username-password'; const insertUser = jest.fn(async (..._args: unknown[]) => ({ id: 'id' })); const findUserById = jest.fn(async (): Promise => mockUser);