Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for IdP AIuthentication [Do not merge yet - Feedback requested] #1465

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions app/addons/auth/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ 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,
Expand Down Expand Up @@ -41,6 +42,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 +71,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