Skip to content

Commit

Permalink
Add Idp support to Fauxton, solves apache#1457
Browse files Browse the repository at this point in the history
  • Loading branch information
Stwissel committed Oct 23, 2024
1 parent a6a11a0 commit df80397
Show file tree
Hide file tree
Showing 25 changed files with 3,345 additions and 12 deletions.
25 changes: 25 additions & 0 deletions app/addons/auth/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ export const validatePasswords = (password, passwordConfirm) => {
);
};

export const validateIdP = (idpurl, idpcallback, idpappid) => {
return validate(!_.isEmpty(idpurl), !_.isEmpty(idpcallback), !_.isEmpty(idpappid));
};

export const login = (username, password, urlBack) => {
if (!validateUser(username, password)) {
return errorHandler({message: app.i18n.en_US['auth-missing-credentials']});
Expand All @@ -66,6 +70,27 @@ export const login = (username, password, urlBack) => {
.catch(errorHandler);
};

export const loginidp = (idpurl, idpcallback, idpappid) => {
if (!validateIdP(idpurl, idpcallback, idpappid)) {
return errorHandler({ message: app.i18n.en_US['auth-missing-idp'] });
}
return Idp.login(idpurl, idpcallback, idpappid)
.then((resp) => {
if (resp.error) {
errorHandler({ message: resp.reason });
return resp;
}

let msg = app.i18n.en_US['auth-logged-in'];
if (msg) {
FauxtonAPI.addNotification({ msg });
}

FauxtonAPI.navigate('/');
})
.catch(errorHandler);
};

export const changePassword = (username, password, passwordConfirm, nodes) => () => {
if (!validatePasswords(password, passwordConfirm)) {
return errorHandler({message: app.i18n.en_US['auth-passwords-not-matching']});
Expand Down
2 changes: 2 additions & 0 deletions app/addons/auth/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions app/addons/auth/components/loginform.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import PropTypes from 'prop-types';

import React from "react";
import FauxtonAPI from '../../../core/base';
import { login } from "./../actions";
import { Button, Form } from 'react-bootstrap';

Expand Down Expand Up @@ -57,6 +58,12 @@ class LoginForm extends React.Component {
login(username, password) {
login(username, password, this.props.urlBack);
}

navigateToIdp(e) {
e.preventDefault();
FauxtonAPI.navigate('/loginidp');
}

componentDidMount() {
this.usernameField.focus();
}
Expand Down Expand Up @@ -97,6 +104,13 @@ class LoginForm extends React.Component {
</div>
</div>
</form>
<div className="row">
<div className="col12 col-md-5 col-xl-4 mb-3">
<Button id="login-idp-btn" variant="cf-secondary" onClick={this.navigateToIdp}>
Log In with your Identity Provider
</Button>
</div>
</div>
</div>
);
}
Expand Down
155 changes: 155 additions & 0 deletions app/addons/auth/components/loginformidp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.

import FauxtonAPI from '../../../core/base';
import React from 'react';
import { loginidp } from './../actions';
import { Button, Form } from 'react-bootstrap';

class LoginFormIdp extends React.Component {
constructor() {
super();
this.state = {
idpurl: localStorage.getItem('FauxtonIdpurl') || '',
idpcallback: localStorage.getItem('FauxtonIdpcallback') || '',
idpappid: localStorage.getItem('FauxtonIdpappid') || ''
};
if (this.state.idpcallback === '') {
let url = new URL(window.location);
let append = url.pathname.startsWith('/_utils') ? '/_utils/' : '/';
this.state.idpcallback = window.location.origin + append;
}
}

onIdpurlChange(e) {
this.setState({ idpurl: e.target.value });
}
onIdpcallbackChange(e) {
this.setState({ idpcallback: e.target.value });
}

onIdpappidChange(e) {
this.setState({ idpappid: e.target.value });
}

submit(e) {
e.preventDefault();
if (!this.checkUnrecognizedAutoFill()) {
this.login(this.state.idpurl, this.state.idpcallback, this.state.idpappid);
}
}

// Safari has a bug where autofill doesn't trigger a change event. This checks for the condition where the state
// and form fields have a mismatch. See: https://issues.apache.org/jira/browse/COUCHDB-2829
checkUnrecognizedAutoFill() {
if (this.state.idpurl !== '' || this.state.idpcallback !== '' || this.state.idpappid !== '') {
return false;
}
let idpurl = this.props.testBlankIdpurl ? this.props.testBlankIdpurl : this.idpurlField.value;
let idpcallback = this.props.testBlankIdpcallback ? this.props.testBlankIdpcallback : this.idpcallbackField.value;
let idpappid = this.props.testBlankIdpappid ? this.props.testBlankIdpappid : this.idpappidField.value;

this.setState({ idpurl: idpurl, idpcallback: idpcallback, idpappid: idpappid }); // doesn't set immediately, hence separate login() call
this.login(idpurl, idpcallback, idpappid);

return true;
}

login(idpurl, idpcallback, idpappid) {
localStorage.setItem('FauxtonIdpurl', idpurl);
localStorage.setItem('FauxtonIdpcallback', idpcallback);
localStorage.setItem('FauxtonIdpappid', idpappid);
loginidp(idpurl, idpcallback, idpappid);
}

navigateToLogin(e) {
e.preventDefault();
FauxtonAPI.navigate('/login');
}

render() {
return (
<div className="couch-login-wrapper">
<form id="login" onSubmit={this.submit.bind(this)}>
<div className="row">
<label htmlFor="idpurl">Identity Provider (IdP) URL</label>
<p className="help-block">
must point to your IdP&apos;s <code>/.well-known/openid-configuration</code>
</p>
<div className="col12 col-md-10 col-xl-8 mb-3">
<Form.Control
type="text"
id="idpurl"
name="idpurl"
ref={(node) => (this.idpurlField = node)}
placeholder="IdP URL"
onChange={this.onIdpurlChange.bind(this)}
value={this.state.idpurl}
/>
</div>
</div>
<div className="row">
<label htmlFor="idpcallback">Callback URL</label>
<p className="help-block">
This should be the URL of your CouchDB instance, including the protocol and port number.{' '}
<span style={{ color: 'red' }}>Should we show this? It can be computed</span>
</p>
<div className="col12 col-md-5 col-xl-4 mb-3">
<Form.Control
type="text"
id="idpcallback"
name="idpcallback"
ref={(node) => (this.idpcallbackField = node)}
placeholder="Callback URL"
onChange={this.onIdpcallbackChange.bind(this)}
value={this.state.idpcallback}
/>
</div>
</div>
<div className="row">
<label htmlFor="idpappid">Application ID</label>
<p className="help-block">
The Application ID gets assigned by the IdP admin, suggested standard is <code>fauxton</code>
</p>
<div className="col12 col-md-5 col-xl-4 mb-3">
<Form.Control
type="text"
id="idpappid"
name="idpappid"
ref={(node) => (this.idpappidField = node)}
placeholder="Applicaiton ID"
onChange={this.onIdpappidChange.bind(this)}
value={this.state.idpappid}
/>
</div>
</div>
<div className="row">
<div className="col12 col-md-5 col-xl-4 mb-3">
<Button id="login-btn" variant="cf-primary" type="submit">
Log In
</Button>
</div>
</div>
</form>
<div className="row">
<div className="col12 col-md-5 col-xl-4 mb-3">
<Button id="login-creds-btn" variant="cf-secondary" onClick={this.navigateToLogin}>
Back to - Log In with CouchDB Credentials
</Button>
</div>
</div>
</div>
);
}
}

export default LoginFormIdp;
Loading

0 comments on commit df80397

Please sign in to comment.