diff --git a/app/addons/auth/actions.js b/app/addons/auth/actions.js
index 1e094c9cf..a514e153a 100644
--- a/app/addons/auth/actions.js
+++ b/app/addons/auth/actions.js
@@ -9,66 +9,86 @@
// 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";
-import app from "../../app";
+import FauxtonAPI from '../../core/api';
+import app from '../../app';
import ActionTypes from './actiontypes';
import Api from './api';
+import Idp from './idp';
-const {
- AUTH_HIDE_PASSWORD_MODAL,
-} = ActionTypes;
+const { AUTH_HIDE_PASSWORD_MODAL } = ActionTypes;
const errorHandler = ({ message }) => {
FauxtonAPI.addNotification({
msg: message,
- type: "error"
+ type: 'error'
});
};
const validate = (...predicates) => {
- return predicates.every(isTrue => isTrue);
+ return predicates.every((isTrue) => isTrue);
};
export const validateUser = (username, password) => {
return validate(!_.isEmpty(username), !_.isEmpty(password));
};
+export const validateIdP = (idpurl, idpcallback, idpappid) => {
+ return validate(!_.isEmpty(idpurl), !_.isEmpty(idpcallback), !_.isEmpty(idpappid));
+};
+
export const validatePasswords = (password, passwordConfirm) => {
- return validate(
- !_.isEmpty(password),
- !_.isEmpty(passwordConfirm),
- password === passwordConfirm
- );
+ return validate(!_.isEmpty(password), !_.isEmpty(passwordConfirm), password === passwordConfirm);
};
export const login = (username, password, urlBack) => {
if (!validateUser(username, password)) {
- return errorHandler({message: app.i18n.en_US['auth-missing-credentials']});
+ return errorHandler({ message: app.i18n.en_US['auth-missing-credentials'] });
}
- return Api.login({name: username, password})
- .then(resp => {
+ return Api.login({ name: username, password })
+ .then((resp) => {
if (resp.error) {
- errorHandler({message: resp.reason});
+ errorHandler({ message: resp.reason });
return resp;
}
let msg = app.i18n.en_US['auth-logged-in'];
if (msg) {
- FauxtonAPI.addNotification({msg});
+ FauxtonAPI.addNotification({ msg });
}
- if (urlBack && !urlBack.includes("login")) {
+ if (urlBack && !urlBack.includes('login')) {
return FauxtonAPI.navigate(urlBack);
}
- FauxtonAPI.navigate("/");
+ FauxtonAPI.navigate('/');
+ })
+ .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']});
+ return errorHandler({ message: app.i18n.en_US['auth-passwords-not-matching'] });
}
//To change an admin's password is the same as creating an admin. So we just use the
//same api function call here.
@@ -76,36 +96,32 @@ export const changePassword = (username, password, passwordConfirm, nodes) => ()
name: username,
password,
node: nodes[0].node
- }).then(
- () => {
- FauxtonAPI.addNotification({
- msg: app.i18n.en_US["auth-change-password"]
- });
- },
- errorHandler
- );
+ }).then(() => {
+ FauxtonAPI.addNotification({
+ msg: app.i18n.en_US['auth-change-password']
+ });
+ }, errorHandler);
};
export const createAdmin = (username, password, loginAfter, nodes) => () => {
const node = nodes[0].node;
if (!validateUser(username, password)) {
- return errorHandler({message: app.i18n.en_US['auth-missing-credentials']});
+ return errorHandler({ message: app.i18n.en_US['auth-missing-credentials'] });
}
- Api.createAdmin({name: username, password, node})
- .then(resp => {
- if (resp.error) {
- return errorHandler({message: `${app.i18n.en_US['auth-admin-creation-failed-prefix']} ${resp.reason}`});
- }
-
- FauxtonAPI.addNotification({
- msg: app.i18n.en_US['auth-admin-created']
- });
+ Api.createAdmin({ name: username, password, node }).then((resp) => {
+ if (resp.error) {
+ return errorHandler({ message: `${app.i18n.en_US['auth-admin-creation-failed-prefix']} ${resp.reason}` });
+ }
- if (loginAfter) {
- return FauxtonAPI.navigate("/login");
- }
+ FauxtonAPI.addNotification({
+ msg: app.i18n.en_US['auth-admin-created']
});
+
+ if (loginAfter) {
+ return FauxtonAPI.navigate('/login');
+ }
+ });
};
// simple authentication method - does nothing other than check creds
@@ -116,15 +132,15 @@ export const authenticate = (username, password, onSuccess) => {
})
.then((resp) => {
if (resp.error) {
- throw (resp);
+ throw resp;
}
hidePasswordModal();
onSuccess(username, password);
})
.catch(() => {
FauxtonAPI.addNotification({
- msg: "Your username or password is incorrect.",
- type: "error",
+ msg: 'Your username or password is incorrect.',
+ type: 'error',
clear: true
});
});
@@ -135,7 +151,7 @@ export const hidePasswordModal = () => {
};
export const logout = () => {
- FauxtonAPI.addNotification({ msg: "You have been logged out." });
+ FauxtonAPI.addNotification({ msg: 'You have been logged out.' });
Api.logout()
.then(Api.getSession)
.then(() => FauxtonAPI.navigate('/'))
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..b79011797 100644
--- a/app/addons/auth/components/loginform.js
+++ b/app/addons/auth/components/loginform.js
@@ -12,23 +12,24 @@
import PropTypes from 'prop-types';
-import React from "react";
-import { login } from "./../actions";
+import FauxtonAPI from '../../../core/base';
+import React from 'react';
+import { login } from './../actions';
import { Button, Form } from 'react-bootstrap';
class LoginForm extends React.Component {
constructor() {
super();
this.state = {
- username: "",
- password: ""
+ username: '',
+ password: ''
};
}
onUsernameChange(e) {
- this.setState({username: e.target.value});
+ this.setState({ username: e.target.value });
}
onPasswordChange(e) {
- this.setState({password: e.target.value});
+ this.setState({ password: e.target.value });
}
submit(e) {
@@ -37,26 +38,29 @@ class LoginForm extends React.Component {
this.login(this.state.username, this.state.password);
}
}
+
// 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.username !== "" || this.state.password !== "") {
+ if (this.state.username !== '' || this.state.password !== '') {
return false;
}
- let username = this.props.testBlankUsername
- ? this.props.testBlankUsername
- : this.usernameField.value;
- let password = this.props.testBlankPassword
- ? this.props.testBlankPassword
- : this.passwordField.value;
+ let username = this.props.testBlankUsername ? this.props.testBlankUsername : this.usernameField.value;
+ let password = this.props.testBlankPassword ? this.props.testBlankPassword : this.passwordField.value;
this.setState({ username: username, password: password }); // doesn't set immediately, hence separate login() call
this.login(username, password);
return true;
}
+
login(username, password) {
login(username, password, this.props.urlBack);
}
+
+ navigateToIdp(e) {
+ e.preventDefault();
+ FauxtonAPI.navigate('/loginidp');
+ }
componentDidMount() {
this.usernameField.focus();
}
@@ -66,27 +70,29 @@ class LoginForm extends React.Component {
+
+
+
+
+
);
}
}
LoginForm.defaultProps = {
- urlBack: ""
+ urlBack: ''
};
LoginForm.propTypes = {
diff --git a/app/addons/auth/components/loginformidp.js b/app/addons/auth/components/loginformidp.js
new file mode 100644
index 000000000..012bb7ec4
--- /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 (
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+export default LoginFormIdp;
diff --git a/app/addons/auth/idp.js b/app/addons/auth/idp.js
new file mode 100644
index 000000000..dfee51c36
--- /dev/null
+++ b/app/addons/auth/idp.js
@@ -0,0 +1,271 @@
+// 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';
+ // TODO: do we need to call the end_session_endpoint?
+};
+
+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..41bf615f2 100644
--- a/app/addons/auth/routes/auth.js
+++ b/app/addons/auth/routes/auth.js
@@ -10,51 +10,50 @@
// License for the specific language governing permissions and limitations under
// the License.
-import React from "react";
-import FauxtonAPI from "../../../core/api";
-import ClusterActions from "../../cluster/actions";
-import { AuthLayout } from "./../layout";
-import app from "../../../app";
-import Components from "./../components";
-import {logout} from '../actions';
+import React from 'react';
+import FauxtonAPI from '../../../core/api';
+import ClusterActions from '../../cluster/actions';
+import { AuthLayout } from './../layout';
+import app from '../../../app';
+import Components from './../components';
+import { logout } from '../actions';
+import Idp from '../idp';
-const {
- LoginForm,
- CreateAdminForm
-} = Components;
+const { LoginForm, LoginFormIdp, CreateAdminForm } = Components;
-const crumbs = [{ name: "Log In to CouchDB" }];
+const crumbs = [{ name: 'Log In to CouchDB' }];
export default FauxtonAPI.RouteObject.extend({
routes: {
- "login?*extra": "login",
- "login": "login",
- "logout": "logout",
- "createAdmin": "checkNodes",
- "createAdmin/:node": "createAdminForNode"
+ 'login?*extra': 'login',
+ login: 'login',
+ loginidp: 'loginidp',
+ logout: 'logout',
+ idpresult: 'idpCallback',
+ createAdmin: 'checkNodes',
+ 'createAdmin/:node': 'createAdminForNode'
},
checkNodes() {
- ClusterActions.navigateToNodeBasedOnNodeCount("/createAdmin/");
+ ClusterActions.navigateToNodeBasedOnNodeCount('/createAdmin/');
},
login() {
- return (
- }
- />
- );
+ return } />;
+ },
+ loginidp() {
+ const crumbs = [{ name: 'Log In to CouchDB using your IdP' }];
+ return } />;
},
logout() {
logout();
},
+ idpCallback() {
+ const url = new URL(window.location.href);
+ Idp.codeToToken(url);
+ },
+
createAdminForNode() {
ClusterActions.fetchNodes();
- const crumbs = [{ name: "Create Admin" }];
- return (
- }
- />
- );
+ const crumbs = [{ name: 'Create Admin' }];
+ return } />;
}
});
diff --git a/app/addons/documents/doc-editor/actions.js b/app/addons/documents/doc-editor/actions.js
index de1373a32..18bd18e84 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;
@@ -26,55 +27,62 @@ const initDocEditor = (params) => (dispatch) => {
// ensure a clean slate
dispatch({ type: ActionTypes.RESET_DOC });
- doc.fetch().then(function () {
- dispatch({
- type: ActionTypes.DOC_LOADED,
- options: {
- doc: doc
+ doc.fetch().then(
+ function () {
+ dispatch({
+ type: ActionTypes.DOC_LOADED,
+ options: {
+ doc: doc
+ }
+ });
+
+ if (params.onLoaded) {
+ params.onLoaded();
+ }
+ },
+ function (xhr) {
+ if (xhr.status === 404) {
+ errorNotification(`The ${doc.id} document does not exist.`);
}
- });
- if (params.onLoaded) {
- params.onLoaded();
- }
- }, function (xhr) {
- if (xhr.status === 404) {
- errorNotification(`The ${doc.id} document does not exist.`);
+ FauxtonAPI.navigate(FauxtonAPI.urls('allDocs', 'app', params.database.id, ''));
}
-
- FauxtonAPI.navigate(FauxtonAPI.urls('allDocs', 'app', params.database.id, ''));
- });
+ );
};
-const saveDoc = (doc, isValidDoc, onSave, navigateToUrl) => dispatch => {
+const saveDoc = (doc, isValidDoc, onSave, navigateToUrl) => (dispatch) => {
if (isValidDoc) {
dispatch({ type: ActionTypes.SAVING_DOCUMENT });
- doc.save().then(function () {
- onSave(doc.prettyJSON());
- dispatch({ type: ActionTypes.SAVING_DOCUMENT_COMPLETED });
- FauxtonAPI.addNotification({
- msg: 'Document saved successfully.',
- type: 'success',
- clear: true
- });
- if (navigateToUrl) {
- FauxtonAPI.navigate(navigateToUrl, {trigger: true});
- } else {
- FauxtonAPI.navigate('#/' + FauxtonAPI.urls('allDocs', 'app', FauxtonAPI.url.encode(doc.database.id)), {trigger: true});
- }
- }).fail(function (xhr) {
- dispatch({ type: ActionTypes.SAVING_DOCUMENT_COMPLETED });
- FauxtonAPI.addNotification({
- msg: 'Save failed: ' + JSON.parse(xhr.responseText).reason,
- type: 'error',
- fade: false,
- clear: true
+ doc
+ .save()
+ .then(function () {
+ onSave(doc.prettyJSON());
+ dispatch({ type: ActionTypes.SAVING_DOCUMENT_COMPLETED });
+ FauxtonAPI.addNotification({
+ msg: 'Document saved successfully.',
+ type: 'success',
+ clear: true
+ });
+ if (navigateToUrl) {
+ FauxtonAPI.navigate(navigateToUrl, { trigger: true });
+ } else {
+ FauxtonAPI.navigate('#/' + FauxtonAPI.urls('allDocs', 'app', FauxtonAPI.url.encode(doc.database.id)), { trigger: true });
+ }
+ })
+ .fail(function (xhr) {
+ dispatch({ type: ActionTypes.SAVING_DOCUMENT_COMPLETED });
+ FauxtonAPI.addNotification({
+ msg: 'Save failed: ' + JSON.parse(xhr.responseText).reason,
+ type: 'error',
+ fade: false,
+ clear: true
+ });
});
- });
-
} else if (doc.validationError && doc.validationError === 'Cannot change a documents id.') {
- errorNotification('You cannot edit the _id of an existing document. Try this: Click \'Clone Document\', then change the _id on the clone before saving.');
+ errorNotification(
+ "You cannot edit the _id of an existing document. Try this: Click 'Clone Document', then change the _id on the clone before saving."
+ );
delete doc.validationError;
} else {
errorNotification('Please fix the JSON errors and try saving again.');
@@ -93,23 +101,25 @@ const deleteDoc = (doc) => {
const databaseName = doc.database.safeID();
const query = '?rev=' + doc.get('_rev');
const url = FauxtonAPI.urls('document', 'server', databaseName, doc.safeID(), query);
- deleteRequest(url).then(res => {
- if (res.error) {
- throw new Error(res.reason || res.error);
- }
- FauxtonAPI.addNotification({
- msg: 'Your document has been successfully deleted.',
- type: 'success',
- clear: true
- });
- FauxtonAPI.navigate(FauxtonAPI.urls('allDocs', 'app', databaseName, ''));
- }).catch(err => {
- FauxtonAPI.addNotification({
- msg: 'Failed to delete your document. Reason: ' + err.message,
- type: 'error',
- clear: true
+ deleteRequest(url)
+ .then((res) => {
+ if (res.error) {
+ throw new Error(res.reason || res.error);
+ }
+ FauxtonAPI.addNotification({
+ msg: 'Your document has been successfully deleted.',
+ type: 'success',
+ clear: true
+ });
+ FauxtonAPI.navigate(FauxtonAPI.urls('allDocs', 'app', databaseName, ''));
+ })
+ .catch((err) => {
+ FauxtonAPI.addNotification({
+ msg: 'Failed to delete your document. Reason: ' + err.message,
+ type: 'error',
+ clear: true
+ });
});
- });
};
const showCloneDocModal = () => (dispatch) => {
@@ -123,21 +133,23 @@ const hideCloneDocModal = () => (dispatch) => {
const cloneDoc = (database, doc, newId) => {
hideCloneDocModal();
- doc.copy(newId).then(() => {
- const url = FauxtonAPI.urls('document', 'app', database.safeID(), encodeURIComponent(newId));
- FauxtonAPI.navigate(url, { trigger: true });
-
- FauxtonAPI.addNotification({
- msg: 'Document has been duplicated.'
- });
+ doc.copy(newId).then(
+ () => {
+ const url = FauxtonAPI.urls('document', 'app', database.safeID(), encodeURIComponent(newId));
+ FauxtonAPI.navigate(url, { trigger: true });
- }, (error) => {
- const errorMsg = `Could not duplicate document, reason: ${error.responseText}.`;
- FauxtonAPI.addNotification({
- msg: errorMsg,
- type: 'error'
- });
- });
+ FauxtonAPI.addNotification({
+ msg: 'Document has been duplicated.'
+ });
+ },
+ (error) => {
+ const errorMsg = `Could not duplicate document, reason: ${error.responseText}.`;
+ FauxtonAPI.addNotification({
+ msg: errorMsg,
+ type: 'error'
+ });
+ }
+ );
};
const showUploadModal = () => (dispatch) => {
@@ -172,7 +184,7 @@ const uploadAttachment = (params) => (dispatch) => {
const onProgress = (evt) => {
if (evt.lengthComputable) {
- const percentComplete = evt.loaded / evt.total * 100;
+ const percentComplete = (evt.loaded / evt.total) * 100;
dispatch({
type: ActionTypes.SET_FILE_UPLOAD_PERCENTAGE,
options: {
@@ -183,17 +195,19 @@ const uploadAttachment = (params) => (dispatch) => {
};
const onSuccess = (doc) => {
// re-initialize the document editor. Only announce it's been updated when
- dispatch(initDocEditor({
- doc: doc,
- onLoaded: () => {
- dispatch({ type: ActionTypes.FILE_UPLOAD_SUCCESS });
- FauxtonAPI.addNotification({
- msg: 'Document saved successfully.',
- type: 'success',
- clear: true
- });
- }
- }));
+ dispatch(
+ initDocEditor({
+ doc: doc,
+ onLoaded: () => {
+ dispatch({ type: ActionTypes.FILE_UPLOAD_SUCCESS });
+ FauxtonAPI.addNotification({
+ msg: 'Document saved successfully.',
+ type: 'success',
+ clear: true
+ });
+ }
+ })
+ );
};
const onError = (msg) => {
dispatch({
@@ -204,6 +218,7 @@ const uploadAttachment = (params) => (dispatch) => {
});
};
const httpRequest = new XMLHttpRequest();
+ addAuthHeader(httpRequest); // for JWT
currentUploadHttpRequest = httpRequest;
httpRequest.withCredentials = true;
if (httpRequest.upload) {
@@ -250,10 +265,9 @@ const resetUploadModal = () => (dispatch) => {
dispatch({ type: ActionTypes.RESET_UPLOAD_MODAL });
};
-
// helpers
-function errorNotification (msg) {
+function errorNotification(msg) {
FauxtonAPI.addNotification({
msg: msg,
type: 'error',
diff --git a/app/core/ajax.js b/app/core/ajax.js
index 95e622462..1ae03b031 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 { 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:
@@ -16,7 +17,7 @@ export const fetchObserver = new Subject();
// The default pre-fetch function which simply resolves to the original parameters.
export function defaultPreFetch(url, options) {
- return Promise.resolve({url, options});
+ return Promise.resolve({ url, options });
}
let _preFetchFn = defaultPreFetch;
@@ -33,8 +34,8 @@ let _preFetchFn = defaultPreFetch;
*
* @param {function} fn The pre-fetch function
*/
-export const setPreFetchFn = fn => {
- if (fn && typeof fn === "function" && fn.length === 2) {
+export const setPreFetchFn = (fn) => {
+ if (fn && typeof fn === 'function' && fn.length === 2) {
_preFetchFn = fn;
} else {
throw new Error('preFetch must be a function that accepts two parameters (url and options) like the native fetch()');
@@ -53,26 +54,22 @@ export const setPreFetchFn = fn => {
*
* @return {Promise}
*/
-export const json = (url, method = "GET", opts = {}) => {
- const fetchOptions = defaultsDeep(
- {},
- opts,
- {
- method,
- credentials: "include",
- headers: {
- accept: "application/json",
- "Content-Type": "application/json",
- "Pragma":"no-cache" //Disables cache for IE11
- },
- cache: "no-cache"
- }
- );
- return _preFetchFn(url, fetchOptions).then((result) => {
- return fetch(
- result.url,
- result.options,
- ).then(resp => {
+export const json = (url, method = 'GET', opts = {}) => {
+ const fetchOptions = defaultsDeep({}, opts, {
+ method,
+ credentials: 'include',
+ headers: {
+ accept: 'application/json',
+ 'Content-Type': 'application/json',
+ Pragma: 'no-cache' //Disables cache for IE11
+ },
+ cache: 'no-cache'
+ });
+
+ const updatedFetchOptions = addAuthToken(fetchOptions);
+
+ return _preFetchFn(url, updatedFetchOptions).then((result) => {
+ return fetch(result.url, result.options).then((resp) => {
fetchObserver.next(resp);
if (opts.raw) {
return resp;
@@ -82,21 +79,12 @@ export const json = (url, method = "GET", opts = {}) => {
});
};
-
-/**
- * get - Get request
- *
- * @param {string} url Url of request
- * @param {object} [opts={}] Opts to add to request
- *
- * @return {Promise} A promise with the request's response
- */
export const get = (url, opts = {}) => {
- return json(url, "GET", opts);
+ return json(url, 'GET', opts);
};
export const deleteRequest = (url, opts = {}) => {
- return json(url, "DELETE", opts);
+ return json(url, 'DELETE', opts);
};
/**
@@ -110,12 +98,10 @@ export const deleteRequest = (url, opts = {}) => {
*/
export const post = (url, body, opts = {}) => {
if (typeof body !== 'undefined') {
- if (opts.rawBody)
- opts.body = body;
- else
- opts.body = JSON.stringify(body);
+ if (opts.rawBody) opts.body = body;
+ else opts.body = JSON.stringify(body);
}
- return json(url, "POST", opts);
+ return json(url, 'POST', opts);
};
/**
@@ -130,39 +116,35 @@ export const post = (url, body, opts = {}) => {
*/
export const put = (url, body, opts = {}) => {
if (typeof body !== 'undefined') {
- if (opts.rawBody)
- opts.body = body;
- else
- opts.body = JSON.stringify(body);
+ if (opts.rawBody) opts.body = body;
+ else opts.body = JSON.stringify(body);
}
- return json(url, "PUT", opts);
+ return json(url, 'PUT', opts);
};
export const formEncoded = (url, method, opts = {}) => {
- return json(url, method, defaultsDeep(
- {},
- opts,
- {
+ return json(
+ url,
+ method,
+ defaultsDeep({}, opts, {
headers: {
- "Content-Type": 'application/x-www-form-urlencoded;charset=UTF-8'
+ 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
}
- }));
+ })
+ );
};
export const postFormEncoded = (url, body, opts = {}) => {
- if (body)
- opts.body = body;
- return formEncoded(url, "POST", opts);
+ if (body) opts.body = body;
+ return formEncoded(url, 'POST', opts);
};
export const putFormEncoded = (url, body, opts = {}) => {
- if (body)
- opts.body = body;
- return formEncoded(url, "PUT", opts);
+ if (body) opts.body = body;
+ return formEncoded(url, 'PUT', opts);
};
export const deleteFormEncoded = (url, body, opts = {}) => {
- if (body)
- opts.body = body;
- return formEncoded(url, "DELETE", opts);
+ if (body) opts.body = body;
+ return formEncoded(url, 'DELETE', opts);
};
diff --git a/app/core/api.js b/app/core/api.js
index 7927397d9..1bacae471 100644
--- a/app/core/api.js
+++ b/app/core/api.js
@@ -10,17 +10,27 @@
// License for the specific language governing permissions and limitations under
// the License.
-import FauxtonAPI from "./base";
-import Router from "./router";
-import RouteObject from "./routeObject";
-import utils from "./utils";
-import Store from "./store";
-import constants from "../constants";
-import dispatcher from "./dispatcher";
-import $ from "jquery";
-import Backbone from "backbone";
-import _ from "lodash";
-import Promise from "bluebird";
+import FauxtonAPI from './base';
+import Router from './router';
+import RouteObject from './routeObject';
+import utils from './utils';
+import Store from './store';
+import constants from '../constants';
+import dispatcher from './dispatcher';
+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 () {
@@ -43,7 +53,7 @@ FauxtonAPI.constants = constants;
FauxtonAPI.dispatch = dispatcher.dispatch;
FauxtonAPI.navigate = function (url, _opts) {
- var options = _.extend({trigger: true}, _opts);
+ var options = _.extend({ trigger: true }, _opts);
FauxtonAPI.router.navigate(url, options);
if (options.trigger) {
FauxtonAPI.router.trigger('trigger-update');
@@ -69,13 +79,13 @@ FauxtonAPI.registerUrls = function (namespace, urls) {
};
FauxtonAPI.url = {
- encode(name = "") {
+ encode(name = '') {
// These special caracters are allowed by couch: _, $, (, ), +, -, and /
// From them only $ + and / are to be escaped in a URI component.
- return (/[$+/]/g.test(name)) ? encodeURIComponent(name) : name;
+ return /[$+/]/g.test(name) ? encodeURIComponent(name) : name;
},
- decode(name = "") {
- return (/[$+/]/g.test(name)) ? decodeURIComponent(name) : name;
+ decode(name = '') {
+ return /[$+/]/g.test(name) ? decodeURIComponent(name) : name;
}
};
@@ -95,7 +105,9 @@ FauxtonAPI.urls = function (name, context) {
return false;
});
- if (!_.isUndefined(url)) { return url; }
+ if (!_.isUndefined(url)) {
+ return url;
+ }
if (!urlPaths[name]) {
console.error('could not find name ', name);
diff --git a/devserver.js b/devserver.js
index 32fba7055..a142d685b 100644
--- a/devserver.js
+++ b/devserver.js
@@ -1,26 +1,27 @@
const spawn = require('child_process').spawn;
-const fs = require("fs");
+const fs = require('fs');
const webpack = require('webpack');
const WebpackDev = require('webpack-dev-server');
const config = require('./webpack.config.dev.js');
const httpProxy = require('http-proxy');
const path = require('path');
-
const loadSettings = function () {
let fileName = './settings.json.default.json';
if (fs.existsSync('./settings.json')) {
fileName = './settings.json';
}
- return require(fileName).couchserver || {
- port: process.env.FAUXTON_PORT || 8000,
- contentSecurityPolicy: true,
- proxy: {
- target: process.env.COUCH_HOST || 'http://127.0.0.1:5984',
- changeOrigin: false
+ return (
+ require(fileName).couchserver || {
+ port: process.env.FAUXTON_PORT || 8000,
+ contentSecurityPolicy: true,
+ proxy: {
+ target: process.env.COUCH_HOST || 'http://127.0.0.1:5984',
+ changeOrigin: false
+ }
}
- };
+ );
};
const settings = loadSettings();
@@ -51,9 +52,10 @@ 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';";
-function getCspHeaders () {
+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;
}
@@ -74,7 +76,7 @@ const runWebpackServer = function () {
proxy.on('proxyRes', function (proxyRes) {
if (proxyRes.headers['set-cookie']) {
- proxyRes.headers['set-cookie'][0] = proxyRes.headers["set-cookie"][0].replace('Secure', '');
+ proxyRes.headers['set-cookie'][0] = proxyRes.headers['set-cookie'][0].replace('Secure', '');
}
});
@@ -89,15 +91,15 @@ const runWebpackServer = function () {
host: '0.0.0.0',
port: process.env.FAUXTON_PORT || 8000,
client: {
- overlay: true,
+ overlay: true
},
hot: false,
historyApiFallback: false,
- allowedHosts: "auto",
+ allowedHosts: 'auto',
devMiddleware: {
stats: {
- colors: true,
- },
+ colors: true
+ }
},
headers: getCspHeaders(),
@@ -106,10 +108,11 @@ const runWebpackServer = function () {
throw new Error('webpack-dev-server is not defined');
}
- middlewares.unshift(
- {
- name: "proxy-to-couchdb",
- middleware: ('*', (req, res, next) => {
+ middlewares.unshift({
+ name: 'proxy-to-couchdb',
+ middleware:
+ ('*',
+ (req, res, next) => {
const accept = req.headers.accept ? req.headers.accept.split(',') : '';
if (/application\/json/.test(accept[0]) || /multipart\/form-data/.test(accept[0])) {
proxy.web(req, res);
@@ -117,12 +120,11 @@ const runWebpackServer = function () {
}
next();
- }),
- }
- );
+ })
+ });
return middlewares;
- },
+ }
};
const compiler = webpack(config);
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..90b3ec36d 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.",
@@ -24,4 +25,4 @@
"auth-passwords-not-matching": "Passwords do not match.",
"create-db-partitioned-help": "This is an advanced feature. If you are unsure whether you need a partitioned database, you probably do not. A partitioned database requires a partition key for every document, where the document _id format is '<partition_key>:<doc_key>'. A partition is a logical grouping of documents. Partition queries are often faster than global ones."
}
-}
+}
\ No newline at end of file
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..070bebf8c 100644
--- a/index.js
+++ b/index.js
@@ -1,5 +1,5 @@
-var path = require("path");
-var http = require("http");
+var path = require('path');
+var http = require('http');
var httpProxy = require('http-proxy');
var send = require('send');
var urlLib = require('url');
@@ -12,7 +12,7 @@ module.exports = function (options) {
var port = options.port;
var proxyUrl = options.couchdb;
- function sendFile (req, res, filePath) {
+ function sendFile(req, res, filePath) {
return send(req, filePath)
.on('error', function (err) {
if (err.status === 404) {
@@ -21,16 +21,16 @@ module.exports = function (options) {
console.error('ERROR', filePath, err);
}
- res.setHeader("Content-Type", "text/javascript");
+ res.setHeader('Content-Type', 'text/javascript');
res.statusCode = 404;
- res.end(JSON.stringify({error: err.message}));
+ res.end(JSON.stringify({ error: err.message }));
})
.pipe(res);
}
var fileTypes = ['.js', '.css', '.png', '.swf', '.eot', '.woff', '.svg', '.ttf', '.swf'];
- function isFile (url) {
+ function isFile(url) {
return _.includes(fileTypes, path.extname(url));
}
@@ -41,33 +41,36 @@ module.exports = function (options) {
target: proxyUrl
});
- this.server = http.createServer((req, res) => {
- var isDocLink = /_utils\/docs/.test(req.url);
- var url = req.url.split(/\?v=|\?noCache/)[0].replace('_utils', '');
- var accept = [];
- if (req.headers.accept) {
- 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';";
- res.setHeader('Content-Security-Policy', headerValue);
- }
-
- if (url === '/' && accept[0] !== 'application/json') {
- // serve main index file from here
- return sendFile(req, res, path.join(dist_dir, 'index.html'));
- } else if (isFile(url) && !isDocLink) {
- return sendFile(req, res, path.join(dist_dir, url));
- }
-
- // This sets the Host header in the proxy so that one can use external
- // CouchDB instances and not have the Host set to 'localhost'
- var urlObj = urlLib.parse(req.url);
- req.headers.host = urlObj.host;
-
- this.proxy.web(req, res);
- }).listen(port, '0.0.0.0');
+ this.server = http
+ .createServer((req, res) => {
+ var isDocLink = /_utils\/docs/.test(req.url);
+ var url = req.url.split(/\?v=|\?noCache/)[0].replace('_utils', '');
+ var accept = [];
+ if (req.headers.accept) {
+ 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'; connect-src http://localhost:8090 'self'; " +
+ "script-src 'self'; style-src 'self'; object-src 'none';";
+ res.setHeader('Content-Security-Policy', headerValue);
+ }
+
+ if (url === '/' && accept[0] !== 'application/json') {
+ // serve main index file from here
+ return sendFile(req, res, path.join(dist_dir, 'index.html'));
+ } else if (isFile(url) && !isDocLink) {
+ return sendFile(req, res, path.join(dist_dir, url));
+ }
+
+ // This sets the Host header in the proxy so that one can use external
+ // CouchDB instances and not have the Host set to 'localhost'
+ var urlObj = urlLib.parse(req.url);
+ req.headers.host = urlObj.host;
+
+ this.proxy.web(req, res);
+ })
+ .listen(port, '0.0.0.0');
this.proxy.on('error', () => {
// don't explode on cancelled requests
@@ -77,19 +80,19 @@ module.exports = function (options) {
// via https.
this.proxy.on('proxyRes', (proxyRes) => {
if (proxyRes.headers['set-cookie']) {
- proxyRes.headers['set-cookie'][0] = proxyRes.headers["set-cookie"][0].replace('Secure', '');
+ proxyRes.headers['set-cookie'][0] = proxyRes.headers['set-cookie'][0].replace('Secure', '');
}
});
var logo = [
- [""],
- [" ______ _ "],
- ["| ____| | | "],
- ["| |__ __ _ _ _ __ __ | |_ ___ _ __ "],
+ [''],
+ [' ______ _ '],
+ ['| ____| | | '],
+ ['| |__ __ _ _ _ __ __ | |_ ___ _ __ '],
["| __| / _` | | | | | \\ \\/ / | __| / _ \\ | '_ \\ "],
- ["| | | (_| | | |_| | > < | |_ | (_) | | | | |"],
- ["|_| \\__,_| \\__,_| /_/\\_\\ \\__| \\___/ |_| |_|"],
- [""]
+ ['| | | (_| | | |_| | > < | |_ | (_) | | | | |'],
+ ['|_| \\__,_| \\__,_| /_/\\_\\ \\__| \\___/ |_| |_|'],
+ ['']
];
_.each(logo, function (line) {
diff --git a/package-lock.json b/package-lock.json
index 9954ce6b6..ab244d958 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.6",
+ "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.6.tgz",
+ "integrity": "sha512-mpzdtpeCLuS3BmE3pO3Cpp5bbjlOPY2Q0PgoF+Od1XZrHLYI28Xe3ossCmYCQt11FQKEYd9+PF8jymTvtWJSHQ==",
+ "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.5",
+ "resolved": "https://registry.npmjs.org/jwk-to-pem/-/jwk-to-pem-2.0.5.tgz",
+ "integrity": "sha512-L90jwellhO8jRKYwbssU9ifaMVqajzj3fpRjDKcsDzrslU9syRbFqfkXtT4B89HYAap+xsxNcxgBSB09ig+a7A==",
+ "dev": true,
+ "dependencies": {
+ "asn1.js": "^5.3.0",
+ "elliptic": "^6.5.4",
+ "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.6",
+ "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.6.tgz",
+ "integrity": "sha512-mpzdtpeCLuS3BmE3pO3Cpp5bbjlOPY2Q0PgoF+Od1XZrHLYI28Xe3ossCmYCQt11FQKEYd9+PF8jymTvtWJSHQ==",
+ "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.5",
+ "resolved": "https://registry.npmjs.org/jwk-to-pem/-/jwk-to-pem-2.0.5.tgz",
+ "integrity": "sha512-L90jwellhO8jRKYwbssU9ifaMVqajzj3fpRjDKcsDzrslU9syRbFqfkXtT4B89HYAap+xsxNcxgBSB09ig+a7A==",
+ "dev": true,
+ "requires": {
+ "asn1.js": "^5.3.0",
+ "elliptic": "^6.5.4",
+ "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..0df8fb83c 100644
--- a/readme.md
+++ b/readme.md
@@ -13,7 +13,6 @@ You can use the latest release of Fauxton via npm:
See `fauxton --help` for extra options.
-
## Setting up Fauxton
Please note that [node.js](http://nodejs.org/) and npm is required. Specifically, Fauxton requires at least Node 6 and npm 3.
@@ -21,23 +20,18 @@ 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:
- * `git remote add upstream https://github.com/apache/couchdb-fauxton.git`
- * `git fetch upstream`
- * `git branch --set-upstream-to=upstream/main main`
+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
-[found here](http://couchdb.readthedocs.org/en/latest/install/index.html)
-
+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
+ [found here](http://couchdb.readthedocs.org/en/latest/install/index.html)
## Running Fauxton
**NOTE: Before you run Fauxton, don't forget to start CouchDB!**
-
### The Dev Server
Using the dev server is the easiest way to use Fauxton, especially when developing for it. In the cloned repo folder,
@@ -49,17 +43,16 @@ npm run dev
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:
```
npm run couchdb
```
-This will install the latest version of Fauxton into `/share/www/`
+This will install the latest version of Fauxton into `/share/www/`
### To Deploy Fauxton
@@ -76,15 +69,14 @@ release artifact. Once everything is finished the files are copied from
part of the deployable release artifact.
### (Optional) To avoid a npm global install
+
# Development mode, non minified files
npm run couchdebug
# Or fully compiled install
npm run couchdb
-
-
-## More information
+## More information
Check out the following pages for a lot more information about Fauxton:
@@ -93,9 +85,8 @@ 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)
-
-------
-
+---
-- The Fauxton Team
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