From 97168f890994989add26f5754142f8db15bdc7f1 Mon Sep 17 00:00:00 2001
From: "Stephan H. Wissel" <stw@linux.com>
Date: Wed, 23 Oct 2024 21:30:51 +0800
Subject: [PATCH] Add Idp support to Fauxton, solves #1457

---
 app/addons/auth/actions.js                 |   25 +
 app/addons/auth/components/index.js        |    2 +
 app/addons/auth/components/loginform.js    |   14 +
 app/addons/auth/components/loginformidp.js |  155 ++
 app/addons/auth/idp.js                     |  273 +++
 app/addons/auth/routes/auth.js             |   23 +
 app/addons/documents/doc-editor/actions.js |    2 +
 app/core/ajax.js                           |    5 +-
 app/core/api.js                            |   10 +
 devserver.js                               |    7 +-
 docker/admin_role.json                     |    5 +
 docker/couchdb-idp.sh                      |  214 ++
 docker/couchdb-idp.yml                     |   20 +
 docker/extractpem.js                       |   16 +
 docker/fauxton_client.json                 |   32 +
 docker/johndoe_role.json                   |    3 +
 docker/johndoe_user.json                   |   20 +
 docker/sofa_realm.json                     |   13 +
 i18n.json.default.json                     |    1 +
 idp.md                                     |   73 +
 index.js                                   |    7 +-
 package-lock.json                          |  169 ++
 package.json                               |    1 +
 readme.md                                  |   12 +-
 test/idp_rsources/realm-export.json        | 2255 ++++++++++++++++++++
 25 files changed, 3345 insertions(+), 12 deletions(-)
 create mode 100644 app/addons/auth/components/loginformidp.js
 create mode 100644 app/addons/auth/idp.js
 create mode 100644 docker/admin_role.json
 create mode 100755 docker/couchdb-idp.sh
 create mode 100644 docker/couchdb-idp.yml
 create mode 100644 docker/extractpem.js
 create mode 100644 docker/fauxton_client.json
 create mode 100644 docker/johndoe_role.json
 create mode 100644 docker/johndoe_user.json
 create mode 100644 docker/sofa_realm.json
 create mode 100644 idp.md
 create mode 100644 test/idp_rsources/realm-export.json

diff --git a/app/addons/auth/actions.js b/app/addons/auth/actions.js
index 1e094c9cf..01a87544f 100644
--- a/app/addons/auth/actions.js
+++ b/app/addons/auth/actions.js
@@ -41,6 +41,10 @@ export const validatePasswords = (password, passwordConfirm) => {
   );
 };
 
