Merge pull request #12552 from whitej6/jlw-generic-oidc

Implement Generic OIDC Provider
This commit is contained in:
Jessica Steurer
2022-08-23 15:38:43 -03:00
committed by GitHub
21 changed files with 815 additions and 3 deletions

View File

@@ -379,6 +379,7 @@ AUTHENTICATION_BACKENDS = (
'social_core.backends.github_enterprise.GithubEnterpriseOAuth2',
'social_core.backends.github_enterprise.GithubEnterpriseOrganizationOAuth2',
'social_core.backends.github_enterprise.GithubEnterpriseTeamOAuth2',
'social_core.backends.open_id_connect.OpenIdConnectAuth',
'social_core.backends.azuread.AzureADOAuth2',
'awx.sso.backends.SAMLAuth',
'awx.main.backends.AWXModelBackend',

View File

@@ -1215,6 +1215,54 @@ register(
placeholder=SOCIAL_AUTH_TEAM_MAP_PLACEHOLDER,
)
###############################################################################
# Generic OIDC AUTHENTICATION SETTINGS
###############################################################################
register(
'SOCIAL_AUTH_OIDC_KEY',
field_class=fields.CharField,
allow_null=False,
default=None,
label=_('OIDC Key'),
help_text='The OIDC key (Client ID) from your IDP.',
category=_('Generic OIDC'),
category_slug='oidc',
)
register(
'SOCIAL_AUTH_OIDC_SECRET',
field_class=fields.CharField,
allow_blank=True,
default='',
label=_('OIDC Secret'),
help_text=_('The OIDC secret (Client Secret) from your IDP.'),
category=_('Generic OIDC'),
category_slug='oidc',
encrypted=True,
)
register(
'SOCIAL_AUTH_OIDC_OIDC_ENDPOINT',
field_class=fields.CharField,
allow_blank=True,
default='',
label=_('OIDC Provider URL'),
help_text=_('The URL for your OIDC provider including the path up to /.well-known/openid-configuration'),
category=_('Generic OIDC'),
category_slug='oidc',
)
register(
'SOCIAL_AUTH_OIDC_VERIFY_SSL',
field_class=fields.BooleanField,
default=True,
label=_('Verify OIDC Provider Certificate'),
help_text=_('Verify the OIDV provider ssl certificate.'),
category=_('Generic OIDC'),
category_slug='oidc',
)
###############################################################################
# SAML AUTHENTICATION SETTINGS
###############################################################################

View File

@@ -149,6 +149,7 @@ class AuthenticationBackendsField(fields.StringListField):
('awx.sso.backends.RADIUSBackend', ['RADIUS_SERVER']),
('social_core.backends.google.GoogleOAuth2', ['SOCIAL_AUTH_GOOGLE_OAUTH2_KEY', 'SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET']),
('social_core.backends.github.GithubOAuth2', ['SOCIAL_AUTH_GITHUB_KEY', 'SOCIAL_AUTH_GITHUB_SECRET']),
('social_core.backends.open_id_connect.OpenIdConnectAuth', ['SOCIAL_AUTH_OIDC_KEY', 'SOCIAL_AUTH_OIDC_SECRET', 'SOCIAL_AUTH_OIDC_OIDC_ENDPOINT']),
(
'social_core.backends.github.GithubOrganizationOAuth2',
['SOCIAL_AUTH_GITHUB_ORG_KEY', 'SOCIAL_AUTH_GITHUB_ORG_SECRET', 'SOCIAL_AUTH_GITHUB_ORG_NAME'],

View File

@@ -346,6 +346,20 @@ function AWXLogin({ alt, isAuthenticated }) {
</LoginMainFooterLinksItem>
);
}
if (authKey === 'oidc') {
return (
<LoginMainFooterLinksItem
data-cy="social-auth-oidc"
href={loginUrl}
key={authKey}
onClick={setSessionRedirect}
>
<Tooltip content={t`Sign in with OIDC`}>
<UserCircleIcon size="lg" />
</Tooltip>
</LoginMainFooterLinksItem>
);
}
if (authKey.startsWith('saml')) {
const samlIDP = authKey.split(':')[1] || null;
return (

View File

@@ -0,0 +1,34 @@
import React from 'react';
import { Link, Redirect, Route, Switch } from 'react-router-dom';
import { t } from '@lingui/macro';
import { PageSection, Card } from '@patternfly/react-core';
import ContentError from 'components/ContentError';
import OIDCDetail from './OIDCDetail';
import OIDCEdit from './OIDCEdit';
function OIDC() {
const baseURL = '/settings/oidc';
return (
<PageSection>
<Card>
<Switch>
<Redirect from={baseURL} to={`${baseURL}/details`} exact />
<Route path={`${baseURL}/details`}>
<OIDCDetail />
</Route>
<Route path={`${baseURL}/edit`}>
<OIDCEdit />
</Route>
<Route key="not-found" path={`${baseURL}/*`}>
<ContentError isNotFound>
<Link to={`${baseURL}/details`}>{t`View OIDC settings`}</Link>
</ContentError>
</Route>
</Switch>
</Card>
</PageSection>
);
}
export default OIDC;

View File

@@ -0,0 +1,75 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import { createMemoryHistory } from 'history';
import { SettingsProvider } from 'contexts/Settings';
import { SettingsAPI } from 'api';
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
import mockAllOptions from '../shared/data.allSettingOptions.json';
import OIDC from './OIDC';
jest.mock('../../../api');
describe('<OIDC />', () => {
let wrapper;
beforeEach(() => {
SettingsAPI.readCategory.mockResolvedValue({
data: {
SOCIAL_AUTH_OIDC_KEY: 'mock key',
SOCIAL_AUTH_OIDC_SECRET: '$encrypted$',
SOCIAL_AUTH_OIDC_OIDC_ENDPOINT: 'https://example.com',
SOCIAL_AUTH_OIDC_VERIFY_SSL: true,
},
});
});
afterEach(() => {
jest.clearAllMocks();
});
test('should render OIDC details', async () => {
const history = createMemoryHistory({
initialEntries: ['/settings/oidc/details'],
});
await act(async () => {
wrapper = mountWithContexts(
<SettingsProvider value={mockAllOptions.actions}>
<OIDC />
</SettingsProvider>,
{
context: { router: { history } },
}
);
});
expect(wrapper.find('OIDCDetail').length).toBe(1);
});
test('should render OIDC edit', async () => {
const history = createMemoryHistory({
initialEntries: ['/settings/oidc/edit'],
});
await act(async () => {
wrapper = mountWithContexts(
<SettingsProvider value={mockAllOptions.actions}>
<OIDC />
</SettingsProvider>,
{
context: { router: { history } },
}
);
});
expect(wrapper.find('OIDCEdit').length).toBe(1);
});
test('should show content error when user navigates to erroneous route', async () => {
const history = createMemoryHistory({
initialEntries: ['/settings/oidc/foo'],
});
await act(async () => {
wrapper = mountWithContexts(<OIDC />, {
context: { router: { history } },
});
});
expect(wrapper.find('ContentError').length).toBe(1);
});
});

View File

@@ -0,0 +1,98 @@
import React, { useEffect, useCallback } from 'react';
import { Link } from 'react-router-dom';
import { t } from '@lingui/macro';
import { Button } from '@patternfly/react-core';
import { CaretLeftIcon } from '@patternfly/react-icons';
import { CardBody, CardActionsRow } from 'components/Card';
import ContentLoading from 'components/ContentLoading';
import ContentError from 'components/ContentError';
import RoutedTabs from 'components/RoutedTabs';
import { SettingsAPI } from 'api';
import useRequest from 'hooks/useRequest';
import { DetailList } from 'components/DetailList';
import { useConfig } from 'contexts/Config';
import { useSettings } from 'contexts/Settings';
import { SettingDetail } from '../../shared';
function OIDCDetail() {
const { me } = useConfig();
const { GET: options } = useSettings();
const {
isLoading,
error,
request,
result: OIDC,
} = useRequest(
useCallback(async () => {
const { data } = await SettingsAPI.readCategory('oidc');
return data;
}, []),
null
);
useEffect(() => {
request();
}, [request]);
const tabsArray = [
{
name: (
<>
<CaretLeftIcon />
{t`Back to Settings`}
</>
),
link: `/settings`,
id: 99,
},
{
name: t`Details`,
link: `/settings/oidc/details`,
id: 0,
},
];
return (
<>
<RoutedTabs tabsArray={tabsArray} />
<CardBody>
{isLoading && <ContentLoading />}
{!isLoading && error && <ContentError error={error} />}
{!isLoading && OIDC && (
<DetailList>
{Object.keys(OIDC).map((key) => {
const record = options?.[key];
return (
<SettingDetail
key={key}
id={key}
helpText={record?.help_text}
label={record?.label}
type={record?.type}
unit={record?.unit}
value={OIDC?.[key]}
/>
);
})}
</DetailList>
)}
{me?.is_superuser && (
<CardActionsRow>
<Button
ouiaId="oidc-detail-edit-button"
aria-label={t`Edit`}
component={Link}
to="/settings/oidc/edit"
>
{t`Edit`}
</Button>
</CardActionsRow>
)}
</CardBody>
</>
);
}
export default OIDCDetail;

View File

@@ -0,0 +1,97 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import { SettingsProvider } from 'contexts/Settings';
import { SettingsAPI } from 'api';
import {
mountWithContexts,
waitForElement,
} from '../../../../../testUtils/enzymeHelpers';
import {
assertDetail,
assertVariableDetail,
} from '../../shared/settingTestUtils';
import mockAllOptions from '../../shared/data.allSettingOptions.json';
import OIDCDetail from './OIDCDetail';
jest.mock('../../../../api');
describe('<OIDCDetail />', () => {
let wrapper;
beforeEach(() => {
SettingsAPI.readCategory.mockResolvedValue({
data: {
SOCIAL_AUTH_OIDC_KEY: 'mock key',
SOCIAL_AUTH_OIDC_SECRET: '$encrypted$',
SOCIAL_AUTH_OIDC_OIDC_ENDPOINT: 'https://example.com',
SOCIAL_AUTH_OIDC_VERIFY_SSL: true,
},
});
});
beforeEach(async () => {
await act(async () => {
wrapper = mountWithContexts(
<SettingsProvider value={mockAllOptions.actions}>
<OIDCDetail />
</SettingsProvider>
);
});
await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
});
afterAll(() => {
jest.clearAllMocks();
});
test('initially renders without crashing', () => {
expect(wrapper.find('OIDCDetail').length).toBe(1);
});
test('should render expected tabs', () => {
const expectedTabs = ['Back to Settings', 'Details'];
wrapper.find('RoutedTabs li').forEach((tab, index) => {
expect(tab.text()).toEqual(expectedTabs[index]);
});
});
test('should render expected details', () => {
assertDetail(wrapper, 'OIDC Key', 'mock key');
assertDetail(wrapper, 'OIDC Secret', 'Encrypted');
assertDetail(wrapper, 'OIDC Provider URL', 'https://example.com');
assertDetail(wrapper, 'Verify OIDC Provider Certificate', 'On');
});
test('should hide edit button from non-superusers', async () => {
const config = {
me: {
is_superuser: false,
},
};
await act(async () => {
wrapper = mountWithContexts(
<SettingsProvider value={mockAllOptions.actions}>
<OIDCDetail />
</SettingsProvider>,
{
context: { config },
}
);
});
await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
expect(wrapper.find('Button[aria-label="Edit"]').exists()).toBeFalsy();
});
test('should display content error when api throws error on initial render', async () => {
SettingsAPI.readCategory.mockRejectedValue(new Error());
await act(async () => {
wrapper = mountWithContexts(
<SettingsProvider value={mockAllOptions.actions}>
<OIDCDetail />
</SettingsProvider>
);
});
await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
expect(wrapper.find('ContentError').length).toBe(1);
});
});

View File

@@ -0,0 +1 @@
export { default } from './OIDCDetail';

View File

@@ -0,0 +1,147 @@
import React, { useCallback, useEffect } from 'react';
import { useHistory } from 'react-router-dom';
import { Formik } from 'formik';
import { Form } from '@patternfly/react-core';
import { CardBody } from 'components/Card';
import ContentError from 'components/ContentError';
import ContentLoading from 'components/ContentLoading';
import { FormSubmitError } from 'components/FormField';
import { FormColumnLayout } from 'components/FormLayout';
import { useSettings } from 'contexts/Settings';
import useModal from 'hooks/useModal';
import useRequest from 'hooks/useRequest';
import { SettingsAPI } from 'api';
import { RevertAllAlert, RevertFormActionGroup } from '../../shared';
import {
EncryptedField,
InputField,
BooleanField,
} from '../../shared/SharedFields';
function OIDCEdit() {
const history = useHistory();
const { isModalOpen, toggleModal, closeModal } = useModal();
const { PUT: options } = useSettings();
const {
isLoading,
error,
request: fetchOIDC,
result: OIDC,
} = useRequest(
useCallback(async () => {
const { data } = await SettingsAPI.readCategory('oidc');
const mergedData = {};
Object.keys(data).forEach((key) => {
if (!options[key]) {
return;
}
mergedData[key] = options[key];
mergedData[key].value = data[key];
});
return mergedData;
}, [options]),
null
);
useEffect(() => {
fetchOIDC();
}, [fetchOIDC]);
const { error: submitError, request: submitForm } = useRequest(
useCallback(
async (values) => {
await SettingsAPI.updateAll(values);
history.push('/settings/oidc/details');
},
[history]
),
null
);
const { error: revertError, request: revertAll } = useRequest(
useCallback(async () => {
await SettingsAPI.revertCategory('oidc');
}, []),
null
);
const handleSubmit = async (form) => {
await submitForm({
...form,
});
};
const handleRevertAll = async () => {
await revertAll();
closeModal();
history.push('/settings/oidc/details');
};
const handleCancel = () => {
history.push('/settings/oidc/details');
};
const initialValues = (fields) =>
Object.keys(fields).reduce((acc, key) => {
if (fields[key].type === 'list' || fields[key].type === 'nested object') {
acc[key] = fields[key].value
? JSON.stringify(fields[key].value, null, 2)
: null;
} else {
acc[key] = fields[key].value ?? '';
}
return acc;
}, {});
return (
<CardBody>
{isLoading && <ContentLoading />}
{!isLoading && error && <ContentError error={error} />}
{!isLoading && OIDC && (
<Formik initialValues={initialValues(OIDC)} onSubmit={handleSubmit}>
{(formik) => (
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
<FormColumnLayout>
<InputField
name="SOCIAL_AUTH_OIDC_KEY"
config={OIDC.SOCIAL_AUTH_OIDC_KEY}
/>
<EncryptedField
name="SOCIAL_AUTH_OIDC_SECRET"
config={OIDC.SOCIAL_AUTH_OIDC_SECRET}
/>
<InputField
name="SOCIAL_AUTH_OIDC_OIDC_ENDPOINT"
config={OIDC.SOCIAL_AUTH_OIDC_OIDC_ENDPOINT}
type="url"
/>
<BooleanField
name="SOCIAL_AUTH_OIDC_VERIFY_SSL"
config={OIDC.SOCIAL_AUTH_OIDC_VERIFY_SSL}
/>
{submitError && <FormSubmitError error={submitError} />}
{revertError && <FormSubmitError error={revertError} />}
</FormColumnLayout>
<RevertFormActionGroup
onCancel={handleCancel}
onSubmit={formik.handleSubmit}
onRevert={toggleModal}
/>
{isModalOpen && (
<RevertAllAlert
onClose={closeModal}
onRevertAll={handleRevertAll}
/>
)}
</Form>
)}
</Formik>
)}
</CardBody>
);
}
export default OIDCEdit;

View File

@@ -0,0 +1,161 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import { createMemoryHistory } from 'history';
import { SettingsProvider } from 'contexts/Settings';
import { SettingsAPI } from 'api';
import {
mountWithContexts,
waitForElement,
} from '../../../../../testUtils/enzymeHelpers';
import mockAllOptions from '../../shared/data.allSettingOptions.json';
import OIDCEdit from './OIDCEdit';
jest.mock('../../../../api');
describe('<OIDCEdit />', () => {
let wrapper;
let history;
beforeEach(() => {
SettingsAPI.revertCategory.mockResolvedValue({});
SettingsAPI.updateAll.mockResolvedValue({});
SettingsAPI.readCategory.mockResolvedValue({
data: {
SOCIAL_AUTH_OIDC_KEY: 'mock key',
SOCIAL_AUTH_OIDC_SECRET: '$encrypted$',
SOCIAL_AUTH_OIDC_OIDC_ENDPOINT: 'https://example.com',
SOCIAL_AUTH_OIDC_VERIFY_SSL: true,
},
});
});
afterEach(() => {
jest.clearAllMocks();
});
beforeEach(async () => {
history = createMemoryHistory({
initialEntries: ['/settings/oidc/edit'],
});
await act(async () => {
wrapper = mountWithContexts(
<SettingsProvider value={mockAllOptions.actions}>
<OIDCEdit />
</SettingsProvider>,
{
context: { router: { history } },
}
);
});
await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
});
test('initially renders without crashing', () => {
expect(wrapper.find('OIDCEdit').length).toBe(1);
});
test('should display expected form fields', async () => {
expect(wrapper.find('FormGroup[label="OIDC Key"]').length).toBe(1);
expect(wrapper.find('FormGroup[label="OIDC Secret"]').length).toBe(1);
expect(wrapper.find('FormGroup[label="OIDC Provider URL"]').length).toBe(1);
expect(
wrapper.find('FormGroup[label="Verify OIDC Provider Certificate"]').length
).toBe(1);
});
test('should successfully send default values to api on form revert all', async () => {
expect(SettingsAPI.revertCategory).toHaveBeenCalledTimes(0);
expect(wrapper.find('RevertAllAlert')).toHaveLength(0);
await act(async () => {
wrapper
.find('button[aria-label="Revert all to default"]')
.invoke('onClick')();
});
wrapper.update();
expect(wrapper.find('RevertAllAlert')).toHaveLength(1);
await act(async () => {
wrapper
.find('RevertAllAlert button[aria-label="Confirm revert all"]')
.invoke('onClick')();
});
wrapper.update();
expect(SettingsAPI.revertCategory).toHaveBeenCalledTimes(1);
expect(SettingsAPI.revertCategory).toHaveBeenCalledWith('oidc');
});
test('should successfully send request to api on form submission', async () => {
act(() => {
wrapper
.find(
'FormGroup[fieldId="SOCIAL_AUTH_OIDC_SECRET"] button[aria-label="Revert"]'
)
.invoke('onClick')();
wrapper.find('input#SOCIAL_AUTH_OIDC_KEY').simulate('change', {
target: { value: 'new key', name: 'SOCIAL_AUTH_OIDC_KEY' },
});
wrapper.find('input#SOCIAL_AUTH_OIDC_OIDC_ENDPOINT').simulate('change', {
target: {
value: 'https://example.com',
name: 'SOCIAL_AUTH_OIDC_OIDC_ENDPOINT',
},
});
});
wrapper.update();
await act(async () => {
wrapper.find('Form').invoke('onSubmit')();
});
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1);
expect(SettingsAPI.updateAll).toHaveBeenCalledWith({
SOCIAL_AUTH_OIDC_KEY: 'new key',
SOCIAL_AUTH_OIDC_SECRET: '',
SOCIAL_AUTH_OIDC_OIDC_ENDPOINT: 'https://example.com',
SOCIAL_AUTH_OIDC_VERIFY_SSL: true,
});
});
test('should navigate to OIDC detail on successful submission', async () => {
await act(async () => {
wrapper.find('Form').invoke('onSubmit')();
});
expect(history.location.pathname).toEqual('/settings/oidc/details');
});
test('should navigate to OIDC detail when cancel is clicked', async () => {
await act(async () => {
wrapper.find('button[aria-label="Cancel"]').invoke('onClick')();
});
expect(history.location.pathname).toEqual('/settings/oidc/details');
});
test('should display error message on unsuccessful submission', async () => {
const error = {
response: {
data: { detail: 'An error occurred' },
},
};
SettingsAPI.updateAll.mockImplementation(() => Promise.reject(error));
expect(wrapper.find('FormSubmitError').length).toBe(0);
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0);
await act(async () => {
wrapper.find('Form').invoke('onSubmit')();
});
wrapper.update();
expect(wrapper.find('FormSubmitError').length).toBe(1);
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1);
});
test('should display ContentError on throw', async () => {
SettingsAPI.readCategory.mockImplementationOnce(() =>
Promise.reject(new Error())
);
await act(async () => {
wrapper = mountWithContexts(
<SettingsProvider value={mockAllOptions.actions}>
<OIDCEdit />
</SettingsProvider>
);
});
await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
expect(wrapper.find('ContentError').length).toBe(1);
});
});

