From 570204a883716e9d45b13b7049083bece34b9cbf Mon Sep 17 00:00:00 2001 From: "Stephan H. Wissel" Date: Sun, 28 Jul 2024 23:33:07 +0800 Subject: [PATCH 01/11] Initial modification to include IdP --- app/addons/auth/actions.js | 106 +++++++++------- app/addons/auth/components/index.js | 2 + app/addons/auth/components/loginform.js | 59 +++++---- app/addons/auth/components/loginformidp.js | 139 +++++++++++++++++++++ app/addons/auth/idp.js | 104 +++++++++++++++ app/addons/auth/routes/auth.js | 71 ++++++----- app/core/ajax.js | 113 +++++++++-------- i18n.json.default.json | 3 +- package-lock.json | 47 +------ package.json | 2 +- 10 files changed, 447 insertions(+), 199 deletions(-) create mode 100644 app/addons/auth/components/loginformidp.js create mode 100644 app/addons/auth/idp.js 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 {
- - Enter your username and password + this.usernameField = node} + ref={(node) => (this.usernameField = node)} placeholder="Username" onChange={this.onUsernameChange.bind(this)} - value={this.state.username} /> + value={this.state.username} + />
- this.passwordField = node} + ref={(node) => (this.passwordField = node)} placeholder="Password" onChange={this.onPasswordChange.bind(this)} - value={this.state.password} /> + value={this.state.password} + />
@@ -97,13 +103,20 @@ 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..82973134d --- /dev/null +++ b/app/addons/auth/components/loginformidp.js @@ -0,0 +1,139 @@ +// 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(); + // TODO: Read local IDP configuration + this.state = { + idpurl: localStorage.getItem('FauxtonIdpurl') || '', + idpcallback: localStorage.getItem('FauxtonIdpcallback') || '', + idpappid: localStorage.getItem('FauxtonIdpappid') || '' + }; + } + + 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 ( +
+
+
+
+ + (this.idpurlField = node)} + placeholder="IdP URL" + onChange={this.onIdpurlChange.bind(this)} + value={this.state.idpurl} + /> +
+
+
+
+ (this.idpcallbackField = node)} + placeholder="Callback URL" + onChange={this.onIdpcallbackChange.bind(this)} + value={this.state.idpcallback} + /> +
+
+
+
+ (this.idpappidField = node)} + placeholder="Applicaiton ID" + onChange={this.onIdpappidChange.bind(this)} + value={this.state.idpappid} + /> +
+
+
+
+ +
+
+
+
+
+ +
+
+
+ ); + } +} + +export default LoginFormIdp; diff --git a/app/addons/auth/idp.js b/app/addons/auth/idp.js new file mode 100644 index 000000000..039561eb9 --- /dev/null +++ b/app/addons/auth/idp.js @@ -0,0 +1,104 @@ +// 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'; + +/** + * 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); + return decodedToken.exp > currentTime; +}; + +/** + * 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 = Buffer.from(base64, 'base64'); //.toString('utf-8'); + 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) => { + const authUrl = `${idpurl}/auth?response_type=token&client_id=${idpappid}&redirect_uri=${idpcallback}`; + window.location.href = authUrl; + return Promise.resolve('Authentication initiated'); +}; + +export const logout = () => { + localStorage.removeItem('fauxtonToken'); + localStorage.removeItem('fauxtonRefreshToken'); + window.location.href = '/_session'; +}; + +export const refreshToken = () => { + const refreshToken = localStorage.getItem('fauxtonRefreshToken'); + const idpurl = localStorage.getItem('FauxtonIdpurl'); + const idpappid = localStorage.getItem('FauxtonIdpappid'); + const authUrl = `${idpurl}/token`; + fetch(authUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: `grant_type=refresh_token&refresh_token=${refreshToken}&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: error.message, + type: 'error' + }); + }); +}; + +export default { + login, + logout, + refreshToken, + jwtStillValid, + decodeToken, + getExpiry +}; diff --git a/app/addons/auth/routes/auth.js b/app/addons/auth/routes/auth.js index d274c089e..443334daf 100644 --- a/app/addons/auth/routes/auth.js +++ b/app/addons/auth/routes/auth.js @@ -10,51 +10,60 @@ // 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', + 'session_state*': '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() { + alert('idpCallback'); + const urlParams = new URLSearchParams(window.location.hash); + alert(window.location.hash); + const accessToken = urlParams.get('access_token'); + const refreshToken = urlParams.get('refresh_token'); + localStorage.setItem('fauxtonToken', accessToken); + localStorage.setItem('fauxtonRefreshToken', refreshToken); + + // Extract expiry from the access token + const expiry = Idp.getExpiry(accessToken); + console.log('Expiry:', expiry); + //setTimeout(Idp.refreshToken, (expiry - 60) * 1000); + }, + createAdminForNode() { ClusterActions.fetchNodes(); - const crumbs = [{ name: "Create Admin" }]; - return ( - } - /> - ); + const crumbs = [{ name: 'Create Admin' }]; + return } />; } }); diff --git a/app/core/ajax.js b/app/core/ajax.js index 95e622462..c4e3bf122 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 { jwtStillValid } 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" - } - ); +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' + }); + + addAuthToken(fetchOptions); + return _preFetchFn(url, fetchOptions).then((result) => { - return fetch( - result.url, - result.options, - ).then(resp => { + return fetch(result.url, result.options).then((resp) => { fetchObserver.next(resp); if (opts.raw) { return resp; @@ -82,21 +79,29 @@ export const json = (url, method = "GET", opts = {}) => { }); }; - /** - * get - Get request + * addAuthToken - Add the JWT token to the fetch options headers if it exists in local storage * - * @param {string} url Url of request - * @param {object} [opts={}] Opts to add to request - * - * @return {Promise} A promise with the request's response + * @param {object} fetchOptions - The fetch options object + * @returns {object} the updated fetch options object */ +const addAuthToken = (fetchOptions) => { + const token = localStorage.getItem('fauxtonToken'); + if (token && jwtStillValid(token)) { + fetchOptions.headers = { + ...fetchOptions.headers, + Authorization: `Bearer ${token}` + }; + } + return fetchOptions; +}; + 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 +115,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 +133,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/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/package-lock.json b/package-lock.json index da511539f..ef0014d8d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13521,17 +13521,6 @@ "tslib": "^2.0.3" } }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/luxon": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.2.1.tgz", @@ -16836,12 +16825,9 @@ } }, "node_modules/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", - "dependencies": { - "lru-cache": "^6.0.0" - }, + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "bin": { "semver": "bin/semver.js" }, @@ -18886,11 +18872,6 @@ "node": ">=10" } }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, "node_modules/yaml": { "version": "1.10.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", @@ -29169,14 +29150,6 @@ "tslib": "^2.0.3" } }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "requires": { - "yallist": "^4.0.0" - } - }, "luxon": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.2.1.tgz", @@ -31652,12 +31625,9 @@ } }, "semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", - "requires": { - "lru-cache": "^6.0.0" - } + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==" }, "send": { "version": "0.18.0", @@ -33196,11 +33166,6 @@ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, "yaml": { "version": "1.10.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", diff --git a/package.json b/package.json index 019e3921c..2956f2820 100644 --- a/package.json +++ b/package.json @@ -146,4 +146,4 @@ ], "author": "The Apache CouchDB contributors", "license": "Apache-2.0" -} +} \ No newline at end of file From 8c5d66a83ab066e6164a40563d4beca7b403b361 Mon Sep 17 00:00:00 2001 From: "Stephan H. Wissel" Date: Mon, 29 Jul 2024 02:49:13 +0800 Subject: [PATCH 02/11] sample realm data --- test/idp_rsources/realm-export.json | 2255 +++++++++++++++++++++++++++ 1 file changed, 2255 insertions(+) create mode 100644 test/idp_rsources/realm-export.json 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 From 6642c3d991f78423925833ed7ea9b18dc9534cf0 Mon Sep 17 00:00:00 2001 From: "Stephan H. Wissel" Date: Mon, 29 Jul 2024 02:49:35 +0800 Subject: [PATCH 03/11] documentation update --- idp.md | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ readme.md | 33 ++++++++++++--------------------- 2 files changed, 62 insertions(+), 21 deletions(-) create mode 100644 idp.md diff --git a/idp.md b/idp.md new file mode 100644 index 000000000..30b7aae80 --- /dev/null +++ b/idp.md @@ -0,0 +1,50 @@ +# 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 +``` + +## 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. 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. + +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/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 From 0342dd9b1a42cb60af088a15d8d09fda48e3b28f Mon Sep 17 00:00:00 2001 From: "Stephan H. Wissel" Date: Mon, 29 Jul 2024 02:50:14 +0800 Subject: [PATCH 04/11] implement code flow --- app/addons/auth/components/loginformidp.js | 1 - app/addons/auth/idp.js | 54 ++++++++++++++++++++-- app/addons/auth/routes/auth.js | 16 ++----- 3 files changed, 53 insertions(+), 18 deletions(-) diff --git a/app/addons/auth/components/loginformidp.js b/app/addons/auth/components/loginformidp.js index 82973134d..2701e27ba 100644 --- a/app/addons/auth/components/loginformidp.js +++ b/app/addons/auth/components/loginformidp.js @@ -18,7 +18,6 @@ import { Button, Form } from 'react-bootstrap'; class LoginFormIdp extends React.Component { constructor() { super(); - // TODO: Read local IDP configuration this.state = { idpurl: localStorage.getItem('FauxtonIdpurl') || '', idpcallback: localStorage.getItem('FauxtonIdpcallback') || '', diff --git a/app/addons/auth/idp.js b/app/addons/auth/idp.js index 039561eb9..90b3fa21c 100644 --- a/app/addons/auth/idp.js +++ b/app/addons/auth/idp.js @@ -55,7 +55,7 @@ export const getExpiry = (token) => { }; export const login = (idpurl, idpcallback, idpappid) => { - const authUrl = `${idpurl}/auth?response_type=token&client_id=${idpappid}&redirect_uri=${idpcallback}`; + const authUrl = `${idpurl}/auth?response_type=code&client_id=${idpappid}&redirect_uri=${idpcallback}&scope=openid#idpresult`; window.location.href = authUrl; return Promise.resolve('Authentication initiated'); }; @@ -66,8 +66,51 @@ export const logout = () => { window.location.href = '/_session'; }; +export const codeToToken = (url) => { + const authCode = url.searchParams.get('code'); + if (authCode) { + const idpurl = localStorage.getItem('FauxtonIdpurl'); + const idpappid = localStorage.getItem('FauxtonIdpappid'); + const callback = localStorage.getItem('FauxtonIdpcallback'); + // eslint-disable-next-line no-debugger + debugger; + const authUrl = `${idpurl}/token`; + 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(() => { + refreshToken(); + }, (expiry - 60) * 1000); + return FauxtonAPI.navigate('/'); + }) + .catch((error) => { + console.error('Error refreshing token:', error); + FauxtonAPI.addNotification({ + msg: error.message, + type: 'error' + }); + }); + } else { + FauxtonAPI.addNotification({ + msg: 'No auth code found', + type: 'error' + }); + } +}; + export const refreshToken = () => { - const refreshToken = localStorage.getItem('fauxtonRefreshToken'); + const jwtRefreshToken = localStorage.getItem('fauxtonRefreshToken'); const idpurl = localStorage.getItem('FauxtonIdpurl'); const idpappid = localStorage.getItem('FauxtonIdpappid'); const authUrl = `${idpurl}/token`; @@ -76,14 +119,16 @@ export const refreshToken = () => { headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: `grant_type=refresh_token&refresh_token=${refreshToken}&client_id=${idpappid}` + 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); + setTimeout(() => { + refreshToken(); + }, (expiry - 60) * 1000); }) .catch((error) => { console.error('Error refreshing token:', error); @@ -98,6 +143,7 @@ export default { login, logout, refreshToken, + codeToToken, jwtStillValid, decodeToken, getExpiry diff --git a/app/addons/auth/routes/auth.js b/app/addons/auth/routes/auth.js index 443334daf..41bf615f2 100644 --- a/app/addons/auth/routes/auth.js +++ b/app/addons/auth/routes/auth.js @@ -29,7 +29,7 @@ export default FauxtonAPI.RouteObject.extend({ login: 'login', loginidp: 'loginidp', logout: 'logout', - 'session_state*': 'idpCallback', + idpresult: 'idpCallback', createAdmin: 'checkNodes', 'createAdmin/:node': 'createAdminForNode' }, @@ -47,18 +47,8 @@ export default FauxtonAPI.RouteObject.extend({ logout(); }, idpCallback() { - alert('idpCallback'); - const urlParams = new URLSearchParams(window.location.hash); - alert(window.location.hash); - const accessToken = urlParams.get('access_token'); - const refreshToken = urlParams.get('refresh_token'); - localStorage.setItem('fauxtonToken', accessToken); - localStorage.setItem('fauxtonRefreshToken', refreshToken); - - // Extract expiry from the access token - const expiry = Idp.getExpiry(accessToken); - console.log('Expiry:', expiry); - //setTimeout(Idp.refreshToken, (expiry - 60) * 1000); + const url = new URL(window.location.href); + Idp.codeToToken(url); }, createAdminForNode() { From 2ae3932ce51c8939b1bb3176d5e77d0aac5d8008 Mon Sep 17 00:00:00 2001 From: "Stephan H. Wissel" Date: Mon, 29 Jul 2024 02:51:58 +0800 Subject: [PATCH 05/11] CORS Hack --- devserver.js | 52 ++++++++++++++++---------------- index.js | 85 +++++++++++++++++++++++++++------------------------- 2 files changed, 71 insertions(+), 66 deletions(-) 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/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) { From 62500d7b27fb793083a4c73ed2e1ae740510dc84 Mon Sep 17 00:00:00 2001 From: "Stephan H. Wissel" Date: Mon, 29 Jul 2024 03:01:39 +0800 Subject: [PATCH 06/11] Update idp.md added screen shots --- idp.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/idp.md b/idp.md index 30b7aae80..6156f576d 100644 --- a/idp.md +++ b/idp.md @@ -38,7 +38,13 @@ 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. 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. +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 From a6b6110d5e00e501ea57feffe37c70ba88199ac6 Mon Sep 17 00:00:00 2001 From: "Stephan H. Wissel" Date: Mon, 29 Jul 2024 19:11:05 +0800 Subject: [PATCH 07/11] auto configure Keycloak (WIP) --- docker/admin_role.json | 5 ++ docker/couchdb-idp.sh | 108 +++++++++++++++++++++++++++++++++++++ docker/couchdb-idp.yml | 20 +++++++ docker/fauxton_client.json | 32 +++++++++++ docker/johndoe_role.json | 3 ++ docker/johndoe_user.json | 20 +++++++ docker/sofa_realm.json | 13 +++++ idp.md | 19 ++++++- 8 files changed, 219 insertions(+), 1 deletion(-) create mode 100644 docker/admin_role.json create mode 100755 docker/couchdb-idp.sh create mode 100644 docker/couchdb-idp.yml create mode 100644 docker/fauxton_client.json create mode 100644 docker/johndoe_role.json create mode 100644 docker/johndoe_user.json create mode 100644 docker/sofa_realm.json 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..fba767133 --- /dev/null +++ b/docker/couchdb-idp.sh @@ -0,0 +1,108 @@ +#!/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" + +# Part2: reusable functions +function kc_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' \ + --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" +} + +function kc_refresh() { + local url=${KC_URL}/realms/master/protocol/openid-connect/token + + curl $url \ + --header 'Content-Type: application/x-www-form-urlencoded' \ + --data-urlencode 'client_id=admin-cli' \ + --data-urlencode 'grant_type=refresh_token' \ + --data-urlencode 'refresh_token={{KEYCLOAK_REFRESH_TOKEN}}' + + # Post the form data and store the response + local response=$(curl $url \ + --header 'Content-Type: application/x-www-form-urlencoded' \ + --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" +} + +function kc_get() { + local url=${KC_URL}$1 + + # Post the form data and store the response + local response=$(curl $url \ + --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 posting to $url + # Post the form data and store the response + local response=$(curl -S -X POST $url \ + --header 'Content-Type: application/json' \ + --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 + adminrole=$(kc_get "/admin/realms/sofa/roles?first=0&max=101&q=_admin" | 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 +} + +# Part3: Launch containers +docker compose -f docker/couchdb-idp.yml up -d + +# Part4: Configure Keycloak +kc_config + +# Part5: Configure CouchDB diff --git a/docker/couchdb-idp.yml b/docker/couchdb-idp.yml new file mode 100644 index 000000000..366bf1a71 --- /dev/null +++ b/docker/couchdb-idp.yml @@ -0,0 +1,20 @@ +services: + couchdb: + container_name: couchdb + image: chouchdb: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_USER: admin + KEYCLOAK_PASSWORD: password + ports: + - "8090:8080" + command: start-dev 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..1d68c9308 --- /dev/null +++ b/docker/johndoe_role.json @@ -0,0 +1,3 @@ +[{"id": "3d4a4ecd-ce24-4483-a9a3-223d8dd46924", +"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/idp.md b/idp.md index 6156f576d..3713f3aa1 100644 --- a/idp.md +++ b/idp.md @@ -23,7 +23,7 @@ You need: ## 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. +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: @@ -32,6 +32,23 @@ For Keycloak, this works: 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 From 5119020b9fc18250304beaa164c361c233ca7909 Mon Sep 17 00:00:00 2001 From: "Stephan H. Wissel" Date: Tue, 30 Jul 2024 22:35:09 +0800 Subject: [PATCH 08/11] Script to create base config --- app/addons/auth/idp.js | 2 +- docker/couchdb-idp.sh | 121 ++++++++++++++++++++++++++-- docker/couchdb-idp.yml | 6 +- docker/extractpem.js | 16 ++++ docker/johndoe_role.json | 2 +- package-lock.json | 169 +++++++++++++++++++++++++++++++++++++++ package.json | 3 +- 7 files changed, 305 insertions(+), 14 deletions(-) create mode 100644 docker/extractpem.js diff --git a/app/addons/auth/idp.js b/app/addons/auth/idp.js index 90b3fa21c..ecfae20af 100644 --- a/app/addons/auth/idp.js +++ b/app/addons/auth/idp.js @@ -42,7 +42,7 @@ export const decodeToken = (token) => { try { const base64Url = token.split('.')[1]; const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); - const jsonPayload = Buffer.from(base64, 'base64'); //.toString('utf-8'); + const jsonPayload = atob(base64); return JSON.parse(jsonPayload); } catch (error) { return null; diff --git a/docker/couchdb-idp.sh b/docker/couchdb-idp.sh index fba767133..e1e7573eb 100755 --- a/docker/couchdb-idp.sh +++ b/docker/couchdb-idp.sh @@ -5,14 +5,18 @@ # 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' \ @@ -25,20 +29,17 @@ function kc_tokens() { # 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 - - curl $url \ - --header 'Content-Type: application/x-www-form-urlencoded' \ - --data-urlencode 'client_id=admin-cli' \ - --data-urlencode 'grant_type=refresh_token' \ - --data-urlencode 'refresh_token={{KEYCLOAK_REFRESH_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') @@ -50,13 +51,17 @@ function kc_refresh() { # 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 - # Post the form data and store the response + # Get an URL and store the response + echo GET from $url local response=$(curl $url \ + --no-progress-meter \ --header 'Content-Type: application/json' \ --header "Authorization: Bearer $KC_ACCESS_TOKEN") echo $response @@ -65,10 +70,11 @@ function kc_get() { function kc_post() { local url=${KC_URL}$1 local data=$2 - echo posting to $url + echo POST to $url # 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 @@ -99,10 +105,109 @@ function kc_config() { 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/ >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 index 366bf1a71..fd3186e95 100644 --- a/docker/couchdb-idp.yml +++ b/docker/couchdb-idp.yml @@ -1,7 +1,7 @@ services: couchdb: container_name: couchdb - image: chouchdb:latest + image: couchdb:latest environment: COUCHDB_USER: tester COUCHDB_PASSWORD: testerpass @@ -13,8 +13,8 @@ services: container_name: keycloak image: quay.io/keycloak/keycloak:latest environment: - KEYCLOAK_USER: admin - KEYCLOAK_PASSWORD: password + 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/johndoe_role.json b/docker/johndoe_role.json index 1d68c9308..589181b71 100644 --- a/docker/johndoe_role.json +++ b/docker/johndoe_role.json @@ -1,3 +1,3 @@ -[{"id": "3d4a4ecd-ce24-4483-a9a3-223d8dd46924", +[{"id": "1dd523e5-90be-44d9-8a63-964d382ec6f4", "name": "_admin", "description": "CouchDB Administrator", "composite": false,"clientRole": false,"containerId": "sofa"}] diff --git a/package-lock.json b/package-lock.json index ef0014d8d..33bb1affd 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", @@ -5085,6 +5086,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", @@ -5597,6 +5610,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.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", @@ -5792,6 +5811,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", @@ -7124,6 +7149,27 @@ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.286.tgz", "integrity": "sha512-Vp3CVhmYpgf4iXNKAucoQUDcCrBQX3XLBtwgFqP9BUXuucgvAV9zWp1kYU7LL9j4++s9O+12cb3wMtN4SJy6UQ==" }, + "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", @@ -9799,6 +9845,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", @@ -9819,6 +9875,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", @@ -13126,6 +13193,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", @@ -13770,6 +13848,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", @@ -22876,6 +22960,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", @@ -23267,6 +23363,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.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", @@ -23411,6 +23513,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", @@ -24398,6 +24506,29 @@ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.286.tgz", "integrity": "sha512-Vp3CVhmYpgf4iXNKAucoQUDcCrBQX3XLBtwgFqP9BUXuucgvAV9zWp1kYU7LL9j4++s9O+12cb3wMtN4SJy6UQ==" }, + "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", @@ -26372,6 +26503,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", @@ -26386,6 +26527,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", @@ -28812,6 +28964,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", @@ -29337,6 +29500,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 2956f2820..34c4c079e 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", @@ -146,4 +147,4 @@ ], "author": "The Apache CouchDB contributors", "license": "Apache-2.0" -} \ No newline at end of file +} From b6603b19689d3812e7b8b99902f6547e01b85325 Mon Sep 17 00:00:00 2001 From: "Stephan H. Wissel" Date: Thu, 1 Aug 2024 19:35:44 +0800 Subject: [PATCH 09/11] Fix echo bug --- docker/couchdb-idp.sh | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docker/couchdb-idp.sh b/docker/couchdb-idp.sh index e1e7573eb..7e8c2b382 100755 --- a/docker/couchdb-idp.sh +++ b/docker/couchdb-idp.sh @@ -59,7 +59,7 @@ function kc_get() { local url=${KC_URL}$1 # Get an URL and store the response - echo GET from $url + echo GET from $url >&2 local response=$(curl $url \ --no-progress-meter \ --header 'Content-Type: application/json' \ @@ -70,7 +70,7 @@ function kc_get() { function kc_post() { local url=${KC_URL}$1 local data=$2 - echo POST to $url + 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' \ @@ -89,7 +89,8 @@ function kc_config() { # Create _admin Role kc_post "/admin/realms/sofa/roles" admin_role.json - adminrole=$(kc_get "/admin/realms/sofa/roles?first=0&max=101&q=_admin" | jq -r .[0].id) + 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 @@ -142,7 +143,7 @@ function couch_put() { } # Part3: Launch containers -docker compose -f docker/couchdb-idp.yml pull +# 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 @@ -152,7 +153,7 @@ curl -k \ --retry-all-errors \ --no-progress-meter \ --fail \ - ${KC_URL}/admin/master/console/ >null + ${KC_URL}/admin/master/console/ >/dev/null if [ "$?" -ne 0 ]; then echo "Failed to start Keycloak" From e858c5f4db2f2cdc1d15c65668bda81d8c2ed9fb Mon Sep 17 00:00:00 2001 From: "Stephan H. Wissel" Date: Thu, 1 Aug 2024 19:36:36 +0800 Subject: [PATCH 10/11] switched to oicd compliant url retrieval --- app/addons/auth/components/loginformidp.js | 27 ++- app/addons/auth/idp.js | 199 +++++++++++++++++---- 2 files changed, 182 insertions(+), 44 deletions(-) diff --git a/app/addons/auth/components/loginformidp.js b/app/addons/auth/components/loginformidp.js index 2701e27ba..012bb7ec4 100644 --- a/app/addons/auth/components/loginformidp.js +++ b/app/addons/auth/components/loginformidp.js @@ -23,6 +23,11 @@ class LoginFormIdp extends React.Component { 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) { @@ -76,8 +81,11 @@ class LoginFormIdp extends React.Component {
-
- + +