+export const validateIdP = (idpurl, idpcallback, idpappid) => {
+  return validate(!_.isEmpty(idpurl), !_.isEmpty(idpcallback), !_.isEmpty(idpappid));
+};
+
 export const login = (username, password, urlBack) => {
   if (!validateUser(username, password)) {
     return errorHandler({message: app.i18n.en_US['auth-missing-credentials']});
@@ -66,6 +70,27 @@ export const login = (username, password, urlBack) => {
     .catch(errorHandler);
 };
 
+export const loginidp = (idpurl, idpcallback, idpappid) => {
+  if (!validateIdP(idpurl, idpcallback, idpappid)) {
+    return errorHandler({ message: app.i18n.en_US['auth-missing-idp'] });
+  }
+  return Idp.login(idpurl, idpcallback, idpappid)
+    .then((resp) => {
+      if (resp.error) {
+        errorHandler({ message: resp.reason });
+        return resp;
+      }
+
+      let msg = app.i18n.en_US['auth-logged-in'];
+      if (msg) {
+        FauxtonAPI.addNotification({ msg });
+      }
+
+      FauxtonAPI.navigate('/');
+    })
+    .catch(errorHandler);
+};
+
 export const changePassword = (username, password, passwordConfirm, nodes) => () => {
   if (!validatePasswords(password, passwordConfirm)) {
     return errorHandler({message: app.i18n.en_US['auth-passwords-not-matching']});
diff --git a/app/addons/auth/components/index.js b/app/addons/auth/components/index.js
index 2c38e5aa2..95f08b5fa 100644
--- a/app/addons/auth/components/index.js
+++ b/app/addons/auth/components/index.js
@@ -11,12 +11,14 @@
 // the License.
 
 import LoginForm from './loginform.js';
+import LoginFormIdp from './loginformidp.js';
 import PasswordModal from './passwordmodal.js';
 import CreateAdminForm from './createadminform.js';
 import ChangePasswordForm from './changepasswordform.js';
 
 export default {
   LoginForm,
+  LoginFormIdp,
   PasswordModal,
   CreateAdminForm,
   ChangePasswordForm
diff --git a/app/addons/auth/components/loginform.js b/app/addons/auth/components/loginform.js
index 34a56ebcd..837a15868 100644
--- a/app/addons/auth/components/loginform.js
+++ b/app/addons/auth/components/loginform.js
@@ -13,6 +13,7 @@
 import PropTypes from 'prop-types';
 
 import React from "react";
+import FauxtonAPI from '../../../core/base';
 import { login } from "./../actions";
 import { Button, Form } from 'react-bootstrap';
 
@@ -57,6 +58,12 @@ class LoginForm extends React.Component {
   login(username, password) {
     login(username, password, this.props.urlBack);
   }
+
+  navigateToIdp(e) {
+    e.preventDefault();
+    FauxtonAPI.navigate('/loginidp');
+  }
+
   componentDidMount() {
     this.usernameField.focus();
   }
@@ -97,6 +104,13 @@ class LoginForm extends React.Component {
             </div>
           </div>
         </form>
+        <div className="row">
+          <div className="col12 col-md-5 col-xl-4 mb-3">
+            <Button id="login-idp-btn" variant="cf-secondary" onClick={this.navigateToIdp}>
+              Log In with your Identity Provider
+            </Button>
+          </div>
+        </div>
       </div>
     );
   }
diff --git a/app/addons/auth/components/loginformidp.js b/app/addons/auth/components/loginformidp.js
new file mode 100644
index 000000000..721a09c04
--- /dev/null
+++ b/app/addons/auth/components/loginformidp.js
@@ -0,0 +1,155 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import FauxtonAPI from '../../../core/base';
+import React from 'react';
+import { loginidp } from './../actions';
+import { Button, Form } from 'react-bootstrap';
+
+class LoginFormIdp extends React.Component {
+  constructor() {
+    super();
+    this.state = {
+      idpurl: localStorage.getItem('FauxtonIdpurl') || '',
+      idpcallback: localStorage.getItem('FauxtonIdpcallback') || '',
+      idpappid: localStorage.getItem('FauxtonIdpappid') || ''
+    };
+    if (this.state.idpcallback === '') {
+      let url = new URL(window.location);
+      let append = url.pathname.startsWith('/_utils') ? '/_utils/' : '/';
+      this.state.idpcallback = window.location.origin + append;
+    }
+  }
+
+  onIdpurlChange(e) {
+    this.setState({ idpurl: e.target.value });
+  }
+  onIdpcallbackChange(e) {
+    this.setState({ idpcallback: e.target.value });
+  }
+
+  onIdpappidChange(e) {
+    this.setState({ idpappid: e.target.value });
+  }
+
+  submit(e) {
+    e.preventDefault();
+    if (!this.checkUnrecognizedAutoFill()) {
+      this.login(this.state.idpurl, this.state.idpcallback, this.state.idpappid);
+    }
+  }
+
+  // Safari has a bug where autofill doesn't trigger a change event. This checks for the condition where the state
+  // and form fields have a mismatch. See: https://issues.apache.org/jira/browse/COUCHDB-2829
+  checkUnrecognizedAutoFill() {
+    if (this.state.idpurl !== '' || this.state.idpcallback !== '' || this.state.idpappid !== '') {
+      return false;
+    }
+    let idpurl = this.props.testBlankIdpurl ? this.props.testBlankIdpurl : this.idpurlField.value;
+    let idpcallback = this.props.testBlankIdpcallback ? this.props.testBlankIdpcallback : this.idpcallbackField.value;
+    let idpappid = this.props.testBlankIdpappid ? this.props.testBlankIdpappid : this.idpappidField.value;
+
+    this.setState({ idpurl: idpurl, idpcallback: idpcallback, idpappid: idpappid }); // doesn't set immediately, hence separate login() call
+    this.login(idpurl, idpcallback, idpappid);
+
+    return true;
+  }
+
+  login(idpurl, idpcallback, idpappid) {
+    localStorage.setItem('FauxtonIdpurl', idpurl);
+    localStorage.setItem('FauxtonIdpcallback', idpcallback);
+    localStorage.setItem('FauxtonIdpappid', idpappid);
+    loginidp(idpurl, idpcallback, idpappid);
+  }
+
+  navigateToLogin(e) {
+    e.preventDefault();
+    FauxtonAPI.navigate('/login');
+  }
+
+  render() {
+    return (
+      <div className="couch-login-wrapper">
+        <form id="login" onSubmit={this.submit.bind(this)}>
+          <div className="row">
+          <label htmlFor="idpurl">Identity Provider (IdP) URL</label>
+            <p className="help-block">
+              must point to your IdP&apos;s <code>/.well-known/openid-configuration</code>
+            </p>
+            <div className="col12 col-md-10 col-xl-8 mb-3">
+              <Form.Control
+                type="text"
+                id="idpurl"
+                name="idpurl"
+                ref={(node) => (this.idpurlField = node)}
+                placeholder="IdP URL"
+                onChange={this.onIdpurlChange.bind(this)}
+                value={this.state.idpurl}
+              />
+            </div>
+          </div>
+          <div className="row">
+          <label htmlFor="idpcallback">Callback URL</label>
+            <p className="help-block">
+              This should be the URL of your CouchDB instance, including the protocol and port number.{' '}
+              <span style={{ color: 'red' }}>Should we show this? It can be computed</span>
+            </p>
+            <div className="col12 col-md-5 col-xl-4 mb-3">
+              <Form.Control
+                type="text"
+                id="idpcallback"
+                name="idpcallback"
+                ref={(node) => (this.idpcallbackField = node)}
+                placeholder="Callback URL"
+                onChange={this.onIdpcallbackChange.bind(this)}
+                value={this.state.idpcallback}
+              />
+            </div>
+          </div>
+          <div className="row">
+          <label htmlFor="idpappid">Application ID</label>
+            <p className="help-block">
+              The Application ID gets assigned by the IdP admin, suggested standard is <code>fauxton</code>
+            </p>
+            <div className="col12 col-md-5 col-xl-4 mb-3">
+              <Form.Control
+                type="text"
+                id="idpappid"
+                name="idpappid"
+                ref={(node) => (this.idpappidField = node)}
+                placeholder="Applicaiton ID"
+                onChange={this.onIdpappidChange.bind(this)}
+                value={this.state.idpappid}
+              />
+            </div>
+          </div>
+          <div className="row">
+            <div className="col12 col-md-5 col-xl-4 mb-3">
+              <Button id="login-btn" variant="cf-primary" type="submit">
+                Log In
+              </Button>
+            </div>
+          </div>
+        </form>
+        <div className="row">
+          <div className="col12 col-md-5 col-xl-4 mb-3">
+            <Button id="login-creds-btn" variant="cf-secondary" onClick={this.navigateToLogin}>
+              Back to - Log In with CouchDB Credentials
+            </Button>
+          </div>
+        </div>
+      </div>
+    );
+  }
+}
+
+export default LoginFormIdp;
\ No newline at end of file
diff --git a/app/addons/auth/idp.js b/app/addons/auth/idp.js
new file mode 100644
index 000000000..c10344eb4
--- /dev/null
+++ b/app/addons/auth/idp.js
@@ -0,0 +1,273 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import FauxtonAPI from '../../core/api';
+
+/**
+ * Keeping track of IdP URLs derived from openid-configuration
+ * with access to auth and token endpoints
+ */
+const idpCache = {};
+
+/**
+ * Reads the openid-configuration from the IdP URL
+ *
+ * @param {string} idpurl
+ * @returns object with authorization and token endpoints
+ */
+const getIdPEndpoints = (idpurl) =>
+  new Promise((resolve, reject) => {
+    if (idpCache[idpurl]) {
+      return resolve(idpCache[idpurl]);
+    }
+    fetch(idpurl)
+      .then((response) => response.json())
+      .then((data) => {
+        let idpData = {
+          authorization_endpoint: data.authorization_endpoint,
+          token_endpoint: data.token_endpoint
+        };
+        idpCache[idpurl] = idpData;
+        resolve(idpData);
+      })
+      .catch((err) => {
+        reject(err);
+      });
+  });
+
+/**
+ * Retrieves the auth end-point from the openid-configuration
+ *
+ * @param {string} idpurl openid-configuration end-point
+ * @returns auth end-point
+ */
+const getAuthEndpoint = (idpurl) =>
+  new Promise((resolve, reject) => {
+    getIdPEndpoints(idpurl)
+      .then((idpData) => {
+        resolve(idpData.authorization_endpoint);
+      })
+      .catch((err) => {
+        reject(err);
+      });
+  });
+
+/**
+ * Retrieves the token end-point from the openid-configuration
+ *
+ * @param {string} idpurl openid-configuration end-point
+ * @returns token end-point
+ */
+const getTokenEndpoint = (idpurl) =>
+  new Promise((resolve, reject) => {
+    getIdPEndpoints(idpurl)
+      .then((idpData) => {
+        resolve(idpData.token_endpoint);
+      })
+      .catch((err) => {
+        reject(err);
+      });
+  });
+
+
+
+/**
+ * jwtStillValid - Check if a JWT token is still valid
+ *
+ * @param {string} token The JWT token
+ * @return {boolean} True if the token is still valid, false otherwise
+ */
+export const jwtStillValid = (token) => {
+  if (!token) {
+    return false;
+  }
+
+  const decodedToken = decodeToken(token);
+  if (!decodedToken) {
+    return false;
+  }
+
+  const currentTime = Math.floor(Date.now() / 1000);
+  let isStillgood = decodedToken.exp > currentTime;
+  return isStillgood;
+};
+
+/**
+ * decodeToken - Decode a JWT token and return the payload
+ *
+ * @param {string} token The JWT token
+ * @return {object} The decoded token payload
+ */
+export const decodeToken = (token) => {
+  try {
+    const base64Url = token.split('.')[1];
+    const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
+    const jsonPayload = atob(base64);
+    return JSON.parse(jsonPayload);
+  } catch (error) {
+    return null;
+  }
+};
+
+export const getExpiry = (token) => {
+  const decodedToken = decodeToken(token);
+  return decodedToken ? decodedToken.exp : 0;
+};
+
+export const login = (idpurl, idpcallback, idpappid) => {
+  return getAuthEndpoint(idpurl)
+    .then((authEndpoint) => {
+      const authUrl = `${authEndpoint}?response_type=code&client_id=${idpappid}&redirect_uri=${idpcallback}&scope=openid#idpresult`;
+      window.location.href = authUrl;
+      return Promise.resolve('Authentication initiated');
+    })
+    .catch((error) => {
+      console.error('Error fetching auth endpoint:', error);
+      FauxtonAPI.addNotification({
+        msg: error.message,
+        type: 'error'
+      });
+    });
+};
+
+export const logout = () => {
+  localStorage.removeItem('fauxtonToken');
+  localStorage.removeItem('fauxtonRefreshToken');
+  window.location.href = '/_session';
+};
+
+export const codeToToken = (url) => {
+  const authCode = url.searchParams.get('code');
+  if (authCode) {
+    const idpurl = localStorage.getItem('FauxtonIdpurl');
+    const idpappid = localStorage.getItem('FauxtonIdpappid');
+    const callback = localStorage.getItem('FauxtonIdpcallback');
+
+    getTokenEndpoint(idpurl)
+      .then((authUrl) => {
+        return fetch(authUrl, {
+          method: 'POST',
+          headers: {
+            'Content-Type': 'application/x-www-form-urlencoded'
+          },
+          body: `grant_type=authorization_code&code=${authCode}&client_id=${idpappid}&redirect_uri=${callback}`
+        });
+      })
+      .then((response) => response.json())
+      .then((data) => {
+        const accessToken = data.access_token;
+        const jwtRefreshToken = data.refresh_token;
+        localStorage.setItem('fauxtonToken', accessToken);
+        localStorage.setItem('fauxtonRefreshToken', jwtRefreshToken);
+        const expiry = getExpiry(accessToken);
+        setTimeout(() => {
+          // eslint-disable-next-line no-console
+          console.log('Refreshing token');
+          refreshToken();
+        }, (expiry - 60) * 1000);
+        return FauxtonAPI.navigate('/');
+      })
+      .catch((error) => {
+        console.error('Error refreshing token:', error);
+        FauxtonAPI.addNotification({
+          msg: `Error refreshing token: ${error.message}`,
+          type: 'error'
+        });
+      });
+  } else {
+    FauxtonAPI.addNotification({
+      msg: 'No auth code found',
+      type: 'error'
+    });
+  }
+};
+
+export const refreshToken = () => {
+  const jwtRefreshToken = localStorage.getItem('fauxtonRefreshToken');
+  const idpurl = localStorage.getItem('FauxtonIdpurl');
+  const idpappid = localStorage.getItem('FauxtonIdpappid');
+  if (!jwtRefreshToken) {
+    FauxtonAPI.addNotification({
+      msg: `Refresh Token missing`,
+      type: 'error'
+    });
+    return;
+  }
+  getTokenEndpoint(idpurl).then((authUrl) =>
+    fetch(authUrl, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/x-www-form-urlencoded'
+      },
+      body: `grant_type=refresh_token&refresh_token=${jwtRefreshToken}&client_id=${idpappid}`
+    })
+      .then((response) => response.json())
+      .then((data) => {
+        const accessToken = data.access_token;
+        localStorage.setItem('fauxtonToken', accessToken);
+        const expiry = getExpiry(accessToken);
+        setTimeout(() => {
+          refreshToken();
+        }, (expiry - 60) * 1000);
+      })
+      .catch((error) => {
+        console.error('Error refreshing token:', error);
+        FauxtonAPI.addNotification({
+          msg: `Refreshing Token failed: ${error.message}`,
+          type: 'error'
+        });
+        localStorage.removeItem('fauxtonToken');
+        localStorage.removeItem('fauxtonRefreshToken');
+      })
+  );
+};
+
+/**
+ * addAuthToken - Add the JWT token to the fetch options headers if it exists in local storage
+ *
+ * @param {object} fetchOptions - The fetch options object
+ * @returns {object} the updated fetch options object
+ */
+export const addAuthToken = (fetchOptions) => {
+  // eslint-disable-next-line no-console
+  console.debug('addAuthToken', fetchOptions);
+  const token = localStorage.getItem('fauxtonToken');
+  if (token && jwtStillValid(token)) {
+    fetchOptions.headers = {
+      ...fetchOptions.headers,
+      Authorization: `Bearer ${token}`
+    };
+  } else {
+    localStorage.removeItem('fauxtonToken');
+  }
+  return fetchOptions;
+};
+
+export const addAuthHeader = (httpRequest) => {
+  const token = localStorage.getItem('fauxtonToken');
+  if (token && jwtStillValid(token)) {
+    httpRequest.setRequestHeader('Authorization', `Bearer ${token}`);
+  }
+  return httpRequest;
+};
+
+export default {
+  login,
+  logout,
+  refreshToken,
+  codeToToken,
+  jwtStillValid,
+  decodeToken,
+  getExpiry,
+  addAuthToken,
+  addAuthHeader
+};
diff --git a/app/addons/auth/routes/auth.js b/app/addons/auth/routes/auth.js
index d274c089e..c99830664 100644
--- a/app/addons/auth/routes/auth.js
+++ b/app/addons/auth/routes/auth.js
@@ -17,9 +17,11 @@ import { AuthLayout } from "./../layout";
 import app from "../../../app";
 import Components from "./../components";
 import {logout} from '../actions';
+import Idp from '../idp';
 
 const {
   LoginForm,
+  LoginFormIdp,
   CreateAdminForm
 } = Components;
 
@@ -29,7 +31,9 @@ export default FauxtonAPI.RouteObject.extend({
   routes: {
     "login?*extra": "login",
     "login": "login",
+    "loginidp": "loginidp",
     "logout": "logout",
+    "session_state*": "idpCallback",
     "createAdmin": "checkNodes",
     "createAdmin/:node": "createAdminForNode"
   },
@@ -44,9 +48,28 @@ export default FauxtonAPI.RouteObject.extend({
       />
     );
   },
+  loginidp() {
+    return (
+      <AuthLayout
+        crumbs={crumbs}
+        component={<LoginFormIdp urlBack={app.getParams().urlback} />}
+      />
+    );
+  },
   logout() {
     logout();
   },
+  idpCallback() {
+    const urlParams = new URLSearchParams(window.location.hash);
+    const accessToken = urlParams.get('access_token');
+    const refreshToken = urlParams.get('refresh_token');
+    localStorage.setItem('fauxtonToken', accessToken);
+    localStorage.setItem('fauxtonRefreshToken', refreshToken);
+    // Extract expiry from the access token
+    const expiry = Idp.getExpiry(accessToken);
+    console.log('Expiry:', expiry);
+    //setTimeout(Idp.refreshToken, (expiry - 60) * 1000);
+  },
   createAdminForNode() {
     ClusterActions.fetchNodes();
     const crumbs = [{ name: "Create Admin" }];
diff --git a/app/addons/documents/doc-editor/actions.js b/app/addons/documents/doc-editor/actions.js
index de1373a32..370afdee7 100644
--- a/app/addons/documents/doc-editor/actions.js
+++ b/app/addons/documents/doc-editor/actions.js
@@ -13,6 +13,7 @@
 import FauxtonAPI from '../../../core/api';
 import { deleteRequest } from '../../../core/ajax';
 import ActionTypes from './actiontypes';
+import { addAuthHeader } from '../../auth/idp';
 
 var currentUploadHttpRequest;
 
@@ -204,6 +205,7 @@ const uploadAttachment = (params) => (dispatch) => {
     });
   };
   const httpRequest = new XMLHttpRequest();
+  addAuthHeader(httpRequest); // for JWT
   currentUploadHttpRequest = httpRequest;
   httpRequest.withCredentials = true;
   if (httpRequest.upload) {
diff --git a/app/core/ajax.js b/app/core/ajax.js
index 95e622462..d174453f2 100644
--- a/app/core/ajax.js
+++ b/app/core/ajax.js
@@ -1,6 +1,7 @@
 import 'whatwg-fetch';
 import {defaultsDeep} from "lodash";
 import {Subject} from 'rxjs';
+import { addAuthToken } from '../addons/auth/idp';
 
 /* Add a multicast observer so that all fetch requests can be observed
   Some usage examples:
@@ -68,7 +69,8 @@ export const json = (url, method = "GET", opts = {}) => {
       cache: "no-cache"
     }
   );
-  return _preFetchFn(url, fetchOptions).then((result) => {
+  const updatedFetchOptions = addAuthToken(fetchOptions);
+  return _preFetchFn(url, updatedFetchOptions).then((result) => {
     return fetch(
       result.url,
       result.options,
@@ -82,7 +84,6 @@ export const json = (url, method = "GET", opts = {}) => {
   });
 };
 
-
 /**
  * get - Get request
  *
diff --git a/app/core/api.js b/app/core/api.js
index 7927397d9..378454c79 100644
--- a/app/core/api.js
+++ b/app/core/api.js
@@ -21,6 +21,16 @@ import $ from "jquery";
 import Backbone from "backbone";
 import _ from "lodash";
 import Promise from "bluebird";
+import { addAuthHeader } from '../addons/auth/idp';
+
+// Monkey patching Backbone.ajax to add the Auth header
+// for JWT authentication
+$.ajaxSetup({
+  beforeSend: function (xhr) {
+    xhr.setRequestHeader('X-Clacks-Overhead', 'GNU Terry Pratchett');
+    addAuthHeader(xhr);
+  }
+});
 
 Backbone.$ = $;
 Backbone.ajax = function () {
diff --git a/devserver.js b/devserver.js
index 32fba7055..56de535eb 100644
--- a/devserver.js
+++ b/devserver.js
@@ -51,8 +51,11 @@ const devSetup = function (cb) {
   });
 };
 
-const defaultHeaderValue = "default-src 'self'; child-src 'self' blob: https://blog.couchdb.org; img-src 'self' data:; font-src 'self'; " +
-                  "script-src 'self'; style-src 'self'; object-src 'none';";
+// const defaultHeaderValue = "default-src 'self'; child-src 'self' blob: https://blog.couchdb.org; img-src 'self' data:; font-src 'self'; " +
+//                  "script-src 'self'; style-src 'self'; object-src 'none';";
+const defaultHeaderValue =
+  "default-src 'self'; child-src 'self' blob: https://blog.couchdb.org; img-src 'self' data:; font-src 'self'; connect-src 'self' http://localhost:8090; " +
+  "script-src 'self'; style-src 'self'; object-src 'none';";
 function getCspHeaders () {
   if (!settings.contentSecurityPolicy) {
     return;
diff --git a/docker/admin_role.json b/docker/admin_role.json
new file mode 100644
index 000000000..c754f4a60
--- /dev/null
+++ b/docker/admin_role.json
@@ -0,0 +1,5 @@
+{
+    "name": "_admin",
+    "description": "CouchDB Administrator",
+    "attributes": {}
+}
\ No newline at end of file
diff --git a/docker/couchdb-idp.sh b/docker/couchdb-idp.sh
new file mode 100755
index 000000000..7e8c2b382
--- /dev/null
+++ b/docker/couchdb-idp.sh
@@ -0,0 +1,214 @@
+#!/bin/bash
+#launches the CouchDB and keycloak containers
+# then configure both to interact with each other
+
+# Part1: Setup
+KC_URL="http://localhost:8090"
+COUCHDB_URL="http://localhost:5984"
+COUCHDB_USER=tester
+COUCHDB_PASSWORD=testerpass
+
+# Part2: reusable functions
+function kc_tokens() {
+    echo "Loading tokens"
+    local url=${KC_URL}/realms/master/protocol/openid-connect/token
+
+    # Post the form data and store the response
+    local response=$(curl $url \
+        --header 'Content-Type: application/x-www-form-urlencoded' \
+        --no-progress-meter \
+        --data-urlencode 'client_id=admin-cli' \
+        --data-urlencode 'username=admin' \
+        --data-urlencode 'password=password' \
+        --data-urlencode 'grant_type=password')
+
+    # Extract the access_token and refresh_token from the response
+    local access_token=$(echo "$response" | jq -r '.access_token')
+    local refresh_token=$(echo "$response" | jq -r '.refresh_token')
+
+    # Set the environment variables
+    export KC_ACCESS_TOKEN="$access_token"
+    export KC_REFRESH_TOKEN="$refresh_token"
+    echo "Tokens loaded"
+}
+
+function kc_refresh() {
+    local url=${KC_URL}/realms/master/protocol/openid-connect/token
+    echo "Refreshing tokens from $url"
+
+    # Post the form data and store the response
+    local response=$(curl $url \
+        --header 'Content-Type: application/x-www-form-urlencoded' \
+        --no-progress-meter \
+        --data-urlencode 'client_id=admin-cli' \
+        --data-urlencode 'refresh_token={{$KC_REFRESH_TOKEN}}' \
+        --data-urlencode 'grant_type=refresh_token')
+
+    # Extract the access_token and refresh_token from the response
+    local access_token=$(echo "$response" | jq -r '.access_token')
+    local refresh_token=$(echo "$response" | jq -r '.refresh_token')
+
+    # Set the environment variables
+    export KC_ACCESS_TOKEN="$access_token"
+    export KC_REFRESH_TOKEN="$refresh_token"
+
+    echo "Tokens refreshed"
+}
+
+function kc_get() {
+    local url=${KC_URL}$1
+
+    # Get an URL and store the response
+    echo GET from $url >&2
+    local response=$(curl $url \
+        --no-progress-meter \
+        --header 'Content-Type: application/json' \
+        --header "Authorization: Bearer $KC_ACCESS_TOKEN")
+    echo $response
+}
+
+function kc_post() {
+    local url=${KC_URL}$1
+    local data=$2
+    echo POST to $url >&2
+    # Post the form data and store the response
+    local response=$(curl -S -X POST $url \
+        --header 'Content-Type: application/json' \
+        --no-progress-meter \
+        --header "Authorization: Bearer $KC_ACCESS_TOKEN" \
+        --data @docker/$data)
+    echo $response
+}
+
+function kc_config() {
+    # Get Tokens
+    kc_tokens
+
+    # Create realm sofa
+    kc_post "/admin/realms" sofa_realm.json
+
+    # Create _admin Role
+    kc_post "/admin/realms/sofa/roles" admin_role.json
+    adminroleRaw=$(kc_get "/admin/realms/sofa/roles?first=0&max=101&q=_admin")
+    adminrole=$(echo $adminroleRaw | jq -r .[0].id)
+    echo adminrole $adminrole
+    echo '[{"id": "'${adminrole}'",' >docker/johndoe_role.json
+    echo '"name": "_admin", "description": "CouchDB Administrator",' >>docker/johndoe_role.json
+    echo '"composite": false,"clientRole": false,"containerId": "sofa"}]' >>docker/johndoe_role.json
+
+    # Create user johndoe and assign _admin role
+    kc_post "/admin/realms/sofa/users" johndoe_user.json
+    johndoe=$(kc_get "/admin/realms/sofa/ui-ext/brute-force-user?briefRepresentation=true&first=0&max=1" | jq -r .[0].id)
+    echo "johndoe $johndoe"
+    kc_post "/admin/realms/sofa/users/${johndoe}/role-mappings/realm" johndoe_role.json
+
+    # Create client fauxton
+    kc_post "/admin/realms/sofa/clients" fauxton_client.json
+}
+
+function couch_get() {
+    local url=${COUCHDB_URL}$1
+
+    # Get an URL and store the response
+    echo GET from $url
+    local response=$(curl $url \
+        --user $COUCHDB_USER:$COUCHDB_PASSWORD \
+        --no-progress-meter \
+        --header 'Content-Type: application/json')
+    echo $response
+}
+
+function couch_post() {
+    local url=${COUCHDB_URL}$1
+    local data=$2
+    echo POST to $url
+    # Post the form data and store the response
+    local response=$(curl -S -X POST $url \
+        --user $COUCHDB_USER:$COUCHDB_PASSWORD \
+        --header 'Content-Type: application/json' \
+        --no-progress-meter \
+        --data @docker/$data)
+    echo $response
+}
+
+function couch_put() {
+    local url=${COUCHDB_URL}$1
+    echo PUT to $url
+    # Post the form data and store the response
+    local response=$(curl -S -X PUT $url \
+        --no-progress-meter \
+        --header 'Content-Type: text/plain' \
+        --user $COUCHDB_USER:$COUCHDB_PASSWORD)
+    echo $response
+}
+
+# Part3: Launch containers
+# docker compose -f docker/couchdb-idp.yml pull
+docker compose -f docker/couchdb-idp.yml up -d
+
+# Pre part 4: Wait for Keycloak to start
+curl -k \
+    --retry 10 \
+    --retry-delay 10 \
+    --retry-all-errors \
+    --no-progress-meter \
+    --fail \
+    ${KC_URL}/admin/master/console/ >/dev/null
+
+if [ "$?" -ne 0 ]; then
+    echo "Failed to start Keycloak"
+    exit 1
+fi
+
+# Part4: Configure Keycloak
+
+kc_config
+
+# Pre part 5: Wait for Couchdb to start
+curl -k \
+    --retry 10 \
+    --retry-delay 10 \
+    --retry-all-errors \
+    --no-progress-meter \
+    --fail \
+    ${COUCHDB_URL}
+
+# Part5: Configure CouchDB
+couch_put /_users
+couch_put /_replicator
+couch_put /_global_changes
+
+# activate jwt authentication
+curl --request PUT ${COUCHDB_URL}/_node/_local/_config/chttpd/authentication_handlers \
+    --header 'Content-Type: text/plain' \
+    --no-progress-meter \
+    --user $COUCHDB_USER:$COUCHDB_PASSWORD \
+    --data '"{chttpd_auth, cookie_authentication_handler}, {chttpd_auth, jwt_authentication_handler}, {chttpd_auth, default_authentication_handler}"'
+
+# Retrieve the public key from Keycloak
+jwks_uri=$(curl --no-progress-meter "${KC_URL}/realms/sofa/.well-known/openid-configuration" | jq -r .jwks_uri)
+raw_key=$(curl $jwks_uri --no-progress-meter | jq -r '.keys[0]')
+kid=$(echo $raw_key | jq -r .kid)
+flat_key=$(echo "$raw_key" | tr -d '\n')
+node docker/extractpem.js "$flat_key" >tmp.key
+
+#Post it to CouchDB
+curl --request PUT ${COUCHDB_URL}/_node/nonode@nohost/_config/jwt_keys/rsa:${kid} \
+    --header 'Content-Type: text/plain' \
+    --no-progress-meter \
+    --user $COUCHDB_USER:$COUCHDB_PASSWORD \
+    --data @tmp.key
+
+rm tmp.key
+
+# Path to roles
+curl --request PUT ${COUCHDB_URL}/_node/nonode@nohost/_config/jwt_auth/roles_claim_path \
+    --header 'Content-Type: text/plain' \
+    --no-progress-meter \
+    --user $COUCHDB_USER:$COUCHDB_PASSWORD \
+    --data '"realm_access.roles"'
+
+# Restart CouchDB
+curl --request POST ${COUCHDB_URL}/_node/_local/_restart \
+    --no-progress-meter \
+    --user $COUCHDB_USER:$COUCHDB_PASSWORD
diff --git a/docker/couchdb-idp.yml b/docker/couchdb-idp.yml
new file mode 100644
index 000000000..fd3186e95
--- /dev/null
+++ b/docker/couchdb-idp.yml
@@ -0,0 +1,20 @@
+services:
+  couchdb:
+    container_name: couchdb
+    image: couchdb:latest
+    environment:
+      COUCHDB_USER: tester
+      COUCHDB_PASSWORD: testerpass
+    ports:
+      - "5984:5984"
+    depends_on:
+      - keycloak
+  keycloak:
+    container_name: keycloak
+    image: quay.io/keycloak/keycloak:latest
+    environment:
+      KEYCLOAK_ADMIN: admin
+      KEYCLOAK_ADMIN_PASSWORD: password
+    ports:
+      - "8090:8080"
+    command: start-dev
diff --git a/docker/extractpem.js b/docker/extractpem.js
new file mode 100644
index 000000000..7636724ee
--- /dev/null
+++ b/docker/extractpem.js
@@ -0,0 +1,16 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+
+const jwkToPem = require('jwk-to-pem');
+let key = process.argv[2];
+let pem = jwkToPem(JSON.parse(key));
+let flatpem = pem.replace(/\n/g, '\\\\n');
+console.log('"' + flatpem + '"');
diff --git a/docker/fauxton_client.json b/docker/fauxton_client.json
new file mode 100644
index 000000000..dbf39ab84
--- /dev/null
+++ b/docker/fauxton_client.json
@@ -0,0 +1,32 @@
+{
+    "protocol": "openid-connect",
+    "clientId": "fauxton",
+    "name": "Fauxton",
+    "description": "Fauxton for CouchDB administration",
+    "publicClient": true,
+    "authorizationServicesEnabled": false,
+    "serviceAccountsEnabled": false,
+    "implicitFlowEnabled": true,
+    "directAccessGrantsEnabled": true,
+    "standardFlowEnabled": true,
+    "frontchannelLogout": true,
+    "attributes": {
+        "saml_idp_initiated_sso_url_name": "",
+        "oauth2.device.authorization.grant.enabled": false,
+        "oidc.ciba.grant.enabled": false
+    },
+    "alwaysDisplayInConsole": false,
+    "rootUrl": "",
+    "baseUrl": "http://localhost:8000",
+    "redirectUris": [
+        "http://localhost:8000",
+        "http://localhost:8000/_callback",
+        "http://localhost:5984",
+        "http://localhost:5984/_callback"
+    ],
+    "webOrigins": [
+        "*",
+        "http://localhost:8000",
+        "http://localhost:5984"
+    ]
+}
\ No newline at end of file
diff --git a/docker/johndoe_role.json b/docker/johndoe_role.json
new file mode 100644
index 000000000..5dbbb1740
--- /dev/null
+++ b/docker/johndoe_role.json
@@ -0,0 +1,3 @@
+[{"id": "29dc4a00-c700-432a-909c-a6fb02556b0c",
+"name": "_admin", "description": "CouchDB Administrator",
+"composite": false,"clientRole": false,"containerId": "sofa"}]
diff --git a/docker/johndoe_user.json b/docker/johndoe_user.json
new file mode 100644
index 000000000..a8c195a4e
--- /dev/null
+++ b/docker/johndoe_user.json
@@ -0,0 +1,20 @@
+{
+    "attributes": {
+        "locale": ""
+    },
+    "requiredActions": [],
+    "emailVerified": true,
+    "username": "johndoe",
+    "email": "john.doe@exsample.com",
+    "firstName": "John",
+    "lastName": "Doe",
+    "groups": [],
+    "enabled": true,
+    "credentials": [
+        {
+            "type": "password",
+            "value": "password",
+            "temporary": false
+        }
+    ]
+}
\ No newline at end of file
diff --git a/docker/sofa_realm.json b/docker/sofa_realm.json
new file mode 100644
index 000000000..3abc3d865
--- /dev/null
+++ b/docker/sofa_realm.json
@@ -0,0 +1,13 @@
+{
+    "id": "sofa",
+    "realm": "sofa",
+    "displayName": "Where documents live",
+    "enabled": true,
+    "sslRequired": "NONE",
+    "registrationAllowed": true,
+    "loginWithEmailAllowed": true,
+    "duplicateEmailsAllowed": false,
+    "resetPasswordAllowed": true,
+    "editUsernameAllowed": true,
+    "bruteForceProtected": true
+}
\ No newline at end of file
diff --git a/i18n.json.default.json b/i18n.json.default.json
index dee3cc0d1..1ccb6c796 100644
--- a/i18n.json.default.json
+++ b/i18n.json.default.json
@@ -17,6 +17,7 @@
     "replication-username-input-placeholder": "Username",
     "replication-password-input-placeholder": "Password",
     "auth-missing-credentials": "Username or password cannot be blank.",
+    "auth-missing-idp": "You need IdPUrl, CallbackUrl and ClientId",
     "auth-logged-in": "You have been logged in.",
     "auth-admin-created": "CouchDB admin created",
     "auth-change-password": "Your password has been updated.",
diff --git a/idp.md b/idp.md
new file mode 100644
index 000000000..3713f3aa1
--- /dev/null
+++ b/idp.md
@@ -0,0 +1,73 @@
+# Configuring an Identity provider for Fauxton
+
+!!! note Configure CouchDB first
+
+    To successfully use an Identiy Provider (IdP), one must first configure
+    CouchDB to recognize the public key of the IdP. Follow [the documentation](https://docs.couchdb.org/en/stable/api/server/authn.html#jwt-authentication)
+    to complete this task.
+
+    Once you are ready for production you might consider [automating key management](https://github.com/beyonddemise/couchdb-idp-updater).
+
+## Preparation
+
+You need:
+
+| Item        | Description                                      | Provided by |
+| ----------- | ------------------------------------------------ | ----------- |
+| IdP Url     | derived from `/.well-known/openid-configuration` | IdP admin   |
+| client id   | a name, suggestion is `fauxton`                  | IdP admin   |
+| CallbackURL | Your couchdb server                              | You         |
+
+- The callback URL is either `http(s)://yourserver/_utils` when you run Fauxton from your CouchDB server or `http(s)://yourserver/` when you run Fauxton standalone.
+- On [Keycloak](https://www.keycloak.org/) (The IdP we develop with) access is organized in realms, so the openid configuration includes the realm name. E.g. when your realm is `sofa`, your openid url is `http(s)://yourkeycloak/realms/sofa/.well-known/openid-configuration`, There you look for `authorization_endpoint` nad use that minus the `/auth`, like this: `http(s)://yourkeycloak/realms/sofa/protocol/openid-connect`
+
+## CouchDB setup
+
+Follow [the documentation](https://docs.couchdb.org/en/stable/api/server/authn.html#jwt-authentication). For role mapping check what the IdP is emitting.
+
+For Keycloak, this works:
+
+```ini
+[jwt_auth]
+roles_claim_path = realm_access.roles
+```
+
+## Development
+
+In the docker directory there is a shell script `couchdb-idp.sh` that uses the `couchdb-idp.yml` configurtion to spin up a couchDB instance and a Keycloak container. Using `curl` it then configures both to interact:
+
+- creates a realm `sofa`
+- creates a user `johndoe` with password `password`
+- creates a client `fauxton`
+- configures couchDB to recognize the Keycloak public key
+
+To make that shell script work you need some utility helpers:
+
+- [jq](https://jqlang.github.io/jq/) command line json processor
+- [curl](https://curl.se/) http command line processor
+- [OpenSSL](https://www.openssl.org/) to deal with certificates
+
+Keycloak and couchDB in this setting don't persist values.
+
+## CORS Setup
+
+Too many moving parts.... later
+
+## Authenticate
+
+On the login page there is a new button `Log In with your Identity provider`, click that and it will open the Idp Login page.
+
+![Login screen](https://github.com/user-attachments/assets/5d15c0ec-93c9-434f-b13f-429eaf813495)
+
+Provide the 3 required values and click login (The values will be retained in localstore). You should get redirected to your IdP's login page. Your IdP could be configured with any authentication method: username/password, 2FA, Social etc.
+
+![IdP Login screen](https://github.com/user-attachments/assets/93d3d11f-decd-4658-9ae8-df588ee2beff)
+
+After succesful login you get redirected to Fauxton and should see the list of databases
+
+## Troubleshooting
+
+- Check the CouchDB [JWT configuration](https://docs.couchdb.org/en/stable/api/server/authn.html#jwt-authentication)
+- Do you have the `_admin` role?, Configure that in CouchDB and you IdP
+- Is the CORS configuration correct? Might require a restart
+- The Chrome developer tools are your friend
diff --git a/index.js b/index.js
index 9846388da..6a4c1cd3c 100644
--- a/index.js
+++ b/index.js
@@ -49,8 +49,11 @@ module.exports = function (options) {
       accept = req.headers.accept.split(',');
     }
     if (setContentSecurityPolicy) {
-      var headerValue = "default-src 'self'; child-src 'self' data: blob: https://blog.couchdb.org; img-src 'self' data:; font-src 'self'; " +
-                        "script-src 'self'; style-src 'self'; object-src 'none';";
+      //var headerValue = "default-src 'self'; child-src 'self' data: blob: https://blog.couchdb.org; img-src 'self' data:; font-src 'self'; " +
+      //                  "script-src 'self'; style-src 'self'; object-src 'none';";
+      const headerValue =
+          "default-src 'self'; child-src 'self' data: blob: https://blog.couchdb.org; img-src 'self' data:; font-src 'self'; connect-src http://localhost:8090 'self'; " +
+          "script-src 'self'; style-src 'self'; object-src 'none';";
       res.setHeader('Content-Security-Policy', headerValue);
     }
 
diff --git a/package-lock.json b/package-lock.json
index 51d86184f..48ec70413 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -91,6 +91,7 @@
         "html-webpack-plugin": "^5.5.0",
         "jest": "^29.3.1",
         "jest-environment-jsdom": "^29.3.1",
+        "jwk-to-pem": "^2.0.5",
         "mini-css-extract-plugin": "^2.6.1",
         "mock-local-storage": "^1.1.23",
         "nightwatch": "^3.2.0",
@@ -5052,6 +5053,18 @@
         "get-intrinsic": "^1.1.3"
       }
     },
+    "node_modules/asn1.js": {
+      "version": "5.4.1",
+      "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz",
+      "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==",
+      "dev": true,
+      "dependencies": {
+        "bn.js": "^4.0.0",
+        "inherits": "^2.0.1",
+        "minimalistic-assert": "^1.0.0",
+        "safer-buffer": "^2.1.0"
+      }
+    },
     "node_modules/assert-plus": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
@@ -5564,6 +5577,12 @@
       "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
       "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="
     },
+    "node_modules/bn.js": {
+      "version": "4.12.0",
+      "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
+      "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
+      "dev": true
+    },
     "node_modules/body-parser": {
       "version": "1.20.3",
       "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
@@ -5759,6 +5778,12 @@
         "node": ">=8"
       }
     },
+    "node_modules/brorand": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
+      "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==",
+      "dev": true
+    },
     "node_modules/browser-stdout": {
       "version": "1.3.1",
       "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
@@ -7187,6 +7212,27 @@
       "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.13.tgz",
       "integrity": "sha512-lbBcvtIJ4J6sS4tb5TLp1b4LyfCdMkwStzXPyAgVgTRAsep4bvrAGaBOP7ZJtQMNJpSQ9SqG4brWOroNaQtm7Q=="
     },
+    "node_modules/elliptic": {
+      "version": "6.5.7",
+      "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.7.tgz",
+      "integrity": "sha512-ESVCtTwiA+XhY3wyh24QqRGBoP3rEdDUl3EDUUo9tft074fi19IrdpH7hLCMMP3CIj7jb3W96rn8lt/BqIlt5Q==",
+      "dev": true,
+      "dependencies": {
+        "bn.js": "^4.11.9",
+        "brorand": "^1.1.0",
+        "hash.js": "^1.0.0",
+        "hmac-drbg": "^1.0.1",
+        "inherits": "^2.0.4",
+        "minimalistic-assert": "^1.0.1",
+        "minimalistic-crypto-utils": "^1.0.1"
+      }
+    },
+    "node_modules/elliptic/node_modules/inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+      "dev": true
+    },
     "node_modules/emittery": {
       "version": "0.13.1",
       "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz",
@@ -9839,6 +9885,16 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/hash.js": {
+      "version": "1.1.7",
+      "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
+      "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
+      "dev": true,
+      "dependencies": {
+        "inherits": "^2.0.3",
+        "minimalistic-assert": "^1.0.1"
+      }
+    },
     "node_modules/hasown": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@@ -9859,6 +9915,17 @@
         "he": "bin/he"
       }
     },
+    "node_modules/hmac-drbg": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
+      "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==",
+      "dev": true,
+      "dependencies": {
+        "hash.js": "^1.0.3",
+        "minimalistic-assert": "^1.0.0",
+        "minimalistic-crypto-utils": "^1.0.1"
+      }
+    },
     "node_modules/hoist-non-react-statics": {
       "version": "3.3.2",
       "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
@@ -13224,6 +13291,17 @@
       "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==",
       "dev": true
     },
+    "node_modules/jwk-to-pem": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/jwk-to-pem/-/jwk-to-pem-2.0.6.tgz",
+      "integrity": "sha512-zPC/5vjyR08TpknpTGW6Z3V3lDf9dU92oHbf0jJlG8tGOzslF9xk2UiO/seSx2llCUrNAe+AvmuGTICSXiYU7A==",
+      "dev": true,
+      "dependencies": {
+        "asn1.js": "^5.3.0",
+        "elliptic": "^6.5.7",
+        "safe-buffer": "^5.0.1"
+      }
+    },
     "node_modules/kind-of": {
       "version": "6.0.3",
       "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
@@ -13865,6 +13943,12 @@
       "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
       "dev": true
     },
+    "node_modules/minimalistic-crypto-utils": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
+      "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==",
+      "dev": true
+    },
     "node_modules/minimatch": {
       "version": "3.0.8",
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz",
@@ -22943,6 +23027,18 @@
         "get-intrinsic": "^1.1.3"
       }
     },
+    "asn1.js": {
+      "version": "5.4.1",
+      "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz",
+      "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==",
+      "dev": true,
+      "requires": {
+        "bn.js": "^4.0.0",
+        "inherits": "^2.0.1",
+        "minimalistic-assert": "^1.0.0",
+        "safer-buffer": "^2.1.0"
+      }
+    },
     "assert-plus": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
@@ -23334,6 +23430,12 @@
       "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
       "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="
     },
+    "bn.js": {
+      "version": "4.12.0",
+      "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
+      "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
+      "dev": true
+    },
     "body-parser": {
       "version": "1.20.3",
       "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
@@ -23478,6 +23580,12 @@
         "fill-range": "^7.1.1"
       }
     },
+    "brorand": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
+      "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==",
+      "dev": true
+    },
     "browser-stdout": {
       "version": "1.3.1",
       "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
@@ -24519,6 +24627,29 @@
       "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.13.tgz",
       "integrity": "sha512-lbBcvtIJ4J6sS4tb5TLp1b4LyfCdMkwStzXPyAgVgTRAsep4bvrAGaBOP7ZJtQMNJpSQ9SqG4brWOroNaQtm7Q=="
     },
+    "elliptic": {
+      "version": "6.5.7",
+      "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.7.tgz",
+      "integrity": "sha512-ESVCtTwiA+XhY3wyh24QqRGBoP3rEdDUl3EDUUo9tft074fi19IrdpH7hLCMMP3CIj7jb3W96rn8lt/BqIlt5Q==",
+      "dev": true,
+      "requires": {
+        "bn.js": "^4.11.9",
+        "brorand": "^1.1.0",
+        "hash.js": "^1.0.0",
+        "hmac-drbg": "^1.0.1",
+        "inherits": "^2.0.4",
+        "minimalistic-assert": "^1.0.1",
+        "minimalistic-crypto-utils": "^1.0.1"
+      },
+      "dependencies": {
+        "inherits": {
+          "version": "2.0.4",
+          "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+          "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+          "dev": true
+        }
+      }
+    },
     "emittery": {
       "version": "0.13.1",
       "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz",
@@ -26479,6 +26610,16 @@
         "has-symbols": "^1.0.3"
       }
     },
+    "hash.js": {
+      "version": "1.1.7",
+      "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
+      "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
+      "dev": true,
+      "requires": {
+        "inherits": "^2.0.3",
+        "minimalistic-assert": "^1.0.1"
+      }
+    },
     "hasown": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@@ -26493,6 +26634,17 @@
       "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
       "dev": true
     },
+    "hmac-drbg": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
+      "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==",
+      "dev": true,
+      "requires": {
+        "hash.js": "^1.0.3",
+        "minimalistic-assert": "^1.0.0",
+        "minimalistic-crypto-utils": "^1.0.1"
+      }
+    },
     "hoist-non-react-statics": {
       "version": "3.3.2",
       "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
@@ -28961,6 +29113,17 @@
       "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==",
       "dev": true
     },
+    "jwk-to-pem": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/jwk-to-pem/-/jwk-to-pem-2.0.6.tgz",
+      "integrity": "sha512-zPC/5vjyR08TpknpTGW6Z3V3lDf9dU92oHbf0jJlG8tGOzslF9xk2UiO/seSx2llCUrNAe+AvmuGTICSXiYU7A==",
+      "dev": true,
+      "requires": {
+        "asn1.js": "^5.3.0",
+        "elliptic": "^6.5.7",
+        "safe-buffer": "^5.0.1"
+      }
+    },
     "kind-of": {
       "version": "6.0.3",
       "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
@@ -29480,6 +29643,12 @@
       "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
       "dev": true
     },
+    "minimalistic-crypto-utils": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
+      "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==",
+      "dev": true
+    },
     "minimatch": {
       "version": "3.0.8",
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz",
diff --git a/package.json b/package.json
index 7679e1465..beb542ee4 100644
--- a/package.json
+++ b/package.json
@@ -49,6 +49,7 @@
     "html-webpack-plugin": "^5.5.0",
     "jest": "^29.3.1",
     "jest-environment-jsdom": "^29.3.1",
+    "jwk-to-pem": "^2.0.5",
     "mini-css-extract-plugin": "^2.6.1",
     "mock-local-storage": "^1.1.23",
     "nightwatch": "^3.2.0",
diff --git a/readme.md b/readme.md
index bad0cb160..0a0721613 100644
--- a/readme.md
+++ b/readme.md
@@ -21,15 +21,15 @@ Please note that [node.js](http://nodejs.org/) and npm is required. Specifically
 1. Fork this repo (see [GitHub help](https://help.github.com/articles/fork-a-repo/) for details)
 1. Clone your fork: `git clone https://github.com/YOUR-USERNAME/couchdb-fauxton.git`
 1. Go to your cloned copy: `cd couchdb-fauxton`
-1. Set up the upstream repo: 
+1. Set up the upstream repo:
     * `git remote add upstream https://github.com/apache/couchdb-fauxton.git`
     * `git fetch upstream`
     * `git branch --set-upstream-to=upstream/main main`
 1. Download all dependencies: `npm install`
 1. Make sure you have CouchDB installed.
     - Option 1 (**recommended**): Use `npm run docker:up` to start a Docker container running CouchDB with user `tester` and password `testerpass`.
-      - You need to have [Docker](https://docs.docker.com/engine/installation/) installed to use this option. 
-    - Option 2: Follow instructions 
+      - You need to have [Docker](https://docs.docker.com/engine/installation/) installed to use this option.
+    - Option 2: Follow instructions
 [found here](http://couchdb.readthedocs.org/en/latest/install/index.html)
 
 
@@ -52,7 +52,7 @@ You should be able to access Fauxton at `http://localhost:8000`
 
 ### Preparing a Fauxton Release
 
-Follow the "Setting up Fauxton" section above, then edit the `settings.json` variable root where the document will live, 
+Follow the "Setting up Fauxton" section above, then edit the `settings.json` variable root where the document will live,
 e.g. `/_utils/`. Then type:
 
 ```
@@ -84,7 +84,7 @@ part of the deployable release artifact.
 
 
 
-## More information 
+## More information
 
 Check out the following pages for a lot more information about Fauxton:
 
@@ -93,7 +93,7 @@ Check out the following pages for a lot more information about Fauxton:
 - [Testing Fauxton](https://github.com/apache/couchdb-fauxton/blob/main/tests.md)
 - [Extensions](https://github.com/apache/couchdb-fauxton/blob/main/extensions.md)
 - [How to contribute](https://github.com/apache/couchdb-fauxton/blob/main/CONTRIBUTING.md)
-
+- [Setting up Fauxton for IdP auth](idp.md)
 
 ------
 
diff --git a/test/idp_rsources/realm-export.json b/test/idp_rsources/realm-export.json
new file mode 100644
index 000000000..aa5cc8e27
--- /dev/null
+++ b/test/idp_rsources/realm-export.json
@@ -0,0 +1,2255 @@
+{
+  "id": "936d58a2-1369-4f57-ae5c-b4d6a0e834b5",
+  "realm": "sofa",
+  "notBefore": 0,
+  "defaultSignatureAlgorithm": "RS256",
+  "revokeRefreshToken": false,
+  "refreshTokenMaxReuse": 0,
+  "accessTokenLifespan": 300,
+  "accessTokenLifespanForImplicitFlow": 900,
+  "ssoSessionIdleTimeout": 1800,
+  "ssoSessionMaxLifespan": 36000,
+  "ssoSessionIdleTimeoutRememberMe": 0,
+  "ssoSessionMaxLifespanRememberMe": 0,
+  "offlineSessionIdleTimeout": 2592000,
+  "offlineSessionMaxLifespanEnabled": false,
+  "offlineSessionMaxLifespan": 5184000,
+  "clientSessionIdleTimeout": 0,
+  "clientSessionMaxLifespan": 0,
+  "clientOfflineSessionIdleTimeout": 0,
+  "clientOfflineSessionMaxLifespan": 0,
+  "accessCodeLifespan": 60,
+  "accessCodeLifespanUserAction": 300,
+  "accessCodeLifespanLogin": 1800,
+  "actionTokenGeneratedByAdminLifespan": 43200,
+  "actionTokenGeneratedByUserLifespan": 300,
+  "oauth2DeviceCodeLifespan": 600,
+  "oauth2DevicePollingInterval": 5,
+  "enabled": true,
+  "sslRequired": "external",
+  "registrationAllowed": false,
+  "registrationEmailAsUsername": false,
+  "rememberMe": false,
+  "verifyEmail": false,
+  "loginWithEmailAllowed": true,
+  "duplicateEmailsAllowed": false,
+  "resetPasswordAllowed": false,
+  "editUsernameAllowed": false,
+  "bruteForceProtected": false,
+  "permanentLockout": false,
+  "maxTemporaryLockouts": 0,
+  "maxFailureWaitSeconds": 900,
+  "minimumQuickLoginWaitSeconds": 60,
+  "waitIncrementSeconds": 60,
+  "quickLoginCheckMilliSeconds": 1000,
+  "maxDeltaTimeSeconds": 43200,
+  "failureFactor": 30,
+  "roles": {
+    "realm": [
+      {
+        "id": "4318da69-06df-4763-b498-4e8d0977ba53",
+        "name": "default-roles-sofa",
+        "description": "${role_default-roles}",
+        "composite": true,
+        "composites": {
+          "realm": [
+            "offline_access",
+            "uma_authorization"
+          ],
+          "client": {
+            "account": [
+              "manage-account",
+              "view-profile"
+            ]
+          }
+        },
+        "clientRole": false,
+        "containerId": "936d58a2-1369-4f57-ae5c-b4d6a0e834b5",
+        "attributes": {}
+      },
+      {
+        "id": "14331674-e7c0-4c59-addd-60885bb3516c",
+        "name": "uma_authorization",
+        "description": "${role_uma_authorization}",
+        "composite": false,
+        "clientRole": false,
+        "containerId": "936d58a2-1369-4f57-ae5c-b4d6a0e834b5",
+        "attributes": {}
+      },
+      {
+        "id": "fd34fe3c-d593-4001-9244-cccc9e5dfd3a",
+        "name": "offline_access",
+        "description": "${role_offline-access}",
+        "composite": false,
+        "clientRole": false,
+        "containerId": "936d58a2-1369-4f57-ae5c-b4d6a0e834b5",
+        "attributes": {}
+      }
+    ],
+    "client": {
+      "realm-management": [
+        {
+          "id": "e81bbc67-9537-4cfc-b543-3e87df6daeb0",
+          "name": "query-groups",
+          "description": "${role_query-groups}",
+          "composite": false,
+          "clientRole": true,
+          "containerId": "e866cebd-64b8-4b84-acda-e4c84928b77e",
+          "attributes": {}
+        },
+        {
+          "id": "a0449a8e-8cb2-42d8-84b4-2e041868824b",
+          "name": "query-clients",
+          "description": "${role_query-clients}",
+          "composite": false,
+          "clientRole": true,
+          "containerId": "e866cebd-64b8-4b84-acda-e4c84928b77e",
+          "attributes": {}
+        },
+        {
+          "id": "0cc6807f-bb36-49e0-99ec-cda8d48212ce",
+          "name": "create-client",
+          "description": "${role_create-client}",
+          "composite": false,
+          "clientRole": true,
+          "containerId": "e866cebd-64b8-4b84-acda-e4c84928b77e",
+          "attributes": {}
+        },
+        {
+          "id": "d240072f-d040-40a4-9f36-60a563bf5cda",
+          "name": "impersonation",
+          "description": "${role_impersonation}",
+          "composite": false,
+          "clientRole": true,
+          "containerId": "e866cebd-64b8-4b84-acda-e4c84928b77e",
+          "attributes": {}
+        },
+        {
+          "id": "fcceda4e-eb74-46cf-9bbb-5fd07d5d5f7d",
+          "name": "realm-admin",
+          "description": "${role_realm-admin}",
+          "composite": true,
+          "composites": {
+            "client": {
+              "realm-management": [
+                "query-groups",
+                "query-clients",
+                "create-client",
+                "impersonation",
+                "view-events",
+                "manage-clients",
+                "view-realm",
+                "manage-realm",
+                "manage-identity-providers",
+                "view-authorization",
+                "view-clients",
+                "query-realms",
+                "manage-users",
+                "manage-events",
+                "query-users",
+                "view-identity-providers",
+                "view-users",
+                "manage-authorization"
+              ]
+            }
+          },
+          "clientRole": true,
+          "containerId": "e866cebd-64b8-4b84-acda-e4c84928b77e",
+          "attributes": {}
+        },
+        {
+          "id": "7b913548-5b34-4677-b2b9-1cd56bdfb97f",
+          "name": "manage-clients",
+          "description": "${role_manage-clients}",
+          "composite": false,
+          "clientRole": true,
+          "containerId": "e866cebd-64b8-4b84-acda-e4c84928b77e",
+          "attributes": {}
+        },
+        {
+          "id": "4fdf0312-06a8-4236-a2b9-a1dc46dd7056",
+          "name": "view-events",
+          "description": "${role_view-events}",
+          "composite": false,
+          "clientRole": true,
+          "containerId": "e866cebd-64b8-4b84-acda-e4c84928b77e",
+          "attributes": {}
+        },
+        {
+          "id": "1c587d37-2da1-4b65-ab0d-05a65e8faa88",
+          "name": "view-realm",
+          "description": "${role_view-realm}",
+          "composite": false,
+          "clientRole": true,
+          "containerId": "e866cebd-64b8-4b84-acda-e4c84928b77e",
+          "attributes": {}
+        },
+        {
+          "id": "db6901bb-b50a-4d76-b2f9-2cd4c9e452b3",
+          "name": "manage-identity-providers",
+          "description": "${role_manage-identity-providers}",
+          "composite": false,
+          "clientRole": true,
+          "containerId": "e866cebd-64b8-4b84-acda-e4c84928b77e",
+          "attributes": {}
+        },
+        {
+          "id": "3ed25b30-c725-4ace-9ee0-3f72d69debb9",
+          "name": "manage-realm",
+          "description": "${role_manage-realm}",
+          "composite": false,
+          "clientRole": true,
+          "containerId": "e866cebd-64b8-4b84-acda-e4c84928b77e",
+          "attributes": {}
+        },
+        {
+          "id": "833cd5f3-febe-4c2f-a6af-b074bb42db17",
+          "name": "view-authorization",
+          "description": "${role_view-authorization}",
+          "composite": false,
+          "clientRole": true,
+          "containerId": "e866cebd-64b8-4b84-acda-e4c84928b77e",
+          "attributes": {}
+        },
+        {
+          "id": "b1827b17-e73f-4f57-9987-7261e7e2f30e",
+          "name": "view-clients",
+          "description": "${role_view-clients}",
+          "composite": true,
+          "composites": {
+            "client": {
+              "realm-management": [
+                "query-clients"
+              ]
+            }
+          },
+          "clientRole": true,
+          "containerId": "e866cebd-64b8-4b84-acda-e4c84928b77e",
+          "attributes": {}
+        },
+        {
+          "id": "466001cb-5d83-42f0-bd30-15967f2b85d3",
+          "name": "query-realms",
+          "description": "${role_query-realms}",
+          "composite": false,
+          "clientRole": true,
+          "containerId": "e866cebd-64b8-4b84-acda-e4c84928b77e",
+          "attributes": {}
+        },
+        {
+          "id": "a6241fa4-2ec7-4c16-b563-f52ca2a3f6d8",
+          "name": "manage-users",
+          "description": "${role_manage-users}",
+          "composite": false,
+          "clientRole": true,
+          "containerId": "e866cebd-64b8-4b84-acda-e4c84928b77e",
+          "attributes": {}
+        },
+        {
+          "id": "89c1b9da-93f5-42cd-8f07-1e7ffe1a4a59",
+          "name": "manage-events",
+          "description": "${role_manage-events}",
+          "composite": false,
+          "clientRole": true,
+          "containerId": "e866cebd-64b8-4b84-acda-e4c84928b77e",
+          "attributes": {}
+        },
+        {
+          "id": "c47319d0-b28f-4144-a1d9-bfcf7b31df98",
+          "name": "query-users",
+          "description": "${role_query-users}",
+          "composite": false,
+          "clientRole": true,
+          "containerId": "e866cebd-64b8-4b84-acda-e4c84928b77e",
+          "attributes": {}
+        },
+        {
+          "id": "8774285a-4fd4-4b56-a6ce-aec8416ee5da",
+          "name": "view-identity-providers",
+          "description": "${role_view-identity-providers}",
+          "composite": false,
+          "clientRole": true,
+          "containerId": "e866cebd-64b8-4b84-acda-e4c84928b77e",
+          "attributes": {}
+        },
+        {
+          "id": "f692bd24-83f1-4fcc-88a2-d52886106e7e",
+          "name": "view-users",
+          "description": "${role_view-users}",
+          "composite": true,
+          "composites": {
+            "client": {
+              "realm-management": [
+                "query-groups",
+                "query-users"
+              ]
+            }
+          },
+          "clientRole": true,
+          "containerId": "e866cebd-64b8-4b84-acda-e4c84928b77e",
+          "attributes": {}
+        },
+        {
+          "id": "861d7f74-ada8-4dad-931f-2fa7192d95c3",
+          "name": "manage-authorization",
+          "description": "${role_manage-authorization}",
+          "composite": false,
+          "clientRole": true,
+          "containerId": "e866cebd-64b8-4b84-acda-e4c84928b77e",
+          "attributes": {}
+        }
+      ],
+      "security-admin-console": [],
+      "admin-cli": [],
+      "account-console": [],
+      "broker": [
+        {
+          "id": "d6a3304e-54fe-4da5-8f70-be24ef01908e",
+          "name": "read-token",
+          "description": "${role_read-token}",
+          "composite": false,
+          "clientRole": true,
+          "containerId": "82634da3-a788-433a-ab39-3a21c603a5ef",
+          "attributes": {}
+        }
+      ],
+      "account": [
+        {
+          "id": "283b4ccd-a072-4be0-b770-31a263f103f3",
+          "name": "delete-account",
+          "description": "${role_delete-account}",
+          "composite": false,
+          "clientRole": true,
+          "containerId": "4821bf2e-32d4-4441-9fc3-777355441a68",
+          "attributes": {}
+        },
+        {
+          "id": "1bf2eec8-b4c5-40c6-b62e-bfa6145e545a",
+          "name": "manage-account-links",
+          "description": "${role_manage-account-links}",
+          "composite": false,
+          "clientRole": true,
+          "containerId": "4821bf2e-32d4-4441-9fc3-777355441a68",
+          "attributes": {}
+        },
+        {
+          "id": "503c637f-625c-446e-87d3-f943f527772e",
+          "name": "view-applications",
+          "description": "${role_view-applications}",
+          "composite": false,
+          "clientRole": true,
+          "containerId": "4821bf2e-32d4-4441-9fc3-777355441a68",
+          "attributes": {}
+        },
+        {
+          "id": "afdc96d6-8292-4178-bb11-2687cc2b180f",
+          "name": "manage-account",
+          "description": "${role_manage-account}",
+          "composite": true,
+          "composites": {
+            "client": {
+              "account": [
+                "manage-account-links"
+              ]
+            }
+          },
+          "clientRole": true,
+          "containerId": "4821bf2e-32d4-4441-9fc3-777355441a68",
+          "attributes": {}
+        },
+        {
+          "id": "20321c70-8863-40c3-a0bf-acdc4f40c51f",
+          "name": "manage-consent",
+          "description": "${role_manage-consent}",
+          "composite": true,
+          "composites": {
+            "client": {
+              "account": [
+                "view-consent"
+              ]
+            }
+          },
+          "clientRole": true,
+          "containerId": "4821bf2e-32d4-4441-9fc3-777355441a68",
+          "attributes": {}
+        },
+        {
+          "id": "aed7b2ea-1b45-408a-a234-93761b2420b9",
+          "name": "view-consent",
+          "description": "${role_view-consent}",
+          "composite": false,
+          "clientRole": true,
+          "containerId": "4821bf2e-32d4-4441-9fc3-777355441a68",
+          "attributes": {}
+        },
+        {
+          "id": "f140c442-ea8a-4a4b-826f-fd304714ca09",
+          "name": "view-profile",
+          "description": "${role_view-profile}",
+          "composite": false,
+          "clientRole": true,
+          "containerId": "4821bf2e-32d4-4441-9fc3-777355441a68",
+          "attributes": {}
+        },
+        {
+          "id": "857ace6b-f4a5-45ff-9edb-ef1981da3239",
+          "name": "view-groups",
+          "description": "${role_view-groups}",
+          "composite": false,
+          "clientRole": true,
+          "containerId": "4821bf2e-32d4-4441-9fc3-777355441a68",
+          "attributes": {}
+        }
+      ],
+      "fauxton": []
+    }
+  },
+  "groups": [],
+  "defaultRole": {
+    "id": "4318da69-06df-4763-b498-4e8d0977ba53",
+    "name": "default-roles-sofa",
+    "description": "${role_default-roles}",
+    "composite": true,
+    "clientRole": false,
+    "containerId": "936d58a2-1369-4f57-ae5c-b4d6a0e834b5"
+  },
+  "requiredCredentials": [
+    "password"
+  ],
+  "otpPolicyType": "totp",
+  "otpPolicyAlgorithm": "HmacSHA1",
+  "otpPolicyInitialCounter": 0,
+  "otpPolicyDigits": 6,
+  "otpPolicyLookAheadWindow": 1,
+  "otpPolicyPeriod": 30,
+  "otpPolicyCodeReusable": false,
+  "otpSupportedApplications": [
+    "totpAppFreeOTPName",
+    "totpAppGoogleName",
+    "totpAppMicrosoftAuthenticatorName"
+  ],
+  "localizationTexts": {},
+  "webAuthnPolicyRpEntityName": "keycloak",
+  "webAuthnPolicySignatureAlgorithms": [
+    "ES256"
+  ],
+  "webAuthnPolicyRpId": "",
+  "webAuthnPolicyAttestationConveyancePreference": "not specified",
+  "webAuthnPolicyAuthenticatorAttachment": "not specified",
+  "webAuthnPolicyRequireResidentKey": "not specified",
+  "webAuthnPolicyUserVerificationRequirement": "not specified",
+  "webAuthnPolicyCreateTimeout": 0,
+  "webAuthnPolicyAvoidSameAuthenticatorRegister": false,
+  "webAuthnPolicyAcceptableAaguids": [],
+  "webAuthnPolicyExtraOrigins": [],
+  "webAuthnPolicyPasswordlessRpEntityName": "keycloak",
+  "webAuthnPolicyPasswordlessSignatureAlgorithms": [
+    "ES256"
+  ],
+  "webAuthnPolicyPasswordlessRpId": "",
+  "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified",
+  "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified",
+  "webAuthnPolicyPasswordlessRequireResidentKey": "not specified",
+  "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified",
+  "webAuthnPolicyPasswordlessCreateTimeout": 0,
+  "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false,
+  "webAuthnPolicyPasswordlessAcceptableAaguids": [],
+  "webAuthnPolicyPasswordlessExtraOrigins": [],
+  "scopeMappings": [
+    {
+      "clientScope": "offline_access",
+      "roles": [
+        "offline_access"
+      ]
+    }
+  ],
+  "clientScopeMappings": {
+    "account": [
+      {
+        "client": "account-console",
+        "roles": [
+          "manage-account",
+          "view-groups"
+        ]
+      }
+    ]
+  },
+  "clients": [
+    {
+      "id": "4821bf2e-32d4-4441-9fc3-777355441a68",
+      "clientId": "account",
+      "name": "${client_account}",
+      "rootUrl": "${authBaseUrl}",
+      "baseUrl": "/realms/sofa/account/",
+      "surrogateAuthRequired": false,
+      "enabled": true,
+      "alwaysDisplayInConsole": false,
+      "clientAuthenticatorType": "client-secret",
+      "redirectUris": [
+        "/realms/sofa/account/*"
+      ],
+      "webOrigins": [],
+      "notBefore": 0,
+      "bearerOnly": false,
+      "consentRequired": false,
+      "standardFlowEnabled": true,
+      "implicitFlowEnabled": false,
+      "directAccessGrantsEnabled": false,
+      "serviceAccountsEnabled": false,
+      "publicClient": true,
+      "frontchannelLogout": false,
+      "protocol": "openid-connect",
+      "attributes": {
+        "post.logout.redirect.uris": "+"
+      },
+      "authenticationFlowBindingOverrides": {},
+      "fullScopeAllowed": false,
+      "nodeReRegistrationTimeout": 0,
+      "defaultClientScopes": [
+        "web-origins",
+        "acr",
+        "profile",
+        "roles",
+        "basic",
+        "email"
+      ],
+      "optionalClientScopes": [
+        "address",
+        "phone",
+        "offline_access",
+        "microprofile-jwt"
+      ]
+    },
+    {
+      "id": "8ccb7dc9-b493-40c1-83fd-054adafc789c",
+      "clientId": "account-console",
+      "name": "${client_account-console}",
+      "rootUrl": "${authBaseUrl}",
+      "baseUrl": "/realms/sofa/account/",
+      "surrogateAuthRequired": false,
+      "enabled": true,
+      "alwaysDisplayInConsole": false,
+      "clientAuthenticatorType": "client-secret",
+      "redirectUris": [
+        "/realms/sofa/account/*"
+      ],
+      "webOrigins": [],
+      "notBefore": 0,
+      "bearerOnly": false,
+      "consentRequired": false,
+      "standardFlowEnabled": true,
+      "implicitFlowEnabled": false,
+      "directAccessGrantsEnabled": false,
+      "serviceAccountsEnabled": false,
+      "publicClient": true,
+      "frontchannelLogout": false,
+      "protocol": "openid-connect",
+      "attributes": {
+        "post.logout.redirect.uris": "+",
+        "pkce.code.challenge.method": "S256"
+      },
+      "authenticationFlowBindingOverrides": {},
+      "fullScopeAllowed": false,
+      "nodeReRegistrationTimeout": 0,
+      "protocolMappers": [
+        {
+          "id": "bd5ba6bf-282a-4aeb-97a8-6e6f970be399",
+          "name": "audience resolve",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-audience-resolve-mapper",
+          "consentRequired": false,
+          "config": {}
+        }
+      ],
+      "defaultClientScopes": [
+        "web-origins",
+        "acr",
+        "profile",
+        "roles",
+        "basic",
+        "email"
+      ],
+      "optionalClientScopes": [
+        "address",
+        "phone",
+        "offline_access",
+        "microprofile-jwt"
+      ]
+    },
+    {
+      "id": "4b873bc3-1258-4e43-9b7b-c4ea0faf4aa5",
+      "clientId": "admin-cli",
+      "name": "${client_admin-cli}",
+      "surrogateAuthRequired": false,
+      "enabled": true,
+      "alwaysDisplayInConsole": false,
+      "clientAuthenticatorType": "client-secret",
+      "redirectUris": [],
+      "webOrigins": [],
+      "notBefore": 0,
+      "bearerOnly": false,
+      "consentRequired": false,
+      "standardFlowEnabled": false,
+      "implicitFlowEnabled": false,
+      "directAccessGrantsEnabled": true,
+      "serviceAccountsEnabled": false,
+      "publicClient": true,
+      "frontchannelLogout": false,
+      "protocol": "openid-connect",
+      "attributes": {},
+      "authenticationFlowBindingOverrides": {},
+      "fullScopeAllowed": false,
+      "nodeReRegistrationTimeout": 0,
+      "defaultClientScopes": [
+        "web-origins",
+        "acr",
+        "profile",
+        "roles",
+        "basic",
+        "email"
+      ],
+      "optionalClientScopes": [
+        "address",
+        "phone",
+        "offline_access",
+        "microprofile-jwt"
+      ]
+    },
+    {
+      "id": "82634da3-a788-433a-ab39-3a21c603a5ef",
+      "clientId": "broker",
+      "name": "${client_broker}",
+      "surrogateAuthRequired": false,
+      "enabled": true,
+      "alwaysDisplayInConsole": false,
+      "clientAuthenticatorType": "client-secret",
+      "redirectUris": [],
+      "webOrigins": [],
+      "notBefore": 0,
+      "bearerOnly": true,
+      "consentRequired": false,
+      "standardFlowEnabled": true,
+      "implicitFlowEnabled": false,
+      "directAccessGrantsEnabled": false,
+      "serviceAccountsEnabled": false,
+      "publicClient": false,
+      "frontchannelLogout": false,
+      "protocol": "openid-connect",
+      "attributes": {},
+      "authenticationFlowBindingOverrides": {},
+      "fullScopeAllowed": false,
+      "nodeReRegistrationTimeout": 0,
+      "defaultClientScopes": [
+        "web-origins",
+        "acr",
+        "profile",
+        "roles",
+        "basic",
+        "email"
+      ],
+      "optionalClientScopes": [
+        "address",
+        "phone",
+        "offline_access",
+        "microprofile-jwt"
+      ]
+    },
+    {
+      "id": "ac8b775c-fe15-4782-9bc8-b09f53152f8f",
+      "clientId": "fauxton",
+      "name": "Fauxton for CouchDB",
+      "description": "",
+      "rootUrl": "",
+      "adminUrl": "",
+      "baseUrl": "http://localhost:8000",
+      "surrogateAuthRequired": false,
+      "enabled": true,
+      "alwaysDisplayInConsole": false,
+      "clientAuthenticatorType": "client-secret",
+      "redirectUris": [
+        "http://localhost:8000/",
+        "http://localhost:8000/callback"
+      ],
+      "webOrigins": [
+        "*"
+      ],
+      "notBefore": 0,
+      "bearerOnly": false,
+      "consentRequired": false,
+      "standardFlowEnabled": true,
+      "implicitFlowEnabled": true,
+      "directAccessGrantsEnabled": true,
+      "serviceAccountsEnabled": false,
+      "publicClient": true,
+      "frontchannelLogout": true,
+      "protocol": "openid-connect",
+      "attributes": {
+        "oidc.ciba.grant.enabled": "false",
+        "backchannel.logout.session.required": "true",
+        "oauth2.device.authorization.grant.enabled": "false",
+        "display.on.consent.screen": "false",
+        "backchannel.logout.revoke.offline.tokens": "false"
+      },
+      "authenticationFlowBindingOverrides": {},
+      "fullScopeAllowed": true,
+      "nodeReRegistrationTimeout": -1,
+      "defaultClientScopes": [
+        "web-origins",
+        "acr",
+        "profile",
+        "roles",
+        "basic",
+        "email"
+      ],
+      "optionalClientScopes": [
+        "address",
+        "phone",
+        "offline_access",
+        "microprofile-jwt"
+      ]
+    },
+    {
+      "id": "e866cebd-64b8-4b84-acda-e4c84928b77e",
+      "clientId": "realm-management",
+      "name": "${client_realm-management}",
+      "surrogateAuthRequired": false,
+      "enabled": true,
+      "alwaysDisplayInConsole": false,
+      "clientAuthenticatorType": "client-secret",
+      "redirectUris": [],
+      "webOrigins": [],
+      "notBefore": 0,
+      "bearerOnly": true,
+      "consentRequired": false,
+      "standardFlowEnabled": true,
+      "implicitFlowEnabled": false,
+      "directAccessGrantsEnabled": false,
+      "serviceAccountsEnabled": false,
+      "publicClient": false,
+      "frontchannelLogout": false,
+      "protocol": "openid-connect",
+      "attributes": {},
+      "authenticationFlowBindingOverrides": {},
+      "fullScopeAllowed": false,
+      "nodeReRegistrationTimeout": 0,
+      "defaultClientScopes": [
+        "web-origins",
+        "acr",
+        "profile",
+        "roles",
+        "basic",
+        "email"
+      ],
+      "optionalClientScopes": [
+        "address",
+        "phone",
+        "offline_access",
+        "microprofile-jwt"
+      ]
+    },
+    {
+      "id": "879a372b-0195-4d2b-a194-588507fbb3ed",
+      "clientId": "security-admin-console",
+      "name": "${client_security-admin-console}",
+      "rootUrl": "${authAdminUrl}",
+      "baseUrl": "/admin/sofa/console/",
+      "surrogateAuthRequired": false,
+      "enabled": true,
+      "alwaysDisplayInConsole": false,
+      "clientAuthenticatorType": "client-secret",
+      "redirectUris": [
+        "/admin/sofa/console/*"
+      ],
+      "webOrigins": [
+        "+"
+      ],
+      "notBefore": 0,
+      "bearerOnly": false,
+      "consentRequired": false,
+      "standardFlowEnabled": true,
+      "implicitFlowEnabled": false,
+      "directAccessGrantsEnabled": false,
+      "serviceAccountsEnabled": false,
+      "publicClient": true,
+      "frontchannelLogout": false,
+      "protocol": "openid-connect",
+      "attributes": {
+        "post.logout.redirect.uris": "+",
+        "pkce.code.challenge.method": "S256"
+      },
+      "authenticationFlowBindingOverrides": {},
+      "fullScopeAllowed": false,
+      "nodeReRegistrationTimeout": 0,
+      "protocolMappers": [
+        {
+          "id": "75e84032-586e-4d19-bf4e-ffbf6a7c302e",
+          "name": "locale",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-usermodel-attribute-mapper",
+          "consentRequired": false,
+          "config": {
+            "introspection.token.claim": "true",
+            "userinfo.token.claim": "true",
+            "user.attribute": "locale",
+            "id.token.claim": "true",
+            "access.token.claim": "true",
+            "claim.name": "locale",
+            "jsonType.label": "String"
+          }
+        }
+      ],
+      "defaultClientScopes": [
+        "web-origins",
+        "acr",
+        "profile",
+        "roles",
+        "basic",
+        "email"
+      ],
+      "optionalClientScopes": [
+        "address",
+        "phone",
+        "offline_access",
+        "microprofile-jwt"
+      ]
+    }
+  ],
+  "clientScopes": [
+    {
+      "id": "86ffc5b0-288e-48f5-b615-76c04f644536",
+      "name": "role_list",
+      "description": "SAML role list",
+      "protocol": "saml",
+      "attributes": {
+        "consent.screen.text": "${samlRoleListScopeConsentText}",
+        "display.on.consent.screen": "true"
+      },
+      "protocolMappers": [
+        {
+          "id": "1103427b-5b36-4b8c-a3e9-1b8cf62dafd6",
+          "name": "role list",
+          "protocol": "saml",
+          "protocolMapper": "saml-role-list-mapper",
+          "consentRequired": false,
+          "config": {
+            "single": "false",
+            "attribute.nameformat": "Basic",
+            "attribute.name": "Role"
+          }
+        }
+      ]
+    },
+    {
+      "id": "52ebab91-71d3-4aec-9351-d9a9f400f056",
+      "name": "microprofile-jwt",
+      "description": "Microprofile - JWT built-in scope",
+      "protocol": "openid-connect",
+      "attributes": {
+        "include.in.token.scope": "true",
+        "display.on.consent.screen": "false"
+      },
+      "protocolMappers": [
+        {
+          "id": "6a0715ab-51a4-4712-b1eb-64102dbb4280",
+          "name": "groups",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-usermodel-realm-role-mapper",
+          "consentRequired": false,
+          "config": {
+            "introspection.token.claim": "true",
+            "multivalued": "true",
+            "user.attribute": "foo",
+            "id.token.claim": "true",
+            "access.token.claim": "true",
+            "claim.name": "groups",
+            "jsonType.label": "String"
+          }
+        },
+        {
+          "id": "88738b3a-0bf6-43ae-9a78-d36a60126391",
+          "name": "upn",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-usermodel-attribute-mapper",
+          "consentRequired": false,
+          "config": {
+            "introspection.token.claim": "true",
+            "userinfo.token.claim": "true",
+            "user.attribute": "username",
+            "id.token.claim": "true",
+            "access.token.claim": "true",
+            "claim.name": "upn",
+            "jsonType.label": "String"
+          }
+        }
+      ]
+    },
+    {
+      "id": "c461cd14-8ff1-4979-a349-547de48198a0",
+      "name": "web-origins",
+      "description": "OpenID Connect scope for add allowed web origins to the access token",
+      "protocol": "openid-connect",
+      "attributes": {
+        "include.in.token.scope": "false",
+        "consent.screen.text": "",
+        "display.on.consent.screen": "false"
+      },
+      "protocolMappers": [
+        {
+          "id": "05a8ac6e-c0da-4e2e-8aeb-85aa990e8ded",
+          "name": "allowed web origins",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-allowed-origins-mapper",
+          "consentRequired": false,
+          "config": {
+            "introspection.token.claim": "true",
+            "access.token.claim": "true"
+          }
+        }
+      ]
+    },
+    {
+      "id": "d97168fb-2f0e-49be-8b2e-8070c409a3a6",
+      "name": "profile",
+      "description": "OpenID Connect built-in scope: profile",
+      "protocol": "openid-connect",
+      "attributes": {
+        "include.in.token.scope": "true",
+        "consent.screen.text": "${profileScopeConsentText}",
+        "display.on.consent.screen": "true"
+      },
+      "protocolMappers": [
+        {
+          "id": "9e5662b4-0f2f-408a-8ec9-cfbeb46e7f7d",
+          "name": "profile",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-usermodel-attribute-mapper",
+          "consentRequired": false,
+          "config": {
+            "introspection.token.claim": "true",
+            "userinfo.token.claim": "true",
+            "user.attribute": "profile",
+            "id.token.claim": "true",
+            "access.token.claim": "true",
+            "claim.name": "profile",
+            "jsonType.label": "String"
+          }
+        },
+        {
+          "id": "e6898180-d6a3-4c8e-a034-c48c99ba8614",
+          "name": "picture",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-usermodel-attribute-mapper",
+          "consentRequired": false,
+          "config": {
+            "introspection.token.claim": "true",
+            "userinfo.token.claim": "true",
+            "user.attribute": "picture",
+            "id.token.claim": "true",
+            "access.token.claim": "true",
+            "claim.name": "picture",
+            "jsonType.label": "String"
+          }
+        },
+        {
+          "id": "7ceb1035-b2f0-462b-a94a-f4e9bee23393",
+          "name": "updated at",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-usermodel-attribute-mapper",
+          "consentRequired": false,
+          "config": {
+            "introspection.token.claim": "true",
+            "userinfo.token.claim": "true",
+            "user.attribute": "updatedAt",
+            "id.token.claim": "true",
+            "access.token.claim": "true",
+            "claim.name": "updated_at",
+            "jsonType.label": "long"
+          }
+        },
+        {
+          "id": "c97cdc8f-48cd-4911-be63-b1b12afae622",
+          "name": "zoneinfo",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-usermodel-attribute-mapper",
+          "consentRequired": false,
+          "config": {
+            "introspection.token.claim": "true",
+            "userinfo.token.claim": "true",
+            "user.attribute": "zoneinfo",
+            "id.token.claim": "true",
+            "access.token.claim": "true",
+            "claim.name": "zoneinfo",
+            "jsonType.label": "String"
+          }
+        },
+        {
+          "id": "edd167f7-89cd-4978-a6ec-a631658b71f7",
+          "name": "full name",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-full-name-mapper",
+          "consentRequired": false,
+          "config": {
+            "id.token.claim": "true",
+            "introspection.token.claim": "true",
+            "access.token.claim": "true",
+            "userinfo.token.claim": "true"
+          }
+        },
+        {
+          "id": "ca0d0518-c63d-4cef-9a85-2126a8ee1ffb",
+          "name": "given name",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-usermodel-attribute-mapper",
+          "consentRequired": false,
+          "config": {
+            "introspection.token.claim": "true",
+            "userinfo.token.claim": "true",
+            "user.attribute": "firstName",
+            "id.token.claim": "true",
+            "access.token.claim": "true",
+            "claim.name": "given_name",
+            "jsonType.label": "String"
+          }
+        },
+        {
+          "id": "5d9c3420-61fd-487b-82e6-806c036bd1de",
+          "name": "website",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-usermodel-attribute-mapper",
+          "consentRequired": false,
+          "config": {
+            "introspection.token.claim": "true",
+            "userinfo.token.claim": "true",
+            "user.attribute": "website",
+            "id.token.claim": "true",
+            "access.token.claim": "true",
+            "claim.name": "website",
+            "jsonType.label": "String"
+          }
+        },
+        {
+          "id": "a8c2e33e-52bb-4ba1-979e-713bde6f4951",
+          "name": "middle name",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-usermodel-attribute-mapper",
+          "consentRequired": false,
+          "config": {
+            "introspection.token.claim": "true",
+            "userinfo.token.claim": "true",
+            "user.attribute": "middleName",
+            "id.token.claim": "true",
+            "access.token.claim": "true",
+            "claim.name": "middle_name",
+            "jsonType.label": "String"
+          }
+        },
+        {
+          "id": "fe332107-f694-41ae-a69d-f5085ab908da",
+          "name": "gender",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-usermodel-attribute-mapper",
+          "consentRequired": false,
+          "config": {
+            "introspection.token.claim": "true",
+            "userinfo.token.claim": "true",
+            "user.attribute": "gender",
+            "id.token.claim": "true",
+            "access.token.claim": "true",
+            "claim.name": "gender",
+            "jsonType.label": "String"
+          }
+        },
+        {
+          "id": "d50bf0ce-9fc3-48c5-a5d8-253295cd534b",
+          "name": "birthdate",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-usermodel-attribute-mapper",
+          "consentRequired": false,
+          "config": {
+            "introspection.token.claim": "true",
+            "userinfo.token.claim": "true",
+            "user.attribute": "birthdate",
+            "id.token.claim": "true",
+            "access.token.claim": "true",
+            "claim.name": "birthdate",
+            "jsonType.label": "String"
+          }
+        },
+        {
+          "id": "0b896c11-f53c-4509-b226-f3c94ee8b514",
+          "name": "nickname",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-usermodel-attribute-mapper",
+          "consentRequired": false,
+          "config": {
+            "introspection.token.claim": "true",
+            "userinfo.token.claim": "true",
+            "user.attribute": "nickname",
+            "id.token.claim": "true",
+            "access.token.claim": "true",
+            "claim.name": "nickname",
+            "jsonType.label": "String"
+          }
+        },
+        {
+          "id": "2c1cfe80-b4d2-4504-8592-ad41286909ea",
+          "name": "locale",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-usermodel-attribute-mapper",
+          "consentRequired": false,
+          "config": {
+            "introspection.token.claim": "true",
+            "userinfo.token.claim": "true",
+            "user.attribute": "locale",
+            "id.token.claim": "true",
+            "access.token.claim": "true",
+            "claim.name": "locale",
+            "jsonType.label": "String"
+          }
+        },
+        {
+          "id": "6cf8e2a4-7a86-4bc9-9e9b-8fca7016ba00",
+          "name": "username",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-usermodel-attribute-mapper",
+          "consentRequired": false,
+          "config": {
+            "introspection.token.claim": "true",
+            "userinfo.token.claim": "true",
+            "user.attribute": "username",
+            "id.token.claim": "true",
+            "access.token.claim": "true",
+            "claim.name": "preferred_username",
+            "jsonType.label": "String"
+          }
+        },
+        {
+          "id": "d8a69957-eb47-4291-90b8-6c59b1bc65e4",
+          "name": "family name",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-usermodel-attribute-mapper",
+          "consentRequired": false,
+          "config": {
+            "introspection.token.claim": "true",
+            "userinfo.token.claim": "true",
+            "user.attribute": "lastName",
+            "id.token.claim": "true",
+            "access.token.claim": "true",
+            "claim.name": "family_name",
+            "jsonType.label": "String"
+          }
+        }
+      ]
+    },
+    {
+      "id": "d3c2fcef-ffb4-4177-a99a-b90ba18cb6e5",
+      "name": "offline_access",
+      "description": "OpenID Connect built-in scope: offline_access",
+      "protocol": "openid-connect",
+      "attributes": {
+        "consent.screen.text": "${offlineAccessScopeConsentText}",
+        "display.on.consent.screen": "true"
+      }
+    },
+    {
+      "id": "1aed987a-268a-43ea-b334-9cde54557faa",
+      "name": "basic",
+      "description": "OpenID Connect scope for add all basic claims to the token",
+      "protocol": "openid-connect",
+      "attributes": {
+        "include.in.token.scope": "false",
+        "display.on.consent.screen": "false"
+      },
+      "protocolMappers": [
+        {
+          "id": "387d9268-5390-4fa3-abb4-a3a6c8f7a6ae",
+          "name": "sub",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-sub-mapper",
+          "consentRequired": false,
+          "config": {
+            "introspection.token.claim": "true",
+            "access.token.claim": "true"
+          }
+        },
+        {
+          "id": "15b27a11-2d53-4ddf-b55a-3c7ae7316752",
+          "name": "auth_time",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-usersessionmodel-note-mapper",
+          "consentRequired": false,
+          "config": {
+            "user.session.note": "AUTH_TIME",
+            "id.token.claim": "true",
+            "introspection.token.claim": "true",
+            "access.token.claim": "true",
+            "claim.name": "auth_time",
+            "jsonType.label": "long"
+          }
+        }
+      ]
+    },
+    {
+      "id": "c369636b-4b3a-4b5e-bde2-62ea3180b74f",
+      "name": "roles",
+      "description": "OpenID Connect scope for add user roles to the access token",
+      "protocol": "openid-connect",
+      "attributes": {
+        "include.in.token.scope": "false",
+        "consent.screen.text": "${rolesScopeConsentText}",
+        "display.on.consent.screen": "true"
+      },
+      "protocolMappers": [
+        {
+          "id": "b6228ee3-204e-4741-9057-e6522dbbf82c",
+          "name": "client roles",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-usermodel-client-role-mapper",
+          "consentRequired": false,
+          "config": {
+            "user.attribute": "foo",
+            "introspection.token.claim": "true",
+            "access.token.claim": "true",
+            "claim.name": "resource_access.${client_id}.roles",
+            "jsonType.label": "String",
+            "multivalued": "true"
+          }
+        },
+        {
+          "id": "e4ba77b0-2762-4eba-8d0e-007429c43371",
+          "name": "audience resolve",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-audience-resolve-mapper",
+          "consentRequired": false,
+          "config": {
+            "introspection.token.claim": "true",
+            "access.token.claim": "true"
+          }
+        },
+        {
+          "id": "98de4f81-f4d8-4508-b8c8-02bc8cc38fc5",
+          "name": "realm roles",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-usermodel-realm-role-mapper",
+          "consentRequired": false,
+          "config": {
+            "user.attribute": "foo",
+            "introspection.token.claim": "true",
+            "access.token.claim": "true",
+            "claim.name": "realm_access.roles",
+            "jsonType.label": "String",
+            "multivalued": "true"
+          }
+        }
+      ]
+    },
+    {
+      "id": "dd6f1142-0e53-496a-8c2e-863a59287e63",
+      "name": "phone",
+      "description": "OpenID Connect built-in scope: phone",
+      "protocol": "openid-connect",
+      "attributes": {
+        "include.in.token.scope": "true",
+        "consent.screen.text": "${phoneScopeConsentText}",
+        "display.on.consent.screen": "true"
+      },
+      "protocolMappers": [
+        {
+          "id": "3e9c7ca9-0ab0-4f90-a0c6-665463980825",
+          "name": "phone number",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-usermodel-attribute-mapper",
+          "consentRequired": false,
+          "config": {
+            "introspection.token.claim": "true",
+            "userinfo.token.claim": "true",
+            "user.attribute": "phoneNumber",
+            "id.token.claim": "true",
+            "access.token.claim": "true",
+            "claim.name": "phone_number",
+            "jsonType.label": "String"
+          }
+        },
+        {
+          "id": "1870cdf3-50de-4fed-ac13-00b22560d6f9",
+          "name": "phone number verified",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-usermodel-attribute-mapper",
+          "consentRequired": false,
+          "config": {
+            "introspection.token.claim": "true",
+            "userinfo.token.claim": "true",
+            "user.attribute": "phoneNumberVerified",
+            "id.token.claim": "true",
+            "access.token.claim": "true",
+            "claim.name": "phone_number_verified",
+            "jsonType.label": "boolean"
+          }
+        }
+      ]
+    },
+    {
+      "id": "fb3b99e4-bbdc-4b07-9747-102938111927",
+      "name": "email",
+      "description": "OpenID Connect built-in scope: email",
+      "protocol": "openid-connect",
+      "attributes": {
+        "include.in.token.scope": "true",
+        "consent.screen.text": "${emailScopeConsentText}",
+        "display.on.consent.screen": "true"
+      },
+      "protocolMappers": [
+        {
+          "id": "81856a94-dfb5-4cd0-8616-e40a211893d2",
+          "name": "email verified",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-usermodel-property-mapper",
+          "consentRequired": false,
+          "config": {
+            "introspection.token.claim": "true",
+            "userinfo.token.claim": "true",
+            "user.attribute": "emailVerified",
+            "id.token.claim": "true",
+            "access.token.claim": "true",
+            "claim.name": "email_verified",
+            "jsonType.label": "boolean"
+          }
+        },
+        {
+          "id": "638ec729-19ab-4d92-893f-6a0f0c6c49ba",
+          "name": "email",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-usermodel-attribute-mapper",
+          "consentRequired": false,
+          "config": {
+            "introspection.token.claim": "true",
+            "userinfo.token.claim": "true",
+            "user.attribute": "email",
+            "id.token.claim": "true",
+            "access.token.claim": "true",
+            "claim.name": "email",
+            "jsonType.label": "String"
+          }
+        }
+      ]
+    },
+    {
+      "id": "2ae030fa-cadd-4d7a-9b07-1d50e88fe608",
+      "name": "acr",
+      "description": "OpenID Connect scope for add acr (authentication context class reference) to the token",
+      "protocol": "openid-connect",
+      "attributes": {
+        "include.in.token.scope": "false",
+        "display.on.consent.screen": "false"
+      },
+      "protocolMappers": [
+        {
+          "id": "912d9d6c-ccf3-4b42-8c3e-3c4e2a15ca45",
+          "name": "acr loa level",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-acr-mapper",
+          "consentRequired": false,
+          "config": {
+            "id.token.claim": "true",
+            "introspection.token.claim": "true",
+            "access.token.claim": "true"
+          }
+        }
+      ]
+    },
+    {
+      "id": "5bd1da03-bb42-4a98-be6e-a43e606a30b0",
+      "name": "address",
+      "description": "OpenID Connect built-in scope: address",
+      "protocol": "openid-connect",
+      "attributes": {
+        "include.in.token.scope": "true",
+        "consent.screen.text": "${addressScopeConsentText}",
+        "display.on.consent.screen": "true"
+      },
+      "protocolMappers": [
+        {
+          "id": "4848cc4c-c9f8-4e25-ba59-cf12ea330332",
+          "name": "address",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-address-mapper",
+          "consentRequired": false,
+          "config": {
+            "user.attribute.formatted": "formatted",
+            "user.attribute.country": "country",
+            "introspection.token.claim": "true",
+            "user.attribute.postal_code": "postal_code",
+            "userinfo.token.claim": "true",
+            "user.attribute.street": "street",
+            "id.token.claim": "true",
+            "user.attribute.region": "region",
+            "access.token.claim": "true",
+            "user.attribute.locality": "locality"
+          }
+        }
+      ]
+    }
+  ],
+  "defaultDefaultClientScopes": [
+    "role_list",
+    "profile",
+    "email",
+    "roles",
+    "web-origins",
+    "acr",
+    "basic"
+  ],
+  "defaultOptionalClientScopes": [
+    "offline_access",
+    "address",
+    "phone",
+    "microprofile-jwt"
+  ],
+  "browserSecurityHeaders": {
+    "contentSecurityPolicyReportOnly": "",
+    "xContentTypeOptions": "nosniff",
+    "referrerPolicy": "no-referrer",
+    "xRobotsTag": "none",
+    "xFrameOptions": "SAMEORIGIN",
+    "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';",
+    "xXSSProtection": "1; mode=block",
+    "strictTransportSecurity": "max-age=31536000; includeSubDomains"
+  },
+  "smtpServer": {},
+  "eventsEnabled": false,
+  "eventsListeners": [
+    "jboss-logging"
+  ],
+  "enabledEventTypes": [],
+  "adminEventsEnabled": false,
+  "adminEventsDetailsEnabled": false,
+  "identityProviders": [],
+  "identityProviderMappers": [],
+  "components": {
+    "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [
+      {
+        "id": "b8edbac2-039a-46ec-8a4a-e57b37330fa4",
+        "name": "Trusted Hosts",
+        "providerId": "trusted-hosts",
+        "subType": "anonymous",
+        "subComponents": {},
+        "config": {
+          "host-sending-registration-request-must-match": [
+            "true"
+          ],
+          "client-uris-must-match": [
+            "true"
+          ]
+        }
+      },
+      {
+        "id": "b0838edf-491b-4efd-b62a-bb3feaddcc86",
+        "name": "Full Scope Disabled",
+        "providerId": "scope",
+        "subType": "anonymous",
+        "subComponents": {},
+        "config": {}
+      },
+      {
+        "id": "f48a74fd-fd0b-4c02-af7e-4e76a47d5a6e",
+        "name": "Max Clients Limit",
+        "providerId": "max-clients",
+        "subType": "anonymous",
+        "subComponents": {},
+        "config": {
+          "max-clients": [
+            "200"
+          ]
+        }
+      },
+      {
+        "id": "02b53ba0-cf24-4bb0-a3ff-3815625ec93b",
+        "name": "Allowed Protocol Mapper Types",
+        "providerId": "allowed-protocol-mappers",
+        "subType": "anonymous",
+        "subComponents": {},
+        "config": {
+          "allowed-protocol-mapper-types": [
+            "oidc-usermodel-attribute-mapper",
+            "saml-user-attribute-mapper",
+            "oidc-full-name-mapper",
+            "saml-user-property-mapper",
+            "saml-role-list-mapper",
+            "oidc-usermodel-property-mapper",
+            "oidc-address-mapper",
+            "oidc-sha256-pairwise-sub-mapper"
+          ]
+        }
+      },
+      {
+        "id": "0b3b17cc-c710-4f57-9170-4fc0fb643c31",
+        "name": "Consent Required",
+        "providerId": "consent-required",
+        "subType": "anonymous",
+        "subComponents": {},
+        "config": {}
+      },
+      {
+        "id": "bc7748a3-51f6-4ef5-ad96-8650adbd17b4",
+        "name": "Allowed Client Scopes",
+        "providerId": "allowed-client-templates",
+        "subType": "authenticated",
+        "subComponents": {},
+        "config": {
+          "allow-default-scopes": [
+            "true"
+          ]
+        }
+      },
+      {
+        "id": "e91f3d3e-91c7-4c18-9081-c42b5904a883",
+        "name": "Allowed Protocol Mapper Types",
+        "providerId": "allowed-protocol-mappers",
+        "subType": "authenticated",
+        "subComponents": {},
+        "config": {
+          "allowed-protocol-mapper-types": [
+            "oidc-usermodel-property-mapper",
+            "oidc-sha256-pairwise-sub-mapper",
+            "oidc-address-mapper",
+            "saml-user-property-mapper",
+            "saml-role-list-mapper",
+            "oidc-usermodel-attribute-mapper",
+            "oidc-full-name-mapper",
+            "saml-user-attribute-mapper"
+          ]
+        }
+      },
+      {
+        "id": "3885af6c-426e-4c4b-bad9-e242ed0d96ac",
+        "name": "Allowed Client Scopes",
+        "providerId": "allowed-client-templates",
+        "subType": "anonymous",
+        "subComponents": {},
+        "config": {
+          "allow-default-scopes": [
+            "true"
+          ]
+        }
+      }
+    ],
+    "org.keycloak.keys.KeyProvider": [
+      {
+        "id": "3db5be8c-fbe5-49d0-a8f6-74c7365adaef",
+        "name": "aes-generated",
+        "providerId": "aes-generated",
+        "subComponents": {},
+        "config": {
+          "priority": [
+            "100"
+          ]
+        }
+      },
+      {
+        "id": "1d8c6348-21f0-406d-9aed-fcda2e4ff52f",
+        "name": "hmac-generated-hs512",
+        "providerId": "hmac-generated",
+        "subComponents": {},
+        "config": {
+          "priority": [
+            "100"
+          ],
+          "algorithm": [
+            "HS512"
+          ]
+        }
+      },
+      {
+        "id": "80b15dbf-afc7-448a-b5c2-5a94096beb2e",
+        "name": "rsa-enc-generated",
+        "providerId": "rsa-enc-generated",
+        "subComponents": {},
+        "config": {
+          "priority": [
+            "100"
+          ],
+          "algorithm": [
+            "RSA-OAEP"
+          ]
+        }
+      },
+      {
+        "id": "63f1abb8-7cf5-45a3-877a-80b7e64dc890",
+        "name": "rsa-generated",
+        "providerId": "rsa-generated",
+        "subComponents": {},
+        "config": {
+          "priority": [
+            "100"
+          ]
+        }
+      }
+    ]
+  },
+  "internationalizationEnabled": false,
+  "supportedLocales": [],
+  "authenticationFlows": [
+    {
+      "id": "93298e16-bc5e-4016-875c-2b834cfa7fb5",
+      "alias": "Account verification options",
+      "description": "Method with which to verity the existing account",
+      "providerId": "basic-flow",
+      "topLevel": false,
+      "builtIn": true,
+      "authenticationExecutions": [
+        {
+          "authenticator": "idp-email-verification",
+          "authenticatorFlow": false,
+          "requirement": "ALTERNATIVE",
+          "priority": 10,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        },
+        {
+          "authenticatorFlow": true,
+          "requirement": "ALTERNATIVE",
+          "priority": 20,
+          "autheticatorFlow": true,
+          "flowAlias": "Verify Existing Account by Re-authentication",
+          "userSetupAllowed": false
+        }
+      ]
+    },
+    {
+      "id": "873fa755-d547-414f-8967-fcbbd8f79e5a",
+      "alias": "Browser - Conditional OTP",
+      "description": "Flow to determine if the OTP is required for the authentication",
+      "providerId": "basic-flow",
+      "topLevel": false,
+      "builtIn": true,
+      "authenticationExecutions": [
+        {
+          "authenticator": "conditional-user-configured",
+          "authenticatorFlow": false,
+          "requirement": "REQUIRED",
+          "priority": 10,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        },
+        {
+          "authenticator": "auth-otp-form",
+          "authenticatorFlow": false,
+          "requirement": "REQUIRED",
+          "priority": 20,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        }
+      ]
+    },
+    {
+      "id": "15296b22-c60a-407a-8312-6cbb241578a0",
+      "alias": "Direct Grant - Conditional OTP",
+      "description": "Flow to determine if the OTP is required for the authentication",
+      "providerId": "basic-flow",
+      "topLevel": false,
+      "builtIn": true,
+      "authenticationExecutions": [
+        {
+          "authenticator": "conditional-user-configured",
+          "authenticatorFlow": false,
+          "requirement": "REQUIRED",
+          "priority": 10,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        },
+        {
+          "authenticator": "direct-grant-validate-otp",
+          "authenticatorFlow": false,
+          "requirement": "REQUIRED",
+          "priority": 20,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        }
+      ]
+    },
+    {
+      "id": "f7da4765-2cf0-445a-8439-23256410d542",
+      "alias": "First broker login - Conditional OTP",
+      "description": "Flow to determine if the OTP is required for the authentication",
+      "providerId": "basic-flow",
+      "topLevel": false,
+      "builtIn": true,
+      "authenticationExecutions": [
+        {
+          "authenticator": "conditional-user-configured",
+          "authenticatorFlow": false,
+          "requirement": "REQUIRED",
+          "priority": 10,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        },
+        {
+          "authenticator": "auth-otp-form",
+          "authenticatorFlow": false,
+          "requirement": "REQUIRED",
+          "priority": 20,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        }
+      ]
+    },
+    {
+      "id": "386a7dc2-fd5a-4784-841f-7bd33be79109",
+      "alias": "Handle Existing Account",
+      "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider",
+      "providerId": "basic-flow",
+      "topLevel": false,
+      "builtIn": true,
+      "authenticationExecutions": [
+        {
+          "authenticator": "idp-confirm-link",
+          "authenticatorFlow": false,
+          "requirement": "REQUIRED",
+          "priority": 10,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        },
+        {
+          "authenticatorFlow": true,
+          "requirement": "REQUIRED",
+          "priority": 20,
+          "autheticatorFlow": true,
+          "flowAlias": "Account verification options",
+          "userSetupAllowed": false
+        }
+      ]
+    },
+    {
+      "id": "364e28a9-b3b9-4fd2-906b-da80b5468073",
+      "alias": "Reset - Conditional OTP",
+      "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.",
+      "providerId": "basic-flow",
+      "topLevel": false,
+      "builtIn": true,
+      "authenticationExecutions": [
+        {
+          "authenticator": "conditional-user-configured",
+          "authenticatorFlow": false,
+          "requirement": "REQUIRED",
+          "priority": 10,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        },
+        {
+          "authenticator": "reset-otp",
+          "authenticatorFlow": false,
+          "requirement": "REQUIRED",
+          "priority": 20,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        }
+      ]
+    },
+    {
+      "id": "bc233079-0e53-4e96-9aca-0a1e7d3f33d6",
+      "alias": "User creation or linking",
+      "description": "Flow for the existing/non-existing user alternatives",
+      "providerId": "basic-flow",
+      "topLevel": false,
+      "builtIn": true,
+      "authenticationExecutions": [
+        {
+          "authenticatorConfig": "create unique user config",
+          "authenticator": "idp-create-user-if-unique",
+          "authenticatorFlow": false,
+          "requirement": "ALTERNATIVE",
+          "priority": 10,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        },
+        {
+          "authenticatorFlow": true,
+          "requirement": "ALTERNATIVE",
+          "priority": 20,
+          "autheticatorFlow": true,
+          "flowAlias": "Handle Existing Account",
+          "userSetupAllowed": false
+        }
+      ]
+    },
+    {
+      "id": "9ee52713-a1ca-4f2a-badb-5bae89d9d5cf",
+      "alias": "Verify Existing Account by Re-authentication",
+      "description": "Reauthentication of existing account",
+      "providerId": "basic-flow",
+      "topLevel": false,
+      "builtIn": true,
+      "authenticationExecutions": [
+        {
+          "authenticator": "idp-username-password-form",
+          "authenticatorFlow": false,
+          "requirement": "REQUIRED",
+          "priority": 10,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        },
+        {
+          "authenticatorFlow": true,
+          "requirement": "CONDITIONAL",
+          "priority": 20,
+          "autheticatorFlow": true,
+          "flowAlias": "First broker login - Conditional OTP",
+          "userSetupAllowed": false
+        }
+      ]
+    },
+    {
+      "id": "3b8bc7c5-5b22-4531-95da-e65922914a78",
+      "alias": "browser",
+      "description": "browser based authentication",
+      "providerId": "basic-flow",
+      "topLevel": true,
+      "builtIn": true,
+      "authenticationExecutions": [
+        {
+          "authenticator": "auth-cookie",
+          "authenticatorFlow": false,
+          "requirement": "ALTERNATIVE",
+          "priority": 10,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        },
+        {
+          "authenticator": "auth-spnego",
+          "authenticatorFlow": false,
+          "requirement": "DISABLED",
+          "priority": 20,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        },
+        {
+          "authenticator": "identity-provider-redirector",
+          "authenticatorFlow": false,
+          "requirement": "ALTERNATIVE",
+          "priority": 25,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        },
+        {
+          "authenticatorFlow": true,
+          "requirement": "ALTERNATIVE",
+          "priority": 30,
+          "autheticatorFlow": true,
+          "flowAlias": "forms",
+          "userSetupAllowed": false
+        }
+      ]
+    },
+    {
+      "id": "7935abb2-5ab6-478a-afe5-5fde8b2ce1eb",
+      "alias": "clients",
+      "description": "Base authentication for clients",
+      "providerId": "client-flow",
+      "topLevel": true,
+      "builtIn": true,
+      "authenticationExecutions": [
+        {
+          "authenticator": "client-secret",
+          "authenticatorFlow": false,
+          "requirement": "ALTERNATIVE",
+          "priority": 10,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        },
+        {
+          "authenticator": "client-jwt",
+          "authenticatorFlow": false,
+          "requirement": "ALTERNATIVE",
+          "priority": 20,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        },
+        {
+          "authenticator": "client-secret-jwt",
+          "authenticatorFlow": false,
+          "requirement": "ALTERNATIVE",
+          "priority": 30,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        },
+        {
+          "authenticator": "client-x509",
+          "authenticatorFlow": false,
+          "requirement": "ALTERNATIVE",
+          "priority": 40,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        }
+      ]
+    },
+    {
+      "id": "1be6d346-c304-4d39-af8a-657e5a948d17",
+      "alias": "direct grant",
+      "description": "OpenID Connect Resource Owner Grant",
+      "providerId": "basic-flow",
+      "topLevel": true,
+      "builtIn": true,
+      "authenticationExecutions": [
+        {
+          "authenticator": "direct-grant-validate-username",
+          "authenticatorFlow": false,
+          "requirement": "REQUIRED",
+          "priority": 10,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        },
+        {
+          "authenticator": "direct-grant-validate-password",
+          "authenticatorFlow": false,
+          "requirement": "REQUIRED",
+          "priority": 20,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        },
+        {
+          "authenticatorFlow": true,
+          "requirement": "CONDITIONAL",
+          "priority": 30,
+          "autheticatorFlow": true,
+          "flowAlias": "Direct Grant - Conditional OTP",
+          "userSetupAllowed": false
+        }
+      ]
+    },
+    {
+      "id": "e3a05a32-e375-49f6-b42b-dfaac7e3dd4a",
+      "alias": "docker auth",
+      "description": "Used by Docker clients to authenticate against the IDP",
+      "providerId": "basic-flow",
+      "topLevel": true,
+      "builtIn": true,
+      "authenticationExecutions": [
+        {
+          "authenticator": "docker-http-basic-authenticator",
+          "authenticatorFlow": false,
+          "requirement": "REQUIRED",
+          "priority": 10,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        }
+      ]
+    },
+    {
+      "id": "c7b5a73e-34d2-4634-8f3e-075d22a1895e",
+      "alias": "first broker login",
+      "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
+      "providerId": "basic-flow",
+      "topLevel": true,
+      "builtIn": true,
+      "authenticationExecutions": [
+        {
+          "authenticatorConfig": "review profile config",
+          "authenticator": "idp-review-profile",
+          "authenticatorFlow": false,
+          "requirement": "REQUIRED",
+          "priority": 10,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        },
+        {
+          "authenticatorFlow": true,
+          "requirement": "REQUIRED",
+          "priority": 20,
+          "autheticatorFlow": true,
+          "flowAlias": "User creation or linking",
+          "userSetupAllowed": false
+        }
+      ]
+    },
+    {
+      "id": "3b087de4-9f1a-4ce5-be4e-44de03125ea1",
+      "alias": "forms",
+      "description": "Username, password, otp and other auth forms.",
+      "providerId": "basic-flow",
+      "topLevel": false,
+      "builtIn": true,
+      "authenticationExecutions": [
+        {
+          "authenticator": "auth-username-password-form",
+          "authenticatorFlow": false,
+          "requirement": "REQUIRED",
+          "priority": 10,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        },
+        {
+          "authenticatorFlow": true,
+          "requirement": "CONDITIONAL",
+          "priority": 20,
+          "autheticatorFlow": true,
+          "flowAlias": "Browser - Conditional OTP",
+          "userSetupAllowed": false
+        }
+      ]
+    },
+    {
+      "id": "db32ea56-f6d6-48f8-8003-f7b043a89f75",
+      "alias": "registration",
+      "description": "registration flow",
+      "providerId": "basic-flow",
+      "topLevel": true,
+      "builtIn": true,
+      "authenticationExecutions": [
+        {
+          "authenticator": "registration-page-form",
+          "authenticatorFlow": true,
+          "requirement": "REQUIRED",
+          "priority": 10,
+          "autheticatorFlow": true,
+          "flowAlias": "registration form",
+          "userSetupAllowed": false
+        }
+      ]
+    },
+    {
+      "id": "a145481e-d0a5-4782-afb2-4640d29e667d",
+      "alias": "registration form",
+      "description": "registration form",
+      "providerId": "form-flow",
+      "topLevel": false,
+      "builtIn": true,
+      "authenticationExecutions": [
+        {
+          "authenticator": "registration-user-creation",
+          "authenticatorFlow": false,
+          "requirement": "REQUIRED",
+          "priority": 20,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        },
+        {
+          "authenticator": "registration-password-action",
+          "authenticatorFlow": false,
+          "requirement": "REQUIRED",
+          "priority": 50,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        },
+        {
+          "authenticator": "registration-recaptcha-action",
+          "authenticatorFlow": false,
+          "requirement": "DISABLED",
+          "priority": 60,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        },
+        {
+          "authenticator": "registration-terms-and-conditions",
+          "authenticatorFlow": false,
+          "requirement": "DISABLED",
+          "priority": 70,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        }
+      ]
+    },
+    {
+      "id": "13f5be91-d7cf-461c-a9c8-dfdf1a5d23e0",
+      "alias": "reset credentials",
+      "description": "Reset credentials for a user if they forgot their password or something",
+      "providerId": "basic-flow",
+      "topLevel": true,
+      "builtIn": true,
+      "authenticationExecutions": [
+        {
+          "authenticator": "reset-credentials-choose-user",
+          "authenticatorFlow": false,
+          "requirement": "REQUIRED",
+          "priority": 10,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        },
+        {
+          "authenticator": "reset-credential-email",
+          "authenticatorFlow": false,
+          "requirement": "REQUIRED",
+          "priority": 20,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        },
+        {
+          "authenticator": "reset-password",
+          "authenticatorFlow": false,
+          "requirement": "REQUIRED",
+          "priority": 30,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        },
+        {
+          "authenticatorFlow": true,
+          "requirement": "CONDITIONAL",
+          "priority": 40,
+          "autheticatorFlow": true,
+          "flowAlias": "Reset - Conditional OTP",
+          "userSetupAllowed": false
+        }
+      ]
+    },
+    {
+      "id": "2210b5f3-17b1-4955-a342-e86c1e1f6f09",
+      "alias": "saml ecp",
+      "description": "SAML ECP Profile Authentication Flow",
+      "providerId": "basic-flow",
+      "topLevel": true,
+      "builtIn": true,
+      "authenticationExecutions": [
+        {
+          "authenticator": "http-basic-authenticator",
+          "authenticatorFlow": false,
+          "requirement": "REQUIRED",
+          "priority": 10,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        }
+      ]
+    }
+  ],
+  "authenticatorConfig": [
+    {
+      "id": "5541ab27-9a24-43d1-ad89-59bc37463dff",
+      "alias": "create unique user config",
+      "config": {
+        "require.password.update.after.registration": "false"
+      }
+    },
+    {
+      "id": "9d4d82a3-9a12-42ab-b20a-6aafd366530e",
+      "alias": "review profile config",
+      "config": {
+        "update.profile.on.first.login": "missing"
+      }
+    }
+  ],
+  "requiredActions": [
+    {
+      "alias": "CONFIGURE_TOTP",
+      "name": "Configure OTP",
+      "providerId": "CONFIGURE_TOTP",
+      "enabled": true,
+      "defaultAction": false,
+      "priority": 10,
+      "config": {}
+    },
+    {
+      "alias": "TERMS_AND_CONDITIONS",
+      "name": "Terms and Conditions",
+      "providerId": "TERMS_AND_CONDITIONS",
+      "enabled": false,
+      "defaultAction": false,
+      "priority": 20,
+      "config": {}
+    },
+    {
+      "alias": "UPDATE_PASSWORD",
+      "name": "Update Password",
+      "providerId": "UPDATE_PASSWORD",
+      "enabled": true,
+      "defaultAction": false,
+      "priority": 30,
+      "config": {}
+    },
+    {
+      "alias": "UPDATE_PROFILE",
+      "name": "Update Profile",
+      "providerId": "UPDATE_PROFILE",
+      "enabled": true,
+      "defaultAction": false,
+      "priority": 40,
+      "config": {}
+    },
+    {
+      "alias": "VERIFY_EMAIL",
+      "name": "Verify Email",
+      "providerId": "VERIFY_EMAIL",
+      "enabled": true,
+      "defaultAction": false,
+      "priority": 50,
+      "config": {}
+    },
+    {
+      "alias": "delete_account",
+      "name": "Delete Account",
+      "providerId": "delete_account",
+      "enabled": false,
+      "defaultAction": false,
+      "priority": 60,
+      "config": {}
+    },
+    {
+      "alias": "webauthn-register",
+      "name": "Webauthn Register",
+      "providerId": "webauthn-register",
+      "enabled": true,
+      "defaultAction": false,
+      "priority": 70,
+      "config": {}
+    },
+    {
+      "alias": "webauthn-register-passwordless",
+      "name": "Webauthn Register Passwordless",
+      "providerId": "webauthn-register-passwordless",
+      "enabled": true,
+      "defaultAction": false,
+      "priority": 80,
+      "config": {}
+    },
+    {
+      "alias": "VERIFY_PROFILE",
+      "name": "Verify Profile",
+      "providerId": "VERIFY_PROFILE",
+      "enabled": true,
+      "defaultAction": false,
+      "priority": 90,
+      "config": {}
+    },
+    {
+      "alias": "delete_credential",
+      "name": "Delete Credential",
+      "providerId": "delete_credential",
+      "enabled": true,
+      "defaultAction": false,
+      "priority": 100,
+      "config": {}
+    },
+    {
+      "alias": "update_user_locale",
+      "name": "Update User Locale",
+      "providerId": "update_user_locale",
+      "enabled": true,
+      "defaultAction": false,
+      "priority": 1000,
+      "config": {}
+    }
+  ],
+  "browserFlow": "browser",
+  "registrationFlow": "registration",
+  "directGrantFlow": "direct grant",
+  "resetCredentialsFlow": "reset credentials",
+  "clientAuthenticationFlow": "clients",
+  "dockerAuthenticationFlow": "docker auth",
+  "firstBrokerLoginFlow": "first broker login",
+  "attributes": {
+    "cibaBackchannelTokenDeliveryMode": "poll",
+    "cibaExpiresIn": "120",
+    "cibaAuthRequestedUserHint": "login_hint",
+    "oauth2DeviceCodeLifespan": "600",
+    "oauth2DevicePollingInterval": "5",
+    "parRequestUriLifespan": "60",
+    "cibaInterval": "5",
+    "realmReusableOtpCode": "false"
+  },
+  "keycloakVersion": "25.0.1",
+  "userManagedAccessAllowed": false,
+  "organizationsEnabled": false,
+  "clientProfiles": {
+    "profiles": []
+  },
+  "clientPolicies": {
+    "policies": []
+  }
+}
\ No newline at end of file