0
Fork 0
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:
Darcy Ye 2024-06-24 12:42:47 +08:00 committed by GitHub
parent a43434c42f
commit 15953609bb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 131 additions and 18 deletions

View file

@ -0,0 +1,5 @@
---
"@logto/connector-kit": minor
---
add OIDC prompt enum, prompt guard, and multi-select typed configuration field

View file

@ -0,0 +1,6 @@
---
"@logto/connector-azuread": minor
"@logto/connector-google": minor
---
support config of `prompt`

View file

@ -0,0 +1,5 @@
---
"@logto/console": minor
---
support the dynamic config rendering for connector multi-select configuration

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,3 +5,8 @@
font: var(--font-body-2);
margin-top: _.unit(0.5);
}
.multiSelect {
padding-top: _.unit(1);
padding-left: _.unit(1);
}

View file

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

View file

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

View file

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