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: +

    +
  1. User authticates via BasicAuth / oAauth
  2. +
  3. Client requests a list of supported OTP delivery methods for the authenticated user(Get Delivery Methods)
  4. +
  5. User selects an OTP delivery method and client sends a request for OTP(Request OTP)
  6. +
  7. User receives an OTP and the client sends it for verification(Validate OTP)
  8. +
  9. If the OTP is valid, an access token is returned
  10. +
  11. The access token is sent in following requestes to the server as a header Fineract-Platform-TFA-Token
  12. +
  13. On session end, the access token should be invalidatedInvalidate Access Token)
  14. +
+

+

+ 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 0ad36120c..93bd160d4 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 @@ -2929,7 +2929,7 @@ public class CommandWrapperBuilder { this.href = "/smscampaigns/"+resourceId; return this; } - + public CommandWrapperBuilder holdAmount(final Long accountId) { this.actionName = "HOLDAMOUNT"; this.entityName = "SAVINGSACCOUNT"; @@ -3041,4 +3041,18 @@ public class CommandWrapperBuilder { this.json = "{}"; 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> toApiJsonSerializer; + + private final PlatformSecurityContext context; + private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; + private final TwoFactorService twoFactorService; + + + + @Autowired + public TwoFactorApiResource(ToApiJsonSerializer otpRequestSerializer, + ToApiJsonSerializer otpDeliveryMethodSerializer, + ToApiJsonSerializer accessTokenSerializer, + DefaultToApiJsonSerializer> toApiJsonSerializer, + PlatformSecurityContext context, + PortfolioCommandSourceWritePlatformService + commandsSourceWritePlatformService, + TwoFactorService twoFactorService) { + this.otpRequestSerializer = otpRequestSerializer; + this.otpDeliveryMethodSerializer = otpDeliveryMethodSerializer; + this.accessTokenSerializer = accessTokenSerializer; + this.toApiJsonSerializer = toApiJsonSerializer; + this.context = context; + this.commandsSourceWritePlatformService = commandsSourceWritePlatformService; + this.twoFactorService = twoFactorService; + } + + + @GET + @Produces({ MediaType.APPLICATION_JSON }) + public String getOTPDeliveryMethods(@Context final UriInfo uriInfo) { + AppUser user = context.authenticatedUser(); + + List otpDeliveryMethods = twoFactorService.getDeliveryMethodsForUser(user); + return this.otpDeliveryMethodSerializer.serialize(otpDeliveryMethods); + } + + @POST + @Produces({ MediaType.APPLICATION_JSON }) + public String requestToken(@QueryParam("deliveryMethod") final String deliveryMethod, + @QueryParam("extendedToken") @DefaultValue("false") boolean extendedAccessToken, + @Context final UriInfo uriInfo) { + final AppUser user = context.authenticatedUser(); + + final OTPRequest request = twoFactorService.createNewOTPToken(user, deliveryMethod, extendedAccessToken); + return this.otpRequestSerializer.serialize(request.getMetadata()); + } + + @Path("validate") + @POST + @Produces({ MediaType.APPLICATION_JSON }) + public String validate(@QueryParam("token") final String token) { + final AppUser user = context.authenticatedUser(); + + TFAccessToken accessToken = twoFactorService.createAccessTokenFromOTP(user, token); + + return accessTokenSerializer.serialize(accessToken.toTokenData()); + } + + @Path("invalidate") + @POST + @Produces({ MediaType.APPLICATION_JSON }) + public String updateConfiguration(final String apiRequestBodyAsJson) { + final CommandWrapper commandRequest = new CommandWrapperBuilder() + .invalidateTwoFactorAccessToken().withJson(apiRequestBodyAsJson).build(); + final CommandProcessingResult result = this.commandsSourceWritePlatformService. + logCommandSource(commandRequest); + + return this.toApiJsonSerializer.serialize(result); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/api/TwoFactorConfigurationApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/api/TwoFactorConfigurationApiResource.java new file mode 100644 index 000000000..13fd69ec3 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/api/TwoFactorConfigurationApiResource.java @@ -0,0 +1,86 @@ +/** + * 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.Map; + +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +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.security.service.PlatformSecurityContext; +import org.apache.fineract.infrastructure.security.service.TwoFactorConfigurationService; +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/configure") +@Consumes({ MediaType.APPLICATION_JSON }) +@Produces({ MediaType.APPLICATION_JSON }) +@Component +@Scope("singleton") +@Profile("twofactor") +public class TwoFactorConfigurationApiResource { + + private final String resourceNameForPermissions = "TWOFACTOR_CONFIG"; + + private final PlatformSecurityContext context; + private final TwoFactorConfigurationService configurationService; + private final DefaultToApiJsonSerializer> toApiJsonSerializer; + private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; + + @Autowired + public TwoFactorConfigurationApiResource(PlatformSecurityContext context, + TwoFactorConfigurationService configurationService, + DefaultToApiJsonSerializer> toApiJsonSerializer, + PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService) { + this.context = context; + this.configurationService = configurationService; + this.toApiJsonSerializer = toApiJsonSerializer; + this.commandsSourceWritePlatformService = commandsSourceWritePlatformService; + } + + + @GET + public String retrieveAll() { + this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermissions); + Map configurationMap = configurationService.retrieveAll(); + return toApiJsonSerializer.serialize(configurationMap); + } + + @PUT + public String updateConfiguration(final String apiRequestBodyAsJson) { + final CommandWrapper commandRequest = new CommandWrapperBuilder() + .updateTwoFactorConfiguration().withJson(apiRequestBodyAsJson).build(); + final CommandProcessingResult result = this.commandsSourceWritePlatformService. + logCommandSource(commandRequest); + + return this.toApiJsonSerializer.serialize(result); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/api/UserDetailsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/api/UserDetailsApiResource.java index 67f061680..d05f589e2 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/api/UserDetailsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/api/UserDetailsApiResource.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.AuthenticatedOauthUserData; 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,14 +58,17 @@ public class UserDetailsApiResource { private final ResourceServerTokenServices tokenServices; private final ToApiJsonSerializer apiJsonSerializerService; private final SpringSecurityPlatformSecurityContext springSecurityPlatformSecurityContext; + private final TwoFactorUtils twoFactorUtils; @Autowired public UserDetailsApiResource(@Qualifier("tokenServices") final ResourceServerTokenServices tokenServices, final ToApiJsonSerializer apiJsonSerializerService, - final SpringSecurityPlatformSecurityContext springSecurityPlatformSecurityContext) { + final SpringSecurityPlatformSecurityContext springSecurityPlatformSecurityContext, + final TwoFactorUtils twoFactorUtils) { this.tokenServices = tokenServices; this.apiJsonSerializerService = apiJsonSerializerService; this.springSecurityPlatformSecurityContext = springSecurityPlatformSecurityContext; + this.twoFactorUtils = twoFactorUtils; } @GET @@ -96,12 +101,16 @@ public class UserDetailsApiResource { final EnumOptionData organisationalRole = principal.organisationalRoleData(); + final boolean requireTwoFactorAuth = twoFactorUtils.isTwoFactorAuthEnabled() + && !principal.hasSpecificPermissionTo(TwoFactorConstants.BYPASS_TWO_FACTOR_PERMISSION); if (this.springSecurityPlatformSecurityContext.doesPasswordHasToBeRenewed(principal)) { - authenticatedUserData = new AuthenticatedOauthUserData(principal.getUsername(), principal.getId(), accessToken); + authenticatedUserData = new AuthenticatedOauthUserData(principal.getUsername(), + principal.getId(), accessToken, requireTwoFactorAuth); } else { - authenticatedUserData = new AuthenticatedOauthUserData(principal.getUsername(), officeId, officeName, staffId, staffDisplayName, - organisationalRole, roles, permissions, principal.getId(), accessToken); + authenticatedUserData = new AuthenticatedOauthUserData(principal.getUsername(), + officeId, officeName, staffId, staffDisplayName, organisationalRole, roles, + permissions, principal.getId(), accessToken, requireTwoFactorAuth); } return this.apiJsonSerializerService.serialize(authenticatedUserData); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/command/InvalidateTFAccessTokenCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/command/InvalidateTFAccessTokenCommandHandler.java new file mode 100644 index 000000000..1ede412aa --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/command/InvalidateTFAccessTokenCommandHandler.java @@ -0,0 +1,108 @@ +/** + * 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.command; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; + +import org.apache.commons.lang.StringUtils; +import org.apache.fineract.commands.annotation.CommandType; +import org.apache.fineract.commands.handler.NewCommandSourceHandler; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.ApiParameterError; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; +import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; +import org.apache.fineract.infrastructure.core.exception.InvalidJsonException; +import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; +import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.apache.fineract.infrastructure.security.constants.TwoFactorConstants; +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.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.google.gson.JsonElement; +import com.google.gson.reflect.TypeToken; + + +@Service +@CommandType(entity = "TWOFACTOR_ACCESSTOKEN", action = "INVALIDATE") +@Profile("twofactor") +public class InvalidateTFAccessTokenCommandHandler implements NewCommandSourceHandler { + + + private final TwoFactorService twoFactorService; + private final PlatformSecurityContext securityContext; + private final FromJsonHelper fromJsonHelper; + + @Autowired + public InvalidateTFAccessTokenCommandHandler(TwoFactorService twoFactorService, + PlatformSecurityContext securityContext, + FromJsonHelper fromJsonHelper) { + this.twoFactorService = twoFactorService; + this.securityContext = securityContext; + this.fromJsonHelper = fromJsonHelper; + } + + @Transactional + @Override + public CommandProcessingResult processCommand(JsonCommand command) { + validateJson(command.json()); + + final AppUser user = securityContext.authenticatedUser(); + + final TFAccessToken accessToken = twoFactorService.invalidateAccessToken(user, command); + + return new CommandProcessingResultBuilder() + .withCommandId(command.commandId()) + .withResourceIdAsString(accessToken.getToken()) + .build(); + } + + private void validateJson(String json) { + if (StringUtils.isBlank(json)) { + throw new InvalidJsonException(); + } + + final Type typeOfMap = new TypeToken>() {}.getType(); + this.fromJsonHelper.checkForUnsupportedParameters(typeOfMap, json, + new HashSet<>(Collections.singletonList("token"))); + final JsonElement element = this.fromJsonHelper.parse(json); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(TwoFactorConstants.ACCESSTOKEN_RESOURCE_NAME); + + final String token = this.fromJsonHelper.extractStringNamed("token", element); + baseDataValidator.reset().parameter("token").value(token).notNull().notBlank(); + + if(!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/command/UpdateTwoFactorConfigCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/command/UpdateTwoFactorConfigCommandHandler.java new file mode 100644 index 000000000..b8c4e60e1 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/command/UpdateTwoFactorConfigCommandHandler.java @@ -0,0 +1,60 @@ +/** + * 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.command; + +import java.util.Map; + +import org.apache.fineract.commands.annotation.CommandType; +import org.apache.fineract.commands.handler.NewCommandSourceHandler; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; +import org.apache.fineract.infrastructure.security.data.TwoFactorConfigurationValidator; +import org.apache.fineract.infrastructure.security.service.TwoFactorConfigurationService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@CommandType(entity = "TWOFACTOR_CONFIGURATION", action = "UPDATE") +@Profile("twofactor") +public class UpdateTwoFactorConfigCommandHandler implements NewCommandSourceHandler { + + private final TwoFactorConfigurationService configurationService; + private final TwoFactorConfigurationValidator dataValidator; + + @Autowired + public UpdateTwoFactorConfigCommandHandler(TwoFactorConfigurationService configurationService, + TwoFactorConfigurationValidator dataValidator) { + this.configurationService = configurationService; + this.dataValidator = dataValidator; + } + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + this.dataValidator.validateForUpdate(command.json()); + final Map changes = configurationService.update(command); + return new CommandProcessingResultBuilder() + .withCommandId(command.commandId()) + .with(changes) + .build(); + } +} \ No newline at end of file diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/constants/TwoFactorConfigurationConstants.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/constants/TwoFactorConfigurationConstants.java new file mode 100644 index 000000000..22818dea5 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/constants/TwoFactorConfigurationConstants.java @@ -0,0 +1,59 @@ +/** + * 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.constants; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class TwoFactorConfigurationConstants { + + public static final String RESOURCE_NAME = "TWOFACTOR_CONFIGURATION"; + + public static final String ENABLE_EMAIL_DELIVERY = "otp-delivery-email-enable"; + public static final String EMAIL_SUBJECT = "otp-delivery-email-subject"; + public static final String EMAIL_BODY = "otp-delivery-email-body"; + + public static final String ENABLE_SMS_DELIVERY = "otp-delivery-sms-enable"; + public static final String SMS_PROVIDER_ID = "otp-delivery-sms-provider"; + public static final String SMS_MESSAGE_TEXT = "otp-delivery-sms-text"; + + public static final String OTP_TOKEN_LIVE_TIME = "otp-token-live-time"; + public static final String OTP_TOKEN_LENGTH = "otp-token-length"; + + public static final String ACCESS_TOKEN_LIVE_TIME = "access-token-live-time"; + public static final String ACCESS_TOKEN_LIVE_TIME_EXTENDED = "access-token-live-time-extended"; + + public static final Set REQUEST_DATA_PARAMETERS = + new HashSet<>(Arrays.asList(ENABLE_EMAIL_DELIVERY, EMAIL_SUBJECT, EMAIL_BODY, + ENABLE_SMS_DELIVERY, SMS_PROVIDER_ID, SMS_MESSAGE_TEXT, OTP_TOKEN_LIVE_TIME, + OTP_TOKEN_LENGTH, ACCESS_TOKEN_LIVE_TIME, ACCESS_TOKEN_LIVE_TIME_EXTENDED)); + + public static final List STRING_PARAMETERS = + Arrays.asList(EMAIL_SUBJECT, EMAIL_BODY, SMS_MESSAGE_TEXT); + + public static final List BOOLEAN_PARAMETERS = + Arrays.asList(ENABLE_EMAIL_DELIVERY, ENABLE_SMS_DELIVERY); + + public static final List NUMBER_PARAMETERS = + Arrays.asList(SMS_PROVIDER_ID, OTP_TOKEN_LIVE_TIME, OTP_TOKEN_LENGTH, + ACCESS_TOKEN_LIVE_TIME, ACCESS_TOKEN_LIVE_TIME_EXTENDED); + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/constants/TwoFactorConstants.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/constants/TwoFactorConstants.java new file mode 100644 index 000000000..7997c3986 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/constants/TwoFactorConstants.java @@ -0,0 +1,30 @@ +/** + * 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.constants; + +public class TwoFactorConstants { + + public static final String ACCESSTOKEN_RESOURCE_NAME = "TWOFACTOR_ACCESSTOKEN"; + + public static final String SMS_DELIVERY_METHOD_NAME = "sms"; + public static final String EMAIL_DELIVERY_METHOD_NAME = "email"; + + public static final String BYPASS_TWO_FACTOR_PERMISSION = "BYPASS_TWOFACTOR"; + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/AccessTokenData.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/AccessTokenData.java new file mode 100644 index 000000000..ebd27f7d1 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/AccessTokenData.java @@ -0,0 +1,47 @@ +/** + * 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.data; + +import org.joda.time.DateTime; + +public class AccessTokenData { + + private final String token; + + private final DateTime validFrom; + private final DateTime validTo; + + public AccessTokenData(String token, DateTime validFrom, DateTime validTo) { + this.token = token; + this.validFrom = validFrom; + this.validTo = validTo; + } + + public String getToken() { + return token; + } + + public DateTime getValidFrom() { + return validFrom; + } + + public DateTime getValidTo() { + return validTo; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/AuthenticatedOauthUserData.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/AuthenticatedOauthUserData.java index 830fc6750..116daa8ba 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/AuthenticatedOauthUserData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/AuthenticatedOauthUserData.java @@ -54,6 +54,9 @@ public class AuthenticatedOauthUserData { @SuppressWarnings("unused") private final boolean shouldRenewPassword; + @SuppressWarnings("unused") + private final boolean isTwoFactorAuthenticationRequired; + public AuthenticatedOauthUserData(final String username, final Collection permissions) { this.username = username; this.userId = null; @@ -67,11 +70,13 @@ public class AuthenticatedOauthUserData { this.roles = null; this.permissions = permissions; this.shouldRenewPassword = false; + this.isTwoFactorAuthenticationRequired = false; } public AuthenticatedOauthUserData(final String username, final Long officeId, final String officeName, final Long staffId, final String staffDisplayName, final EnumOptionData organisationalRole, final Collection roles, - final Collection permissions, final Long userId, final String accessToken) { + final Collection permissions, final Long userId, final String accessToken, + final boolean isTwoFactorAuthenticationRequired) { this.username = username; this.officeId = officeId; this.officeName = officeName; @@ -84,9 +89,11 @@ public class AuthenticatedOauthUserData { this.roles = roles; this.permissions = permissions; this.shouldRenewPassword = false; + this.isTwoFactorAuthenticationRequired = isTwoFactorAuthenticationRequired; } - public AuthenticatedOauthUserData(final String username, final Long userId, final String accessToken) { + public AuthenticatedOauthUserData(final String username, final Long userId, final String accessToken, + final boolean isTwoFactorAuthenticationRequired) { this.username = username; this.officeId = null; this.officeName = null; @@ -99,5 +106,6 @@ public class AuthenticatedOauthUserData { this.roles = null; this.permissions = null; this.shouldRenewPassword = true; + this.isTwoFactorAuthenticationRequired = isTwoFactorAuthenticationRequired; } } \ No newline at end of file diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/AuthenticatedUserData.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/AuthenticatedUserData.java index 513f5dffc..c4713fcfb 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/AuthenticatedUserData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/AuthenticatedUserData.java @@ -54,6 +54,9 @@ public class AuthenticatedUserData { @SuppressWarnings("unused") private final boolean shouldRenewPassword; + @SuppressWarnings("unused") + private final boolean isTwoFactorAuthenticationRequired; + public AuthenticatedUserData(final String username, final Collection permissions) { this.username = username; this.userId = null; @@ -67,11 +70,13 @@ public class AuthenticatedUserData { this.roles = null; this.permissions = permissions; this.shouldRenewPassword = false; + this.isTwoFactorAuthenticationRequired = false; } public AuthenticatedUserData(final String username, final Long officeId, final String officeName, final Long staffId, final String staffDisplayName, final EnumOptionData organisationalRole, final Collection roles, - final Collection permissions, final Long userId, final String base64EncodedAuthenticationKey) { + final Collection permissions, final Long userId, final String base64EncodedAuthenticationKey, + final boolean isTwoFactorAuthenticationRequired) { this.username = username; this.officeId = officeId; this.officeName = officeName; @@ -84,9 +89,11 @@ public class AuthenticatedUserData { this.roles = roles; this.permissions = permissions; this.shouldRenewPassword = false; + this.isTwoFactorAuthenticationRequired = isTwoFactorAuthenticationRequired; } - public AuthenticatedUserData(final String username, final Long userId, final String base64EncodedAuthenticationKey) { + public AuthenticatedUserData(final String username, final Long userId, final String base64EncodedAuthenticationKey, + final boolean isTwoFactorAuthenticationRequired) { this.username = username; this.officeId = null; this.officeName = null; @@ -99,5 +106,6 @@ public class AuthenticatedUserData { this.roles = null; this.permissions = null; this.shouldRenewPassword = true; + this.isTwoFactorAuthenticationRequired = isTwoFactorAuthenticationRequired; } } \ No newline at end of file diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/OTPDeliveryMethod.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/OTPDeliveryMethod.java new file mode 100644 index 000000000..f05c7ae83 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/OTPDeliveryMethod.java @@ -0,0 +1,38 @@ +/** + * 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.data; + +public class OTPDeliveryMethod { + + private final String name; + private final String target; + + public OTPDeliveryMethod(String name, String target) { + this.name = name; + this.target = target; + } + + public String getName() { + return name; + } + + public String getTarget() { + return target; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/OTPMetadata.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/OTPMetadata.java new file mode 100644 index 000000000..0365fffe4 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/OTPMetadata.java @@ -0,0 +1,53 @@ +/** + * 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.data; + +import org.joda.time.DateTime; + +public class OTPMetadata { + + private final DateTime requestTime; + private final int tokenLiveTimeInSec; + private final boolean extendedAccessToken; + private final OTPDeliveryMethod deliveryMethod; + + public OTPMetadata(DateTime requestTime, int tokenLiveTimeInSec, + boolean extendedAccessToken, OTPDeliveryMethod deliveryMethod) { + this.requestTime = requestTime; + this.tokenLiveTimeInSec = tokenLiveTimeInSec; + this.extendedAccessToken = extendedAccessToken; + this.deliveryMethod = deliveryMethod; + } + + public DateTime getRequestTime() { + return requestTime; + } + + public int getTokenLiveTimeInSec() { + return tokenLiveTimeInSec; + } + + public boolean isExtendedAccessToken() { + return extendedAccessToken; + } + + public OTPDeliveryMethod getDeliveryMethod() { + return deliveryMethod; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/OTPRequest.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/OTPRequest.java new file mode 100644 index 000000000..f9f3e2433 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/OTPRequest.java @@ -0,0 +1,53 @@ +/** + * 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.data; + +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.joda.time.DateTime; + +public class OTPRequest { + + private final String token; + private final OTPMetadata metadata; + + public OTPRequest(String token, OTPMetadata metadata) { + this.token = token; + this.metadata = metadata; + } + + public static OTPRequest create(String token, int tokenLiveTimeInSec, boolean extendedAccessToken, + OTPDeliveryMethod deliveryMethod) { + final OTPMetadata metadata = new OTPMetadata(DateUtils.getLocalDateTimeOfTenant().toDateTime(), + tokenLiveTimeInSec, extendedAccessToken, deliveryMethod); + return new OTPRequest(token, metadata); + } + + public String getToken() { + return token; + } + + public OTPMetadata getMetadata() { + return metadata; + } + + public boolean isValid() { + DateTime expireTime = metadata.getRequestTime().plusSeconds(metadata.getTokenLiveTimeInSec()); + return DateTime.now().isBefore(expireTime); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/TwoFactorConfigurationValidator.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/TwoFactorConfigurationValidator.java new file mode 100644 index 000000000..41cf69bd4 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/TwoFactorConfigurationValidator.java @@ -0,0 +1,120 @@ +/** + * 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.data; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.apache.commons.lang.StringUtils; +import org.apache.fineract.infrastructure.core.data.ApiParameterError; +import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; +import org.apache.fineract.infrastructure.core.exception.InvalidJsonException; +import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; +import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.apache.fineract.infrastructure.security.constants.TwoFactorConfigurationConstants; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +import com.google.gson.JsonElement; +import com.google.gson.reflect.TypeToken; + +@Component +@Profile("twofactor") +public class TwoFactorConfigurationValidator { + + private final FromJsonHelper fromJsonHelper; + + @Autowired + public TwoFactorConfigurationValidator(FromJsonHelper fromJsonHelper) { + this.fromJsonHelper = fromJsonHelper; + } + + public void validateForUpdate(final String json) { + if (StringUtils.isBlank(json)) { + throw new InvalidJsonException(); + } + + boolean atLeastOneParameterPassedForUpdate = false; + final Type typeOfMap = new TypeToken>() {}.getType(); + this.fromJsonHelper.checkForUnsupportedParameters(typeOfMap, json, + TwoFactorConfigurationConstants.REQUEST_DATA_PARAMETERS); + final JsonElement element = this.fromJsonHelper.parse(json); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(TwoFactorConfigurationConstants.RESOURCE_NAME); + + + for(String parameterName : TwoFactorConfigurationConstants.BOOLEAN_PARAMETERS) { + if(this.fromJsonHelper.parameterExists(parameterName, element)) { + atLeastOneParameterPassedForUpdate = true; + validateBooleanParameter(parameterName, element, baseDataValidator); + } + } + + for(String parameterName : TwoFactorConfigurationConstants.STRING_PARAMETERS) { + if(this.fromJsonHelper.parameterExists(parameterName, element)) { + atLeastOneParameterPassedForUpdate = true; + validateStringParameter(parameterName, element, baseDataValidator); + } + } + + for(String parameterName : TwoFactorConfigurationConstants.NUMBER_PARAMETERS) { + if (this.fromJsonHelper.parameterExists(parameterName, element)) { + atLeastOneParameterPassedForUpdate = true; + validateNumberParameter(parameterName, element, baseDataValidator); + } + } + + if(!atLeastOneParameterPassedForUpdate) { + final Object forceError = null; + baseDataValidator.reset().anyOfNotNull(forceError); + } + + throwExceptionIfValidationWarningsExist(dataValidationErrors); + } + + private void throwExceptionIfValidationWarningsExist( + final List dataValidationErrors) { + if(!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + private void validateBooleanParameter(final String name, final JsonElement element, + final DataValidatorBuilder baseDataValidator) { + final String value = this.fromJsonHelper.extractStringNamed(name, element); + baseDataValidator.reset().parameter(name).value(value).notNull().trueOrFalseRequired(value); + } + + private void validateStringParameter(final String name, final JsonElement element, + final DataValidatorBuilder baseDataValidator) { + final String value = this.fromJsonHelper.extractStringNamed(name, element); + baseDataValidator.reset().parameter(name).value(value).notBlank().notExceedingLengthOf(1000); + } + + private void validateNumberParameter(final String name, final JsonElement element, + final DataValidatorBuilder baseDataValidator) { + final Integer value = this.fromJsonHelper.extractIntegerSansLocaleNamed(name, element); + baseDataValidator.reset().parameter(name).value(value).notNull().integerGreaterThanZero(); + } +} \ No newline at end of file diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/domain/OTPRequestRepository.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/domain/OTPRequestRepository.java new file mode 100644 index 000000000..fc11300da --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/domain/OTPRequestRepository.java @@ -0,0 +1,51 @@ +/** + * 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.domain; + +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.fineract.infrastructure.security.data.OTPRequest; +import org.apache.fineract.useradministration.domain.AppUser; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Repository; +import org.springframework.util.Assert; + +@Repository +@Profile("twofactor") +public class OTPRequestRepository { + + private final ConcurrentHashMap OTPrequests = new ConcurrentHashMap<>(); + + + public OTPRequest getOTPRequestForUser(AppUser user) { + Assert.notNull(user); + + return this.OTPrequests.get(user.getId()); + } + + public void addOTPRequest(AppUser user, OTPRequest request) { + Assert.notNull(user); + Assert.notNull(request); + this.OTPrequests.put(user.getId(), request); + } + + public void deleteOTPRequestForUser(AppUser user) { + this.OTPrequests.remove(user.getId()); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/domain/TFAccessToken.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/domain/TFAccessToken.java new file mode 100644 index 000000000..694046f96 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/domain/TFAccessToken.java @@ -0,0 +1,137 @@ +/** + * 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.domain; + +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; +import javax.persistence.UniqueConstraint; + +import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.security.data.AccessTokenData; +import org.apache.fineract.useradministration.domain.AppUser; +import org.joda.time.DateTime; +import org.joda.time.LocalDateTime; + +@Entity +@Table(name = "twofactor_access_token", + uniqueConstraints = {@UniqueConstraint(columnNames = { "token", "appuser_id" }, name = "token_appuser_UNIQUE")}) +public class TFAccessToken extends AbstractPersistableCustom { + + @Column(name = "token", nullable = false, length = 32) + private String token; + + @ManyToOne + @JoinColumn(name = "appuser_id", nullable = false) + private AppUser user; + + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "valid_from", nullable = false) + private Date validFrom; + + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "valid_to", nullable = false) + private Date validTo; + + @Column(name = "enabled", nullable = false) + private boolean enabled; + + public TFAccessToken() { + } + + public static TFAccessToken create(String token, AppUser user, int tokenLiveTimeInSec) { + DateTime validFrom = DateUtils.getLocalDateTimeOfTenant().toDateTime(); + DateTime validTo = validFrom.plusSeconds(tokenLiveTimeInSec); + + return new TFAccessToken(token, user, validFrom.toDate(), validTo.toDate(), true); + } + + public TFAccessToken(String token, AppUser user, Date validFrom, Date validTo, boolean enabled) { + this.token = token; + this.user = user; + this.validFrom = validFrom; + this.validTo = validTo; + this.enabled = enabled; + } + + public boolean isValid() { + return this.enabled && isDateInTheFuture(getValidToDate()) + && isDateInThePast(getValidFromDate()); + } + + public AccessTokenData toTokenData() { + return new AccessTokenData(this.token, getValidFromDate().toDateTime(), + getValidToDate().toDateTime()); + } + + public String getToken() { + return token; + } + + public AppUser getUser() { + return user; + } + + public boolean isEnabled() { + return enabled; + } + + public LocalDateTime getValidFromDate() { + return new LocalDateTime(validFrom); + } + + public LocalDateTime getValidToDate() { + return new LocalDateTime(validTo); + } + + public void setToken(String token) { + this.token = token; + } + + public void setUser(AppUser user) { + this.user = user; + } + + public void setValidFrom(Date validFrom) { + this.validFrom = validFrom; + } + + public void setValidTo(Date validTo) { + this.validTo = validTo; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + private boolean isDateInTheFuture(LocalDateTime dateTime) { + return dateTime.isAfter(DateUtils.getLocalDateTimeOfTenant()); + } + + private boolean isDateInThePast(LocalDateTime dateTime) { + return dateTime.isBefore(DateUtils.getLocalDateTimeOfTenant()); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/domain/TFAccessTokenRepository.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/domain/TFAccessTokenRepository.java new file mode 100644 index 000000000..cbdbc8fcf --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/domain/TFAccessTokenRepository.java @@ -0,0 +1,31 @@ +/** + * 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.domain; + +import org.apache.fineract.useradministration.domain.AppUser; +import org.springframework.context.annotation.Profile; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +@Profile("twofactor") +public interface TFAccessTokenRepository extends JpaRepository, JpaSpecificationExecutor { + + TFAccessToken findByUserAndToken(AppUser user, String token); + +} \ No newline at end of file diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/domain/TwoFactorConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/domain/TwoFactorConfiguration.java new file mode 100644 index 000000000..e45af00ee --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/domain/TwoFactorConfiguration.java @@ -0,0 +1,84 @@ +/** + * 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.domain; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; + +import org.apache.commons.lang.BooleanUtils; +import org.apache.commons.lang.math.NumberUtils; +import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; +import org.apache.fineract.infrastructure.security.constants.TwoFactorConfigurationConstants; + +@Entity +@Table(name = "twofactor_configuration", + uniqueConstraints = {@UniqueConstraint(columnNames = { "name" }, name = "name_UNIQUE")}) +public class TwoFactorConfiguration extends AbstractPersistableCustom { + + @Column(name = "name", nullable = false, length = 32) + private String name; + + @Column(name = "value", nullable = true, length = 1024) + private String value; + + public String getName() { + return name; + } + + public String getStringValue() { + return value; + } + + public Boolean getBooleanValue() { + return BooleanUtils.toBooleanObject(value); + } + + public Integer getIntegerValue() { + try { + return NumberUtils.createInteger(value); + } catch (NumberFormatException e) { + return null; + } + } + + public Object getObjectValue() { + if(TwoFactorConfigurationConstants.NUMBER_PARAMETERS.contains(name)) { + return getIntegerValue(); + } + if(TwoFactorConfigurationConstants.BOOLEAN_PARAMETERS.contains(name)) { + return getBooleanValue(); + } + + return getStringValue(); + } + + public void setStringValue(String value) { + this.value = value; + } + + public void setBooleanValue(boolean value) { + this.value = String.valueOf(value); + } + + public void setIntegerValue(long value) { + this.value = String.valueOf(value); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/domain/TwoFactorConfigurationRepository.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/domain/TwoFactorConfigurationRepository.java new file mode 100644 index 000000000..0407f0cb2 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/domain/TwoFactorConfigurationRepository.java @@ -0,0 +1,34 @@ +/** + * 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.domain; + +import java.util.List; + +import org.springframework.context.annotation.Profile; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +@Profile("twofactor") +public interface TwoFactorConfigurationRepository extends + JpaRepository, JpaSpecificationExecutor { + + TwoFactorConfiguration findByName(final String name); + + List findAll(); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/exception/AccessTokenInvalidIException.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/exception/AccessTokenInvalidIException.java new file mode 100644 index 000000000..4c39bd10f --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/exception/AccessTokenInvalidIException.java @@ -0,0 +1,28 @@ +/** + * 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.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +public class AccessTokenInvalidIException extends AbstractPlatformDomainRuleException { + + public AccessTokenInvalidIException() { + super("error.msg.twofactor.access.token.invalid", "The provided access token is invalid"); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/exception/OTPDeliveryMethodInvalidException.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/exception/OTPDeliveryMethodInvalidException.java new file mode 100644 index 000000000..0fe02376e --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/exception/OTPDeliveryMethodInvalidException.java @@ -0,0 +1,29 @@ +/** + * 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.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +public class OTPDeliveryMethodInvalidException extends AbstractPlatformDomainRuleException { + + public OTPDeliveryMethodInvalidException() { + super("error.msg.twofactor.otp.delivery.invalid", "The requested OTP delivery method " + + "is not supported or not currently unavailable."); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/exception/OTPTokenInvalidException.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/exception/OTPTokenInvalidException.java new file mode 100644 index 000000000..12d11e9f4 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/exception/OTPTokenInvalidException.java @@ -0,0 +1,28 @@ +/** + * 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.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +public class OTPTokenInvalidException extends AbstractPlatformDomainRuleException { + + public OTPTokenInvalidException() { + super("error.msg.twofactor.otp.token.invalid", "The provided one time token is invalid"); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/InsecureTwoFactorAuthenticationFilter.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/InsecureTwoFactorAuthenticationFilter.java new file mode 100644 index 000000000..2f63eba6b --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/InsecureTwoFactorAuthenticationFilter.java @@ -0,0 +1,82 @@ +/** + * 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.filter; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +import org.apache.fineract.useradministration.domain.AppUser; +import org.springframework.context.annotation.Profile; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; + +/** + * A dummy {@link TwoFactorAuthenticationFilter} filter used when 'twofactor' + * environment profile is not active. + * + * This filter adds 'TWOFACTOR_AUTHENTICATED' authority to every authenticated + * platform user. + */ +@Service(value = "twoFactorAuthFilter") +@Profile("!twofactor") +public class InsecureTwoFactorAuthenticationFilter extends TwoFactorAuthenticationFilter { + + public InsecureTwoFactorAuthenticationFilter() { + super(null); + } + + @Override + public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { + + SecurityContext context = SecurityContextHolder.getContext(); + Authentication authentication = null; + if(context != null) { + authentication = context.getAuthentication(); + } + + // Add two-factor authenticated authority if user is authenticated + if(authentication != null && authentication.isAuthenticated()) { + AppUser user = (AppUser) authentication.getPrincipal(); + + if(user == null) { + return; + } + + List updatedAuthorities = new ArrayList<>(authentication.getAuthorities()); + updatedAuthorities.add(new SimpleGrantedAuthority("TWOFACTOR_AUTHENTICATED")); + UsernamePasswordAuthenticationToken updatedAuthentication = + new UsernamePasswordAuthenticationToken(authentication.getPrincipal(), + authentication.getCredentials(), updatedAuthorities); + context.setAuthentication(updatedAuthentication); + } + + chain.doFilter(req, res); + } +} \ No newline at end of file diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/TwoFactorAuthenticationFilter.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/TwoFactorAuthenticationFilter.java new file mode 100644 index 000000000..6db141e65 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/TwoFactorAuthenticationFilter.java @@ -0,0 +1,139 @@ +/** + * 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.filter; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.fineract.infrastructure.security.constants.TwoFactorConstants; +import org.apache.fineract.infrastructure.security.domain.TFAccessToken; +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.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.stereotype.Service; +import org.springframework.web.filter.GenericFilterBean; + + +/** + * This filter is responsible for handling two-factor authentication. + * The filter is enabled when 'twofactor' environment profile is active, otherwise + * {@link InsecureTwoFactorAuthenticationFilter} is used. + * + * This filter validates an access-token provided as a header 'Fineract-Platform-TFA-Token'. + * If a valid token is provided, a 'TWOFACTOR_AUTHENTICATED' authority is added to the current + * authentication. + * If an invalid(non-existent or invalid) token is provided, 403 response is returned. + * + * An authenticated platform user with permission 'BYPASS_TWOFACTOR' will always be granted + * 'TWOFACTOR_AUTHENTICATED' authority regardless of the value of the 'Fineract-Platform-TFA-Token' + * header. + */ +@Service(value = "twoFactorAuthFilter") +@Profile("twofactor") +public class TwoFactorAuthenticationFilter extends GenericFilterBean { + + private final TwoFactorService twoFactorService; + + @Autowired + public TwoFactorAuthenticationFilter(TwoFactorService twoFactorService) { + this.twoFactorService = twoFactorService; + } + + @Override + public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) + throws IOException, ServletException { + + final HttpServletRequest request = (HttpServletRequest) req; + final HttpServletResponse response = (HttpServletResponse) res; + + SecurityContext context = SecurityContextHolder.getContext(); + Authentication authentication = null; + if(context != null) { + authentication = context.getAuthentication(); + } + + // Process two-factor only when user is authenticated + if(authentication != null && authentication.isAuthenticated()) { + AppUser user = (AppUser) authentication.getPrincipal(); + + if(user == null) { + return; + } + + if(!user.hasSpecificPermissionTo(TwoFactorConstants.BYPASS_TWO_FACTOR_PERMISSION)) { + // User can't bypass two-factor auth, check two-factor access token + String token = request.getHeader("Fineract-Platform-TFA-Token"); + if(token != null) { + TFAccessToken accessToken = twoFactorService.fetchAccessTokenForUser(user, token); + // Token is non-existent or invalid + if(accessToken == null || !accessToken.isValid()) { + response.addHeader("WWW-Authenticate", + "Basic realm=\"Fineract Platform API Two Factor\""); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, + "Invalid two-factor access token provided"); + return; + } + } else { + // No token provided + chain.doFilter(req, res); + return; + } + } + + List updatedAuthorities = new ArrayList<>(authentication.getAuthorities()); + updatedAuthorities.add(new SimpleGrantedAuthority("TWOFACTOR_AUTHENTICATED")); + final Authentication updatedAuthentication = createUpdatedAuthentication(authentication, + updatedAuthorities); + context.setAuthentication(updatedAuthentication); + } + + chain.doFilter(req, res); + } + + private Authentication createUpdatedAuthentication(final Authentication currentAuthentication, + final List updatedAuthorities) { + + final UsernamePasswordAuthenticationToken authentication = new + UsernamePasswordAuthenticationToken(currentAuthentication.getPrincipal(), + currentAuthentication.getCredentials(), updatedAuthorities); + + if(currentAuthentication instanceof OAuth2Authentication) { + final OAuth2Authentication oAuth2Authentication = (OAuth2Authentication) currentAuthentication; + return new OAuth2Authentication(oAuth2Authentication.getOAuth2Request(), authentication); + } + + return authentication; + } +} \ No newline at end of file diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/AccessTokenGenerationService.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/AccessTokenGenerationService.java new file mode 100644 index 000000000..e477593e9 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/AccessTokenGenerationService.java @@ -0,0 +1,24 @@ +/** + * 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.service; + +public interface AccessTokenGenerationService { + + String generateRandomToken(); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/RandomOTPGenerator.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/RandomOTPGenerator.java new file mode 100644 index 000000000..d3bf5518d --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/RandomOTPGenerator.java @@ -0,0 +1,38 @@ +/** + * 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.service; + +public class RandomOTPGenerator { + + private static final String allowedCharacters = "0123456789ABCDEFGHIJKLMNOPQRSTUVQXYZ"; + private final int tokenLength; + + public RandomOTPGenerator(int tokenLength) { + this.tokenLength = tokenLength; + } + + public String generate() { + StringBuilder builder = new StringBuilder(); + for(int i = 0; i < tokenLength; i++) { + builder.append(allowedCharacters.charAt((int) (Math.random() * (allowedCharacters.length())))); + } + + return builder.toString(); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TwoFactorConfigurationService.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TwoFactorConfigurationService.java new file mode 100644 index 000000000..aaf9354ef --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TwoFactorConfigurationService.java @@ -0,0 +1,51 @@ +/** + * 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.service; + +import java.util.Map; + +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.security.data.OTPRequest; +import org.apache.fineract.useradministration.domain.AppUser; + +public interface TwoFactorConfigurationService { + + + Map retrieveAll(); + + boolean isSMSEnabled(); + Integer getSMSProviderId(); + String getSmsText(); + + boolean isEmailEnabled(); + String getEmailSubject(); + String getEmailBody(); + + String getFormattedEmailSubjectFor(AppUser user, OTPRequest request); + String getFormattedEmailBodyFor(AppUser user, OTPRequest request); + String getFormattedSmsTextFor(AppUser user, OTPRequest request); + + Integer getOTPTokenLength(); + Integer getOTPTokenLiveTime(); + + Integer getAccessTokenLiveTime(); + Integer getAccessTokenExtendedLiveTime(); + + Map update(JsonCommand command); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TwoFactorConfigurationServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TwoFactorConfigurationServiceImpl.java new file mode 100644 index 000000000..bd4c108d8 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TwoFactorConfigurationServiceImpl.java @@ -0,0 +1,304 @@ +/** + * 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.service; + + +import java.io.StringReader; +import java.io.StringWriter; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.security.constants.TwoFactorConfigurationConstants; +import org.apache.fineract.infrastructure.security.constants.TwoFactorConstants; +import org.apache.fineract.infrastructure.security.data.OTPRequest; +import org.apache.fineract.infrastructure.security.domain.TwoFactorConfiguration; +import org.apache.fineract.infrastructure.security.domain.TwoFactorConfigurationRepository; +import org.apache.fineract.useradministration.domain.AppUser; +import org.joda.time.LocalDateTime; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; + +import com.github.mustachejava.DefaultMustacheFactory; +import com.github.mustachejava.Mustache; +import com.github.mustachejava.MustacheFactory; + +@Service +@Profile("twofactor") +public class TwoFactorConfigurationServiceImpl implements TwoFactorConfigurationService { + + private static final String DEFAULT_EMAIL_SUBJECT = "Fineract Two-Factor Authentication Token"; + private static final String DEFAULT_EMAIL_BODY = "Hello {username}.\n" + + "Your OTP login token is {token}."; + private static final String DEFAULT_SMS_TEXT = "Your authentication token for Fineract is " + + "{token}."; + + private final TwoFactorConfigurationRepository configurationRepository; + + + @Autowired + public TwoFactorConfigurationServiceImpl(TwoFactorConfigurationRepository configurationRepository) { + this.configurationRepository = configurationRepository; + } + + @Override + @Cacheable(value = "tfConfig", key = "T(org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil).getTenant().getTenantIdentifier()") + public Map retrieveAll() { + List configurationList = configurationRepository.findAll(); + Map configurationMap = new HashMap<>(); + for(final TwoFactorConfiguration configuration : configurationList) { + configurationMap.put(configuration.getName(), configuration.getObjectValue()); + } + return configurationMap; + } + + @Override + @CacheEvict(value = "tfConfig", allEntries = true) + public Map update(JsonCommand command) { + Map actualChanges = new HashMap<>(); + + + for(final String parameterName : TwoFactorConfigurationConstants.BOOLEAN_PARAMETERS) { + TwoFactorConfiguration configuration = configurationRepository.findByName(parameterName); + if(configuration == null) { + continue; + } + + if(command.isChangeInBooleanParameterNamed(parameterName, configuration.getBooleanValue())) { + final boolean newValue = command.booleanPrimitiveValueOfParameterNamed(parameterName); + actualChanges.put(parameterName, newValue); + configuration.setBooleanValue(newValue); + configurationRepository.save(configuration); + } + } + + for(final String parameterName : TwoFactorConfigurationConstants.STRING_PARAMETERS) { + TwoFactorConfiguration configuration = configurationRepository.findByName(parameterName); + if(configuration == null) { + continue; + } + + if(command.isChangeInStringParameterNamed(parameterName, configuration.getStringValue())) { + final String newValue = command.stringValueOfParameterNamed(parameterName).trim(); + actualChanges.put(parameterName, newValue); + configuration.setStringValue(newValue); + configurationRepository.save(configuration); + } + } + + for(final String parameterName : TwoFactorConfigurationConstants.NUMBER_PARAMETERS) { + TwoFactorConfiguration configuration = configurationRepository.findByName(parameterName); + if(configuration == null) { + continue; + } + + if(command.isChangeInIntegerSansLocaleParameterNamed(parameterName, configuration.getIntegerValue())) { + final Long newValue = command.longValueOfParameterNamed(parameterName); + actualChanges.put(parameterName, newValue); + configuration.setIntegerValue(newValue); + configurationRepository.save(configuration); + } + } + + if(!actualChanges.isEmpty()) { + configurationRepository.flush(); + } + + return actualChanges; + } + + @Override + @Cacheable(value = "tfConfig", key = "T(org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil).getTenant().getTenantIdentifier()+'|smsEnabled'") + public boolean isSMSEnabled() { + return getBooleanConfig(TwoFactorConfigurationConstants.ENABLE_SMS_DELIVERY, false); + } + + @Override + @Cacheable(value = "tfConfig", key = "T(org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil).getTenant().getTenantIdentifier()+'|smsProvider'") + public Integer getSMSProviderId() { + Integer value = getIntegerConfig(TwoFactorConfigurationConstants.SMS_PROVIDER_ID, + null); + if(value < 1) { + return null; + } + return value; + } + + @Override + @Cacheable(value = "tfConfig", key = "T(org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil).getTenant().getTenantIdentifier()+'|smsText'") + public String getSmsText() { + return getStringConfig(TwoFactorConfigurationConstants.SMS_MESSAGE_TEXT, DEFAULT_SMS_TEXT); + } + + @Override + @Cacheable(value = "tfConfig", key = "T(org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil).getTenant().getTenantIdentifier()+'|emailEnabled'") + public boolean isEmailEnabled() { + return getBooleanConfig(TwoFactorConfigurationConstants.ENABLE_EMAIL_DELIVERY, false); + } + + @Override + @Cacheable(value = "tfConfig", key = "T(org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil).getTenant().getTenantIdentifier()+'|emailSubject'") + public String getEmailSubject() { + return getStringConfig(TwoFactorConfigurationConstants.EMAIL_SUBJECT, DEFAULT_EMAIL_SUBJECT); + } + + @Override + @Cacheable(value = "tfConfig", key = "T(org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil).getTenant().getTenantIdentifier()+'|emailBody'") + public String getEmailBody() { + return getStringConfig(TwoFactorConfigurationConstants.EMAIL_BODY, DEFAULT_EMAIL_BODY); + } + + @Override + public String getFormattedEmailSubjectFor(AppUser user, OTPRequest request) { + final Map templateData = processTemplateDataFor(user, request); + return compileTextTemplate(getEmailSubject(), TwoFactorConstants.EMAIL_DELIVERY_METHOD_NAME, templateData); + } + + @Override + public String getFormattedEmailBodyFor(AppUser user, OTPRequest request) { + final Map templateData = processTemplateDataFor(user, request); + return compileTextTemplate(getEmailBody(), TwoFactorConstants.EMAIL_DELIVERY_METHOD_NAME, templateData); + } + + @Override + public String getFormattedSmsTextFor(AppUser user, OTPRequest request) { + final Map templateData = processTemplateDataFor(user, request); + return compileTextTemplate(getSmsText(), TwoFactorConstants.SMS_DELIVERY_METHOD_NAME, templateData); + } + + @Override + @Cacheable(value = "tfConfig", key = "T(org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil).getTenant().getTenantIdentifier()+'|otpLength'") + public Integer getOTPTokenLength() { + Integer defaultValue = 1; + return getIntegerConfig(TwoFactorConfigurationConstants.OTP_TOKEN_LENGTH, + defaultValue); + } + + @Override + @Cacheable(value = "tfConfig", key = "T(org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil).getTenant().getTenantIdentifier()+'|otpTime'") + public Integer getOTPTokenLiveTime() { + Integer defaultValue = 300; + Integer value = getIntegerConfig(TwoFactorConfigurationConstants.OTP_TOKEN_LIVE_TIME, + defaultValue); + if(value < 1) { + return defaultValue; + } + return value; + } + + @Override + @Cacheable(value = "tfConfig", key = "T(org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil).getTenant().getTenantIdentifier()+'|tokenTime'") + public Integer getAccessTokenLiveTime() { + Integer defaultValue = 86400; + Integer value = getIntegerConfig(TwoFactorConfigurationConstants.ACCESS_TOKEN_LIVE_TIME, + defaultValue); + if(value < 1) { + return defaultValue; + } + return value; + } + + @Override + @Cacheable(value = "tfConfig", key = "T(org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil).getTenant().getTenantIdentifier()+'|tokenExtendedTime'") + public Integer getAccessTokenExtendedLiveTime() { + Integer defaultValue = 604800; + Integer value = getIntegerConfig(TwoFactorConfigurationConstants.ACCESS_TOKEN_LIVE_TIME_EXTENDED, + defaultValue); + if(value < 1) { + return defaultValue; + } + return value; + } + + private boolean getBooleanConfig(final String name, final boolean defaultValue) { + final TwoFactorConfiguration configuration = + configurationRepository.findByName(name); + Boolean value = configuration.getBooleanValue(); + if(value == null) { + return defaultValue; + } + return value; + } + + private String getStringConfig(final String name, final String defaultValue) { + final TwoFactorConfiguration configuration = + configurationRepository.findByName(name); + String value = configuration.getStringValue(); + if(value == null) { + return defaultValue; + } + return value; + } + + private Integer getIntegerConfig(final String name, final Integer defaultValue) { + final TwoFactorConfiguration configuration = + configurationRepository.findByName(name); + Integer value = configuration.getIntegerValue(); + if(value == null) { + return defaultValue; + } + return value; + } + + private Map processTemplateDataFor(AppUser user, OTPRequest request) { + Map templateData = new HashMap<>(); + + templateData.put("username", user.getUsername()); + templateData.put("email", user.getEmail()); + templateData.put("firstname", user.getFirstname()); + templateData.put("lastname", user.getLastname()); + if(user.getStaff() != null && user.getStaff().mobileNo() != null) { + templateData.put("mobileno", user.getStaff().mobileNo()); + } + + templateData.put("token", request.getToken()); + templateData.put("tokenlivetime", request.getMetadata().getTokenLiveTimeInSec()); + + DateTimeFormatter timeFormatter = DateTimeFormat.forPattern("HH:mm:ss"); + DateTimeFormatter dateFormatter = DateTimeFormat.forPattern("dd.MM.yyyy"); + + final LocalDateTime requestTime = request.getMetadata().getRequestTime().toLocalDateTime(); + final LocalDateTime expireTime = requestTime.plusSeconds(request.getMetadata().getTokenLiveTimeInSec()); + + templateData.put("requestdate", requestTime.toLocalDate().toString(dateFormatter)); + templateData.put("requesttime", requestTime.toLocalTime().toString(timeFormatter)); + + templateData.put("expiredate", expireTime.toLocalDate().toString(dateFormatter)); + templateData.put("expiretime", expireTime.toLocalTime().toString(timeFormatter)); + + return templateData; + } + + private String compileTextTemplate(final String template, final String name, + final Map params) { + final MustacheFactory mf = new DefaultMustacheFactory(); + final Mustache mustache = mf.compile(new StringReader(template), name); + + final StringWriter stringWriter = new StringWriter(); + mustache.execute(stringWriter, params); + + return stringWriter.toString(); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TwoFactorService.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TwoFactorService.java new file mode 100644 index 000000000..da556d0b5 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TwoFactorService.java @@ -0,0 +1,43 @@ +/** + * 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.service; + +import java.util.List; + +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.security.data.OTPDeliveryMethod; +import org.apache.fineract.infrastructure.security.data.OTPRequest; +import org.apache.fineract.infrastructure.security.domain.TFAccessToken; +import org.apache.fineract.useradministration.domain.AppUser; + +public interface TwoFactorService { + + List getDeliveryMethodsForUser(AppUser user); + + OTPRequest createNewOTPToken(AppUser user, String deliveryMethodName, boolean extendedAccessToken); + + TFAccessToken createAccessTokenFromOTP(AppUser user, String otpToken); + + void validateTwoFactorAccessToken(AppUser user, String token); + + TFAccessToken fetchAccessTokenForUser(AppUser user, String token); + + TFAccessToken invalidateAccessToken(AppUser user, JsonCommand command); + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TwoFactorServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TwoFactorServiceImpl.java new file mode 100644 index 000000000..64361c20c --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TwoFactorServiceImpl.java @@ -0,0 +1,229 @@ +/** + * 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.service; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.lang.StringUtils; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.domain.EmailDetail; +import org.apache.fineract.infrastructure.core.service.PlatformEmailService; +import org.apache.fineract.infrastructure.security.constants.TwoFactorConstants; +import org.apache.fineract.infrastructure.security.data.OTPDeliveryMethod; +import org.apache.fineract.infrastructure.security.data.OTPRequest; +import org.apache.fineract.infrastructure.security.domain.OTPRequestRepository; +import org.apache.fineract.infrastructure.security.domain.TFAccessToken; +import org.apache.fineract.infrastructure.security.domain.TFAccessTokenRepository; +import org.apache.fineract.infrastructure.security.exception.AccessTokenInvalidIException; +import org.apache.fineract.infrastructure.security.exception.OTPDeliveryMethodInvalidException; +import org.apache.fineract.infrastructure.security.exception.OTPTokenInvalidException; +import org.apache.fineract.infrastructure.sms.domain.SmsMessage; +import org.apache.fineract.infrastructure.sms.domain.SmsMessageRepository; +import org.apache.fineract.infrastructure.sms.scheduler.SmsMessageScheduledJobService; +import org.apache.fineract.useradministration.domain.AppUser; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; + +@Service +@Profile("twofactor") +public class TwoFactorServiceImpl implements TwoFactorService { + + + + private final AccessTokenGenerationService accessTokenGenerationService; + private final PlatformEmailService emailService; + private final SmsMessageScheduledJobService smsMessageScheduledJobService; + + private final OTPRequestRepository otpRequestRepository; + private final TFAccessTokenRepository tfAccessTokenRepository; + private final SmsMessageRepository smsMessageRepository; + + private final TwoFactorConfigurationService configurationService; + + @Autowired + public TwoFactorServiceImpl(AccessTokenGenerationService accessTokenGenerationService, + PlatformEmailService emailService, + SmsMessageScheduledJobService smsMessageScheduledJobService, + OTPRequestRepository otpRequestRepository, + TFAccessTokenRepository tfAccessTokenRepository, + SmsMessageRepository smsMessageRepository, + TwoFactorConfigurationService configurationService) { + this.accessTokenGenerationService = accessTokenGenerationService; + this.emailService = emailService; + this.smsMessageScheduledJobService = smsMessageScheduledJobService; + this.otpRequestRepository = otpRequestRepository; + this.tfAccessTokenRepository = tfAccessTokenRepository; + this.smsMessageRepository = smsMessageRepository; + this.configurationService = configurationService; + } + + + @Override + public List getDeliveryMethodsForUser(final AppUser user) { + List deliveryMethods = new ArrayList<>(); + + OTPDeliveryMethod smsMethod = getSMSDeliveryMethodForUser(user); + if(smsMethod != null) { + deliveryMethods.add(smsMethod); + } + OTPDeliveryMethod emailDelivery = getEmailDeliveryMethodForUser(user); + if(emailDelivery != null) { + deliveryMethods.add(emailDelivery); + } + + return deliveryMethods; + } + + @Override + public OTPRequest createNewOTPToken(final AppUser user, final String deliveryMethodName, + final boolean extendedAccessToken) { + if(TwoFactorConstants.SMS_DELIVERY_METHOD_NAME.equalsIgnoreCase(deliveryMethodName)) { + OTPDeliveryMethod smsDelivery = getSMSDeliveryMethodForUser(user); + if(smsDelivery == null) { + throw new OTPDeliveryMethodInvalidException(); + } + final OTPRequest request = generateNewToken(smsDelivery, extendedAccessToken); + final String smsText = configurationService.getFormattedSmsTextFor(user, request); + SmsMessage smsMessage = SmsMessage.pendingSms(null, null, null, user.getStaff(), smsText, + user.getStaff().mobileNo(), null); + this.smsMessageRepository.save(smsMessage); + smsMessageScheduledJobService.sendTriggeredMessage(Collections.singleton(smsMessage), + configurationService.getSMSProviderId()); + otpRequestRepository.addOTPRequest(user, request); + return request; + } else if(TwoFactorConstants.EMAIL_DELIVERY_METHOD_NAME.equalsIgnoreCase(deliveryMethodName)) { + OTPDeliveryMethod emailDelivery = getEmailDeliveryMethodForUser(user); + if(emailDelivery == null) { + throw new OTPDeliveryMethodInvalidException(); + } + final OTPRequest request = generateNewToken(emailDelivery, extendedAccessToken); + final String emailSubject = configurationService.getFormattedEmailSubjectFor(user, request); + final String emailBody = configurationService.getFormattedEmailBodyFor(user, request); + final EmailDetail emailData = new EmailDetail(emailSubject, emailBody, user.getEmail(), + user.getFirstname() + " " + user.getLastname()); + emailService.sendDefinedEmail(emailData); + otpRequestRepository.addOTPRequest(user, request); + return request; + } + + throw new OTPDeliveryMethodInvalidException(); + } + + @Override + @CachePut(value = "userTFAccessToken", + key = "T(org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil)" + + ".getTenant().getTenantIdentifier().concat(#user.username).concat(#result.token + 'tok')") + public TFAccessToken createAccessTokenFromOTP(final AppUser user, final String otpToken) { + + OTPRequest otpRequest = otpRequestRepository.getOTPRequestForUser(user); + if(otpRequest == null || !otpRequest.isValid() || !otpRequest.getToken().equalsIgnoreCase(otpToken)) { + throw new OTPTokenInvalidException(); + } + + otpRequestRepository.deleteOTPRequestForUser(user); + + String token = accessTokenGenerationService.generateRandomToken(); + int liveTime; + if(otpRequest.getMetadata().isExtendedAccessToken()) { + liveTime = configurationService.getAccessTokenExtendedLiveTime(); + } else { + liveTime = configurationService.getAccessTokenLiveTime(); + } + TFAccessToken accessToken = TFAccessToken.create(token, user, liveTime); + tfAccessTokenRepository.save(accessToken); + return accessToken; + } + + @Override + public void validateTwoFactorAccessToken(AppUser user, String token) { + TFAccessToken accessToken = fetchAccessTokenForUser(user, token); + + if(accessToken == null || !accessToken.isValid()) { + throw new AccessTokenInvalidIException(); + } + } + + @Override + @CacheEvict(value = "userTFAccessToken", + key = "T(org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil)" + + ".getTenant().getTenantIdentifier().concat(#user.username).concat(#result.token + 'tok')") + public TFAccessToken invalidateAccessToken(final AppUser user, final JsonCommand command) { + + final String token = command.stringValueOfParameterNamed("token"); + final TFAccessToken accessToken = fetchAccessTokenForUser(user, token); + + if(accessToken == null || !accessToken.isValid()) { + throw new AccessTokenInvalidIException(); + } + + accessToken.setEnabled(false); + tfAccessTokenRepository.save(accessToken); + + return accessToken; + } + + @Override + @Cacheable(value = "userTFAccessToken", + key = "T(org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil)" + + ".getTenant().getTenantIdentifier().concat(#user.username).concat(#token + 'tok')") + public TFAccessToken fetchAccessTokenForUser(final AppUser user, final String token) { + return tfAccessTokenRepository.findByUserAndToken(user, token); + } + + private OTPDeliveryMethod getSMSDeliveryMethodForUser(final AppUser user) { + if(!configurationService.isSMSEnabled()) { + return null; + } + + if(configurationService.getSMSProviderId() == null) { + return null; + } + + if(user.getStaff() == null) { + return null; + } + String mobileNo = user.getStaff().mobileNo(); + if(StringUtils.isBlank(mobileNo)) { + return null; + } + + return new OTPDeliveryMethod(TwoFactorConstants.SMS_DELIVERY_METHOD_NAME, mobileNo); + } + + private OTPDeliveryMethod getEmailDeliveryMethodForUser(final AppUser user) { + if(!configurationService.isEmailEnabled()) { + return null; + } + + return new OTPDeliveryMethod(TwoFactorConstants.EMAIL_DELIVERY_METHOD_NAME, user.getEmail()); + } + + private OTPRequest generateNewToken(final OTPDeliveryMethod deliveryMethod, final boolean extendedAccessToken) { + int tokenLiveTime = configurationService.getOTPTokenLiveTime(); + int otpLength = configurationService.getOTPTokenLength(); + String token = new RandomOTPGenerator(otpLength).generate(); + return OTPRequest.create(token, tokenLiveTime, extendedAccessToken, deliveryMethod); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TwoFactorUtils.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TwoFactorUtils.java new file mode 100644 index 000000000..89292463b --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TwoFactorUtils.java @@ -0,0 +1,47 @@ +/** + * 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.service; + + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +@Component +public class TwoFactorUtils { + + private static final String TWO_FACTOR_PROFILE_NAME = "twofactor"; + + private final Environment environment; + + @Autowired + public TwoFactorUtils(Environment environment) { + this.environment = environment; + } + + + public boolean isTwoFactorAuthEnabled() { + for(final String profile : this.environment.getActiveProfiles()) { + if(TWO_FACTOR_PROFILE_NAME.equals(profile)) { + return true; + } + } + return false; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/UUIDAccessTokenGenerationService.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/UUIDAccessTokenGenerationService.java new file mode 100644 index 000000000..12e09c4a7 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/UUIDAccessTokenGenerationService.java @@ -0,0 +1,32 @@ +/** + * 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.service; + +import java.util.UUID; + +import org.springframework.stereotype.Service; + +@Service +public class UUIDAccessTokenGenerationService implements AccessTokenGenerationService { + + @Override + public String generateRandomToken() { + return UUID.randomUUID().toString().replaceAll("-", ""); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/useradministration/domain/AppUser.java b/fineract-provider/src/main/java/org/apache/fineract/useradministration/domain/AppUser.java index 9c8a86f86..d5bcfd522 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/useradministration/domain/AppUser.java +++ b/fineract-provider/src/main/java/org/apache/fineract/useradministration/domain/AppUser.java @@ -491,6 +491,23 @@ public class AppUser extends AbstractPersistableCustom implements Platform return hasNotPermission; } + /** + * Checks whether the user has a given permission explicitly. + * + * @param permissionCode the permission code to check for. + * @return whether the user has the specified permission + */ + public boolean hasSpecificPermissionTo(final String permissionCode) { + boolean hasPermission = false; + for (final Role role : this.roles) { + if(role.hasPermissionTo(permissionCode)) { + hasPermission = true; + break; + } + } + return hasPermission; + } + public void validateHasReadPermission(final String resourceType) { final String authorizationMessage = "User has no authority to view " + resourceType.toLowerCase() + "s"; diff --git a/fineract-provider/src/main/resources/META-INF/spring/ehcache.xml b/fineract-provider/src/main/resources/META-INF/spring/ehcache.xml index b991c6996..b5a442df5 100644 --- a/fineract-provider/src/main/resources/META-INF/spring/ehcache.xml +++ b/fineract-provider/src/main/resources/META-INF/spring/ehcache.xml @@ -48,4 +48,8 @@ overflowToDisk="false" /> + + \ No newline at end of file diff --git a/fineract-provider/src/main/resources/META-INF/spring/securityContext.xml b/fineract-provider/src/main/resources/META-INF/spring/securityContext.xml index f03220f79..a4db7fef5 100644 --- a/fineract-provider/src/main/resources/META-INF/spring/securityContext.xml +++ b/fineract-provider/src/main/resources/META-INF/spring/securityContext.xml @@ -42,19 +42,26 @@ method="POST" requires-channel="https" /> - + + + - - - - + - + - + + + + - - - - + diff --git a/fineract-provider/src/main/resources/sql/migrations/core_db/V336__two_factor_authentication.sql b/fineract-provider/src/main/resources/sql/migrations/core_db/V336__two_factor_authentication.sql new file mode 100644 index 000000000..bd5d35950 --- /dev/null +++ b/fineract-provider/src/main/resources/sql/migrations/core_db/V336__two_factor_authentication.sql @@ -0,0 +1,63 @@ +-- +-- 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. +-- + +-- Access Token Table + +CREATE TABLE `twofactor_access_token` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `token` varchar(32) NOT NULL, + `appuser_id` bigint(20) NOT NULL, + `valid_from` datetime NOT NULL, + `valid_to` datetime NOT NULL, + `enabled` bit(1) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `token_appuser_UNIQUE` (`token`,`appuser_id`), + KEY `user` (`appuser_id`), + KEY `token` (`token`), + CONSTRAINT `fk_2fa_access_token_user_id` FOREIGN KEY (`appuser_id`) REFERENCES `m_appuser` (`id`) +); + +-- Configuration + +CREATE TABLE `twofactor_configuration` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `name` varchar(40) NOT NULL, + `value` varchar(1024) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_UNIQUE` (`name`) +); + +INSERT INTO `twofactor_configuration` (`name`, `value`) VALUES + ('otp-delivery-email-enable', 'true'), + ('otp-delivery-email-subject', 'Fineract Two-Factor Authentication Token'), + ('otp-delivery-email-body', 'Hello {{username}}.\nYour OTP login token is {{token}}.'), + ('otp-delivery-sms-enable', 'false'), + ('otp-delivery-sms-provider', '1'), + ('otp-delivery-sms-text', 'Your authentication token for Fineract is {{token}}.'), + ('otp-token-live-time', '300'), + ('otp-token-length', '5'), + ('access-token-live-time', '86400'), + ('access-token-live-time-extended', '604800'); + + +INSERT INTO `m_permission` (`grouping`, `code`, `entity_name`, `action_name`, `can_maker_checker`) VALUES + ('authorisation', 'INVALIDATE_TWOFACTOR_ACCESSTOKEN', 'TWOFACTOR_ACCESSTOKEN', 'INVALIDATE', '0'), + ('configuration', 'READ_TWOFACTOR_CONFIGURATION', 'TWOFACTOR_CONFIGURATION', 'READ', '0'), + ('configuration', 'UPDATE_TWOFACTOR_CONFIGURATION', 'TWOFACTOR_CONFIGURATION', 'UPDATE', '0'), + ('special', 'BYPASS_TWOFACTOR', NULL, NULL, '0'); \ No newline at end of file