OidcSecuritySettingContents.tsx 20 KB

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