+ must point to your IdP's /.well-known/openid-configuration +

+
-
+ +

+ This should be the URL of your CouchDB instance, including the protocol and port number.{' '} + Should we show this? It can be computed +

+
+ +

+ The Application ID gets assigned by the IdP admin, suggested standard is fauxton +

-
+
diff --git a/app/addons/auth/idp.js b/app/addons/auth/idp.js index ecfae20af..dfee51c36 100644 --- a/app/addons/auth/idp.js +++ b/app/addons/auth/idp.js @@ -12,6 +12,72 @@ 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 * @@ -29,7 +95,8 @@ export const jwtStillValid = (token) => { } const currentTime = Math.floor(Date.now() / 1000); - return decodedToken.exp > currentTime; + let isStillgood = decodedToken.exp > currentTime; + return isStillgood; }; /** @@ -55,15 +122,26 @@ export const getExpiry = (token) => { }; export const login = (idpurl, idpcallback, idpappid) => { - const authUrl = `${idpurl}/auth?response_type=code&client_id=${idpappid}&redirect_uri=${idpcallback}&scope=openid#idpresult`; - window.location.href = authUrl; - return Promise.resolve('Authentication initiated'); + 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) => { @@ -72,16 +150,16 @@ export const codeToToken = (url) => { const idpurl = localStorage.getItem('FauxtonIdpurl'); const idpappid = localStorage.getItem('FauxtonIdpappid'); const callback = localStorage.getItem('FauxtonIdpcallback'); - // eslint-disable-next-line no-debugger - debugger; - const authUrl = `${idpurl}/token`; - 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}` - }) + 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; @@ -90,6 +168,8 @@ export const codeToToken = (url) => { 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('/'); @@ -97,7 +177,7 @@ export const codeToToken = (url) => { .catch((error) => { console.error('Error refreshing token:', error); FauxtonAPI.addNotification({ - msg: error.message, + msg: `Error refreshing token: ${error.message}`, type: 'error' }); }); @@ -113,30 +193,69 @@ export const refreshToken = () => { const jwtRefreshToken = localStorage.getItem('fauxtonRefreshToken'); const idpurl = localStorage.getItem('FauxtonIdpurl'); const idpappid = localStorage.getItem('FauxtonIdpappid'); - const authUrl = `${idpurl}/token`; - 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: error.message, - type: 'error' - }); + 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 { @@ -146,5 +265,7 @@ export default { codeToToken, jwtStillValid, decodeToken, - getExpiry + getExpiry, + addAuthToken, + addAuthHeader }; From 16786501084135430f3e6478dd09e6d32b129c0f Mon Sep 17 00:00:00 2001 From: "Stephan H. Wissel" Date: Thu, 1 Aug 2024 19:37:28 +0800 Subject: [PATCH 11/11] Add auth headers everwhere --- app/addons/documents/doc-editor/actions.js | 180 +++++++++++---------- app/core/ajax.js | 23 +-- app/core/api.js | 46 ++++-- docker/johndoe_role.json | 2 +- 4 files changed, 130 insertions(+), 121 deletions(-) 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 c4e3bf122..1ae03b031 100644 --- a/app/core/ajax.js +++ b/app/core/ajax.js @@ -1,7 +1,7 @@ import 'whatwg-fetch'; import { defaultsDeep } from 'lodash'; import { Subject } from 'rxjs'; -import { jwtStillValid } from '../addons/auth/idp'; +import { addAuthToken } from '../addons/auth/idp'; /* Add a multicast observer so that all fetch requests can be observed Some usage examples: @@ -66,9 +66,9 @@ export const json = (url, method = 'GET', opts = {}) => { cache: 'no-cache' }); - addAuthToken(fetchOptions); + const updatedFetchOptions = addAuthToken(fetchOptions); - return _preFetchFn(url, fetchOptions).then((result) => { + return _preFetchFn(url, updatedFetchOptions).then((result) => { return fetch(result.url, result.options).then((resp) => { fetchObserver.next(resp); if (opts.raw) { @@ -79,23 +79,6 @@ export const json = (url, method = 'GET', opts = {}) => { }); }; -/** - * 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 - */ -const addAuthToken = (fetchOptions) => { - const token = localStorage.getItem('fauxtonToken'); - if (token && jwtStillValid(token)) { - fetchOptions.headers = { - ...fetchOptions.headers, - Authorization: `Bearer ${token}` - }; - } - return fetchOptions; -}; - export const get = (url, opts = {}) => { return json(url, 'GET', 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/docker/johndoe_role.json b/docker/johndoe_role.json index 589181b71..5dbbb1740 100644 --- a/docker/johndoe_role.json +++ b/docker/johndoe_role.json @@ -1,3 +1,3 @@ -[{"id": "1dd523e5-90be-44d9-8a63-964d382ec6f4", +[{"id": "29dc4a00-c700-432a-909c-a6fb02556b0c", "name": "_admin", "description": "CouchDB Administrator", "composite": false,"clientRole": false,"containerId": "sofa"}]