diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 461652ea2..fc1fd8499 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,14 @@ # Change Log +## 1.4.0 + +### Patch Changes + +- 5d6720805: update default OpenAI concurrency to 1 for sync command +- Updated dependencies [5d6720805] + - @logto/phrases@1.3.0 + - @logto/schemas@1.4.0 + ## 1.3.1 ### Patch Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index 256ea1d0f..8fc0b789f 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@logto/cli", - "version": "1.3.1", + "version": "1.4.0", "description": "Logto CLI.", "author": "Silverhand Inc. ", "homepage": "https://github.com/logto-io/logto#readme", @@ -46,9 +46,9 @@ "@logto/connector-kit": "workspace:^1.1.1", "@logto/core-kit": "workspace:^2.0.0", "@logto/language-kit": "workspace:^1.0.0", - "@logto/phrases": "workspace:^1.2.0", + "@logto/phrases": "workspace:^1.3.0", "@logto/phrases-ui": "workspace:^1.2.0", - "@logto/schemas": "workspace:1.3.1", + "@logto/schemas": "workspace:1.4.0", "@logto/shared": "workspace:^2.0.0", "@silverhand/essentials": "^2.5.0", "chalk": "^5.0.0", diff --git a/packages/cli/src/commands/translate/sync.ts b/packages/cli/src/commands/translate/sync.ts index fcd3c7d60..81d617680 100644 --- a/packages/cli/src/commands/translate/sync.ts +++ b/packages/cli/src/commands/translate/sync.ts @@ -13,7 +13,7 @@ const sync: CommandModule<{ path?: string }, { path?: string }> = { describe: 'Translate all untranslated phrases using ChatGPT. Note the environment variable `OPENAI_API_KEY` is required to work.', handler: async ({ path: inputPath }) => { - const queue = new PQueue({ concurrency: 5 }); + const queue = new PQueue({ concurrency: 1 }); const instancePath = await inquireInstancePath(inputPath); for (const languageTag of Object.keys(languages)) { diff --git a/packages/console/CHANGELOG.md b/packages/console/CHANGELOG.md index b8cfdc38e..2d272ae0d 100644 --- a/packages/console/CHANGELOG.md +++ b/packages/console/CHANGELOG.md @@ -1,5 +1,13 @@ # Change Log +## 1.3.0 + +### Minor Changes + +- 5d6720805: add config `alwaysIssueRefreshToken` for web apps to unblock OAuth integrations that are not strictly conform OpenID Connect. + + when it's enabled, Refresh Tokens will be always issued regardless if `prompt=consent` was present in the authorization request. + ## 1.2.4 ### Patch Changes diff --git a/packages/console/package.json b/packages/console/package.json index 8121d2884..36e6ea805 100644 --- a/packages/console/package.json +++ b/packages/console/package.json @@ -1,6 +1,6 @@ { "name": "@logto/console", - "version": "1.2.4", + "version": "1.3.0", "description": "> TODO: description", "author": "Silverhand Inc. ", "homepage": "https://github.com/logto-io/logto#readme", @@ -29,10 +29,10 @@ "@logto/connector-kit": "workspace:^1.1.1", "@logto/core-kit": "workspace:^2.0.0", "@logto/language-kit": "workspace:^1.0.0", - "@logto/phrases": "workspace:^1.2.0", + "@logto/phrases": "workspace:^1.3.0", "@logto/phrases-ui": "workspace:^1.2.0", "@logto/react": "^2.0.0", - "@logto/schemas": "workspace:^1.3.0", + "@logto/schemas": "workspace:^1.4.0", "@logto/shared": "workspace:^2.0.0", "@mdx-js/react": "^1.6.22", "@parcel/compressor-brotli": "2.8.3", diff --git a/packages/console/src/pages/ApplicationDetails/components/AdvancedSettings.tsx b/packages/console/src/pages/ApplicationDetails/components/AdvancedSettings.tsx index 74253f4f4..5b2fd725a 100644 --- a/packages/console/src/pages/ApplicationDetails/components/AdvancedSettings.tsx +++ b/packages/console/src/pages/ApplicationDetails/components/AdvancedSettings.tsx @@ -64,6 +64,14 @@ function AdvancedSettings({ applicationType, oidcConfig }: Props) { variant="border" /> + {[ApplicationType.Traditional, ApplicationType.SPA].includes(applicationType) && ( + + + + )} {applicationType === ApplicationType.MachineToMachine && ( ", @@ -29,15 +29,15 @@ "@azure/storage-blob": "^12.13.0", "@koa/cors": "^4.0.0", "@logto/app-insights": "workspace:^1.2.0", - "@logto/cli": "workspace:^1.3.1", + "@logto/cli": "workspace:^1.4.0", "@logto/connector-kit": "workspace:^1.1.1", "@logto/console": "workspace:*", "@logto/core-kit": "workspace:^2.0.0", "@logto/demo-app": "workspace:*", "@logto/language-kit": "workspace:^1.0.0", - "@logto/phrases": "workspace:^1.2.0", + "@logto/phrases": "workspace:^1.3.0", "@logto/phrases-ui": "workspace:^1.2.0", - "@logto/schemas": "workspace:^1.3.1", + "@logto/schemas": "workspace:^1.4.0", "@logto/shared": "workspace:^2.0.0", "@logto/ui": "workspace:*", "@silverhand/essentials": "^2.5.0", diff --git a/packages/core/src/oidc/init.ts b/packages/core/src/oidc/init.ts index 18042abbd..dbe9f9621 100644 --- a/packages/core/src/oidc/init.ts +++ b/packages/core/src/oidc/init.ts @@ -12,6 +12,7 @@ import { } from '@logto/schemas'; import { conditional } from '@silverhand/essentials'; import i18next from 'i18next'; +import koaBody from 'koa-body'; import Provider, { errors, type ResourceServer } from 'oidc-provider'; import snakecaseKeys from 'snakecase-keys'; @@ -151,6 +152,16 @@ export default function initOidc( }, }, }, + issueRefreshToken: (_, client, code) => { + if (!client.grantTypeAllowed('refresh_token')) { + return false; + } + + return ( + code.scopes.has('offline_access') || + (client.applicationType === 'web' && Boolean(client.metadata().alwaysIssueRefreshToken)) + ); + }, interactions: { url: (ctx, { params: { client_id: appId }, prompt }) => { const isDemoApp = appId === demoAppApplicationId; @@ -256,7 +267,7 @@ export default function initOidc( }, pkce: { required: (ctx, client) => { - return client.tokenEndpointAuthMethod !== 'client_secret_basic'; + return client.clientAuthMethod !== 'client_secret_basic'; }, methods: ['S256'], }, @@ -266,6 +277,30 @@ export default function initOidc( // Provide audit log context for event listeners oidc.use(koaAuditLog(queries)); + /** + * Create a middleware function that transpile requests with content type `application/json` + * since `oidc-provider` only accepts `application/x-www-form-urlencoded` for most of routes. + * + * Other parsers are explicitly disabled to keep it neat. + */ + oidc.use(koaBody({ urlencoded: false, text: false })); + /** + * `oidc-provider` [strictly checks](https://github.com/panva/node-oidc-provider/blob/6a0bcbcd35ed3e6179e81f0ab97a45f5e4e58f48/lib/shared/selective_body.js#L11) + * the `content-type` header for further processing. + * + * It will [directly use the `ctx.req.body` for parsing](https://github.com/panva/node-oidc-provider/blob/6a0bcbcd35ed3e6179e81f0ab97a45f5e4e58f48/lib/shared/selective_body.js#L39) + * so there's no need to change the raw request body as we uses `koaBody()` above. + * + * However, this is not recommended for other routes rather since it causes a header-body format mismatch. + */ + oidc.use(async (ctx, next) => { + // WARNING: [Registration actions](https://github.com/panva/node-oidc-provider/blob/6a0bcbcd35ed3e6179e81f0ab97a45f5e4e58f48/lib/actions/registration.js#L4) are using + // 'application/json' for body parsing. Update relatively when we enable that feature. + if (ctx.headers['content-type'] === 'application/json') { + ctx.headers['content-type'] = 'application/x-www-form-urlencoded'; + } + return next(); + }); oidc.use(koaBodyEtag()); return oidc; diff --git a/packages/core/src/routes/interaction/actions/submit-interaction.test.ts b/packages/core/src/routes/interaction/actions/submit-interaction.test.ts index f026a572d..d366c5430 100644 --- a/packages/core/src/routes/interaction/actions/submit-interaction.test.ts +++ b/packages/core/src/routes/interaction/actions/submit-interaction.test.ts @@ -73,7 +73,13 @@ describe('submit action', () => { connectorId: 'logto', }; - const userInfo = { id: 'foo', name: 'foo_social', avatar: 'avatar' }; + const userInfo = { + id: 'foo', + name: 'foo_social', + avatar: 'avatar', + email: 'email@socail.com', + phone: '123123', + }; const identifiers: Identifier[] = [ { @@ -127,6 +133,37 @@ describe('submit action', () => { }); }); + it('register new social user', async () => { + const interaction: VerifiedRegisterInteractionResult = { + event: InteractionEvent.Register, + profile: { connectorId: 'logto', username: 'username' }, + identifiers, + }; + + await submitInteraction(interaction, ctx, tenant); + + expect(generateUserId).toBeCalled(); + expect(hasActiveUsers).not.toBeCalled(); + expect(encryptUserPassword).not.toBeCalled(); + expect(getLogtoConnectorById).toBeCalledWith('logto'); + + expect(insertUser).toBeCalledWith( + { + id: 'uid', + username: 'username', + identities: { + logto: { userId: userInfo.id, details: userInfo }, + }, + name: userInfo.name, + avatar: userInfo.avatar, + primaryEmail: userInfo.email, + primaryPhone: userInfo.phone, + lastSignInAt: now, + }, + ['user'] + ); + }); + it('admin user register', async () => { hasActiveUsers.mockResolvedValueOnce(false); const adminConsoleCtx = { diff --git a/packages/core/src/routes/interaction/actions/submit-interaction.ts b/packages/core/src/routes/interaction/actions/submit-interaction.ts index 6a305e579..c15897caa 100644 --- a/packages/core/src/routes/interaction/actions/submit-interaction.ts +++ b/packages/core/src/routes/interaction/actions/submit-interaction.ts @@ -1,6 +1,6 @@ import { Component, CoreEvent, getEventName } from '@logto/app-insights/custom-event'; import { appInsights } from '@logto/app-insights/node'; -import type { User, Profile } from '@logto/schemas'; +import type { User, Profile, CreateUser } from '@logto/schemas'; import { AdminTenantRole, SignInMode, @@ -10,6 +10,7 @@ import { InteractionEvent, adminConsoleApplicationId, } from '@logto/schemas'; +import { type OmitAutoSetFields } from '@logto/shared'; import { conditional, conditionalArray } from '@silverhand/essentials'; import { EnvSet } from '#src/env-set/index.js'; @@ -46,7 +47,6 @@ const getNewSocialProfile = async ( } ) => { // TODO: @simeng refactor me. This step should be verified by the previous profile verification cycle Already. - // Should pickup the verified social user info result automatically const socialIdentifier = identifiers.find((identifier) => identifier.connectorId === connectorId); if (!socialIdentifier) { @@ -59,26 +59,43 @@ const getNewSocialProfile = async ( } = await getLogtoConnectorById(connectorId); const { userInfo } = socialIdentifier; - const { name, avatar, id } = userInfo; + const { name, avatar, id, email, phone } = userInfo; - // Update the user name and avatar if the connector has syncProfile enabled or is new registered user - const profileUpdate = conditional( - (syncProfile || !user) && { + const identities = { ...user?.identities, [target]: { userId: id, details: userInfo } }; + + // Sync the name, avatar, email and phone for new user + if (!user) { + return { + identities, ...conditional(name && { name }), ...conditional(avatar && { avatar }), - } - ); + ...conditional(email && { primaryEmail: email }), + ...conditional(phone && { primaryPhone: phone }), + }; + } + // Sync the user name and avatar if the connector has syncProfile enabled return { - identities: { ...user?.identities, [target]: { userId: id, details: userInfo } }, - ...profileUpdate, + identities, + ...conditional( + syncProfile && { + ...conditional(name && { name }), + ...conditional(avatar && { avatar }), + } + ), }; }; -const getSyncedSocialUserProfile = async ( +const getLatestUserProfileFromSocial = async ( { getLogtoConnectorById }: ConnectorLibrary, - socialIdentifier: SocialIdentifier + authIdentifiers: Identifier[] ) => { + const socialIdentifier = filterSocialIdentifiers(authIdentifiers).slice(-1)[0]; + + if (!socialIdentifier) { + return; + } + const { userInfo: { name, avatar }, connectorId, @@ -117,11 +134,11 @@ const parseNewUserProfile = async ( ]); return { + ...socialProfile, // SocialProfile should be applied first + ...passwordProfile, ...conditional(phone && { primaryPhone: phone }), ...conditional(username && { username }), ...conditional(email && { primaryEmail: email }), - ...passwordProfile, - ...socialProfile, }; }; @@ -129,17 +146,15 @@ const parseUserProfile = async ( connectorLibrary: ConnectorLibrary, { profile, identifiers }: VerifiedSignInInteractionResult | VerifiedRegisterInteractionResult, user?: User -) => { +): Promise, 'id'>> => { const { authIdentifiers, profileIdentifiers } = categorizeIdentifiers(identifiers ?? [], profile); const newUserProfile = profile && (await parseNewUserProfile(connectorLibrary, profile, profileIdentifiers, user)); - // Sync the last social profile - const socialIdentifier = filterSocialIdentifiers(authIdentifiers).slice(-1)[0]; - + // Sync from the latest social identity profile for existing users const syncedSocialUserProfile = - socialIdentifier && (await getSyncedSocialUserProfile(connectorLibrary, socialIdentifier)); + user && (await getLatestUserProfileFromSocial(connectorLibrary, authIdentifiers)); return { ...syncedSocialUserProfile, @@ -151,7 +166,7 @@ const parseUserProfile = async ( export default async function submitInteraction( interaction: VerifiedInteractionResult, ctx: WithInteractionDetailsContext, - { provider, libraries, connectors, queries, id: tenantId }: TenantContext, + { provider, libraries, connectors, queries }: TenantContext, log?: LogEntry ) { const { hasActiveUsers, findUserById, updateUserById } = queries.users; @@ -164,7 +179,7 @@ export default async function submitInteraction( if (event === InteractionEvent.Register) { const id = await generateUserId(); - const upsertProfile = await parseUserProfile(connectors, interaction); + const userProfile = await parseUserProfile(connectors, interaction); const { client_id } = ctx.interactionDetails.params; @@ -178,7 +193,7 @@ export default async function submitInteraction( await insertUser( { id, - ...upsertProfile, + ...userProfile, }, conditionalArray( isInAdminTenant && AdminTenantRole.User, @@ -208,9 +223,9 @@ export default async function submitInteraction( if (event === InteractionEvent.SignIn) { const user = await findUserById(accountId); - const upsertProfile = await parseUserProfile(connectors, interaction, user); + const updateUserProfile = await parseUserProfile(connectors, interaction, user); - await updateUserById(accountId, upsertProfile); + await updateUserById(accountId, updateUserProfile); await assignInteractionResults(ctx, provider, { login: { accountId } }); appInsights.client?.trackEvent({ name: getEventName(Component.Core, CoreEvent.SignIn) }); diff --git a/packages/create/CHANGELOG.md b/packages/create/CHANGELOG.md index ff03c692e..1d34a2448 100644 --- a/packages/create/CHANGELOG.md +++ b/packages/create/CHANGELOG.md @@ -1,5 +1,12 @@ # Change Log +## 1.4.0 + +### Patch Changes + +- Updated dependencies [5d6720805] + - @logto/cli@1.4.0 + ## 1.3.1 ### Patch Changes diff --git a/packages/create/package.json b/packages/create/package.json index ff1f13860..b19e0da5a 100644 --- a/packages/create/package.json +++ b/packages/create/package.json @@ -1,6 +1,6 @@ { "name": "@logto/create", - "version": "1.3.1", + "version": "1.4.0", "author": "Silverhand Inc. ", "license": "MPL-2.0", "type": "module", @@ -15,6 +15,6 @@ "node": "^18.12.0" }, "dependencies": { - "@logto/cli": "workspace:^1.3.1" + "@logto/cli": "workspace:^1.4.0" } } diff --git a/packages/integration-tests/CHANGELOG.md b/packages/integration-tests/CHANGELOG.md index 5ed46421e..8688513d9 100644 --- a/packages/integration-tests/CHANGELOG.md +++ b/packages/integration-tests/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## 1.2.0 + +### Minor Changes + +- 9a3aa3aae: Automatically sync the trusted social email and phone info to the new registered user profile + ## 1.1.0 ## 1.0.3 diff --git a/packages/integration-tests/package.json b/packages/integration-tests/package.json index d68452486..5d1cf13e6 100644 --- a/packages/integration-tests/package.json +++ b/packages/integration-tests/package.json @@ -1,6 +1,6 @@ { "name": "@logto/integration-tests", - "version": "1.1.0", + "version": "1.2.0", "description": "Integration tests for Logto.", "author": "Silverhand Inc. ", "license": "MPL-2.0", @@ -26,7 +26,7 @@ "@logto/connector-kit": "workspace:^1.1.0", "@logto/js": "^2.0.1", "@logto/node": "^2.0.0", - "@logto/schemas": "workspace:^1.1.0", + "@logto/schemas": "workspace:^1.4.0", "@silverhand/eslint-config": "3.0.1", "@silverhand/essentials": "^2.5.0", "@silverhand/ts-config": "3.0.0", diff --git a/packages/integration-tests/src/api/application.ts b/packages/integration-tests/src/api/application.ts index 946706dc5..6d1476e84 100644 --- a/packages/integration-tests/src/api/application.ts +++ b/packages/integration-tests/src/api/application.ts @@ -7,12 +7,17 @@ import type { import { authedAdminApi } from './api.js'; -export const createApplication = async (name: string, type: ApplicationType) => +export const createApplication = async ( + name: string, + type: ApplicationType, + rest?: Partial +) => authedAdminApi .post('applications', { json: { name, type, + ...rest, }, }) .json(); diff --git a/packages/integration-tests/src/tests/api/interaction/social-interaction.test.ts b/packages/integration-tests/src/tests/api/interaction/social-interaction.test.ts index eb2cc5616..bdc4b9579 100644 --- a/packages/integration-tests/src/tests/api/interaction/social-interaction.test.ts +++ b/packages/integration-tests/src/tests/api/interaction/social-interaction.test.ts @@ -4,6 +4,7 @@ import { mockSocialConnectorId } from '#src/__mocks__/connectors-mock.js'; import { createSocialAuthorizationUri, putInteraction, + getUser, deleteUser, putInteractionEvent, patchInteractionIdentifiers, @@ -97,6 +98,72 @@ describe('Social Identifier Interactions', () => { await logoutClient(client); await deleteUser(id); }); + + it('register with social and synced email', async () => { + const client = await initClient(); + const socialEmail = generateEmail(); + + const connectorId = connectorIdMap.get(mockSocialConnectorId) ?? ''; + + await client.successSend(putInteraction, { + event: InteractionEvent.SignIn, + }); + + await client.successSend(createSocialAuthorizationUri, { state, redirectUri, connectorId }); + + await client.successSend(patchInteractionIdentifiers, { + connectorId, + connectorData: { state, redirectUri, code, userId: socialUserId, email: socialEmail }, + }); + + await expectRejects(client.submitInteraction(), 'user.identity_not_exist'); + + await client.successSend(putInteractionEvent, { event: InteractionEvent.Register }); + await client.successSend(putInteractionProfile, { connectorId }); + + const { redirectTo } = await client.submitInteraction(); + + const uid = await processSession(client, redirectTo); + + const { primaryEmail } = await getUser(uid); + expect(primaryEmail).toBe(socialEmail); + + await logoutClient(client); + await deleteUser(uid); + }); + + it('register with social and synced phone', async () => { + const client = await initClient(); + const socialPhone = generatePhone(); + + const connectorId = connectorIdMap.get(mockSocialConnectorId) ?? ''; + + await client.successSend(putInteraction, { + event: InteractionEvent.SignIn, + }); + + await client.successSend(createSocialAuthorizationUri, { state, redirectUri, connectorId }); + + await client.successSend(patchInteractionIdentifiers, { + connectorId, + connectorData: { state, redirectUri, code, userId: socialUserId, phone: socialPhone }, + }); + + await expectRejects(client.submitInteraction(), 'user.identity_not_exist'); + + await client.successSend(putInteractionEvent, { event: InteractionEvent.Register }); + await client.successSend(putInteractionProfile, { connectorId }); + + const { redirectTo } = await client.submitInteraction(); + + const uid = await processSession(client, redirectTo); + + const { primaryPhone } = await getUser(uid); + expect(primaryPhone).toBe(socialPhone); + + await logoutClient(client); + await deleteUser(uid); + }); }); describe('bind with existing email account', () => { diff --git a/packages/integration-tests/src/tests/api/oidc/always-issue-refresh-token.test.ts b/packages/integration-tests/src/tests/api/oidc/always-issue-refresh-token.test.ts new file mode 100644 index 000000000..d5a7cd82a --- /dev/null +++ b/packages/integration-tests/src/tests/api/oidc/always-issue-refresh-token.test.ts @@ -0,0 +1,68 @@ +import { Prompt } from '@logto/js'; +import { ApplicationType, InteractionEvent } from '@logto/schemas'; + +import { createApplication, deleteApplication, putInteraction } from '#src/api/index.js'; +import MockClient from '#src/client/index.js'; +import { demoAppRedirectUri } from '#src/constants.js'; +import { processSession } from '#src/helpers/client.js'; +import { createUserByAdmin } from '#src/helpers/index.js'; +import { enableAllPasswordSignInMethods } from '#src/helpers/sign-in-experience.js'; +import { generateUsername, generatePassword } from '#src/utils.js'; + +describe('always issue Refresh Token config', () => { + const username = generateUsername(); + const password = generatePassword(); + + const validateRefreshToken = async (appId: string, redirectUri: string, expectToken: boolean) => { + const client = new MockClient({ + appId, + prompt: Prompt.Login, + }); + await client.initSession(redirectUri); + await client.successSend(putInteraction, { + event: InteractionEvent.SignIn, + identifier: { username, password }, + }); + const { redirectTo } = await client.submitInteraction(); + await processSession(client, redirectTo); + + if (expectToken) { + expect(await client.getRefreshToken()).not.toBeNull(); + } else { + expect(await client.getRefreshToken()).toBeNull(); + } + }; + + beforeAll(async () => { + await createUserByAdmin(username, password); + await enableAllPasswordSignInMethods(); + }); + + it('can sign in and fetch Refresh Token without `prompt=consent` when always issue Refresh Token is set', async () => { + const app = await createApplication('Integration test app', ApplicationType.SPA, { + oidcClientMetadata: { redirectUris: [demoAppRedirectUri], postLogoutRedirectUris: [] }, + customClientMetadata: { alwaysIssueRefreshToken: true }, + }); + await validateRefreshToken(app.id, demoAppRedirectUri, true); + await deleteApplication(app.id); + }); + + it('cannot fetch Refresh Token if alwaysIssueRefreshToken is false and prompt is not consent', async () => { + const app = await createApplication('Integration test app', ApplicationType.SPA, { + oidcClientMetadata: { redirectUris: [demoAppRedirectUri], postLogoutRedirectUris: [] }, + customClientMetadata: { alwaysIssueRefreshToken: false }, + }); + await validateRefreshToken(app.id, demoAppRedirectUri, false); + await deleteApplication(app.id); + }); + + it('cannot fetch Refresh Token for non-web apps', async () => { + const redirectUri = 'io.logto://callback'; + const app = await createApplication('Integration test app', ApplicationType.Native, { + oidcClientMetadata: { redirectUris: [redirectUri], postLogoutRedirectUris: [] }, + customClientMetadata: { alwaysIssueRefreshToken: true }, + }); + await validateRefreshToken(app.id, redirectUri, false); + await deleteApplication(app.id); + }); +}); diff --git a/packages/integration-tests/src/tests/api/oidc/content-type-json.test.ts b/packages/integration-tests/src/tests/api/oidc/content-type-json.test.ts new file mode 100644 index 000000000..c423d8e8a --- /dev/null +++ b/packages/integration-tests/src/tests/api/oidc/content-type-json.test.ts @@ -0,0 +1,54 @@ +import { demoAppApplicationId } from '@logto/schemas'; +import { trySafe } from '@silverhand/essentials'; +import { HTTPError, type Headers, got } from 'got'; + +import { logtoUrl } from '#src/constants.js'; + +describe('content-type: application/json compatibility', () => { + const api = got.extend({ + prefixUrl: new URL('/oidc', logtoUrl), + }); + + const expectErrorMessageForPayload = async ( + payload: Record, + errorMessage: string, + headers: Headers = {} + ) => { + return trySafe( + api.post('token', { + headers, + json: payload, + }), + (error) => { + if (!(error instanceof HTTPError)) { + throw new TypeError('Error is not a HTTPError instance.'); + } + expect(JSON.parse(String(error.response.body))).toHaveProperty( + 'error_description', + errorMessage + ); + } + ); + }; + + it('recognizes `application/json` content-type in OIDC token endpoints', async () => { + await Promise.all([ + expectErrorMessageForPayload( + { client_id: demoAppApplicationId }, + "missing required parameter 'grant_type'" + ), + expectErrorMessageForPayload( + { client_id: demoAppApplicationId, grant_type: 'refresh_token' }, + "missing required parameter 'refresh_token'" + ), + ]); + }); + + it('does not recognize `application/json1` content-type', async () => { + await expectErrorMessageForPayload( + { client_id: demoAppApplicationId }, + 'only application/x-www-form-urlencoded content-type bodies are supported on POST /token', + { 'content-type': 'application/json1' } + ); + }); +}); diff --git a/packages/integration-tests/src/tests/api/get-access-token.test.ts b/packages/integration-tests/src/tests/api/oidc/get-access-token.test.ts similarity index 93% rename from packages/integration-tests/src/tests/api/get-access-token.test.ts rename to packages/integration-tests/src/tests/api/oidc/get-access-token.test.ts index b5cbde339..6acf8b53b 100644 --- a/packages/integration-tests/src/tests/api/get-access-token.test.ts +++ b/packages/integration-tests/src/tests/api/oidc/get-access-token.test.ts @@ -26,7 +26,7 @@ describe('get access token', () => { await enableAllPasswordSignInMethods(); }); - it('sign-in and getAccessToken with admin user', async () => { + it('can sign in and getAccessToken with admin user', async () => { const client = new MockClient({ resources: [defaultManagementApi.resource.indicator], scopes: [defaultManagementApi.scope.name], @@ -49,7 +49,7 @@ describe('get access token', () => { void expect(client.getAccessToken('api.foo.com')).rejects.toThrow(); }); - it('sign-in and getAccessToken with guest user', async () => { + it('can sign in and getAccessToken with guest user', async () => { const client = new MockClient({ resources: [defaultManagementApi.resource.indicator], scopes: [defaultManagementApi.scope.name], @@ -69,7 +69,7 @@ describe('get access token', () => { ); }); - it('sign-in and get multiple Access Token by the same Refresh Token within refreshTokenReuseInterval', async () => { + it('can sign in and get multiple Access Tokens by the same Refresh Token within refreshTokenReuseInterval', async () => { const client = new MockClient({ resources: [defaultManagementApi.resource.indicator] }); await client.initSession(); diff --git a/packages/phrases/CHANGELOG.md b/packages/phrases/CHANGELOG.md index b3ce916b0..d4a259c3f 100644 --- a/packages/phrases/CHANGELOG.md +++ b/packages/phrases/CHANGELOG.md @@ -1,5 +1,13 @@ # Change Log +## 1.3.0 + +### Minor Changes + +- 5d6720805: add config `alwaysIssueRefreshToken` for web apps to unblock OAuth integrations that are not strictly conform OpenID Connect. + + when it's enabled, Refresh Tokens will be always issued regardless if `prompt=consent` was present in the authorization request. + ## 1.2.0 ### Minor Changes diff --git a/packages/phrases/package.json b/packages/phrases/package.json index 888228d06..1ae3dbe3a 100644 --- a/packages/phrases/package.json +++ b/packages/phrases/package.json @@ -1,6 +1,6 @@ { "name": "@logto/phrases", - "version": "1.2.0", + "version": "1.3.0", "description": "Logto shared phrases (i18n).", "author": "Silverhand Inc. ", "homepage": "https://github.com/logto-io/logto#readme", diff --git a/packages/phrases/src/locales/de/translation/admin-console/application-details.ts b/packages/phrases/src/locales/de/translation/admin-console/application-details.ts index d9140d740..d2a4f88e9 100644 --- a/packages/phrases/src/locales/de/translation/admin-console/application-details.ts +++ b/packages/phrases/src/locales/de/translation/admin-console/application-details.ts @@ -41,6 +41,9 @@ const application_details = { enable_admin_access: 'Admin-Zugang aktivieren', enable_admin_access_label: 'Zugang zur Management API aktivieren oder deaktivieren. Wenn aktiviert, können Access Tokens verwendet werden, um die Management API im Namen der Anwendung aufzurufen.', + always_issue_refresh_token: 'Immer den Refresh Token ausgeben', + always_issue_refresh_token_label: + 'Durch Aktivieren dieser Konfiguration kann Logto immer Refresh Tokens ausgeben, unabhängig davon, ob in der Authentifizierungsanforderung "prompt=consent" angegeben ist. Diese Praxis wird jedoch nur dann empfohlen, wenn es notwendig ist, da sie nicht mit OpenID Connect kompatibel ist und möglicherweise Probleme verursacht.', delete_description: 'Diese Aktion kann nicht rückgängig gemacht werden. Die Anwendung wird permanent gelöscht. Bitte gib den Anwendungsnamen {{name}} zur Bestätigung ein.', enter_your_application_name: 'Gib einen Anwendungsnamen ein', diff --git a/packages/phrases/src/locales/en/translation/admin-console/application-details.ts b/packages/phrases/src/locales/en/translation/admin-console/application-details.ts index 9757177fe..fdf1e3bb5 100644 --- a/packages/phrases/src/locales/en/translation/admin-console/application-details.ts +++ b/packages/phrases/src/locales/en/translation/admin-console/application-details.ts @@ -41,6 +41,9 @@ const application_details = { enable_admin_access: 'Enable admin access', enable_admin_access_label: 'Enable or disable the access to Management API. Once enabled, you can use access tokens to call Management API on behalf on this application.', + always_issue_refresh_token: 'Always issue Refresh Token', + always_issue_refresh_token_label: + 'Enabling this configuration will allow Logto to always issue Refresh Tokens, regardless of whether `prompt=consent` is presented in the authentication request. However, this practice is discouraged unless necessary, as it is not compatible with OpenID Connect and may potentially cause issues.', delete_description: 'This action cannot be undone. It will permanently delete the application. Please enter the application name {{name}} to confirm.', enter_your_application_name: 'Enter your application name', diff --git a/packages/phrases/src/locales/es/translation/admin-console/application-details.ts b/packages/phrases/src/locales/es/translation/admin-console/application-details.ts index 899c595a9..8990386c7 100644 --- a/packages/phrases/src/locales/es/translation/admin-console/application-details.ts +++ b/packages/phrases/src/locales/es/translation/admin-console/application-details.ts @@ -41,6 +41,9 @@ const detalles_aplicacion = { enable_admin_access: 'Habilitar acceso de administrador', enable_admin_access_label: 'Habilita o deshabilita el acceso a la API de Gestión. Una vez habilitado, puedes utilizar tokens de acceso para llamar a la API de Gestión en nombre de esta aplicación.', + always_issue_refresh_token: 'Siempre emitir Token de Refresco', + always_issue_refresh_token_label: + 'Al habilitar esta configuración, Logto siempre emitirá Tokens de Refresco, independientemente de si se presenta o no “prompt=consent” en la solicitud de autenticación. Sin embargo, esta práctica no está recomendada a menos que sea necesario, ya que no es compatible con OpenID Connect y puede causar problemas potenciales.', delete_description: 'Esta acción no se puede deshacer. Eliminará permanentemente la aplicación. Ingresa el nombre de la aplicación {{name}} para confirmar.', enter_your_application_name: 'Ingresa el nombre de tu aplicación', diff --git a/packages/phrases/src/locales/fr/translation/admin-console/application-details.ts b/packages/phrases/src/locales/fr/translation/admin-console/application-details.ts index f44514358..4f31622c3 100644 --- a/packages/phrases/src/locales/fr/translation/admin-console/application-details.ts +++ b/packages/phrases/src/locales/fr/translation/admin-console/application-details.ts @@ -41,6 +41,9 @@ const application_details = { enable_admin_access: "Activer l'accès administrateur", enable_admin_access_label: "Activer ou désactiver l'accès à l'API de gestion. Une fois activé, vous pouvez utiliser des jetons d'accès pour appeler l'API de gestion au nom de cette application.", + always_issue_refresh_token: 'Toujours émettre le Refresh Token.', + always_issue_refresh_token_label: + "En activant cette configuration, Logto pourra toujours émettre des Refresh Tokens, indépendamment de la présentation ou non de la demande d'authentification `prompt=consent`. Cependant, cette pratique est découragée sauf si nécessaire, car elle n'est pas compatible avec OpenID Connect et peut potentiellement causer des problèmes.", delete_description: "Cette action ne peut être annulée. Elle supprimera définitivement l'application. Veuillez entrer le nom de l'application {{nom}} pour confirmer.", enter_your_application_name: 'Entrez le nom de votre application', diff --git a/packages/phrases/src/locales/it/translation/admin-console/application-details.ts b/packages/phrases/src/locales/it/translation/admin-console/application-details.ts index 2787dae46..58874ef21 100644 --- a/packages/phrases/src/locales/it/translation/admin-console/application-details.ts +++ b/packages/phrases/src/locales/it/translation/admin-console/application-details.ts @@ -41,6 +41,9 @@ const application_details = { enable_admin_access: "Abilita l'accesso amministratore", enable_admin_access_label: "Abilita o disabilita l'accesso all'API di gestione. Una volta abilitato, puoi utilizzare i token di accesso per chiamare l'API di gestione a nome di questa applicazione.", + always_issue_refresh_token: 'Rilascia sempre il token di aggiornamento', + always_issue_refresh_token_label: + 'Abilitando questa configurazione, Logto potrà rilasciare sempre token di aggiornamento, anche se `prompt=consent` non viene presentata nella richiesta di autenticazione. Tuttavia, questa pratica è scoraggiata a meno che non sia necessaria, in quanto non è compatibile con OpenID Connect e potrebbe potenzialmente causare problemi.', delete_description: "Questa azione non può essere annullata. Eliminerà definitivamente l'applicazione. Inserisci il nome dell'applicazione {{name}} per confermare.", enter_your_application_name: 'Inserisci il nome della tua applicazione', diff --git a/packages/phrases/src/locales/ja/translation/admin-console/application-details.ts b/packages/phrases/src/locales/ja/translation/admin-console/application-details.ts index 6855f282a..a8d9d73fe 100644 --- a/packages/phrases/src/locales/ja/translation/admin-console/application-details.ts +++ b/packages/phrases/src/locales/ja/translation/admin-console/application-details.ts @@ -41,6 +41,9 @@ const application_details = { enable_admin_access: '管理者アクセスを有効にする', enable_admin_access_label: '管理APIへのアクセスを有効または無効にします。有効にすると、アクセストークンを使用してこのアプリケーションを代表して管理APIを呼び出すことができます。', + always_issue_refresh_token: '常にRefresh Tokenを発行する', + always_issue_refresh_token_label: + 'この設定を有効にすると、Logtoは、認証要求に「prompt = consent」が提示されたかどうかにかかわらず、常にRefresh Tokenを発行することができます。ただし、OpenID Connectと互換性がないため、必要でない限りこのプラクティスは推奨されず、問題が発生する可能性があります。', delete_description: 'この操作は元に戻すことはできません。アプリケーション名「{{name}}」を入力して確認してください。', enter_your_application_name: 'アプリケーション名を入力してください', diff --git a/packages/phrases/src/locales/ko/translation/admin-console/application-details.ts b/packages/phrases/src/locales/ko/translation/admin-console/application-details.ts index 0962f8aaf..50bd80746 100644 --- a/packages/phrases/src/locales/ko/translation/admin-console/application-details.ts +++ b/packages/phrases/src/locales/ko/translation/admin-console/application-details.ts @@ -41,6 +41,9 @@ const application_details = { enable_admin_access: '관리자 접근 활성화', enable_admin_access_label: '관리 API에 대한 접근을 활성화, 비활성화할 수 있어요. 활성화한다면, 이 어플리케이션에서 Access 토큰을 통해 관리 API를 사용할 수 있어요.', + always_issue_refresh_token: '항상 Refresh Token을 발급하세요', + always_issue_refresh_token_label: + '다음 구성을 활성화하면 Logto가 인증 요청에 `prompt=consent`가 제시되었는지 여부와 상관없이 항상 Refresh Token을 발급할 수 있게 됩니다. 그러나 OpenID Connect와 호환되지 않으며 문제가 발생할 수 있으므로 필요하지 않은 경우에는 이러한 방법을 권장하지 않습니다. ', delete_description: '이 행동은 취소될 수 없어요. 어플리케이션을 영원히 삭제할 거에요. 삭제를 진행하기 위해 {{name}} 를 입력해주세요.', enter_your_application_name: '어플리케이션 이름을 입력해 주세요.', diff --git a/packages/phrases/src/locales/pl-pl/translation/admin-console/application-details.ts b/packages/phrases/src/locales/pl-pl/translation/admin-console/application-details.ts index ed6efbf6e..f2fe89ce3 100644 --- a/packages/phrases/src/locales/pl-pl/translation/admin-console/application-details.ts +++ b/packages/phrases/src/locales/pl-pl/translation/admin-console/application-details.ts @@ -41,6 +41,9 @@ const application_details = { enable_admin_access: 'Włącz dostęp administratora', enable_admin_access_label: 'Włącz lub wyłącz dostęp do interfejsu API zarządzania. Po włączeniu możesz używać tokenów dostępu do wywoływania interfejsu API zarządzania w imieniu tej aplikacji.', + always_issue_refresh_token: 'Zawsze wydawaj Refresh Token', + always_issue_refresh_token_label: + 'Rozwiazanie tej konfiguracji pozwoli Logto zawsze wydawac cwiecze tokeny, bez wzgledu na to, czy w zadaniu autoryzacji zostal przedstawiony `prompt=consent`. Jednak ta praktyka jest odstraszana, chyba ze konieczne, jak nie jest w pelni kompatybilna z OpenID Connect i moze potencjalnie powodowac problemy.', delete_description: 'Ta operacja nie może zostać cofnięta. Skutkuje ona trwałym usunięciem aplikacji. Aby potwierdzić, wpisz nazwę aplikacji {{name}}.', enter_your_application_name: 'Wpisz nazwę swojej aplikacji', diff --git a/packages/phrases/src/locales/pt-br/translation/admin-console/application-details.ts b/packages/phrases/src/locales/pt-br/translation/admin-console/application-details.ts index 0e6fa3d02..f623310d4 100644 --- a/packages/phrases/src/locales/pt-br/translation/admin-console/application-details.ts +++ b/packages/phrases/src/locales/pt-br/translation/admin-console/application-details.ts @@ -41,6 +41,9 @@ const application_details = { enable_admin_access: 'Ativar acesso de administrador', enable_admin_access_label: 'Ative ou desative o acesso à API de gerenciamento. Uma vez ativado, você pode usar tokens de acesso para chamar a API de gerenciamento em nome deste aplicativo.', + always_issue_refresh_token: 'Emitir sempre o token de refresh', + always_issue_refresh_token_label: + 'Ativar esta configuração permitirá que a Logto emita sempre tokens de Refresh, independentemente de "prompt=consent" ser apresentado na solicitação de autenticação. No entanto, essa prática é desencorajada, a menos que seja necessária, pois não é compatível com o OpenID Connect e pode potencialmente causar problemas.', delete_description: 'Essa ação não pode ser desfeita. Isso excluirá permanentemente o aplicativo. Insira o nome do aplicativo {{name}} para confirmar.', enter_your_application_name: 'Digite o nome do seu aplicativo', diff --git a/packages/phrases/src/locales/pt-pt/translation/admin-console/application-details.ts b/packages/phrases/src/locales/pt-pt/translation/admin-console/application-details.ts index 8e836b734..147238cb1 100644 --- a/packages/phrases/src/locales/pt-pt/translation/admin-console/application-details.ts +++ b/packages/phrases/src/locales/pt-pt/translation/admin-console/application-details.ts @@ -41,6 +41,9 @@ const application_details = { enable_admin_access: 'Ativar o acesso de administrador', enable_admin_access_label: 'Ativar ou desativar o acesso à API de gestão. Uma vez ativado, pode utilizar tokens de acesso para chamar a API de gestão em nome desta aplicação.', + always_issue_refresh_token: 'Sempre emitir Refresh Token', + always_issue_refresh_token_label: + 'Ao ativar essa configuração, a Logto sempre emitirá tokens de atualização, independentemente de `prompt=consent`ser apresentado na solicitação de autenticação. No entanto, essa prática é desencorajada, a menos que seja necessária, pois não é compatível com OpenID Connect e pode causar problemas.', delete_description: 'Esta ação não pode ser revertida. Esta ação irá eliminar permanentemente a aplicação. Insira o nome da aplicação {{name}} para confirmar.', enter_your_application_name: 'Insira o nome da aplicação', diff --git a/packages/phrases/src/locales/ru/translation/admin-console/application-details.ts b/packages/phrases/src/locales/ru/translation/admin-console/application-details.ts index 9f3271382..510eafdcc 100644 --- a/packages/phrases/src/locales/ru/translation/admin-console/application-details.ts +++ b/packages/phrases/src/locales/ru/translation/admin-console/application-details.ts @@ -41,6 +41,9 @@ const application_details = { enable_admin_access: 'Включить доступ администратора', enable_admin_access_label: 'Включить или отключить доступ к API управления. После включения вы можете использовать токены доступа для вызова API управления от имени этого приложения.', + always_issue_refresh_token: 'Всегда выдавать Refresh Token', + always_issue_refresh_token_label: + 'Включение этой настройки позволит Logto всегда выдавать Refresh Tokens, независимо от того, была ли в запросе на аутентификацию предложена команда `prompt=consent`. Однако данная практика не рекомендуется, если это необходимо, поскольку она несовместима с OpenID Connect и может вызвать проблемы.', delete_description: 'Это действие нельзя отменить. Оно навсегда удалит приложение. Введите название приложения {{name}} , чтобы подтвердить.', enter_your_application_name: 'Введите название своего приложения', diff --git a/packages/phrases/src/locales/tr-tr/translation/admin-console/application-details.ts b/packages/phrases/src/locales/tr-tr/translation/admin-console/application-details.ts index edeeccd53..e1c0d2956 100644 --- a/packages/phrases/src/locales/tr-tr/translation/admin-console/application-details.ts +++ b/packages/phrases/src/locales/tr-tr/translation/admin-console/application-details.ts @@ -41,6 +41,9 @@ const application_details = { enable_admin_access: 'Yönetici erişimini etkinleştir', enable_admin_access_label: "Yönetim API erişimine izin verme veya engelleme. Etkinleştirildikten sonra, bu uygulama adına yönetim API'sini çağırmak için erişim belirteçleri kullanabilirsiniz.", + always_issue_refresh_token: 'Her zaman Refresh Token ver', + always_issue_refresh_token_label: + "Bu yapılandırmayı etkinleştirmek, Logto'nun OpenID Connect ile uyumlu olmayan ve olası sorunlara neden olabilecek her zaman Refresh Token çıkarmasına izin verir `prompt=consent` kimlik doğrulama isteğinin sunulup sunulmadığına bakılmaksızın. Ancak, bu uygulama yalnızca zorunlu olduğunda caydırılmayan bir uygulamadır.", delete_description: 'Bu eylem geri alınamaz. Uygulama kalıcı olarak silinecektir. Lütfen onaylamak için uygulama adı {{name}} girin.', enter_your_application_name: 'Uygulama adı giriniz', diff --git a/packages/phrases/src/locales/zh-cn/translation/admin-console/application-details.ts b/packages/phrases/src/locales/zh-cn/translation/admin-console/application-details.ts index 5519c484a..b74873c40 100644 --- a/packages/phrases/src/locales/zh-cn/translation/admin-console/application-details.ts +++ b/packages/phrases/src/locales/zh-cn/translation/admin-console/application-details.ts @@ -39,10 +39,13 @@ const application_details = { enable_admin_access: '启用管理访问', enable_admin_access_label: '启用或禁用对管理 API 的访问。启用后,你可以使用访问令牌代表该应用程序调用管理 API。', + always_issue_refresh_token: '总是颁发 Refresh Token', + always_issue_refresh_token_label: + '启用此配置将允许 Logto 始终颁发 Refresh Token,无论身份验证请求中是否呈现 `prompt=consent`。 然而,除非必要,否则不推荐这样做,因为它与 OpenID Connect 不兼容,可能会导致问题。', delete_description: '本操作会永久性地删除该应用,且不可撤销。输入 {{name}} 确认。', enter_your_application_name: '输入你的应用名称', application_deleted: '应用 {{name}} 成功删除。', - redirect_uri_required: '至少需要输入一个重定向 URL。', + redirect_uri_required: '至少需要输入一个重定向 URI。', }; export default application_details; diff --git a/packages/phrases/src/locales/zh-hk/translation/admin-console/application-details.ts b/packages/phrases/src/locales/zh-hk/translation/admin-console/application-details.ts index c0987288c..153b0cbdf 100644 --- a/packages/phrases/src/locales/zh-hk/translation/admin-console/application-details.ts +++ b/packages/phrases/src/locales/zh-hk/translation/admin-console/application-details.ts @@ -39,6 +39,9 @@ const application_details = { enable_admin_access: '啟用管理訪問', enable_admin_access_label: '啟用或禁用對管理 API 的訪問。啟用後,你可以使用訪問權杖代表該應用程式調用管理 API。', + always_issue_refresh_token: '始終發放 Refresh Token', + always_issue_refresh_token_label: + '啟用此配置將允許 Logto 始終發行 Refresh Token,無論是否在驗證請求中呈現 `prompt=consent`。但是,除非必要,否則不建議這樣做,因為它不兼容 OpenID Connect,可能會引起問題。', delete_description: '本操作會永久性地刪除該應用,且不可撤銷。輸入 {{name}} 確認。', enter_your_application_name: '輸入你的應用程式名稱', application_deleted: '應用 {{name}} 成功刪除。', diff --git a/packages/phrases/src/locales/zh-tw/translation/admin-console/application-details.ts b/packages/phrases/src/locales/zh-tw/translation/admin-console/application-details.ts index 4514fa4da..8f1084087 100644 --- a/packages/phrases/src/locales/zh-tw/translation/admin-console/application-details.ts +++ b/packages/phrases/src/locales/zh-tw/translation/admin-console/application-details.ts @@ -39,6 +39,9 @@ const application_details = { enable_admin_access: '啟用管理訪問', enable_admin_access_label: '啟用或禁用對管理 API 的訪問。啟用後,你可以使用訪問令牌代表該應用程式調用管理 API。', + always_issue_refresh_token: '始終發放 Refresh Token', + always_issue_refresh_token_label: + '啟用此配置將使 Logto 無論在驗證請求中是否提供 prompt=consent,都能始終發放 Refresh Token。然而,除非必要,否則不鼓勵這種做法,因為它與 OpenID Connect 不相容並可能引起問題。', delete_description: '本操作會永久性地刪除該應用程式,且不可撤銷。輸入 {{name}} 確認。', enter_your_application_name: '輸入你的應用程式姓名', diff --git a/packages/schemas/CHANGELOG.md b/packages/schemas/CHANGELOG.md index 4d084225d..b6ae86a76 100644 --- a/packages/schemas/CHANGELOG.md +++ b/packages/schemas/CHANGELOG.md @@ -1,5 +1,18 @@ # Change Log +## 1.4.0 + +### Minor Changes + +- 5d6720805: add config `alwaysIssueRefreshToken` for web apps to unblock OAuth integrations that are not strictly conform OpenID Connect. + + when it's enabled, Refresh Tokens will be always issued regardless if `prompt=consent` was present in the authorization request. + +### Patch Changes + +- Updated dependencies [5d6720805] + - @logto/phrases@1.3.0 + ## 1.3.1 ## 1.3.0 diff --git a/packages/schemas/package.json b/packages/schemas/package.json index 507fbd69e..0b0419b5f 100644 --- a/packages/schemas/package.json +++ b/packages/schemas/package.json @@ -1,6 +1,6 @@ { "name": "@logto/schemas", - "version": "1.3.1", + "version": "1.4.0", "author": "Silverhand Inc. ", "license": "MPL-2.0", "type": "module", @@ -83,7 +83,7 @@ "@logto/connector-kit": "workspace:^1.1.1", "@logto/core-kit": "workspace:^2.0.0", "@logto/language-kit": "workspace:^1.0.0", - "@logto/phrases": "workspace:^1.2.0", + "@logto/phrases": "workspace:^1.3.0", "@logto/phrases-ui": "workspace:^1.2.0", "@logto/shared": "workspace:^2.0.0", "@withtyped/server": "^0.9.0", diff --git a/packages/schemas/src/foundations/jsonb-types.ts b/packages/schemas/src/foundations/jsonb-types.ts index b8d1e8300..d71d935de 100644 --- a/packages/schemas/src/foundations/jsonb-types.ts +++ b/packages/schemas/src/foundations/jsonb-types.ts @@ -69,6 +69,14 @@ export enum CustomClientMetadataKey { IdTokenTtl = 'idTokenTtl', RefreshTokenTtl = 'refreshTokenTtl', TenantId = 'tenantId', + /** + * Enabling this configuration will allow Logto to always issue Refresh Tokens, regardless of whether `prompt=consent` is presented in the authentication request. + * + * It only works for web applications when the client allowed grant types includes `refresh_token`. + * + * This config is for the third-party integrations that do not strictly follow OpenID Connect standards due to some reasons (e.g. they only know OAuth, but requires a Refresh Token to be returned anyway). + */ + AlwaysIssueRefreshToken = 'alwaysIssueRefreshToken', } export const customClientMetadataGuard = z.object({ @@ -76,8 +84,12 @@ export const customClientMetadataGuard = z.object({ [CustomClientMetadataKey.IdTokenTtl]: z.number().optional(), [CustomClientMetadataKey.RefreshTokenTtl]: z.number().optional(), [CustomClientMetadataKey.TenantId]: z.string().optional(), + [CustomClientMetadataKey.AlwaysIssueRefreshToken]: z.boolean().optional(), }); +/** + * @see {@link CustomClientMetadataKey} for key descriptions. + */ export type CustomClientMetadata = z.infer; /* === Users === */ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9f5a70ddc..8c597c751 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -103,13 +103,13 @@ importers: specifier: workspace:^1.0.0 version: link:../toolkit/language-kit '@logto/phrases': - specifier: workspace:^1.2.0 + specifier: workspace:^1.3.0 version: link:../phrases '@logto/phrases-ui': specifier: workspace:^1.2.0 version: link:../phrases-ui '@logto/schemas': - specifier: workspace:1.3.1 + specifier: workspace:1.4.0 version: link:../schemas '@logto/shared': specifier: workspace:^2.0.0 @@ -2781,7 +2781,7 @@ importers: specifier: workspace:^1.0.0 version: link:../toolkit/language-kit '@logto/phrases': - specifier: workspace:^1.2.0 + specifier: workspace:^1.3.0 version: link:../phrases '@logto/phrases-ui': specifier: workspace:^1.2.0 @@ -2790,7 +2790,7 @@ importers: specifier: ^2.0.0 version: 2.0.0(react@18.2.0) '@logto/schemas': - specifier: workspace:^1.3.0 + specifier: workspace:^1.4.0 version: link:../schemas '@logto/shared': specifier: workspace:^2.0.0 @@ -3057,7 +3057,7 @@ importers: specifier: workspace:^1.2.0 version: link:../app-insights '@logto/cli': - specifier: workspace:^1.3.1 + specifier: workspace:^1.4.0 version: link:../cli '@logto/connector-kit': specifier: workspace:^1.1.1 @@ -3075,13 +3075,13 @@ importers: specifier: workspace:^1.0.0 version: link:../toolkit/language-kit '@logto/phrases': - specifier: workspace:^1.2.0 + specifier: workspace:^1.3.0 version: link:../phrases '@logto/phrases-ui': specifier: workspace:^1.2.0 version: link:../phrases-ui '@logto/schemas': - specifier: workspace:^1.3.1 + specifier: workspace:^1.4.0 version: link:../schemas '@logto/shared': specifier: workspace:^2.0.0 @@ -3295,7 +3295,7 @@ importers: packages/create: dependencies: '@logto/cli': - specifier: workspace:^1.3.1 + specifier: workspace:^1.4.0 version: link:../cli packages/demo-app: @@ -3403,7 +3403,7 @@ importers: specifier: ^2.0.0 version: 2.0.0 '@logto/schemas': - specifier: workspace:^1.1.0 + specifier: workspace:^1.4.0 version: link:../schemas '@silverhand/eslint-config': specifier: 3.0.1 @@ -3540,7 +3540,7 @@ importers: specifier: workspace:^1.0.0 version: link:../toolkit/language-kit '@logto/phrases': - specifier: workspace:^1.2.0 + specifier: workspace:^1.3.0 version: link:../phrases '@logto/phrases-ui': specifier: workspace:^1.2.0