diff --git a/frontend/src/component/common/flags.js b/frontend/src/component/common/flags.js index ad16f84839..155282a816 100644 --- a/frontend/src/component/common/flags.js +++ b/frontend/src/component/common/flags.js @@ -1,3 +1,4 @@ export const P = 'P'; export const C = 'C'; export const RBAC = 'RBAC'; +export const OIDC = 'OIDC'; diff --git a/frontend/src/page/admin/auth/authentication.jsx b/frontend/src/page/admin/auth/authentication.jsx index 31f9dc34af..482a404d86 100644 --- a/frontend/src/page/admin/auth/authentication.jsx +++ b/frontend/src/page/admin/auth/authentication.jsx @@ -4,11 +4,12 @@ import AdminMenu from '../admin-menu'; import { Alert } from '@material-ui/lab'; import GoogleAuth from './google-auth-container'; import SamlAuth from './saml-auth-container'; +import OidcAuth from './oidc-auth-container'; import TabNav from '../../../component/common/TabNav/TabNav'; import PageContent from '../../../component/common/PageContent/PageContent'; import ConditionallyRender from '../../../component/common/ConditionallyRender/ConditionallyRender'; -function AdminAuthPage({ authenticationType, history }) { +function AdminAuthPage({ authenticationType, history, enableOIDC }) { const tabs = [ { label: 'SAML 2.0', @@ -20,6 +21,13 @@ function AdminAuthPage({ authenticationType, history }) { }, ]; + if(enableOIDC) { + tabs.unshift( { + label: 'OpenID Connect', + component: , + },) + } + return (
@@ -63,6 +71,7 @@ AdminAuthPage.propTypes = { match: PropTypes.object.isRequired, history: PropTypes.object.isRequired, authenticationType: PropTypes.string, + enableOIDC: PropTypes.bool, }; export default AdminAuthPage; diff --git a/frontend/src/page/admin/auth/google-auth.jsx b/frontend/src/page/admin/auth/google-auth.jsx index 7773419909..d2e694f31a 100644 --- a/frontend/src/page/admin/auth/google-auth.jsx +++ b/frontend/src/page/admin/auth/google-auth.jsx @@ -115,10 +115,11 @@ function GoogleAuth({ label="Client ID" name="clientId" placeholder="" - value={data.clientId} + value={data.clientId || ''} style={{ width: '400px' }} variant="outlined" size="small" + required /> @@ -135,11 +136,12 @@ function GoogleAuth({ onChange={updateField} label="Client Secret" name="clientSecret" - value={data.clientSecret} + value={data.clientSecret || ''} placeholder="" style={{ width: '400px' }} variant="outlined" size="small" + required /> @@ -163,7 +165,7 @@ function GoogleAuth({ label="Unleash Hostname" name="unleashHostname" placeholder="" - value={data.unleashHostname} + value={data.unleashHostname || ''} style={{ width: '400px' }} variant="outlined" size="small" diff --git a/frontend/src/page/admin/auth/index.js b/frontend/src/page/admin/auth/index.js index 07d89be806..cc34e9cfa4 100644 --- a/frontend/src/page/admin/auth/index.js +++ b/frontend/src/page/admin/auth/index.js @@ -1,8 +1,10 @@ import { connect } from 'react-redux'; import component from './authentication'; +import { OIDC } from '../../../component/common/flags'; const mapStateToProps = state => ({ authenticationType: state.uiConfig.toJS().authenticationType, + enableOIDC: !!state.uiConfig.toJS().flags[OIDC], }); const Container = connect(mapStateToProps, { })(component); diff --git a/frontend/src/page/admin/auth/oidc-auth-container.js b/frontend/src/page/admin/auth/oidc-auth-container.js new file mode 100644 index 0000000000..903a76a203 --- /dev/null +++ b/frontend/src/page/admin/auth/oidc-auth-container.js @@ -0,0 +1,12 @@ +import { connect } from 'react-redux'; +import OidcAuth from './oidc-auth'; +import { getOidcConfig, updateOidcConfig } from '../../../store/e-admin-auth/actions'; + +const mapStateToProps = state => ({ + config: state.authAdmin.get('oidc'), + unleashUrl: state.uiConfig.toJS().unleashUrl, +}); + +const OidcContainer = connect(mapStateToProps, { getOidcConfig, updateOidcConfig })(OidcAuth); + +export default OidcContainer; diff --git a/frontend/src/page/admin/auth/oidc-auth.jsx b/frontend/src/page/admin/auth/oidc-auth.jsx new file mode 100644 index 0000000000..226f7c0406 --- /dev/null +++ b/frontend/src/page/admin/auth/oidc-auth.jsx @@ -0,0 +1,225 @@ +import React, { useState, useEffect, useContext } from 'react'; +import PropTypes from 'prop-types'; +import { Button, Grid, Switch, TextField } from '@material-ui/core'; +import { Alert } from '@material-ui/lab'; +import PageContent from '../../../component/common/PageContent/PageContent'; +import AccessContext from '../../../contexts/AccessContext'; +import { ADMIN } from '../../../component/AccessProvider/permissions'; + +const initialState = { + enabled: false, + autoCreate: false, + unleashHostname: location.hostname, +}; + +function OidcAuth({ config, getOidcConfig, updateOidcConfig, unleashUrl }) { + const [data, setData] = useState(initialState); + const [info, setInfo] = useState(); + const [error, setError] = useState(); + const { hasAccess } = useContext(AccessContext); + + useEffect(() => { + getOidcConfig(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + if (config.discoverUrl) { + setData(config); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [config]); + + if (!hasAccess(ADMIN)) { + return ( + + You need to be a root admin to access this section. + + ); + } + + const updateField = e => { + setData({ + ...data, + [e.target.name]: e.target.value, + }); + }; + + const updateEnabled = () => { + setData({ ...data, enabled: !data.enabled }); + }; + + const updateAutoCreate = () => { + setData({ ...data, autoCreate: !data.autoCreate }); + }; + + const onSubmit = async e => { + e.preventDefault(); + setInfo('...saving'); + setError(''); + try { + await updateOidcConfig(data); + setInfo('Settings stored'); + setTimeout(() => setInfo(''), 2000); + } catch (e) { + setInfo(''); + setError(e.message); + } + }; + return ( + + + + + Please read the{' '} + + documentation + {' '} + to learn how to integrate with specific Open Id Connect + providers (Okta, Keycloak, Google, etc).
+ Callback URL:{' '} + {unleashUrl}/auth/oidc/callback +
+
+
+
+ + + Enable +

Enable Open Id Connect Authentication.

+
+ + + {data.enabled ? 'Enabled' : 'Disabled'} + + +
+ + + Discover URL +

(Required) Issuer discover metadata URL

+
+ + + +
+ + + Client ID +

(Required) Client ID of your OpenID application

+
+ + + +
+ + + Client secret +

(Required) Client secret of your OpenID application.

+
+ + + +
+ + + Auto-create users +

+ Enable automatic creation of new users when signing + in with Open ID connect. +

+
+ + + Auto-create users + + +
+ + + Email domains +

+ (Optional) Comma separated list of email domains + that should be allowed to sign in. +

+
+ + + +
+ + + {' '} + {info} + {error} + + +
+
+ ); +} + +OidcAuth.propTypes = { + config: PropTypes.object, + unleash: PropTypes.string, + getOidcConfig: PropTypes.func.isRequired, + updateOidcConfig: PropTypes.func.isRequired, +}; + +export default OidcAuth; diff --git a/frontend/src/page/admin/auth/saml-auth.jsx b/frontend/src/page/admin/auth/saml-auth.jsx index 04ef60ec44..1496e9ad33 100644 --- a/frontend/src/page/admin/auth/saml-auth.jsx +++ b/frontend/src/page/admin/auth/saml-auth.jsx @@ -114,6 +114,7 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) { style={{ width: '400px' }} variant="outlined" size="small" + required /> @@ -134,6 +135,7 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) { style={{ width: '400px' }} variant="outlined" size="small" + required /> @@ -161,6 +163,7 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) { rowsMax={14} variant="outlined" size="small" + required /> diff --git a/frontend/src/store/e-admin-auth/actions.js b/frontend/src/store/e-admin-auth/actions.js index 6a071e5574..5d91ceb428 100644 --- a/frontend/src/store/e-admin-auth/actions.js +++ b/frontend/src/store/e-admin-auth/actions.js @@ -8,6 +8,10 @@ export const RECIEVE_SAML_CONFIG = 'RECIEVE_SAML_CONFIG'; export const RECIEVE_SAML_CONFIG_ERROR = 'RECIEVE_SAML_CONFIG_ERROR'; export const UPDATE_SAML_AUTH = 'UPDATE_SAML_AUTH'; export const UPDATE_SAML_AUTH_ERROR = 'UPDATE_SAML_AUTH_ERROR'; +export const RECIEVE_OIDC_CONFIG = 'RECIEVE_OIDC_CONFIG'; +export const RECIEVE_OIDC_CONFIG_ERROR = 'RECIEVE_OIDC_CONFIG_ERROR'; +export const UPDATE_OIDC_AUTH = 'UPDATE_OIDC_AUTH'; +export const UPDATE_OIDC_AUTH_ERROR = 'UPDATE_OIDC_AUTH_ERROR'; const debug = require('debug')('unleash:e-admin-auth-actions'); @@ -30,7 +34,10 @@ export function updateGoogleConfig(data) { api .updateGoogleConfig(data) .then(config => dispatch({ type: UPDATE_GOOGLE_AUTH, config })) - .catch(dispatchError(dispatch, UPDATE_GOOGLE_AUTH_ERROR)); + .catch(e => { + dispatchError(dispatch, UPDATE_GOOGLE_AUTH_ERROR)(e); + throw e; + }); } export function getSamlConfig() { @@ -52,5 +59,33 @@ export function updateSamlConfig(data) { api .updateSamlConfig(data) .then(config => dispatch({ type: UPDATE_SAML_AUTH, config })) - .catch(dispatchError(dispatch, UPDATE_SAML_AUTH_ERROR)); + .catch(e => { + dispatchError(dispatch, UPDATE_SAML_AUTH_ERROR)(e); + throw e; + }); } + +export function getOidcConfig() { + debug('Start fetching OIDC-auth config'); + return dispatch => + api + .getOidcConfig() + .then(config => + dispatch({ + type: RECIEVE_OIDC_CONFIG, + config, + }) + ) + .catch(dispatchError(dispatch, RECIEVE_OIDC_CONFIG_ERROR)); +} + +export function updateOidcConfig(data) { + return dispatch => + api + .updateOidcConfig(data) + .then(config => dispatch({ type: UPDATE_OIDC_AUTH, config })) + .catch(e => { + dispatchError(dispatch, UPDATE_OIDC_AUTH_ERROR)(e); + throw e; + }); +} \ No newline at end of file diff --git a/frontend/src/store/e-admin-auth/api.js b/frontend/src/store/e-admin-auth/api.js index 00b6e20688..35333d6c06 100644 --- a/frontend/src/store/e-admin-auth/api.js +++ b/frontend/src/store/e-admin-auth/api.js @@ -3,6 +3,7 @@ import { formatApiPath } from '../../utils/format-path'; const GOOGLE_URI = formatApiPath('api/admin/auth/google/settings'); const SAML_URI = formatApiPath('api/admin/auth/saml/settings'); +const OIDC_URI = formatApiPath('api/admin/auth/oidc/settings'); function getGoogleConfig() { return fetch(GOOGLE_URI, { headers, credentials: 'include' }) @@ -38,9 +39,28 @@ function updateSamlConfig(data) { .then(response => response.json()); } +function getOidcConfig() { + return fetch(OIDC_URI, { headers, credentials: 'include' }) + .then(throwIfNotSuccess) + .then(response => response.json()); +} + +function updateOidcConfig(data) { + return fetch(OIDC_URI, { + method: 'POST', + headers, + body: JSON.stringify(data), + credentials: 'include', + }) + .then(throwIfNotSuccess) + .then(response => response.json()); +} + export default { getGoogleConfig, updateGoogleConfig, getSamlConfig, updateSamlConfig, + getOidcConfig, + updateOidcConfig, }; diff --git a/frontend/src/store/e-admin-auth/index.js b/frontend/src/store/e-admin-auth/index.js index 0f45ce9b4c..4f0a546000 100644 --- a/frontend/src/store/e-admin-auth/index.js +++ b/frontend/src/store/e-admin-auth/index.js @@ -1,7 +1,7 @@ import { Map as $Map } from 'immutable'; -import { RECIEVE_GOOGLE_CONFIG, UPDATE_GOOGLE_AUTH, RECIEVE_SAML_CONFIG, UPDATE_SAML_AUTH } from './actions'; +import { RECIEVE_GOOGLE_CONFIG, UPDATE_GOOGLE_AUTH, RECIEVE_SAML_CONFIG, UPDATE_SAML_AUTH, UPDATE_OIDC_AUTH, RECIEVE_OIDC_CONFIG } from './actions'; -const store = (state = new $Map({ google: {}, saml: {} }), action) => { +const store = (state = new $Map({ google: {}, saml: {}, oidc: {} }), action) => { switch (action.type) { case UPDATE_GOOGLE_AUTH: case RECIEVE_GOOGLE_CONFIG: @@ -9,6 +9,9 @@ const store = (state = new $Map({ google: {}, saml: {} }), action) => { case UPDATE_SAML_AUTH: case RECIEVE_SAML_CONFIG: return state.set('saml', action.config); + case UPDATE_OIDC_AUTH: + case RECIEVE_OIDC_CONFIG: + return state.set('oidc', action.config); default: return state; }