Skip to content

Commit 1fb7d53

Browse files
authored
Merge pull request #7949 from logto-io/charles-log-12430-core-experience-maintain-app_id-in-url-search-params
feat(core,experience): maintain app_id in auth related URL search params
2 parents dcf3ef5 + dc86b00 commit 1fb7d53

File tree

10 files changed

+61
-24
lines changed

10 files changed

+61
-24
lines changed

packages/core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@
8080
"lru-cache": "^11.0.0",
8181
"nanoid": "^5.0.9",
8282
"node-forge": "^1.3.1",
83-
"oidc-provider": "github:logto-io/node-oidc-provider#c8f353f8593b951156e730b96da38a14386a3338",
83+
"oidc-provider": "github:logto-io/node-oidc-provider#aa47a2b000d08e28c1d212aac1899eddd13009e9",
8484
"openapi-types": "^12.1.3",
8585
"otplib": "^12.0.1",
8686
"p-map": "^7.0.2",

packages/core/src/oidc/init.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import type { I18nKey } from '@logto/phrases';
1010
import {
1111
customClientMetadataDefault,
1212
CustomClientMetadataKey,
13-
experience,
1413
extraParamsObjectGuard,
1514
inSeconds,
1615
logtoCookieKey,
@@ -33,6 +32,7 @@ import koaBodyEtag from '#src/middleware/koa-body-etag.js';
3332
import koaResourceParam from '#src/middleware/koa-resource-param.js';
3433
import postgresAdapter from '#src/oidc/adapter.js';
3534
import {
35+
buildConsentPromptUrl,
3636
buildLoginPromptUrl,
3737
isOriginAllowed,
3838
validateCustomClientMetadata,
@@ -244,7 +244,7 @@ export default function initOidc(
244244
}
245245

246246
case 'consent': {
247-
return '/' + experience.routes.consent;
247+
return '/' + buildConsentPromptUrl(appId);
248248
}
249249

250250
default: {

packages/core/src/oidc/utils.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,3 +145,13 @@ export const buildLoginPromptUrl = (params: ExtraParamsObject, appId?: unknown):
145145

146146
return firstScreen + getSearchParamString();
147147
};
148+
149+
export const buildConsentPromptUrl = (appId?: unknown): string => {
150+
const searchParams = new URLSearchParams();
151+
if (appId) {
152+
searchParams.append('app_id', String(appId));
153+
}
154+
const searchParamString = searchParams.size > 0 ? `?${searchParams.toString()}` : '';
155+
156+
return experience.routes.consent + searchParamString;
157+
};

packages/experience/src/apis/api.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
11
import i18next from 'i18next';
22
import ky from 'ky';
33

4+
import { searchKeys } from '@/utils/search-parameters';
5+
46
export default ky.extend({
57
hooks: {
68
beforeRequest: [
79
(request) => {
810
request.headers.set('Accept-Language', i18next.language);
11+
/**
12+
* Attach app ID to HTTP header in order to support client-specific interaction cookie
13+
* for Experience API requests. The header is used by the oidc-provider library to identify
14+
* the client, and hence the related interaction details.
15+
*/
16+
const appId = sessionStorage.getItem(searchKeys.appId);
17+
if (appId) {
18+
request.headers.set('Logto-App-Id', appId);
19+
}
920
},
1021
],
1122
},

packages/experience/src/utils/search-parameters.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,11 @@ export const handleSearchParametersData = () => {
3535
const value = parameters.get(key);
3636
if (value) {
3737
sessionStorage.setItem(key, value);
38-
parameters.delete(key);
39-
} else {
38+
if (key !== searchKeys.appId) {
39+
// Keep app_id in the URL for resuming sessions
40+
parameters.delete(key);
41+
}
42+
} else if (key !== searchKeys.appId) {
4043
sessionStorage.removeItem(key);
4144
}
4245
}

packages/integration-tests/src/client/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,8 @@ export default class MockClient {
112112

113113
// Note: Should redirect to logto consent page
114114
assert(
115-
authResponse.status === 303 && authResponse.headers.get('location') === '/consent',
115+
authResponse.status === 303 &&
116+
authResponse.headers.get('location') === `/consent?app_id=${this.config.appId}`,
116117
new Error('Invoke auth before consent failed')
117118
);
118119

packages/integration-tests/src/tests/api/audit-logs/index.test.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { getAuditLogs } from '#src/api/logs.js';
77
import MockClient from '#src/client/index.js';
88
import { enableAllPasswordSignInMethods } from '#src/helpers/sign-in-experience.js';
99
import { generateNewUserProfile } from '#src/helpers/user.js';
10+
import { parseInteractionCookie } from '#src/utils.js';
1011

1112
describe('audit logs for interaction', () => {
1213
beforeAll(async () => {
@@ -20,10 +21,12 @@ describe('audit logs for interaction', () => {
2021
it('should insert log after interaction started and ended', async () => {
2122
const client = new MockClient();
2223
await client.initSession();
23-
const interactionId = client.parsedCookies.get('_interaction');
24+
const interactionCookie = client.parsedCookies.get('_interaction');
2425

25-
assert(interactionId, new Error('No interaction found in cookie'));
26-
console.debug('Testing interaction', interactionId);
26+
assert(interactionCookie, new Error('No interaction found in cookie'));
27+
console.debug('Testing interaction', interactionCookie);
28+
29+
const interactionId = parseInteractionCookie(interactionCookie)['demo-app'];
2730

2831
// Expect interaction create log
2932
const createLogs = await getAuditLogs(

packages/integration-tests/src/tests/console/bootstrap.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ describe('smoke testing for console admin account creation and sign-in', () => {
108108
)
109109
);
110110

111-
expect(page.url()).toBe(new URL('sign-in', logtoConsoleUrl).href);
111+
expect(page.url()).toBe(new URL('sign-in?app_id=admin-console', logtoConsoleUrl).href);
112112
});
113113

114114
it('can sign in to admin console again', async () => {

packages/integration-tests/src/utils.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,3 +148,12 @@ export const devFeatureDisabledTest = Object.freeze({
148148
it: isDevFeaturesEnabled ? it.skip : it,
149149
describe: isDevFeaturesEnabled ? describe.skip : describe,
150150
});
151+
152+
export const parseInteractionCookie = (cookie: string): Record<string, string> => {
153+
try {
154+
// eslint-disable-next-line no-restricted-syntax
155+
return JSON.parse(cookie) as Record<string, string>;
156+
} catch {
157+
return {};
158+
}
159+
};

pnpm-lock.yaml

Lines changed: 14 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)