Skip to content

Commit

Permalink
Merge pull request #5067 from camptocamp/GSGMF-1078
Browse files Browse the repository at this point in the history
2FA auth support
  • Loading branch information
fredj committed Oct 24, 2019
2 parents 193489f + 0c480f7 commit a6ac2b5
Show file tree
Hide file tree
Showing 8 changed files with 100 additions and 16 deletions.
6 changes: 3 additions & 3 deletions buildtools/check-example.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,11 @@ function loaded(page, browser) {
request.respond(ASITVDCapabilities);
} else if (parse(url).host == parse(page_url).host ||
url.startsWith('http://localhost:3000/') ||
url.startsWith('https://geomapfish-demo-2-5.camptocamp.com/') ||
url.startsWith('https://geomapfish-demo') ||
url.startsWith('https://wmts.geo.admin.ch/') ||
url.startsWith('https://wms.geo.admin.ch/')) {
requestsURL.add(url);
if (url.startsWith('https://geomapfish-demo-2-5.camptocamp.com/')) {
if (url.startsWith('https://geomapfish-demo')) {
request.headers().origin = 'http://localhost:3000';
}
request.continue();
Expand All @@ -131,7 +131,7 @@ function loaded(page, browser) {
const url = request.url();
requestsURL.delete(url);
loaded(page, browser);
if (ci && url.startsWith('https://geomapfish-demo-2-5.camptocamp.com/') &&
if (ci && url.startsWith('https://geomapfish-demo') &&
request.headers()['sec-fetch-mode'] == 'cors' &&
request.response().headers()['access-control-allow-origin'] == undefined) {
console.log(`CORS error on: ${url}`);
Expand Down
13 changes: 12 additions & 1 deletion contribs/gmf/apps/desktop_alt/index.html.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<title ng-bind-template="{{'Alternative Desktop Application'|translate}}">GeoMapFish</title>
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1.0, user-scalable=no, width=device-width">
<meta name="dynamicUrl" content="https://geomapfish-demo-2-5.camptocamp.com/dynamic.json">
<meta name="dynamicUrl" content="https://geomapfish-demo-2-5-2fa.paas-ch-3.camptocamp.com/dynamic.json">
<meta name="interface" content="desktop_alt">
<link rel="shortcut icon" href="<%=require("./image/favicon.ico")%>" />
<% for (var css in htmlWebpackPlugin.files.css) { %>
Expand Down Expand Up @@ -352,6 +352,17 @@
</div>
</div>
</div>
<ngeo-modal ngeo-modal-closable="false" ng-model="mainCtrl.userMustChangeItsPassword">
<div class="modal-header ui-draggable-handle">
<h4 class="modal-title">
{{'You must change your password' | translate}}
</h4>
</div>
<div class="modal-body">
<gmf-authentication gmf-authentication-force-password-change="::true"></gmf-authentication>
</div>
</ngeo-modal>

<ngeo-modal ng-model="mainCtrl.modalShareShown">
<gmf-share ng-if="mainCtrl.modalShareShown" gmf-share-email="false"></gmf-share>
</ngeo-modal>
Expand Down
24 changes: 17 additions & 7 deletions contribs/gmf/src/authentication/Service.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import * as Sentry from '@sentry/browser';
* False otherwise.
* @property {RoleInfo[]} roles Roles information.
* @property {string|null} username The name of the user.
* @property {string|null} otp_key
* @property {string|null} otp_uri
*/


Expand All @@ -50,6 +52,8 @@ import * as Sentry from '@sentry/browser';
* @property {boolean} [is_password_changed]
* @property {RoleInfo[]} [roles]
* @property {string} [username]
* @property {string} [otp_key]
* @property {string} [otp_uri]
*/


Expand Down Expand Up @@ -154,33 +158,42 @@ export class AuthenticationService extends olEventsEventTarget {
* @param {string} oldPwd Old password.
* @param {string} newPwd New password.
* @param {string} confPwd New password confirmation.
* @param {string} [otp]
* @return {angular.IPromise<void>} Promise.
*/
changePassword(login, oldPwd, newPwd, confPwd) {
changePassword(login, oldPwd, newPwd, confPwd, otp = undefined) {
const url = `${this.baseUrl_}/${RouteSuffix.CHANGE_PASSWORD}`;

return this.$http_.post(url, $.param({
'login': login,
'oldPassword': oldPwd,
'otp': otp,
'newPassword': newPwd,
'confirmNewPassword': confPwd
}), {
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
withCredentials: true
}).then(((response) => {
}).then((() => {
this.user_.is_password_changed = true;
const event = new ngeoCustomEvent('ready', {user: this.user_});
this.dispatchEvent(event);
}));
}

/**
* @param {string} login Login name.
* @param {string} pwd Password.
* @param {string} [otp]
* @return {angular.IPromise<angular.IHttpResponse<AuthenticationLoginResponse>>} Promise.
*/
login(login, pwd) {
login(login, pwd, otp = undefined) {
const url = `${this.baseUrl_}/${RouteSuffix.LOGIN}`;
const params = {'login': login, 'password': pwd};
if (otp) {
Object.assign(params, {'otp': otp});
}

return this.$http_.post(url, $.param({'login': login, 'password': pwd}), {
return this.$http_.post(url, $.param(params), {
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
withCredentials: true
}).then(
Expand Down Expand Up @@ -241,7 +254,6 @@ export class AuthenticationService extends olEventsEventTarget {
handleLogin_(checkingLoginStatus, resp) {
this.setUser_(resp.data, !checkingLoginStatus);
if (checkingLoginStatus) {
/** @type {AuthenticationEvent} */
const event = new ngeoCustomEvent('ready', {user: this.user_});
this.dispatchEvent(event);
}
Expand All @@ -267,7 +279,6 @@ export class AuthenticationService extends olEventsEventTarget {
this.user_[key] = respData[key];
}
if (emitEvent && respData.username !== undefined) {
/** @type {AuthenticationEvent} */
const event = new ngeoCustomEvent('login', {user: this.user_});
this.dispatchEvent(event);
}
Expand All @@ -284,7 +295,6 @@ export class AuthenticationService extends olEventsEventTarget {
// @ts-ignore: unsupported syntax
this.user_[key] = null;
}
/** @type {AuthenticationEvent} */
const event = new ngeoCustomEvent('logout', {user: this.user_});
this.dispatchEvent(event);
if (!noReload) {
Expand Down
28 changes: 27 additions & 1 deletion contribs/gmf/src/authentication/component.html
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,24 @@
ng-model="$ctrl.newPwdConfVal"
ng-attr-placeholder="{{'Confirm new password' | translate}}"/>
</div>
<div class="form-group">
<div ng-if="$ctrl.gmfUser.otp_uri" class="form-group">
<label translate>Two factor authentication barcode:</label>
<div><img class="" ng-src="{{$ctrl.otpImage}}"></div>
</div>
<div ng-if="$ctrl.gmfUser.two_factor_totp_secret" class="form-group">
<label translate>Two factor authentication key:</label>
<code>{{$ctrl.gmfUser.two_factor_totp_secret}}</code>
</div>
<div ng-if="$ctrl.twoFactorAuth" class="form-group">
<input
type="text"
autocomplete="off"
class="form-control"
name="otp"
ng-model="$ctrl.otpVal"
ng-attr-placeholder="{{'Authentication code' | translate}}"/>
</div>
<div class="form-group">
<input
type="submit"
class="form-control btn prime"
Expand Down Expand Up @@ -116,6 +133,15 @@
ng-model="$ctrl.pwdVal"
ng-attr-placeholder="{{'Password' | translate}}"/>
</div>
<div ng-if="$ctrl.twoFactorAuth" class="form-group">
<input
type="text"
autocomplete="off"
class="form-control"
name="otp"
ng-model="$ctrl.otpVal"
ng-attr-placeholder="{{'Authentication code' | translate}}"/>
</div>
<div class="form-group">
<input
type="submit"
Expand Down
39 changes: 35 additions & 4 deletions contribs/gmf/src/authentication/component.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import angular from 'angular';
import gmfAuthenticationService from 'gmf/authentication/Service.js';
import {MessageType} from 'ngeo/message/Message.js';
import ngeoMessageNotification from 'ngeo/message/Notification.js';

import ngeoMessageModalComponent from 'ngeo/message/modalComponent.js';

import qruri from 'qruri';

/**
* Password validator function with an error message.
Expand Down Expand Up @@ -145,7 +145,9 @@ module.component('gmfAuthentication', authenticationComponent);
*/
class AuthenticationController {
/**
* @param {angular.IScope} $scope Scope.
* @param {JQuery} $element Element.
* @param {boolean} gmfTwoFactorAuth Two factor authentication is required.
* @param {angular.gettext.gettextCatalog} gettextCatalog Gettext catalog.
* @param {import("gmf/authentication/Service.js").AuthenticationService} gmfAuthenticationService
* GMF Authentication service
Expand All @@ -156,7 +158,8 @@ class AuthenticationController {
* @ngdoc controller
* @ngname GmfAuthenticationController
*/
constructor($element, gettextCatalog, gmfAuthenticationService, gmfUser, ngeoNotification) {
constructor($scope, $element, gmfTwoFactorAuth, gettextCatalog, gmfAuthenticationService,
gmfUser, ngeoNotification) {

/**
* @type {JQuery}
Expand Down Expand Up @@ -187,6 +190,11 @@ class AuthenticationController {
*/
this.notification_ = ngeoNotification;

/**
* @type {boolean}
*/
this.twoFactorAuth = gmfTwoFactorAuth;

/**
* @type {boolean}
*/
Expand Down Expand Up @@ -244,6 +252,11 @@ class AuthenticationController {
*/
this.pwdVal = '';

/**
* @type {string}
*/
this.otpVal;

// CHANGE PASSWORD form values

/**
Expand All @@ -260,6 +273,20 @@ class AuthenticationController {
* @type {string}
*/
this.newPwdConfVal = '';

this.otpImage;

$scope.$watch(
() => this.gmfUser.otp_uri,
(val) => {
if (val) {
this.otpImage = qruri(val, {
margin: 2
});
}
}
);

}

/**
Expand Down Expand Up @@ -321,7 +348,8 @@ class AuthenticationController {
this.setError_(errors);
} else {
// Send request with current credentials, which may fail if the old password given is incorrect.
this.gmfAuthenticationService_.changePassword(this.gmfUser.username, oldPwd, newPwd, confPwd)
const username = this.gmfUser.username;
this.gmfAuthenticationService_.changePassword(username, oldPwd, newPwd, confPwd, this.otpVal)
.then(() => {
this.changePasswordReset();
this.setError_(
Expand All @@ -331,6 +359,7 @@ class AuthenticationController {
})
.catch((err) => {
this.oldPwdVal = '';
this.otpVal = '';
this.setError_(gettextCatalog.getString('Incorrect old password.'));
});
}
Expand All @@ -353,14 +382,16 @@ class AuthenticationController {
if (errors.length) {
this.setError_(errors);
} else {
this.gmfAuthenticationService_.login(this.loginVal, this.pwdVal)
this.gmfAuthenticationService_.login(this.loginVal, this.pwdVal, this.otpVal)
.then(() => {
this.loginVal = '';
this.pwdVal = '';
this.otpVal = '';
this.resetError_();
})
.catch(() => {
this.pwdVal = '';
this.otpVal = '';
this.setError_(gettextCatalog.getString('Incorrect credentials or disabled account.'));
});
}
Expand Down
4 changes: 4 additions & 0 deletions contribs/gmf/src/controllers/AbstractAppController.js
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,10 @@ export function AbstractAppController(config, map, $scope, $injector) {
* @param {Event|import('ol/events/Event.js').default} evt Event.
*/
const userChange = (evt) => {
// password change is in progress, don't reload the theme yet.
if (this.gmfUser.is_password_changed === false) {
return;
}
if (this.loginRedirectUrl) {
window.location.href = this.loginRedirectUrl;
return;
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@
"popper.js": "~1.16.0",
"proj4": "~2.5.0",
"puppeteer": "~1.20.0",
"qruri": "0.0.4",
"resize-observer-polyfill": "~1.5.1",
"simple-html-tokenizer": "~0.5.7",
"sinon": "~7.5.0",
Expand Down
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"jsts/*": ["node_modules/@types/jsts/index.d.ts"],
"localforage/*": ["node_modules/localforage/*"],
"resize-observer-polyfill": ["node_modules/resize-observer-polyfill/src/index.d.ts"],
"qruri": ["node_modules/qruri/index.js"],
}
},
"include": [
Expand Down

0 comments on commit a6ac2b5

Please sign in to comment.