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:
parent
49ca7d01a8
commit
b483800e6a
3 changed files with 67 additions and 4 deletions
|
@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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([
|
||||||
|
|
|
@ -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]) {
|
||||||
|
|
Loading…
Reference in a new issue