OidcSecuritySettingContents.tsx 20 KB

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