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

Merge pull request #3879 from logto-io/gao-feature-updates-for-openai-plugins

feat: updates for openai plugins
This commit is contained in:
Gao Sun 2023-05-23 22:55:47 +08:00 committed by GitHub
commit 02eee1956f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 475 additions and 59 deletions

View file

@ -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

View file

@ -1,6 +1,6 @@
{
"name": "@logto/cli",
"version": "1.3.1",
"version": "1.4.0",
"description": "Logto CLI.",
"author": "Silverhand Inc. <contact@silverhand.io>",
"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",

View file

@ -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)) {

View file

@ -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

View file

@ -1,6 +1,6 @@
{
"name": "@logto/console",
"version": "1.2.4",
"version": "1.3.0",
"description": "> TODO: description",
"author": "Silverhand Inc. <contact@silverhand.io>",
"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",

View file

@ -64,6 +64,14 @@ function AdvancedSettings({ applicationType, oidcConfig }: Props) {
variant="border"
/>
</FormField>
{[ApplicationType.Traditional, ApplicationType.SPA].includes(applicationType) && (
<FormField title="application_details.always_issue_refresh_token">
<Switch
label={t('application_details.always_issue_refresh_token_label')}
{...register('customClientMetadata.alwaysIssueRefreshToken')}
/>
</FormField>
)}
{applicationType === ApplicationType.MachineToMachine && (
<FormField title="application_details.enable_admin_access">
<Switch

View file

@ -1,5 +1,24 @@
# Change Log
## 1.4.0
### Minor Changes
- 9a3aa3aae: Automatically sync the trusted social email and phone info to the new registered user profile
- 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
- 5d6720805: parse requests with `application/json` content-type for `/oidc` APIs to increase compatibility
- Updated dependencies [5d6720805]
- Updated dependencies [5d6720805]
- @logto/cli@1.4.0
- @logto/console@1.3.0
- @logto/phrases@1.3.0
- @logto/schemas@1.4.0
## 1.3.1
### Patch Changes

View file

@ -1,6 +1,6 @@
{
"name": "@logto/core",
"version": "1.3.1",
"version": "1.4.0",
"description": "The open source identity solution.",
"main": "build/index.js",
"author": "Silverhand Inc. <contact@silverhand.io>",
@ -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",

View file

@ -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;

View file

@ -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 = {

View file

@ -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<Omit<OmitAutoSetFields<CreateUser>, '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<string>(
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) });

View file

@ -1,5 +1,12 @@
# Change Log
## 1.4.0
### Patch Changes
- Updated dependencies [5d6720805]
- @logto/cli@1.4.0
## 1.3.1
### Patch Changes

View file

@ -1,6 +1,6 @@
{
"name": "@logto/create",
"version": "1.3.1",
"version": "1.4.0",
"author": "Silverhand Inc. <contact@silverhand.io>",
"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"
}
}

View file

@ -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

View file

@ -1,6 +1,6 @@
{
"name": "@logto/integration-tests",
"version": "1.1.0",
"version": "1.2.0",
"description": "Integration tests for Logto.",
"author": "Silverhand Inc. <contact@silverhand.io>",
"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",

View file

@ -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<CreateApplication>
) =>
authedAdminApi
.post('applications', {
json: {
name,
type,
...rest,
},
})
.json<Application>();

View file

@ -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', () => {

View file

@ -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);
});
});

View file

@ -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<string, unknown>,
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' }
);
});
});

View file

@ -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();

View file

@ -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

View file

@ -1,6 +1,6 @@
{
"name": "@logto/phrases",
"version": "1.2.0",
"version": "1.3.0",
"description": "Logto shared phrases (i18n).",
"author": "Silverhand Inc. <contact@silverhand.io>",
"homepage": "https://github.com/logto-io/logto#readme",

View file

@ -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 <span>{{name}}</span> zur Bestätigung ein.',
enter_your_application_name: 'Gib einen Anwendungsnamen ein',

View file

@ -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 <span>{{name}}</span> to confirm.',
enter_your_application_name: 'Enter your application name',

View file

@ -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 <span>{{name}}</span> para confirmar.',
enter_your_application_name: 'Ingresa el nombre de tu aplicación',

View file

@ -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 <span>{{nom}}</span> pour confirmer.",
enter_your_application_name: 'Entrez le nom de votre application',

View file

@ -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 <span>{{name}}</span> per confermare.",
enter_your_application_name: 'Inserisci il nome della tua applicazione',

View file

@ -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:
'この操作は元に戻すことはできません。アプリケーション名「<span>{{name}}</span>」を入力して確認してください。',
enter_your_application_name: 'アプリケーション名を入力してください',

View file

@ -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:
'이 행동은 취소될 수 없어요. 어플리케이션을 영원히 삭제할 거에요. 삭제를 진행하기 위해 <span>{{name}}</span> 를 입력해주세요.',
enter_your_application_name: '어플리케이션 이름을 입력해 주세요.',

View file

@ -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 <span>{{name}}</span>.',
enter_your_application_name: 'Wpisz nazwę swojej aplikacji',

View file

@ -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 <span>{{name}}</span> para confirmar.',
enter_your_application_name: 'Digite o nome do seu aplicativo',

View file

@ -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 <span>{{name}}</span> para confirmar.',
enter_your_application_name: 'Insira o nome da aplicação',

View file

@ -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:
'Это действие нельзя отменить. Оно навсегда удалит приложение. Введите название приложения <span> {{name}} </span>, чтобы подтвердить.',
enter_your_application_name: 'Введите название своего приложения',

View file

@ -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ı <span>{{name}}</span> girin.',
enter_your_application_name: 'Uygulama adı giriniz',

View file

@ -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: '本操作会永久性地删除该应用,且不可撤销。输入 <span>{{name}}</span> 确认。',
enter_your_application_name: '输入你的应用名称',
application_deleted: '应用 {{name}} 成功删除。',
redirect_uri_required: '至少需要输入一个重定向 URL。',
redirect_uri_required: '至少需要输入一个重定向 URI。',
};
export default application_details;

View file

@ -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: '本操作會永久性地刪除該應用,且不可撤銷。輸入 <span>{{name}}</span> 確認。',
enter_your_application_name: '輸入你的應用程式名稱',
application_deleted: '應用 {{name}} 成功刪除。',

View file

@ -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:
'本操作會永久性地刪除該應用程式,且不可撤銷。輸入 <span>{{name}}</span> 確認。',
enter_your_application_name: '輸入你的應用程式姓名',

View file

@ -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

View file

@ -1,6 +1,6 @@
{
"name": "@logto/schemas",
"version": "1.3.1",
"version": "1.4.0",
"author": "Silverhand Inc. <contact@silverhand.io>",
"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",

View file

@ -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<typeof customClientMetadataGuard>;
/* === Users === */

View file

@ -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