mirror of
https://github.com/logto-io/logto.git
synced 2025-03-17 22:31:28 -05:00
feat: support prompt config for some built-in connectors (#6023)
* feat: support prompt config for some built-in connectors * chore: adopt code review suggestions Co-authored-by: Gao Sun <gao@silverhand.io> --------- Co-authored-by: Gao Sun <gao@silverhand.io>
This commit is contained in:
parent
a43434c42f
commit
15953609bb
14 changed files with 131 additions and 18 deletions
5
.changeset/breezy-dodos-cheer.md
Normal file
5
.changeset/breezy-dodos-cheer.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@logto/connector-kit": minor
|
||||
---
|
||||
|
||||
add OIDC prompt enum, prompt guard, and multi-select typed configuration field
|
6
.changeset/orange-dryers-joke.md
Normal file
6
.changeset/orange-dryers-joke.md
Normal file
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
"@logto/connector-azuread": minor
|
||||
"@logto/connector-google": minor
|
||||
---
|
||||
|
||||
support config of `prompt`
|
5
.changeset/quick-schools-obey.md
Normal file
5
.changeset/quick-schools-obey.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@logto/console": minor
|
||||
---
|
||||
|
||||
support the dynamic config rendering for connector multi-select configuration
|
|
@ -6,9 +6,12 @@ The Microsoft Azure AD connector provides a succinct way for your application to
|
|||
|
||||
- [Microsoft Azure AD connector](#microsoft-azure-ad-connector)
|
||||
- [Set up Microsoft Azure AD in the Azure Portal](#set-up-microsoft-azure-ad-in-the-azure-portal)
|
||||
- [Fill in the configuration](#fill-in-the-configuration)
|
||||
- [Configure your client secret](#configure-your-client-secret)
|
||||
- [Config types](#config-types)
|
||||
- [Fill in the configuration in Logto](#fill-in-the-configuration-in-logto)
|
||||
- [Client ID](#client-id)
|
||||
- [Client Secret](#client-secret)
|
||||
- [Cloud Instance](#cloud-instance)
|
||||
- [Tenant ID](#tenant-id)
|
||||
- [Prompts](#prompts)
|
||||
- [References](#references)
|
||||
|
||||
## Set up Microsoft Azure AD in the Azure Portal
|
||||
|
@ -21,12 +24,13 @@ The Microsoft Azure AD connector provides a succinct way for your application to
|
|||
|
||||
## Fill in the configuration in Logto
|
||||
|
||||
| Name | Type |
|
||||
| ------------- | ------ |
|
||||
| clientId | string |
|
||||
| clientSecret | string |
|
||||
| tenantId | string |
|
||||
| cloudInstance | string |
|
||||
| Name | Type |
|
||||
| ------------- | -------- |
|
||||
| clientId | string |
|
||||
| clientSecret | string |
|
||||
| tenantId | string |
|
||||
| cloudInstance | string |
|
||||
| prompts | string[] |
|
||||
|
||||
### Client ID
|
||||
|
||||
|
@ -51,6 +55,17 @@ Logto will use this field to construct the authorization endpoints. This value i
|
|||
- If you select **Accounts in any organizational directory or personal Microsoft accounts** for access type then you need to enter **common**.
|
||||
- If you select **Personal Microsoft accounts only** for access type then you need to enter **consumers**.
|
||||
|
||||
### Prompts
|
||||
|
||||
The `prompts` field is an array of strings that specifies the type of user interaction that is required. The string can be one of the following values:
|
||||
|
||||
- `prompt=login` forces the user to enter their credentials on that request, negating single-sign on.
|
||||
- `prompt=none` is the opposite. It ensures that the user isn't presented with any interactive prompt. If the request can't be completed silently by using single-sign on, the Microsoft identity platform returns an `interaction_required` error.
|
||||
- `prompt=consent` triggers the OAuth consent dialog after the user signs in, asking the user to grant permissions to the app.
|
||||
- `prompt=select_account` interrupts single sign-on providing account selection experience listing all the accounts either in session or any remembered account or an option to choose to use a different account altogether.
|
||||
|
||||
Logto will concatenate the prompts with a space as the value of `prompt` in the authorization URL.
|
||||
|
||||
## References
|
||||
|
||||
- [Web app that signs in users](https://docs.microsoft.com/en-us/azure/active-directory/develop/scenario-web-app-sign-user-overview)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import type { ConnectorMetadata } from '@logto/connector-kit';
|
||||
import { ConnectorPlatform, ConnectorConfigFormItemType } from '@logto/connector-kit';
|
||||
import { ConnectorPlatform, ConnectorConfigFormItemType, OidcPrompt } from '@logto/connector-kit';
|
||||
|
||||
export const graphAPIEndpoint = 'https://graph.microsoft.com/v1.0/me';
|
||||
export const scopes = ['User.Read'];
|
||||
|
@ -53,6 +53,15 @@ export const defaultMetadata: ConnectorMetadata = {
|
|||
label: 'Tenant ID',
|
||||
placeholder: '<tenant-id>',
|
||||
},
|
||||
{
|
||||
key: 'prompts',
|
||||
type: ConnectorConfigFormItemType.MultiSelect,
|
||||
required: false,
|
||||
label: 'Prompts',
|
||||
selectItems: Object.values(OidcPrompt).map((prompt) => ({
|
||||
value: prompt,
|
||||
})),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
|
|
@ -37,12 +37,13 @@ const getAuthorizationUri =
|
|||
const config = await getConfig(defaultMetadata.id);
|
||||
|
||||
validateConfig(config, azureADConfigGuard);
|
||||
const { clientId, clientSecret, cloudInstance, tenantId } = config;
|
||||
const { clientId, clientSecret, cloudInstance, tenantId, prompts } = config;
|
||||
|
||||
const defaultAuthCodeUrlParameters: AuthorizationUrlRequest = {
|
||||
scopes,
|
||||
state,
|
||||
redirectUri,
|
||||
...conditional(prompts && prompts.length > 0 && { prompt: prompts.join(' ') }),
|
||||
};
|
||||
|
||||
const clientApplication = new ConfidentialClientApplication({
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { oidcPromptsGuard } from '@logto/connector-kit';
|
||||
|
||||
export const azureADConfigGuard = z.object({
|
||||
clientId: z.string(),
|
||||
clientSecret: z.string(),
|
||||
cloudInstance: z.string(),
|
||||
tenantId: z.string(),
|
||||
prompts: oidcPromptsGuard,
|
||||
});
|
||||
|
||||
export type AzureADConfig = z.infer<typeof azureADConfigGuard>;
|
||||
|
|
|
@ -66,13 +66,20 @@ Fill out the `clientId` and `clientSecret` field with _Client ID_ and _Client Se
|
|||
|
||||
`scope` is a space-delimited list of [scopes](https://developers.google.com/identity/protocols/oauth2/scopes). If not provided, scope defaults to be `openid profile email`.
|
||||
|
||||
`prompts` is an array of strings that specifies the type of user interaction that is required. The string can be one of the following values:
|
||||
|
||||
- `none`: The authorization server does not display any authentication or user consent screens; it will return an error if the user is not already authenticated and has not pre-configured consent for the requested scopes. You can use none to check for existing authentication and/or consent.
|
||||
- `consent`: The authorization server prompts the user for consent before returning information to the client.
|
||||
- `select_account`: The authorization server prompts the user to select a user account. This allows a user who has multiple accounts at the authorization server to select amongst the multiple accounts that they may have current sessions for.
|
||||
|
||||
### Config types
|
||||
|
||||
| Name | Type |
|
||||
|--------------|--------|
|
||||
| clientId | string |
|
||||
| clientSecret | string |
|
||||
| scope | string |
|
||||
| Name | Type |
|
||||
|--------------|----------|
|
||||
| clientId | string |
|
||||
| clientSecret | string |
|
||||
| scope | string |
|
||||
| prompts | string[] |
|
||||
|
||||
## References
|
||||
* [Google Identity: Setting up OAuth 2.0](https://developers.google.com/identity/protocols/oauth2/openid-connect#appsetup)
|
||||
|
|
|
@ -3,6 +3,7 @@ import {
|
|||
ConnectorConfigFormItemType,
|
||||
ConnectorPlatform,
|
||||
GoogleConnector,
|
||||
OidcPrompt,
|
||||
} from '@logto/connector-kit';
|
||||
|
||||
export const authorizationEndpoint = 'https://accounts.google.com/o/oauth2/v2/auth';
|
||||
|
@ -56,6 +57,19 @@ export const defaultMetadata: ConnectorMetadata = {
|
|||
description:
|
||||
"The `scope` determines permissions granted by the user's authorization. If you are not sure what to enter, do not worry, just leave it blank.",
|
||||
},
|
||||
{
|
||||
key: 'prompts',
|
||||
type: ConnectorConfigFormItemType.MultiSelect,
|
||||
required: false,
|
||||
label: 'Prompts',
|
||||
// Google does not support `login` prompt.
|
||||
// Ref: https://developers.google.com/identity/openid-connect/openid-connect#authenticationuriparameters
|
||||
selectItems: Object.values(OidcPrompt)
|
||||
.filter((prompt) => prompt !== OidcPrompt.Login)
|
||||
.map((prompt) => ({
|
||||
value: prompt,
|
||||
})),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
|
|
@ -45,12 +45,15 @@ const getAuthorizationUri =
|
|||
const config = await getConfig(defaultMetadata.id);
|
||||
validateConfig(config, GoogleConnector.configGuard);
|
||||
|
||||
const { clientId, scope, prompts } = config;
|
||||
|
||||
const queryParameters = new URLSearchParams({
|
||||
client_id: config.clientId,
|
||||
client_id: clientId,
|
||||
redirect_uri: redirectUri,
|
||||
response_type: 'code',
|
||||
state,
|
||||
scope: config.scope ?? defaultScope,
|
||||
scope: scope ?? defaultScope,
|
||||
...conditional(prompts && prompts.length > 0 && { prompt: prompts.join(' ') }),
|
||||
});
|
||||
|
||||
return `${authorizationEndpoint}?${queryParameters.toString()}`;
|
||||
|
|
|
@ -5,3 +5,8 @@
|
|||
font: var(--font-body-2);
|
||||
margin-top: _.unit(0.5);
|
||||
}
|
||||
|
||||
.multiSelect {
|
||||
padding-top: _.unit(1);
|
||||
padding-left: _.unit(1);
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import { useCallback } from 'react';
|
|||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { CheckboxGroup } from '@/ds-components/Checkbox';
|
||||
import CodeEditor from '@/ds-components/CodeEditor';
|
||||
import DangerousRaw from '@/ds-components/DangerousRaw';
|
||||
import FormField from '@/ds-components/FormField';
|
||||
|
@ -120,6 +121,22 @@ function ConfigFormFields({ formItems }: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
if (item.type === ConnectorConfigFormItemType.MultiSelect) {
|
||||
return (
|
||||
<CheckboxGroup
|
||||
options={item.selectItems}
|
||||
value={
|
||||
Array.isArray(value) &&
|
||||
value.every((item): item is string => typeof item === 'string')
|
||||
? value
|
||||
: []
|
||||
}
|
||||
className={styles.multiSelect}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (item.type === ConnectorConfigFormItemType.Json) {
|
||||
return (
|
||||
<CodeEditor
|
||||
|
|
|
@ -6,6 +6,7 @@ export enum ConnectorConfigFormItemType {
|
|||
MultilineText = 'MultilineText',
|
||||
Switch = 'Switch',
|
||||
Select = 'Select',
|
||||
MultiSelect = 'MultiSelect',
|
||||
Json = 'Json',
|
||||
}
|
||||
|
||||
|
@ -29,6 +30,11 @@ export const connectorConfigFormItemGuard = z.discriminatedUnion('type', [
|
|||
selectItems: z.array(z.object({ value: z.string(), title: z.string() })),
|
||||
...baseConfigFormItem,
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal(ConnectorConfigFormItemType.MultiSelect),
|
||||
selectItems: z.array(z.object({ value: z.string() })),
|
||||
...baseConfigFormItem,
|
||||
}),
|
||||
z.object({
|
||||
type: z.enum([
|
||||
ConnectorConfigFormItemType.Text,
|
||||
|
|
|
@ -1,9 +1,25 @@
|
|||
// MARK: Social connector
|
||||
import { type Optional } from '@silverhand/essentials';
|
||||
import { type Json } from '@withtyped/server';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { type ToZodObject, type BaseConnector, type ConnectorType } from './foundation.js';
|
||||
|
||||
// Ref: `prompt` in https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
|
||||
export enum OidcPrompt {
|
||||
None = 'none',
|
||||
Login = 'login',
|
||||
Consent = 'consent',
|
||||
SelectAccount = 'select_account',
|
||||
}
|
||||
|
||||
export type OidcPrompts = OidcPrompt[];
|
||||
|
||||
export const oidcPromptsGuard: z.ZodType<Optional<OidcPrompts>> = z
|
||||
.nativeEnum(OidcPrompt)
|
||||
.array()
|
||||
.optional();
|
||||
|
||||
// This type definition is for SAML connector
|
||||
export type ValidateSamlAssertion = (
|
||||
assertion: Record<string, unknown>,
|
||||
|
@ -125,6 +141,7 @@ export const GoogleConnector = Object.freeze({
|
|||
clientId: z.string(),
|
||||
clientSecret: z.string(),
|
||||
scope: z.string().optional(),
|
||||
prompts: oidcPromptsGuard,
|
||||
oneTap: googleOneTapConfigGuard.optional(),
|
||||
}) satisfies ToZodObject<GoogleConnectorConfig>,
|
||||
});
|
||||
|
|
Loading…
Add table
Reference in a new issue