View File

@@ -0,0 +1 @@
export { default } from './OIDCEdit';

View File

@@ -0,0 +1 @@
export { default } from './OIDC';

View File

@@ -81,6 +81,10 @@ function SettingList() {
title: t`TACACS+ settings`,
path: '/settings/tacacs',
},
{
title: t`Generic OIDC settings`,
path: '/settings/oidc',
},
],
},
{

View File

@@ -12,6 +12,7 @@ import useRequest from 'hooks/useRequest';
import AzureAD from './AzureAD';
import GitHub from './GitHub';
import GoogleOAuth2 from './GoogleOAuth2';
import OIDC from './OIDC';
import Jobs from './Jobs';
import LDAP from './LDAP';
import Subscription from './Subscription';
@@ -68,6 +69,9 @@ function Settings() {
'/settings/google_oauth2': t`Google OAuth2`,
'/settings/google_oauth2/details': t`Details`,
'/settings/google_oauth2/edit': t`Edit Details`,
'/settings/oidc': t`Generic OIDC`,
'/settings/oidc/details': t`Details`,
'/settings/oidc/edit': t`Edit Details`,
'/settings/jobs': t`Jobs`,
'/settings/jobs/details': t`Details`,
'/settings/jobs/edit': t`Edit Details`,
@@ -153,6 +157,9 @@ function Settings() {
<Route path="/settings/google_oauth2">
<GoogleOAuth2 />
</Route>
<Route path="/settings/oidc">
<OIDC />
</Route>
<Route path="/settings/jobs">
<Jobs />
</Route>

View File

@@ -840,6 +840,39 @@
"read_only": false
}
},
"SOCIAL_AUTH_OIDC_KEY": {
"type": "string",
"label": "OIDC Key",
"help_text": "The OIDC key (Client ID) from your IDP.",
"category": "Generic OIDC",
"category_slug": "oidc",
"default": ""
},
"SOCIAL_AUTH_OIDC_SECRET": {
"type": "string",
"label": "OIDC Secret",
"help_text": "The OIDC secret (Client Secret) from your IDP.",
"category": "Generic OIDC",
"category_slug": "oidc",
"default": ""
},
"SOCIAL_AUTH_OIDC_OIDC_ENDPOINT": {
"type": "string",
"label": "OIDC Provider URL",
"help_text": "The URL for your OIDC provider, e.g.: http(s)://hostname/.",
"category": "Generic OIDC",
"category_slug": "oidc",
"default": ""
},
"SOCIAL_AUTH_OIDC_VERIFY_SSL": {
"type": "boolean",
"required": false,
"label": "Verify OIDC Provider Certificate",
"help_text": "Verify the OIDV provider ssl certificate.",
"category": "Generic OIDC",
"category_slug": "oidc",
"default": true
},
"AUTH_LDAP_SERVER_URI": {
"type": "string",
"required": false,
@@ -4485,6 +4518,38 @@
"type": "string"
}
},
"SOCIAL_AUTH_OIDC_KEY": {
"type": "string",
"label": "OIDC Key",
"help_text": "The OIDC key (Client ID) from your IDP.",
"category": "Generic OIDC",
"category_slug": "oidc",
"default": ""
},
"SOCIAL_AUTH_OIDC_SECRET": {
"type": "string",
"label": "OIDC Secret",
"help_text": "The OIDC secret (Client Secret) from your IDP.",
"category": "Generic OIDC",
"category_slug": "oidc",
"default": ""
},
"SOCIAL_AUTH_OIDC_OIDC_ENDPOINT": {
"type": "string",
"label": "OIDC Provider URL",
"help_text": "The URL for your OIDC provider, e.g.: http(s)://hostname/.",
"category": "Generic OIDC",
"category_slug": "oidc",
"default": ""
},
"SOCIAL_AUTH_OIDC_VERIFY_SSL": {
"type": "boolean",
"label": "Verify OIDC Provider Certificate",
"help_text": "Verify the OIDV provider ssl certificate.",
"category": "Generic OIDC",
"category_slug": "oidc",
"default": true
},
"AUTH_LDAP_SERVER_URI": {
"type": "string",
"label": "LDAP Server URI",

View File

@@ -253,6 +253,10 @@
"SOCIAL_AUTH_SAML_ORGANIZATION_ATTR":{},
"SOCIAL_AUTH_SAML_TEAM_ATTR":{},
"SOCIAL_AUTH_SAML_USER_FLAGS_BY_ATTR":{},
"SOCIAL_AUTH_OIDC_KEY":"",
"SOCIAL_AUTH_OIDC_SECRET":"",
"SOCIAL_AUTH_OIDC_OIDC_ENDPOINT":"",
"SOCIAL_AUTH_OIDC_VERIFY_SSL":true,
"NAMED_URL_FORMATS":{
"organizations":"<name>",
"teams":"<name>++<organization.name>",