OidcSecuritySettingContents.jsx 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488
  1. import React from 'react';
  2. import { pathUtils } from '@growi/core/dist/utils';
  3. import { useTranslation } from 'next-i18next';
  4. import PropTypes from 'prop-types';
  5. import urljoin from 'url-join';
  6. import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
  7. import AdminOidcSecurityContainer from '~/client/services/AdminOidcSecurityContainer';
  8. import { toastSuccess, toastError } from '~/client/util/toastr';
  9. import { useSiteUrl } from '~/stores-universal/context';
  10. import { withUnstatedContainers } from '../../UnstatedUtils';
  11. class OidcSecurityManagementContents extends React.Component {
  12. constructor(props) {
  13. super(props);
  14. this.onClickSubmit = this.onClickSubmit.bind(this);
  15. }
  16. async onClickSubmit() {
  17. const { t, adminOidcSecurityContainer, adminGeneralSecurityContainer } = this.props;
  18. try {
  19. await adminOidcSecurityContainer.updateOidcSetting();
  20. await adminGeneralSecurityContainer.retrieveSetupStratedies();
  21. toastSuccess(t('security_settings.OAuth.OIDC.updated_oidc'));
  22. }
  23. catch (err) {
  24. toastError(err);
  25. }
  26. }
  27. render() {
  28. const {
  29. t, adminGeneralSecurityContainer, adminOidcSecurityContainer, siteUrl,
  30. } = this.props;
  31. const { isOidcEnabled } = adminGeneralSecurityContainer.state;
  32. const oidcCallbackUrl = urljoin(pathUtils.removeTrailingSlash(siteUrl), '/passport/oidc/callback');
  33. return (
  34. <>
  35. <h2 className="alert-anchor border-bottom">
  36. {t('security_settings.OAuth.OIDC.name')}
  37. </h2>
  38. <div className="row my-4">
  39. <div className="offset-3 col-6">
  40. <div className="form-check form-switch form-check-success">
  41. <input
  42. id="isOidcEnabled"
  43. className="form-check-input"
  44. type="checkbox"
  45. checked={adminGeneralSecurityContainer.state.isOidcEnabled}
  46. onChange={() => { adminGeneralSecurityContainer.switchIsOidcEnabled() }}
  47. />
  48. <label className="form-label form-check-label" htmlFor="isOidcEnabled">
  49. {t('security_settings.OAuth.enable_oidc')}
  50. </label>
  51. </div>
  52. {(!adminGeneralSecurityContainer.state.setupStrategies.includes('oidc') && isOidcEnabled)
  53. && <div className="badge text-bg-warning">{t('security_settings.setup_is_not_yet_complete')}</div>}
  54. </div>
  55. </div>
  56. <div className="row mb-5">
  57. <label className="text-start text-md-end col-md-3 col-form-label">{t('security_settings.callback_URL')}</label>
  58. <div className="col-md-6">
  59. <input
  60. className="form-control"
  61. type="text"
  62. value={oidcCallbackUrl}
  63. readOnly
  64. />
  65. <p className="form-text text-muted small">{t('security_settings.desc_of_callback_URL', { AuthName: 'OAuth' })}</p>
  66. {(siteUrl == null || siteUrl === '') && (
  67. <div className="alert alert-danger">
  68. <span className="material-symbols-outlined">error</span>
  69. <span
  70. // eslint-disable-next-line max-len
  71. dangerouslySetInnerHTML={{ __html: t('alert.siteUrl_is_not_set', { link: `<a href="/admin/app">${t('headers.app_settings', { ns: 'commons' })}<span class="material-symbols-outlined">login</span></a>`, ns: 'commons' }) }}
  72. />
  73. </div>
  74. )}
  75. </div>
  76. </div>
  77. {isOidcEnabled && (
  78. <>
  79. <h3 className="border-bottom mb-4">{t('security_settings.configuration')}</h3>
  80. <div className="row mb-4">
  81. <label htmlFor="oidcProviderName" className="text-start text-md-end col-md-3 col-form-label">{t('security_settings.providerName')}</label>
  82. <div className="col-md-6">
  83. <input
  84. className="form-control"
  85. type="text"
  86. name="oidcProviderName"
  87. value={adminOidcSecurityContainer.state.oidcProviderName || ''}
  88. onChange={e => adminOidcSecurityContainer.changeOidcProviderName(e.target.value)}
  89. />
  90. </div>
  91. </div>
  92. <div className="row mb-4">
  93. <label htmlFor="oidcIssuerHost" className="text-start text-md-end col-md-3 col-form-label">{t('security_settings.issuerHost')}</label>
  94. <div className="col-md-6">
  95. <input
  96. className="form-control"
  97. type="text"
  98. name="oidcIssuerHost"
  99. value={adminOidcSecurityContainer.state.oidcIssuerHost || ''}
  100. onChange={e => adminOidcSecurityContainer.changeOidcIssuerHost(e.target.value)}
  101. />
  102. <p className="form-text text-muted">
  103. <small dangerouslySetInnerHTML={{ __html: t('security_settings.Use env var if empty', { env: 'OAUTH_OIDC_ISSUER_HOST' }) }} />
  104. </p>
  105. </div>
  106. </div>
  107. <div className="row mb-4">
  108. <label htmlFor="oidcClientId" className="text-start text-md-end col-md-3 col-form-label">{t('security_settings.clientID')}</label>
  109. <div className="col-md-6">
  110. <input
  111. className="form-control"
  112. type="text"
  113. name="oidcClientId"
  114. value={adminOidcSecurityContainer.state.oidcClientId || ''}
  115. onChange={e => adminOidcSecurityContainer.changeOidcClientId(e.target.value)}
  116. />
  117. <p className="form-text text-muted">
  118. <small dangerouslySetInnerHTML={{ __html: t('security_settings.Use env var if empty', { env: 'OAUTH_OIDC_CLIENT_ID' }) }} />
  119. </p>
  120. </div>
  121. </div>
  122. <div className="row mb-4">
  123. <label htmlFor="oidcClientSecret" className="text-start text-md-end col-md-3 col-form-label">{t('security_settings.client_secret')}</label>
  124. <div className="col-md-6">
  125. <input
  126. className="form-control"
  127. type="text"
  128. name="oidcClientSecret"
  129. value={adminOidcSecurityContainer.state.oidcClientSecret || ''}
  130. onChange={e => adminOidcSecurityContainer.changeOidcClientSecret(e.target.value)}
  131. />
  132. <p className="form-text text-muted">
  133. <small dangerouslySetInnerHTML={{ __html: t('security_settings.Use env var if empty', { env: 'OAUTH_OIDC_CLIENT_SECRET' }) }} />
  134. </p>
  135. </div>
  136. </div>
  137. <div className="row mb-4">
  138. <label htmlFor="oidcAuthorizationEndpoint" className="text-start text-md-end col-md-3 col-form-label">
  139. {t('security_settings.authorization_endpoint')}
  140. </label>
  141. <div className="col-md-6">
  142. <input
  143. className="form-control"
  144. type="text"
  145. name="oidcAuthorizationEndpoint"
  146. value={adminOidcSecurityContainer.state.oidcAuthorizationEndpoint || ''}
  147. onChange={e => adminOidcSecurityContainer.changeOidcAuthorizationEndpoint(e.target.value)}
  148. />
  149. <p className="form-text text-muted">
  150. <small dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.OIDC.Use discovered URL if empty') }} />
  151. </p>
  152. </div>
  153. </div>
  154. <div className="row mb-4">
  155. <label htmlFor="oidcTokenEndpoint" className="text-start text-md-end col-md-3 col-form-label">{t('security_settings.token_endpoint')}</label>
  156. <div className="col-md-6">
  157. <input
  158. className="form-control"
  159. type="text"
  160. name="oidcTokenEndpoint"
  161. value={adminOidcSecurityContainer.state.oidcTokenEndpoint || ''}
  162. onChange={e => adminOidcSecurityContainer.changeOidcTokenEndpoint(e.target.value)}
  163. />
  164. <p className="form-text text-muted">
  165. <small dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.OIDC.Use discovered URL if empty') }} />
  166. </p>
  167. </div>
  168. </div>
  169. <div className="row mb-4">
  170. <label htmlFor="oidcRevocationEndpoint" className="text-start text-md-end col-md-3 col-form-label">
  171. {t('security_settings.revocation_endpoint')}
  172. </label>
  173. <div className="col-md-6">
  174. <input
  175. className="form-control"
  176. type="text"
  177. name="oidcRevocationEndpoint"
  178. value={adminOidcSecurityContainer.state.oidcRevocationEndpoint || ''}
  179. onChange={e => adminOidcSecurityContainer.changeOidcRevocationEndpoint(e.target.value)}
  180. />
  181. <p className="form-text text-muted">
  182. <small dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.OIDC.Use discovered URL if empty') }} />
  183. </p>
  184. </div>
  185. </div>
  186. <div className="row mb-4">
  187. <label htmlFor="oidcIntrospectionEndpoint" className="text-start text-md-end col-md-3 col-form-label">
  188. {t('security_settings.introspection_endpoint')}
  189. </label>
  190. <div className="col-md-6">
  191. <input
  192. className="form-control"
  193. type="text"
  194. name="oidcIntrospectionEndpoint"
  195. value={adminOidcSecurityContainer.state.oidcIntrospectionEndpoint || ''}
  196. onChange={e => adminOidcSecurityContainer.changeOidcIntrospectionEndpoint(e.target.value)}
  197. />
  198. <p className="form-text text-muted">
  199. <small dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.OIDC.Use discovered URL if empty') }} />
  200. </p>
  201. </div>
  202. </div>
  203. <div className="row mb-4">
  204. <label htmlFor="oidcUserInfoEndpoint" className="text-start text-md-end col-md-3 col-form-label">
  205. {t('security_settings.userinfo_endpoint')}
  206. </label>
  207. <div className="col-md-6">
  208. <input
  209. className="form-control"
  210. type="text"
  211. name="oidcUserInfoEndpoint"
  212. value={adminOidcSecurityContainer.state.oidcUserInfoEndpoint || ''}
  213. onChange={e => adminOidcSecurityContainer.changeOidcUserInfoEndpoint(e.target.value)}
  214. />
  215. <p className="form-text text-muted">
  216. <small dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.OIDC.Use discovered URL if empty') }} />
  217. </p>
  218. </div>
  219. </div>
  220. <div className="row mb-4">
  221. <label htmlFor="oidcEndSessionEndpoint" className="text-start text-md-end col-md-3 col-form-label">
  222. {t('security_settings.end_session_endpoint')}
  223. </label>
  224. <div className="col-md-6">
  225. <input
  226. className="form-control"
  227. type="text"
  228. name="oidcEndSessionEndpoint"
  229. value={adminOidcSecurityContainer.state.oidcEndSessionEndpoint || ''}
  230. onChange={e => adminOidcSecurityContainer.changeOidcEndSessionEndpoint(e.target.value)}
  231. />
  232. <p className="form-text text-muted">
  233. <small dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.OIDC.Use discovered URL if empty') }} />
  234. </p>
  235. </div>
  236. </div>
  237. <div className="row mb-4">
  238. <label htmlFor="oidcRegistrationEndpoint" className="text-start text-md-end col-md-3 col-form-label">
  239. {t('security_settings.registration_endpoint')}
  240. </label>
  241. <div className="col-md-6">
  242. <input
  243. className="form-control"
  244. type="text"
  245. name="oidcRegistrationEndpoint"
  246. value={adminOidcSecurityContainer.state.oidcRegistrationEndpoint || ''}
  247. onChange={e => adminOidcSecurityContainer.changeOidcRegistrationEndpoint(e.target.value)}
  248. />
  249. <p className="form-text text-muted">
  250. <small dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.OIDC.Use discovered URL if empty') }} />
  251. </p>
  252. </div>
  253. </div>
  254. <div className="row mb-4">
  255. <label htmlFor="oidcJWKSUri" className="text-start text-md-end col-md-3 col-form-label">{t('security_settings.jwks_uri')}</label>
  256. <div className="col-md-6">
  257. <input
  258. className="form-control"
  259. type="text"
  260. name="oidcJWKSUri"
  261. value={adminOidcSecurityContainer.state.oidcJWKSUri || ''}
  262. onChange={e => adminOidcSecurityContainer.changeOidcJWKSUri(e.target.value)}
  263. />
  264. <p className="form-text text-muted">
  265. <small dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.OIDC.Use discovered URL if empty') }} />
  266. </p>
  267. </div>
  268. </div>
  269. <h3 className="alert-anchor border-bottom mb-4">
  270. Attribute Mapping ({t('optional')})
  271. </h3>
  272. <div className="row mb-4">
  273. <label htmlFor="oidcAttrMapId" className="text-start text-md-end col-md-3 col-form-label">Identifier</label>
  274. <div className="col-md-6">
  275. <input
  276. className="form-control"
  277. type="text"
  278. name="oidcAttrMapId"
  279. value={adminOidcSecurityContainer.state.oidcAttrMapId || ''}
  280. onChange={e => adminOidcSecurityContainer.changeOidcAttrMapId(e.target.value)}
  281. />
  282. <p className="form-text text-muted">
  283. <small dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.OIDC.id_detail') }} />
  284. </p>
  285. </div>
  286. </div>
  287. <div className="row mb-4">
  288. <label htmlFor="oidcAttrMapUserName" className="text-start text-md-end col-md-3 col-form-label">{t('username')}</label>
  289. <div className="col-md-6">
  290. <input
  291. className="form-control"
  292. type="text"
  293. name="oidcAttrMapUserName"
  294. value={adminOidcSecurityContainer.state.oidcAttrMapUserName || ''}
  295. onChange={e => adminOidcSecurityContainer.changeOidcAttrMapUserName(e.target.value)}
  296. />
  297. <p className="form-text text-muted">
  298. <small dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.OIDC.username_detail') }} />
  299. </p>
  300. </div>
  301. </div>
  302. <div className="row mb-4">
  303. <label htmlFor="oidcAttrMapName" className="text-start text-md-end col-md-3 col-form-label">{t('Name')}</label>
  304. <div className="col-md-6">
  305. <input
  306. className="form-control"
  307. type="text"
  308. name="oidcAttrMapName"
  309. value={adminOidcSecurityContainer.state.oidcAttrMapName || ''}
  310. onChange={e => adminOidcSecurityContainer.changeOidcAttrMapName(e.target.value)}
  311. />
  312. <p className="form-text text-muted">
  313. <small dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.OIDC.name_detail') }} />
  314. </p>
  315. </div>
  316. </div>
  317. <div className="row mb-4">
  318. <label htmlFor="oidcAttrMapEmail" className="text-start text-md-end col-md-3 col-form-label">{t('Email')}</label>
  319. <div className="col-md-6">
  320. <input
  321. className="form-control"
  322. type="text"
  323. name="oidcAttrMapEmail"
  324. value={adminOidcSecurityContainer.state.oidcAttrMapEmail || ''}
  325. onChange={e => adminOidcSecurityContainer.changeOidcAttrMapEmail(e.target.value)}
  326. />
  327. <p className="form-text text-muted">
  328. <small dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.OIDC.mapping_detail', { target: t('Email') }) }} />
  329. </p>
  330. </div>
  331. </div>
  332. <div className="row mb-4">
  333. <label className="form-label text-start text-md-end col-md-3 col-form-label">{t('security_settings.callback_URL')}</label>
  334. <div className="col-md-6">
  335. <input
  336. className="form-control"
  337. type="text"
  338. defaultValue={oidcCallbackUrl}
  339. readOnly
  340. />
  341. <p className="form-text text-muted small">{t('security_settings.desc_of_callback_URL', { AuthName: 'OAuth' })}</p>
  342. {(siteUrl == null || siteUrl === '') && (
  343. <div className="alert alert-danger">
  344. <span className="material-symbols-outlined">error</span>
  345. <span
  346. // eslint-disable-next-line max-len
  347. dangerouslySetInnerHTML={{ __html: t('alert.siteUrl_is_not_set', { link: `<a href="/admin/app">${t('headers.app_settings', { ns: 'commons' })}<span class="material-symbols-outlined">login</span></a>`, ns: 'commons' }) }}
  348. />
  349. </div>
  350. )}
  351. </div>
  352. </div>
  353. <div className="row mb-4">
  354. <div className="offset-md-3 col-md-6">
  355. <div className="form-check form-check-success">
  356. <input
  357. id="bindByUserName-oidc"
  358. className="form-check-input"
  359. type="checkbox"
  360. checked={adminOidcSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser}
  361. onChange={() => { adminOidcSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser() }}
  362. />
  363. <label
  364. className="form-label form-check-label"
  365. htmlFor="bindByUserName-oidc"
  366. dangerouslySetInnerHTML={{ __html: t('security_settings.Treat username matching as identical') }}
  367. />
  368. </div>
  369. <p className="form-text text-muted">
  370. <small dangerouslySetInnerHTML={{ __html: t('security_settings.Treat username matching as identical_warn') }} />
  371. </p>
  372. </div>
  373. </div>
  374. <div className="row mb-4">
  375. <div className="offset-md-3 col-md-6">
  376. <div className="form-check form-check-success">
  377. <input
  378. id="bindByEmail-oidc"
  379. className="form-check-input"
  380. type="checkbox"
  381. checked={adminOidcSecurityContainer.state.isSameEmailTreatedAsIdenticalUser || false}
  382. onChange={() => { adminOidcSecurityContainer.switchIsSameEmailTreatedAsIdenticalUser() }}
  383. />
  384. <label
  385. className="form-label form-check-label"
  386. htmlFor="bindByEmail-oidc"
  387. dangerouslySetInnerHTML={{ __html: t('security_settings.Treat email matching as identical') }}
  388. />
  389. </div>
  390. <p className="form-text text-muted">
  391. <small dangerouslySetInnerHTML={{ __html: t('security_settings.Treat email matching as identical_warn') }} />
  392. </p>
  393. </div>
  394. </div>
  395. <div className="row my-3">
  396. <div className="offset-3 col-5">
  397. <button
  398. type="button"
  399. className="btn btn-primary"
  400. disabled={adminOidcSecurityContainer.state.retrieveError != null}
  401. onClick={this.onClickSubmit}
  402. >
  403. {t('Update')}
  404. </button>
  405. </div>
  406. </div>
  407. </>
  408. )}
  409. <hr />
  410. <div style={{ minHeight: '300px' }}>
  411. <h4>
  412. <span className="material-symbols-outlined" aria-hidden="true">help</span>
  413. <a href="#collapseHelpForOidcOauth" data-bs-toggle="collapse"> {t('security_settings.OAuth.how_to.oidc')}</a>
  414. </h4>
  415. <div className=" card custom-card bg-body-tertiary">
  416. <ol id="collapseHelpForOidcOauth" className="collapse mb-0">
  417. <li>{t('security_settings.OAuth.OIDC.register_1')}</li>
  418. <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.OIDC.register_2', { url: oidcCallbackUrl }) }} />
  419. <li>{t('security_settings.OAuth.OIDC.register_3')}</li>
  420. </ol>
  421. </div>
  422. </div>
  423. </>
  424. );
  425. }
  426. }
  427. OidcSecurityManagementContents.propTypes = {
  428. t: PropTypes.func.isRequired, // i18next
  429. adminGeneralSecurityContainer: PropTypes.instanceOf(AdminGeneralSecurityContainer).isRequired,
  430. adminOidcSecurityContainer: PropTypes.instanceOf(AdminOidcSecurityContainer).isRequired,
  431. siteUrl: PropTypes.string,
  432. };
  433. const OidcSecurityManagementContentsWrapperFC = (props) => {
  434. const { t } = useTranslation('admin');
  435. const { data: siteUrl } = useSiteUrl();
  436. return <OidcSecurityManagementContents t={t} {...props} siteUrl={siteUrl} />;
  437. };
  438. const OidcSecurityManagementContentsWrapper = withUnstatedContainers(OidcSecurityManagementContentsWrapperFC, [
  439. AdminGeneralSecurityContainer,
  440. AdminOidcSecurityContainer,
  441. ]);
  442. export default OidcSecurityManagementContentsWrapper;