SSO: Add server discovery URL on Generic OAuth config (#91047)
* add OpenID metadata URL field * add i18n generated content * configure server discovery URL in a modal * remove getValues * address feedback * update field name to OpenID Connect Discovery URL * add well-known suffix to the URL provided * address feedback * add isLoading state to ServerDiscoveryField
This commit is contained in:
@@ -147,6 +147,12 @@ export const FieldRenderer = ({
|
||||
className={css({ marginBottom: theme.spacing(2) })}
|
||||
/>
|
||||
);
|
||||
case 'custom':
|
||||
return (
|
||||
<Field key={name} {...fieldProps}>
|
||||
{fieldData.content ? fieldData.content(setValue) : <></>}
|
||||
</Field>
|
||||
);
|
||||
default:
|
||||
console.error(`Unknown field type: ${fieldData.type}`);
|
||||
return null;
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { useState } from 'react';
|
||||
import { UseFormSetValue } from 'react-hook-form/dist/types/form';
|
||||
|
||||
import { AppEvents } from '@grafana/data';
|
||||
import { getAppEvents, getBackendSrv } from '@grafana/runtime';
|
||||
import { Button } from '@grafana/ui';
|
||||
|
||||
import { Trans } from '../../../core/internationalization';
|
||||
import { ServerDiscoveryFormData, SSOProviderDTO } from '../types';
|
||||
|
||||
import { ServerDiscoveryModal } from './ServerDiscoveryModal';
|
||||
|
||||
interface Props {
|
||||
setValue: UseFormSetValue<SSOProviderDTO>;
|
||||
}
|
||||
|
||||
export const ServerDiscoveryField = ({ setValue }: Props) => {
|
||||
const appEvents = getAppEvents();
|
||||
const [isModalOpen, setModalVisibility] = useState(false);
|
||||
const [isLoading, setLoading] = useState(false);
|
||||
|
||||
const onClose = () => setModalVisibility(false);
|
||||
const onSuccess = async (data: ServerDiscoveryFormData) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const wellKnownSuffix = '/.well-known/openid-configuration';
|
||||
const url = new URL(data.url);
|
||||
if (!url.pathname.includes(wellKnownSuffix)) {
|
||||
data.url = url.origin + wellKnownSuffix;
|
||||
}
|
||||
|
||||
const res = await getBackendSrv().get(data.url);
|
||||
|
||||
if (!res['token_endpoint'] || !res['authorization_endpoint']) {
|
||||
appEvents.publish({
|
||||
type: AppEvents.alertWarning.name,
|
||||
payload: ['The URL provided is not a valid .well-known/openid-configuration endpoint'],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setValue('tokenUrl', res['token_endpoint']);
|
||||
setValue('authUrl', res['authorization_endpoint']);
|
||||
if (res['userinfo_endpoint']) {
|
||||
setValue('apiUrl', res['userinfo_endpoint']);
|
||||
}
|
||||
|
||||
appEvents.publish({
|
||||
type: AppEvents.alertSuccess.name,
|
||||
payload: ['OpenID Connect Discovery URL has been successfully fetched.'],
|
||||
});
|
||||
} catch (error) {
|
||||
appEvents.publish({
|
||||
type: AppEvents.alertWarning.name,
|
||||
payload: ['Failed to fetch URL or invalid content'],
|
||||
});
|
||||
} finally {
|
||||
onClose();
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setModalVisibility(true);
|
||||
}}
|
||||
>
|
||||
<Trans i18nKey={'oauth.form.server-discovery-action-button'}>Enter OpenID Connect Discovery URL</Trans>
|
||||
</Button>
|
||||
<ServerDiscoveryModal isOpen={isModalOpen} onClose={onClose} onSuccess={onSuccess} isLoading={isLoading} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { Button, Input, Field, Modal } from '@grafana/ui';
|
||||
|
||||
import { Trans } from '../../../core/internationalization';
|
||||
import { ServerDiscoveryFormData } from '../types';
|
||||
import { isUrlValid } from '../utils/url';
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean | undefined;
|
||||
onClose: () => void;
|
||||
onSuccess: (data: ServerDiscoveryFormData) => void;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export const ServerDiscoveryModal = ({ isOpen, onClose, onSuccess, isLoading }: Props) => {
|
||||
const {
|
||||
handleSubmit,
|
||||
register,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
mode: 'onBlur',
|
||||
defaultValues: {
|
||||
url: '',
|
||||
},
|
||||
});
|
||||
|
||||
const validateUrl = (value?: string) => {
|
||||
if (value === '') {
|
||||
return 'Please enter the .well-known/openid-configuration endpoint for your IdP';
|
||||
}
|
||||
|
||||
if (!isUrlValid(value)) {
|
||||
return 'Please enter a valid URL';
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal title="OpenID Connect Discovery URL" onDismiss={onClose} onClickBackdrop={onClose} isOpen={isOpen}>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.stopPropagation();
|
||||
return handleSubmit(onSuccess)(e);
|
||||
}}
|
||||
>
|
||||
<Field
|
||||
label="The .well-known/openid-configuration endpoint for your IdP"
|
||||
invalid={!!errors.url}
|
||||
error={errors.url?.message}
|
||||
htmlFor="url"
|
||||
>
|
||||
<Input {...register('url', { validate: validateUrl })} width={80} id="url" />
|
||||
</Field>
|
||||
<Modal.ButtonRow>
|
||||
<Button type="submit" variant="primary" disabled={isLoading}>
|
||||
{isLoading ? (
|
||||
<Trans i18nKey={'oauth.form.server-discovery-modal-loading'}>Loading...</Trans>
|
||||
) : (
|
||||
<Trans i18nKey={'oauth.form.server-discovery-modal-submit'}>Submit</Trans>
|
||||
)}
|
||||
</Button>
|
||||
<Button type="button" variant="secondary" onClick={onClose}>
|
||||
<Trans i18nKey={'oauth.form.server-discovery-modal-close'}>Close</Trans>
|
||||
</Button>
|
||||
</Modal.ButtonRow>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -4,6 +4,7 @@ import { config } from '@grafana/runtime';
|
||||
import { TextLink } from '@grafana/ui';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
|
||||
import { ServerDiscoveryField } from './components/ServerDiscoveryField';
|
||||
import { FieldData, SSOProvider, SSOSettingsField } from './types';
|
||||
import { isSelectableValue } from './utils/guards';
|
||||
import { isUrlValid } from './utils/url';
|
||||
@@ -67,6 +68,7 @@ export const sectionFields: Section = {
|
||||
'clientSecret',
|
||||
'authStyle',
|
||||
'scopes',
|
||||
'serverDiscoveryUrl',
|
||||
'authUrl',
|
||||
'tokenUrl',
|
||||
'apiUrl',
|
||||
@@ -620,6 +622,13 @@ export function fieldMap(provider: string): Record<string, FieldData> {
|
||||
'If enabled, Grafana will match the Hosted Domain retrieved from the Google ID Token against the Allowed Domains list specified by the user.',
|
||||
type: 'checkbox',
|
||||
},
|
||||
serverDiscoveryUrl: {
|
||||
label: 'OpenID Connect Discovery URL',
|
||||
description:
|
||||
'The .well-known/openid-configuration endpoint for your IdP. The info extracted from this URL will be used to populate the Auth URL, Token URL and API URL fields.',
|
||||
type: 'custom',
|
||||
content: (setValue) => <ServerDiscoveryField setValue={setValue} />,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ReactElement } from 'react';
|
||||
import { Validate } from 'react-hook-form';
|
||||
import { UseFormSetValue } from 'react-hook-form/dist/types/form';
|
||||
|
||||
import { IconName, SelectableValue } from '@grafana/data';
|
||||
import { Settings } from 'app/types';
|
||||
@@ -72,6 +73,7 @@ export type SSOProvider = {
|
||||
allowedGroups?: string;
|
||||
scopes?: string;
|
||||
orgMapping?: string;
|
||||
serverDiscoveryUrl?: string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -83,6 +85,7 @@ export type SSOProviderDTO = Partial<SSOProviderSettingsBase> & {
|
||||
allowedGroups?: Array<SelectableValue<string>>;
|
||||
scopes?: Array<SelectableValue<string>>;
|
||||
orgMapping?: Array<SelectableValue<string>>;
|
||||
serverDiscoveryUrl?: string;
|
||||
};
|
||||
|
||||
export interface AuthConfigState {
|
||||
@@ -123,8 +126,13 @@ export type FieldData = {
|
||||
placeholder?: string;
|
||||
defaultValue?: SelectableValue<string>;
|
||||
hidden?: boolean;
|
||||
content?: (setValue: UseFormSetValue<SSOProviderDTO>) => ReactElement;
|
||||
};
|
||||
|
||||
export type SSOSettingsField =
|
||||
| keyof SSOProvider['settings']
|
||||
| { name: keyof SSOProvider['settings']; dependsOn: keyof SSOProvider['settings']; hidden?: boolean };
|
||||
|
||||
export interface ServerDiscoveryFormData {
|
||||
url: string;
|
||||
}
|
||||
|
||||
@@ -1604,6 +1604,14 @@
|
||||
"starred-dashboard": "Dashboard starred",
|
||||
"unstarred-dashboard": "Dashboard unstarred"
|
||||
},
|
||||
"oauth": {
|
||||
"form": {
|
||||
"server-discovery-action-button": "Enter OpenID Connect Discovery URL",
|
||||
"server-discovery-modal-close": "Close",
|
||||
"server-discovery-modal-loading": "Loading...",
|
||||
"server-discovery-modal-submit": "Submit"
|
||||
}
|
||||
},
|
||||
"panel": {
|
||||
"header-menu": {
|
||||
"copy": "Copy",
|
||||
|
||||
@@ -1604,6 +1604,14 @@
|
||||
"starred-dashboard": "Đäşĥþőäřđ şŧäřřęđ",
|
||||
"unstarred-dashboard": "Đäşĥþőäřđ ūʼnşŧäřřęđ"
|
||||
},
|
||||
"oauth": {
|
||||
"form": {
|
||||
"server-discovery-action-button": "Ēʼnŧęř ØpęʼnĨĐ Cőʼnʼnęčŧ Đįşčővęřy ŮŖĿ",
|
||||
"server-discovery-modal-close": "Cľőşę",
|
||||
"server-discovery-modal-loading": "Ŀőäđįʼnģ...",
|
||||
"server-discovery-modal-submit": "Ŝūþmįŧ"
|
||||
}
|
||||
},
|
||||
"panel": {
|
||||
"header-menu": {
|
||||
"copy": "Cőpy",
|
||||
|
||||
Reference in New Issue
Block a user