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

feat(core): redirect unknown session to app fallback uri

redirect unknown session to app fallback uri
This commit is contained in:
simeng-li 2024-11-11 10:44:03 +08:00
parent 49ca7d01a8
commit b483800e6a
No known key found for this signature in database
GPG key ID: 14EA7BB1541E8075
3 changed files with 67 additions and 4 deletions

View file

@ -6,6 +6,8 @@ import { EnvSet, UserApps } from '#src/env-set/index.js';
import { MockQueries } from '#src/test-utils/tenant.js'; import { MockQueries } from '#src/test-utils/tenant.js';
import { createContextWithRouteParameters } from '#src/utils/test-utils.js'; import { createContextWithRouteParameters } from '#src/utils/test-utils.js';
import { LoginQueryParamsKey } from '../oidc/utils.js';
const { jest } = import.meta; const { jest } = import.meta;
const { mockEsmWithActual } = createMockUtils(jest); const { mockEsmWithActual } = createMockUtils(jest);
@ -25,7 +27,11 @@ describe('koaSpaSessionGuard', () => {
const provider = new Provider('https://logto.test'); const provider = new Provider('https://logto.test');
const interactionDetails = jest.spyOn(provider, 'interactionDetails'); const interactionDetails = jest.spyOn(provider, 'interactionDetails');
const getRowsByKeys = jest.fn().mockResolvedValue({ rows: [] }); const getRowsByKeys = jest.fn().mockResolvedValue({ rows: [] });
const queries = new MockQueries({ logtoConfigs: { getRowsByKeys } }); const findApplicationById = jest.fn();
const queries = new MockQueries({
logtoConfigs: { getRowsByKeys },
applications: { findApplicationById },
});
beforeEach(() => { beforeEach(() => {
process.env = { ...envBackup }; process.env = { ...envBackup };
@ -115,4 +121,21 @@ describe('koaSpaSessionGuard', () => {
expect(ctx.redirect).toBeCalledWith('https://test.com/unknown-session'); expect(ctx.redirect).toBeCalledWith('https://test.com/unknown-session');
stub.restore(); stub.restore();
}); });
it('should redirect to configured application fallback URL if session not found', async () => {
const unknownSessionFallbackUri = 'https://foo.bar';
findApplicationById.mockResolvedValueOnce({
unknownSessionFallbackUri,
});
const appId = '123';
interactionDetails.mockRejectedValue(new Error('session not found'));
const ctx = createContextWithRouteParameters({
url: `${guardedPath[0]!}?${LoginQueryParamsKey.AppId}=${appId}`,
});
await koaSpaSessionGuard(provider, queries)(ctx, next);
expect(findApplicationById).toBeCalledWith(appId);
expect(ctx.redirect).toBeCalledWith(unknownSessionFallbackUri);
});
}); });

View file

@ -1,6 +1,11 @@
import { logtoConfigGuards, LogtoTenantConfigKey } from '@logto/schemas'; import {
logtoConfigGuards,
logtoCookieKey,
LogtoTenantConfigKey,
logtoUiCookieGuard,
} from '@logto/schemas';
import { appendPath, trySafe } from '@silverhand/essentials'; import { appendPath, trySafe } from '@silverhand/essentials';
import type { MiddlewareType } from 'koa'; import type { MiddlewareType, ParameterizedContext } from 'koa';
import type { IRouterParamContext } from 'koa-router'; import type { IRouterParamContext } from 'koa-router';
import type Provider from 'oidc-provider'; import type Provider from 'oidc-provider';
@ -9,6 +14,8 @@ import RequestError from '#src/errors/RequestError/index.js';
import type Queries from '#src/tenants/Queries.js'; import type Queries from '#src/tenants/Queries.js';
import { getTenantId } from '#src/utils/tenant.js'; import { getTenantId } from '#src/utils/tenant.js';
import { LoginQueryParamsKey } from '../oidc/utils.js';
// Need To Align With UI // Need To Align With UI
export const sessionNotFoundPath = '/unknown-session'; export const sessionNotFoundPath = '/unknown-session';
@ -21,6 +28,24 @@ export const guardedPath = [
'/forgot-password', '/forgot-password',
]; ];
/**
* Retrieve the appId from ctx for find the fallback url
* First check the query param appId
* If not found, check the logto ui cookie
*/
const getAppIdFromContext = (ctx: ParameterizedContext) => {
const appId = ctx.request.URL.searchParams.get(LoginQueryParamsKey.AppId);
if (appId) {
return appId;
}
const cookie = ctx.cookies.get(logtoCookieKey);
const parsed = trySafe(() => logtoUiCookieGuard.parse(cookie));
return parsed?.appId;
};
export default function koaSpaSessionGuard< export default function koaSpaSessionGuard<
StateT, StateT,
ContextT extends IRouterParamContext, ContextT extends IRouterParamContext,
@ -37,6 +62,17 @@ export default function koaSpaSessionGuard<
try { try {
await provider.interactionDetails(ctx.req, ctx.res); await provider.interactionDetails(ctx.req, ctx.res);
} catch { } catch {
// Try to find the fallback url for the application
const appId = getAppIdFromContext(ctx);
const application = appId
? await trySafe(async () => queries.applications.findApplicationById(appId))
: undefined;
if (application?.unknownSessionFallbackUri) {
ctx.redirect(application.unknownSessionFallbackUri);
return;
}
const { const {
rows: [data], rows: [data],
} = await queries.logtoConfigs.getRowsByKeys([ } = await queries.logtoConfigs.getRowsByKeys([

View file

@ -96,6 +96,10 @@ const firstScreenRouteMapping: Record<FirstScreen, keyof typeof experience.route
[FirstScreen.SignInDeprecated]: 'signIn', [FirstScreen.SignInDeprecated]: 'signIn',
}; };
export enum LoginQueryParamsKey {
AppId = 'app_id',
}
// Note: this eslint comment can be removed once the dev feature flag is removed // Note: this eslint comment can be removed once the dev feature flag is removed
export const buildLoginPromptUrl = (params: ExtraParamsObject, appId?: unknown): string => { export const buildLoginPromptUrl = (params: ExtraParamsObject, appId?: unknown): string => {
@ -114,7 +118,7 @@ export const buildLoginPromptUrl = (params: ExtraParamsObject, appId?: unknown):
const getSearchParamString = () => (searchParams.size > 0 ? `?${searchParams.toString()}` : ''); const getSearchParamString = () => (searchParams.size > 0 ? `?${searchParams.toString()}` : '');
if (appId) { if (appId) {
searchParams.append('app_id', String(appId)); searchParams.append(LoginQueryParamsKey.AppId, String(appId));
} }
if (params[ExtraParamsKey.OrganizationId]) { if (params[ExtraParamsKey.OrganizationId]) {