diff --git a/api-docs/apiLive.htm b/api-docs/apiLive.htm
index bc0c43e2d..9a87171cc 100644
--- a/api-docs/apiLive.htm
+++ b/api-docs/apiLive.htm
@@ -2629,6 +2629,30 @@
|
+ | Two-Factor Authentication |
+ twofactor |
+ Request OTP |
+ List OTP delivery methods |
+ |
+ |
+
+
+ |
+ twofactor/validate |
+ Validate OTP |
+ |
+ |
+ |
+
+
+ |
+ twofactor/invalidate |
+ Invalidate Access Token |
+ |
+ |
+ |
+
+
| User |
users |
Create a User |
@@ -2942,6 +2966,16 @@
external service Configuration
|
+
+ | Two-Factor Configuration |
+ twofactor/configure |
+ |
+ Retrieve
+ Two-Factor Configuration |
+ Update
+ Two-Factor Configuration |
+ |
+
@@ -3930,6 +3964,11 @@
Default authentication is using HTTP Basic Auth. Oauth2 can be enabled by using -Psecurity=oauth option on gradle build command , refer
the platform setup wiki for additional details.
+
+ Optionally, two-factor authentication can be enabled by using
+ -Ptwofactor=enabled on gradle build.
+ Details of the authentication workflow with two-factor authentication enabled can be found here.
+
The platform has been configured to reject plain HTTP requests and
to expect all API requests to be made over
+
+
+
+
Two-Factor Authentication
+
+ Two-Factor authentication is supported by requesting & verifying
+ one-time passwords(OTP). OTPs are sent via SMS & email.
+
+
+ By default, two-factor authentication is disabled by default.
+ More information on how to enable TFA can be found here.
+
+
+ Two-factor authentication workflow:
+
+ - User authticates via BasicAuth / oAauth
+ - Client requests a list of supported OTP delivery methods for the authenticated user(Get Delivery Methods)
+ - User selects an OTP delivery method and client sends a request for OTP(Request OTP)
+ - User receives an OTP and the client sends it for verification(Validate OTP)
+ - If the OTP is valid, an access token is returned
+ - The access token is sent in following requestes to the server as a header Fineract-Platform-TFA-Token
+ - On session end, the access token should be invalidatedInvalidate Access Token)
+
+
+
+ Two-Factor authentication and delivery methods can be configured via
+ the /twofactor/configure endpoint.
+
+
+
+
+
+
+
+
+
+
+
Get Delivery Methods
+
Returns a list of possible OTP delivery methods for the current user
+
Requires first-factor authenticated user.
+
+
+ GET https://DomainName/api/v1/twofactor
+
+[
+ {
+ "name": "sms",
+ "target": "08888888888"
+ },
+ {
+ "name": "email",
+ "target": "user@example.com"
+ }
+]
+
+
+
+
+
+
+
Request OTP
+
Requests an OTP.
+
Requires first-factor authenticated user.
+
Arguments
+
+ - deliveryMethod
+ -
+ String mandatory, the delivery method name
+
+ - extendedToken
+ -
+ boolean optional, whether to request an extended token, default false
+
+
+
+
+ POST https://DomainName/api/v1/twofactor?deliveryMethod=sms&extendedToken=false
+
+{
+ "requestTime": 1500000000000,
+ "tokenLiveTimeInSec": 300,
+ "extendedAccessToken": false,
+ "deliveryMethod": {
+ "name": "sms",
+ "target": "08888888888"
+ }
+}
+
+
+
+
+
+
+
Validate OTP
+
Validates an OTP. If the OTP is valid, an access token is created.
+
The returned access token is later sent as a header Fineract-Platform-TFA-Token.
+
Requires first-factor authenticated user.
+
Arguments
+
+ - token
+ -
+ String mandatory, the OTP to validate
+
+
+
+
+ POST https://DomainName/api/v1/twofactor/validate?token=YYYYY
+
+{
+ "token": "cb0bb6e33fc540709d50a16eb2e555f9",
+ "validFrom": 1501530702801,
+ "validTo": 1501617102801
+}
+
+
+
+
+
+
+
Invalidate Access Token
+
Invalidates an access token.
+
Two factor access tokens should be invalidated on logout.
+
Requires fully authenticated user.
+
+
+ POST https://DomainName/api/v1/twofactor/invalidate
+
+{
+ "token": "cb0bb6e33fc540709d50a16eb2e555f9"
+}
+
+{
+ "resourceIdentifier": "cb0bb6e33fc540709d50a16eb2e555f9"
+}
+
+
@@ -19391,6 +19566,73 @@ Content-Type: application/json
+
+
+
+
Two-Factor Configuration
+
The following section describes the way to configure two-factor authentication
+
Two-Factor Authentication has to be enabled by either building with Gradle arguments
+ -Ptwofactor=enabled or enabling the twofactor profile via env. variable
+
+
In order for SMS to be enabled an SMS bridge has to be setup with the message-gateway service.
+
+
+
+
+
+
+
+
+
Retrieve Two-Factor Configuration
+
Returns available two-factor configuration.
+
+
+ GET https://DomainName/api/v1/twofactor/configure
+
+
+{
+ "otp-delivery-email-body": "Hello {{username}}.\n\nYour OTP login token is {{token}}.",
+ "otp-delivery-sms-enable": true,
+ "otp-delivery-sms-provider": 6,
+ "otp-delivery-email-subject": "Fineract Two-Factor Authentication Token",
+ "otp-token-length": 5,
+ "access-token-live-time-extended": 604800,
+ "otp-delivery-email-enable": true,
+ "otp-token-live-time": 300,
+ "otp-delivery-sms-text": "Your authentication token for Fineract is {{token}}.",
+ "access-token-live-time": 86400
+}
+
+
+
+
+
+
+
+
Update Two-Factor Configuration
+
Update two-factor configuration.
+
+
+ PUT https://DomainName/api/v1/twofactor/configure
+
+
+{
+ "otp-delivery-sms-provider": 7
+ "otp-delivery-sms-enable": false
+}
+
+
+{
+ "changes": {
+ "otp-delivery-sms-enable": false,
+ "otp-delivery-sms-provider": 7
+ }
+}
+
+
+
+
diff --git a/api-docs/apidocs.css b/api-docs/apidocs.css
index f8816a7de..21bc11c27 100644
--- a/api-docs/apidocs.css
+++ b/api-docs/apidocs.css
@@ -745,4 +745,8 @@ ul.field li {
tt {
font-size: 9.5pt;
+}
+
+ol.normalli li {
+ list-style-type: decimal;
}
\ No newline at end of file
diff --git a/fineract-provider/build.gradle b/fineract-provider/build.gradle
index 771fcde58..5a2b8b31c 100644
--- a/fineract-provider/build.gradle
+++ b/fineract-provider/build.gradle
@@ -193,17 +193,33 @@ if (project.hasProperty('env') && project.getProperty('env') == 'dev') {
/* Enable Oauth2 authentication based on environment, default to HTTP basic auth */
if (project.hasProperty('security') && project.getProperty('security') == 'oauth') {
- copy {
- from './properties/oauth/'
- into 'src/main/resources/'
- include '*.properties'
- }
+ if(project.hasProperty('twofactor') && project.getProperty('twofactor') == 'enabled') {
+ copy {
+ from './properties/oauth/twofactor/'
+ into 'src/main/resources/'
+ include '*.properties'
+ }
+ } else {
+ copy {
+ from './properties/oauth/'
+ into 'src/main/resources/'
+ include '*.properties'
+ }
+ }
} else {
- copy {
- from './properties/basicauth/'
- into 'src/main/resources/'
- include '*.properties'
- }
+ if(project.hasProperty('twofactor') && project.getProperty('twofactor') == 'enabled') {
+ copy {
+ from './properties/basicauth/twofactor/'
+ into 'src/main/resources/'
+ include '*.properties'
+ }
+ } else {
+ copy {
+ from './properties/basicauth/'
+ into 'src/main/resources/'
+ include '*.properties'
+ }
+ }
}
task dist(type:Zip){
diff --git a/fineract-provider/properties/basicauth/twofactor/application.properties b/fineract-provider/properties/basicauth/twofactor/application.properties
new file mode 100644
index 000000000..5b4d49668
--- /dev/null
+++ b/fineract-provider/properties/basicauth/twofactor/application.properties
@@ -0,0 +1,21 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+#
+
+spring.profiles.default=basicauth
+spring.profiles.active=basicauth,twofactor
\ No newline at end of file
diff --git a/fineract-provider/properties/oauth/twofactor/application.properties b/fineract-provider/properties/oauth/twofactor/application.properties
new file mode 100644
index 000000000..12cbfbe80
--- /dev/null
+++ b/fineract-provider/properties/oauth/twofactor/application.properties
@@ -0,0 +1,21 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+#
+
+spring.profiles.default=basicauth
+spring.profiles.active=oauth,twofactor
\ No newline at end of file
diff --git a/fineract-provider/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java b/fineract-provider/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java
index 359e4278a..ae53bb78c 100755
--- a/fineract-provider/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java
@@ -3118,4 +3118,18 @@ public class CommandWrapperBuilder {
this.href = "/emailcampaigns/configuration/";
return this;
}
+
+ public CommandWrapperBuilder invalidateTwoFactorAccessToken() {
+ this.actionName = "INVALIDATE";
+ this.entityName = "TWOFACTOR_ACCESSTOKEN";
+ this.href = "/twofactor/invalidate";
+ return this;
+ }
+
+ public CommandWrapperBuilder updateTwoFactorConfiguration() {
+ this.actionName = "UPDATE";
+ this.entityName = "TWOFACTOR_CONFIGURATION";
+ this.href = "/twofactor/configure";
+ return this;
+ }
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java
index 4abbb4b77..f2eab6429 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java
@@ -85,5 +85,12 @@ public interface ConfigurationDomainService {
Long getDailyTPTLimit();
void removeGlobalConfigurationPropertyDataFromCache(String propertyName);
-
+
+ boolean isSMSOTPDeliveryEnabled();
+
+ boolean isEmailOTPDeliveryEnabled();
+
+ Integer retrieveOTPCharacterLength();
+
+ Integer retrieveOTPLiveTime();
}
\ No newline at end of file
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java
index 95170f82e..3d51ddd9e 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java
@@ -287,6 +287,43 @@ public class ConfigurationDomainServiceJpa implements ConfigurationDomainService
configurations.remove(key);
}
+ @Override
+ public boolean isSMSOTPDeliveryEnabled() {
+ final String propertyName = "use-sms-for-2fa";
+ final GlobalConfigurationPropertyData property = getGlobalConfigurationPropertyData(propertyName);
+ return property.isEnabled();
+ }
+
+ @Override
+ public boolean isEmailOTPDeliveryEnabled() {
+ final String propertyName = "use-email-for-2fa";
+ final GlobalConfigurationPropertyData property = getGlobalConfigurationPropertyData(propertyName);
+ return property.isEnabled();
+ }
+
+ @Override
+ public Integer retrieveOTPCharacterLength() {
+ final String propertyName = "otp-character-length";
+ final GlobalConfigurationPropertyData property = getGlobalConfigurationPropertyData(propertyName);
+ int defaultValue = 6;
+ int value = property.getValue().intValue();
+ if(value < 1)
+ return defaultValue;
+ return value;
+ }
+
+ @Override
+ public Integer retrieveOTPLiveTime() {
+ final String propertyName = "otp-validity-period";
+ final GlobalConfigurationPropertyData property = getGlobalConfigurationPropertyData(propertyName);
+ int defaultValue = 300;
+ int value = property.getValue().intValue();
+ if(value < 1) {
+ return defaultValue;
+ }
+ return value;
+ }
+
private GlobalConfigurationPropertyData getGlobalConfigurationPropertyData(final String propertyName) {
String identifier = ThreadLocalContextUtil.getTenant().getTenantIdentifier();
String key = identifier + "_" + propertyName;
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/boot/AbstractApplicationConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/boot/AbstractApplicationConfiguration.java
index 7823a0dd7..dd8fc2043 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/boot/AbstractApplicationConfiguration.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/boot/AbstractApplicationConfiguration.java
@@ -40,7 +40,8 @@ import org.springframework.context.annotation.PropertySource;
* and MariaDB4j (because those differ in the subclasses).
*/
@Configuration
-@Import({ WebXmlConfiguration.class, WebXmlOauthConfiguration.class, WebFrontEndConfiguration.class })
+@Import({ WebXmlConfiguration.class, WebXmlOauthConfiguration.class, WebFrontEndConfiguration.class,
+ WebTwoFactorXmlConfiguration.class })
@ImportResource({ "classpath*:META-INF/spring/appContext.xml" })
@PropertySource(value="classpath:META-INF/spring/jdbc.properties")
@EnableAutoConfiguration(exclude = { DataSourceAutoConfiguration.class,
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/boot/WebTwoFactorXmlConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/boot/WebTwoFactorXmlConfiguration.java
new file mode 100644
index 000000000..e8f16882d
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/boot/WebTwoFactorXmlConfiguration.java
@@ -0,0 +1,36 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.fineract.infrastructure.core.boot;
+
+import org.apache.fineract.infrastructure.security.filter.TwoFactorAuthenticationFilter;
+import org.springframework.boot.context.embedded.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+
+@Configuration
+public class WebTwoFactorXmlConfiguration {
+
+ @Bean
+ public FilterRegistrationBean twoFactorFilterBean(TwoFactorAuthenticationFilter filter) {
+ FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
+ registrationBean.setEnabled(false);
+ return registrationBean;
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/api/AuthenticationApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/api/AuthenticationApiResource.java
index 163835931..e5c20ed6e 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/api/AuthenticationApiResource.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/api/AuthenticationApiResource.java
@@ -30,8 +30,10 @@ import javax.ws.rs.core.MediaType;
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
import org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer;
+import org.apache.fineract.infrastructure.security.constants.TwoFactorConstants;
import org.apache.fineract.infrastructure.security.data.AuthenticatedUserData;
import org.apache.fineract.infrastructure.security.service.SpringSecurityPlatformSecurityContext;
+import org.apache.fineract.infrastructure.security.service.TwoFactorUtils;
import org.apache.fineract.useradministration.data.RoleData;
import org.apache.fineract.useradministration.domain.AppUser;
import org.apache.fineract.useradministration.domain.Role;
@@ -56,15 +58,17 @@ public class AuthenticationApiResource {
private final DaoAuthenticationProvider customAuthenticationProvider;
private final ToApiJsonSerializer
apiJsonSerializerService;
private final SpringSecurityPlatformSecurityContext springSecurityPlatformSecurityContext;
+ private final TwoFactorUtils twoFactorUtils;
@Autowired
public AuthenticationApiResource(
@Qualifier("customAuthenticationProvider") final DaoAuthenticationProvider customAuthenticationProvider,
final ToApiJsonSerializer apiJsonSerializerService,
- final SpringSecurityPlatformSecurityContext springSecurityPlatformSecurityContext) {
+ final SpringSecurityPlatformSecurityContext springSecurityPlatformSecurityContext, TwoFactorUtils twoFactorUtils) {
this.customAuthenticationProvider = customAuthenticationProvider;
this.apiJsonSerializerService = apiJsonSerializerService;
this.springSecurityPlatformSecurityContext = springSecurityPlatformSecurityContext;
+ this.twoFactorUtils = twoFactorUtils;
}
@POST
@@ -100,12 +104,16 @@ public class AuthenticationApiResource {
final EnumOptionData organisationalRole = principal.organisationalRoleData();
+ boolean isTwoFactorRequired = twoFactorUtils.isTwoFactorAuthEnabled() && !
+ principal.hasSpecificPermissionTo(TwoFactorConstants.BYPASS_TWO_FACTOR_PERMISSION);
if (this.springSecurityPlatformSecurityContext.doesPasswordHasToBeRenewed(principal)) {
- authenticatedUserData = new AuthenticatedUserData(username, principal.getId(), new String(base64EncodedAuthenticationKey));
+ authenticatedUserData = new AuthenticatedUserData(username, principal.getId(),
+ new String(base64EncodedAuthenticationKey), isTwoFactorRequired);
} else {
authenticatedUserData = new AuthenticatedUserData(username, officeId, officeName, staffId, staffDisplayName,
- organisationalRole, roles, permissions, principal.getId(), new String(base64EncodedAuthenticationKey));
+ organisationalRole, roles, permissions, principal.getId(),
+ new String(base64EncodedAuthenticationKey), isTwoFactorRequired);
}
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/api/TwoFactorApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/api/TwoFactorApiResource.java
new file mode 100644
index 000000000..482906af1
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/api/TwoFactorApiResource.java
@@ -0,0 +1,132 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.fineract.infrastructure.security.api;
+
+import java.util.List;
+import java.util.Map;
+
+import javax.ws.rs.DefaultValue;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.UriInfo;
+
+import org.apache.fineract.commands.domain.CommandWrapper;
+import org.apache.fineract.commands.service.CommandWrapperBuilder;
+import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
+import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
+import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
+import org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer;
+import org.apache.fineract.infrastructure.security.data.AccessTokenData;
+import org.apache.fineract.infrastructure.security.data.OTPDeliveryMethod;
+import org.apache.fineract.infrastructure.security.data.OTPMetadata;
+import org.apache.fineract.infrastructure.security.data.OTPRequest;
+import org.apache.fineract.infrastructure.security.domain.TFAccessToken;
+import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
+import org.apache.fineract.infrastructure.security.service.TwoFactorService;
+import org.apache.fineract.useradministration.domain.AppUser;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Profile;
+import org.springframework.context.annotation.Scope;
+import org.springframework.stereotype.Component;
+
+@Path("/twofactor")
+@Component
+@Profile("twofactor")
+@Scope("singleton")
+public class TwoFactorApiResource {
+
+
+ private final ToApiJsonSerializer otpRequestSerializer;
+ private final ToApiJsonSerializer otpDeliveryMethodSerializer;
+ private final ToApiJsonSerializer accessTokenSerializer;
+ private final DefaultToApiJsonSerializer