diff --git a/public/app/features/auth-config/FieldRenderer.tsx b/public/app/features/auth-config/FieldRenderer.tsx
index faba435a5dd..ce870c7fef4 100644
--- a/public/app/features/auth-config/FieldRenderer.tsx
+++ b/public/app/features/auth-config/FieldRenderer.tsx
@@ -147,6 +147,12 @@ export const FieldRenderer = ({
className={css({ marginBottom: theme.spacing(2) })}
/>
);
+ case 'custom':
+ return (
+
+ {fieldData.content ? fieldData.content(setValue) : <>>}
+
+ );
default:
console.error(`Unknown field type: ${fieldData.type}`);
return null;
diff --git a/public/app/features/auth-config/components/ServerDiscoveryField.tsx b/public/app/features/auth-config/components/ServerDiscoveryField.tsx
new file mode 100644
index 00000000000..5c8a2e3f441
--- /dev/null
+++ b/public/app/features/auth-config/components/ServerDiscoveryField.tsx
@@ -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;
+}
+
+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 (
+ <>
+
+
+ >
+ );
+};
diff --git a/public/app/features/auth-config/components/ServerDiscoveryModal.tsx b/public/app/features/auth-config/components/ServerDiscoveryModal.tsx
new file mode 100644
index 00000000000..e54cdf0c213
--- /dev/null
+++ b/public/app/features/auth-config/components/ServerDiscoveryModal.tsx
@@ -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 (
+
+
+
+ );
+};
diff --git a/public/app/features/auth-config/fields.tsx b/public/app/features/auth-config/fields.tsx
index e210d39707d..5f81ee11bde 100644
--- a/public/app/features/auth-config/fields.tsx
+++ b/public/app/features/auth-config/fields.tsx
@@ -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 {
'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) => ,
+ },
};
}
diff --git a/public/app/features/auth-config/types.ts b/public/app/features/auth-config/types.ts
index 19c827fd356..e35622b06c5 100644
--- a/public/app/features/auth-config/types.ts
+++ b/public/app/features/auth-config/types.ts
@@ -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 & {
allowedGroups?: Array>;
scopes?: Array>;
orgMapping?: Array>;
+ serverDiscoveryUrl?: string;
};
export interface AuthConfigState {
@@ -123,8 +126,13 @@ export type FieldData = {
placeholder?: string;
defaultValue?: SelectableValue;
hidden?: boolean;
+ content?: (setValue: UseFormSetValue) => ReactElement;
};
export type SSOSettingsField =
| keyof SSOProvider['settings']
| { name: keyof SSOProvider['settings']; dependsOn: keyof SSOProvider['settings']; hidden?: boolean };
+
+export interface ServerDiscoveryFormData {
+ url: string;
+}
diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json
index d71b34de7ae..e813d8380fe 100644
--- a/public/locales/en-US/grafana.json
+++ b/public/locales/en-US/grafana.json
@@ -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",
diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json
index 20588e9ddd4..f8591176bf9 100644
--- a/public/locales/pseudo-LOCALE/grafana.json
+++ b/public/locales/pseudo-LOCALE/grafana.json
@@ -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",