Merge branch 'TwoFactor' into develop

This commit is contained in:
Nazeer Hussain Shaik
2017-11-27 15:04:28 +05:30
47 changed files with 2762 additions and 35 deletions
+242
View File
@@ -2629,6 +2629,30 @@
<td></td>
</tr>
<tr>
<td><a href="#twofactor">Two-Factor Authentication</a></td>
<td>twofactor</td>
<td><a href="#twofactor_request">Request OTP</a></td>
<td><a href="#twofactor_deliverymethods">List OTP delivery methods</a></td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td>twofactor/validate</td>
<td><a href="#twofactor_validate">Validate OTP</a></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td>twofactor/invalidate</td>
<td><a href="#twofactor_invalidate">Invalidate Access Token</a></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td><a href="#users">User</a></td>
<td>users</td>
<td><a href="#users_create">Create a User</a></td>
@@ -2942,6 +2966,16 @@
external service Configuration</a></td>
<td></td>
</tr>
<tr>
<td><a href="#twofactor_config_get">Two-Factor Configuration</a></td>
<td>twofactor/configure</td>
<td></td>
<td><a href="#twofactor_config_get">Retrieve
Two-Factor Configuration</a></td>
<td><a href="#twofactor_config_update">Update
Two-Factor Configuration</a></td>
<td></td>
</tr>
</table>
</div>
</div>
@@ -3930,6 +3964,11 @@
Default authentication is using HTTP Basic Auth. Oauth2 can be enabled by using <i>-Psecurity=oauth</i> option on gradle build command , refer
the platform setup <a href="https://github.com/openMF/mifosx/wiki/Launching-platform-server-locally-from-the-command-line#choosing-authentication-mechanism"> wiki</a> for additional details.
</p>
<p>
Optionally, two-factor authentication can be enabled by using
<i>-Ptwofactor=enabled</i> on gradle build.
Details of the authentication workflow with two-factor authentication enabled can be found <a href="#twofactor">here</a>.
</p>
<p>
The platform has been configured to reject plain HTTP requests and
to expect all API requests to be made over <a
@@ -4057,6 +4096,142 @@ function executeAjaxRequest(url, verbType, jsonData, authKey, successFunction, e
</code>
</div>
</div>
<a id="twofactor" name="twofactor" class="old-syle-anchor">&nbsp;</a>
<div class="method-section">
<div class="method-description">
<h3>Two-Factor Authentication</h3>
<p>
Two-Factor authentication is supported by requesting & verifying
one-time passwords(OTP). OTPs are sent via SMS & email.
</p>
<p>
By default, two-factor authentication is disabled by default.
More information on how to enable TFA can be found <a href="#authentication_overview">here</a>.
</p>
<p>
Two-factor authentication workflow:
<ol class="normalli">
<li class="normalli">User authticates via BasicAuth / oAauth</li>
<li>Client requests a list of supported OTP delivery methods for the authenticated user(<a href="#twofactor_deliverymethods">Get Delivery Methods</a>)</li>
<li>User selects an OTP delivery method and client sends a request for OTP(<a href="#twofactor_request">Request OTP</a>)</li>
<li>User receives an OTP and the client sends it for verification(<a href="#twofactor_validate">Validate OTP</a>)</li>
<li>If the OTP is valid, an access token is returned</li>
<li>The access token is sent in following requestes to the server as a header <i>Fineract-Platform-TFA-Token</i></li>
<li>On session end, the access token should be invalidated<a href="#twofactor_invalidate">Invalidate Access Token</a>)</li>
</ol>
</p>
<p>
Two-Factor authentication and delivery methods can be configured via
the <a href="#twofactor_configure"<i>/twofactor/configure</i> endpoint.</a>
</p>
</div>
<div class="method-example">
</div>
</div>
<a id="twofactor_deliverymethods" name="twofactor_deliverymethods" class="old-syle-anchor">&nbsp;</a>
<div class="method-section">
<div class="method-description">
<h4>Get Delivery Methods</h4>
<p>Returns a list of possible OTP delivery methods for the current user</p>
<p>Requires first-factor authenticated user.</p>
</div>
<div class="method-example">
<code class="method-declaration">GET https://DomainName/api/v1/twofactor</code>
<code class="method-response">
[
{
"name": "sms",
"target": "08888888888"
},
{
"name": "email",
"target": "user@example.com"
}
]</code>
</div>
</div>
<a id="twofactor_request" name="twofactor_request" class="old-syle-anchor">&nbsp;</a>
<div class="method-section">
<div class="method-description">
<h4>Request OTP</h4>
<p>Requests an OTP.</p>
<p>Requires first-factor authenticated user.</p>
<h5>Arguments</h5>
<dl class="argument-list">
<dt>deliveryMethod</dt>
<dd>
String<span> mandatory, the delivery method name</span>
</dd>
<dt>extendedToken</dt>
<dd>
boolean<span> optional, whether to request an extended token, default false</span>
</dd>
</dl>
</div>
<div class="method-example">
<code class="method-declaration">POST https://DomainName/api/v1/twofactor?deliveryMethod=sms&extendedToken=false</code>
<code class="method-response">
{
"requestTime": 1500000000000,
"tokenLiveTimeInSec": 300,
"extendedAccessToken": false,
"deliveryMethod": {
"name": "sms",
"target": "08888888888"
}
}</code>
</div>
</div>
<a id="twofactor_validate" name="twofactor_validate" class="old-syle-anchor">&nbsp;</a>
<div class="method-section">
<div class="method-description">
<h4>Validate OTP</h4>
<p>Validates an OTP. If the OTP is valid, an access token is created.</p>
<p>The returned access token is later sent as a header <i>Fineract-Platform-TFA-Token</i>.</p>
<p>Requires first-factor authenticated user.</p>
<h5>Arguments</h5>
<dl class="argument-list">
<dt>token</dt>
<dd>
String<span> mandatory, the OTP to validate</span>
</dd>
</dl>
</div>
<div class="method-example">
<code class="method-declaration">POST https://DomainName/api/v1/twofactor/validate?token=YYYYY</code>
<code class="method-response">
{
"token": "cb0bb6e33fc540709d50a16eb2e555f9",
"validFrom": 1501530702801,
"validTo": 1501617102801
}</code>
</div>
</div>
<a id="twofactor_invalidate" name="twofactor_invalidate" class="old-syle-anchor">&nbsp;</a>
<div class="method-section">
<div class="method-description">
<h4>Invalidate Access Token</h4>
<p>Invalidates an access token.</p>
<p>Two factor access tokens should be invalidated on logout.</p>
<p>Requires fully authenticated user.</p>
</div>
<div class="method-example">
<code class="method-declaration">POST https://DomainName/api/v1/twofactor/invalidate</code>
<code class="method-request">
{
"token": "cb0bb6e33fc540709d50a16eb2e555f9"
}</code>
<code class="method-response">
{
"resourceIdentifier": "cb0bb6e33fc540709d50a16eb2e555f9"
}</code>
</div>
</div>
<a id="batch_api" name="batch_api" class="old-syle-anchor">&nbsp;</a>
<div class="method-section">
@@ -19391,6 +19566,73 @@ Content-Type: application/json
</div>
</div>
<a id="twofactor_config" name="twofactor_config"
class="old-syle-anchor">&nbsp;</a>
<div class="method-section">
<div class="method-description">
<h3>Two-Factor Configuration</h3>
<p>The following section describes the way to configure two-factor authentication</p>
<p>Two-Factor Authentication has to be enabled by either building with Gradle arguments
<i>-Ptwofactor=enabled</i> or enabling the <i>twofactor</i> profile via env. variable
</p>
<p>In order for SMS to be enabled an SMS bridge has to be setup with the message-gateway service.</p>
</div>
<div class="method-example">
</div>
</div>
<a id="twofactor_config_get" name="twofactor_config_get" class="old-syle-anchor">&nbsp;</a>
<div class="method-section">
<div class="method-description">
<h4>Retrieve Two-Factor Configuration</h4>
<p>Returns available two-factor configuration.</p>
</div>
<div class="method-example">
<code class="method-declaration"> GET https://DomainName/api/v1/twofactor/configure
</code>
<code class="method-response">
{
"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
}
</code>
</div>
</div>
<a id="twofactor_config_update" name="twofactor_config_update" class="old-syle-anchor">&nbsp;</a>
<div class="method-section">
<div class="method-description">
<h4>Update Two-Factor Configuration</h4>
<p>Update two-factor configuration.</p>
</div>
<div class="method-example">
<code class="method-declaration"> PUT https://DomainName/api/v1/twofactor/configure
</code>
<code class="method-request">
{
"otp-delivery-sms-provider": 7
"otp-delivery-sms-enable": false
}
</code>
<code class="method-response">
{
"changes": {
"otp-delivery-sms-enable": false,
"otp-delivery-sms-provider": 7
}
}
</code>
</div>
</div>
<!-- Fund starts here -->
<a id="funds" name="funds" class="old-syle-anchor">&nbsp;</a>
<div class="method-section">
+4
View File
@@ -745,4 +745,8 @@ ul.field li {
tt {
font-size: 9.5pt;
}
ol.normalli li {
list-style-type: decimal;
}
+26 -10
View File
@@ -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){
@@ -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
@@ -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
@@ -3118,4 +3118,18 @@ public class CommandWrapperBuilder {
this.href = "/emailcampaigns/configuration/";
return this;
}
public CommandWrapperBuilder invalidateTwoFactorAccessToken() {
this.actionName = "INVALIDATE";
this.entityName = "TWOFACTOR_ACCESSTOKEN";
this.href = "/twofactor/invalidate";
return this;
}
public CommandWrapperBuilder updateTwoFactorConfiguration() {
this.actionName = "UPDATE";
this.entityName = "TWOFACTOR_CONFIGURATION";
this.href = "/twofactor/configure";
return this;
}
}
@@ -85,5 +85,12 @@ public interface ConfigurationDomainService {
Long getDailyTPTLimit();
void removeGlobalConfigurationPropertyDataFromCache(String propertyName);
boolean isSMSOTPDeliveryEnabled();
boolean isEmailOTPDeliveryEnabled();
Integer retrieveOTPCharacterLength();
Integer retrieveOTPLiveTime();
}
@@ -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;
@@ -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,
@@ -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;
}
}
@@ -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<AuthenticatedUserData> apiJsonSerializerService;
private final SpringSecurityPlatformSecurityContext springSecurityPlatformSecurityContext;
private final TwoFactorUtils twoFactorUtils;
@Autowired
public AuthenticationApiResource(
@Qualifier("customAuthenticationProvider") final DaoAuthenticationProvider customAuthenticationProvider,
final ToApiJsonSerializer<AuthenticatedUserData> 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);
}
}
@@ -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<OTPMetadata> otpRequestSerializer;
private final ToApiJsonSerializer<OTPDeliveryMethod> otpDeliveryMethodSerializer;
private final ToApiJsonSerializer<AccessTokenData> accessTokenSerializer;
private final DefaultToApiJsonSerializer<Map<String, Object>> toApiJsonSerializer;
private final PlatformSecurityContext context;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
private final TwoFactorService twoFactorService;
@Autowired
public TwoFactorApiResource(ToApiJsonSerializer<OTPMetadata> otpRequestSerializer,
ToApiJsonSerializer<OTPDeliveryMethod> otpDeliveryMethodSerializer,
ToApiJsonSerializer<AccessTokenData> accessTokenSerializer,
DefaultToApiJsonSerializer<Map<String, Object>> 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<OTPDeliveryMethod> 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);
}
}
@@ -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<Map<String, Object>> toApiJsonSerializer;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
@Autowired
public TwoFactorConfigurationApiResource(PlatformSecurityContext context,
TwoFactorConfigurationService configurationService,
DefaultToApiJsonSerializer<Map<String, Object>> 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<String, Object> 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);
}
}
@@ -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<AuthenticatedOauthUserData> apiJsonSerializerService;
private final SpringSecurityPlatformSecurityContext springSecurityPlatformSecurityContext;
private final TwoFactorUtils twoFactorUtils;
@Autowired
public UserDetailsApiResource(@Qualifier("tokenServices") final ResourceServerTokenServices tokenServices,
final ToApiJsonSerializer<AuthenticatedOauthUserData> 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);
}
@@ -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<Map<String, Object>>() {}.getType();
this.fromJsonHelper.checkForUnsupportedParameters(typeOfMap, json,
new HashSet<>(Collections.singletonList("token")));
final JsonElement element = this.fromJsonHelper.parse(json);
final List<ApiParameterError> 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);
}
}
}
@@ -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<String, Object> changes = configurationService.update(command);
return new CommandProcessingResultBuilder()
.withCommandId(command.commandId())
.with(changes)
.build();
}
}
@@ -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<String> 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> STRING_PARAMETERS =
Arrays.asList(EMAIL_SUBJECT, EMAIL_BODY, SMS_MESSAGE_TEXT);
public static final List<String> BOOLEAN_PARAMETERS =
Arrays.asList(ENABLE_EMAIL_DELIVERY, ENABLE_SMS_DELIVERY);
public static final List<String> NUMBER_PARAMETERS =
Arrays.asList(SMS_PROVIDER_ID, OTP_TOKEN_LIVE_TIME, OTP_TOKEN_LENGTH,
ACCESS_TOKEN_LIVE_TIME, ACCESS_TOKEN_LIVE_TIME_EXTENDED);
}
@@ -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";
}
@@ -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;
}
}
@@ -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<String> 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<RoleData> roles,
final Collection<String> permissions, final Long userId, final String accessToken) {
final Collection<String> 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;
}
}
@@ -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<String> 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<RoleData> roles,
final Collection<String> permissions, final Long userId, final String base64EncodedAuthenticationKey) {
final Collection<String> 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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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);
}
}
@@ -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<Map<String, Object>>() {}.getType();
this.fromJsonHelper.checkForUnsupportedParameters(typeOfMap, json,
TwoFactorConfigurationConstants.REQUEST_DATA_PARAMETERS);
final JsonElement element = this.fromJsonHelper.parse(json);
final List<ApiParameterError> 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<ApiParameterError> 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();
}
}
@@ -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<Long, OTPRequest> 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());
}
}
@@ -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<Long> {
@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());
}
}
@@ -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<TFAccessToken, Long>, JpaSpecificationExecutor<TFAccessToken> {
TFAccessToken findByUserAndToken(AppUser user, String token);
}
@@ -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<Long> {
@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);
}
}
@@ -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<TwoFactorConfiguration, Long>, JpaSpecificationExecutor<TwoFactorConfiguration> {
TwoFactorConfiguration findByName(final String name);
List<TwoFactorConfiguration> findAll();
}
@@ -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");
}
}
@@ -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.");
}
}
@@ -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");
}
}
@@ -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<GrantedAuthority> 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);
}
}
@@ -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<GrantedAuthority> 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<GrantedAuthority> 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;
}
}
@@ -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();
}
@@ -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();
}
}
@@ -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<String, Object> 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<String,Object> update(JsonCommand command);
}
@@ -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<String, Object> retrieveAll() {
List<TwoFactorConfiguration> configurationList = configurationRepository.findAll();
Map<String, Object> configurationMap = new HashMap<>();
for(final TwoFactorConfiguration configuration : configurationList) {
configurationMap.put(configuration.getName(), configuration.getObjectValue());
}
return configurationMap;
}
@Override
@CacheEvict(value = "tfConfig", allEntries = true)
public Map<String, Object> update(JsonCommand command) {
Map<String, Object> 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<String, Object> templateData = processTemplateDataFor(user, request);
return compileTextTemplate(getEmailSubject(), TwoFactorConstants.EMAIL_DELIVERY_METHOD_NAME, templateData);
}
@Override
public String getFormattedEmailBodyFor(AppUser user, OTPRequest request) {
final Map<String, Object> templateData = processTemplateDataFor(user, request);
return compileTextTemplate(getEmailBody(), TwoFactorConstants.EMAIL_DELIVERY_METHOD_NAME, templateData);
}
@Override
public String getFormattedSmsTextFor(AppUser user, OTPRequest request) {
final Map<String, Object> 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<String, Object> processTemplateDataFor(AppUser user, OTPRequest request) {
Map<String, Object> 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<String, Object> 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();
}
}
@@ -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<OTPDeliveryMethod> 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);
}
@@ -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<OTPDeliveryMethod> getDeliveryMethodsForUser(final AppUser user) {
List<OTPDeliveryMethod> 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);
}
}
@@ -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;
}
}
@@ -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("-", "");
}
}
@@ -491,6 +491,23 @@ public class AppUser extends AbstractPersistableCustom<Long> 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";
@@ -48,4 +48,8 @@
overflowToDisk="false" />
<cache name="hooks" maxEntriesLocalHeap="10000" eternal="true"
overflowToDisk="false" />
<cache name="userTFAccessToken" maxEntriesLocalHeap="10000"
overflowToDisk="false" timeToIdleSeconds="7200" />
<cache name="tfConfig" maxEntriesLocalHeap="10000" eternal="true"
overflowToDisk="false" />
</ehcache>
@@ -42,19 +42,26 @@
method="POST" requires-channel="https" />
<intercept-url pattern="/api/*/self/registration/user" access="permitAll"
method="POST" requires-channel="https" />
<intercept-url pattern="/api/**" access="isFullyAuthenticated()"
<intercept-url pattern="/api/*/twofactor" access="isFullyAuthenticated()"
method="GET" requires-channel="https" />
<intercept-url pattern="/api/*/twofactor" access="isFullyAuthenticated()"
method="POST" requires-channel="https" />
<intercept-url pattern="/api/*/twofactor/validate" access="isFullyAuthenticated()"
method="POST" requires-channel="https" />
<intercept-url pattern="/api/**" access="isFullyAuthenticated() and hasAuthority('TWOFACTOR_AUTHENTICATED')"
method="GET" requires-channel="https" />
<intercept-url pattern="/api/**" access="isFullyAuthenticated()"
<intercept-url pattern="/api/**" access="isFullyAuthenticated() and hasAuthority('TWOFACTOR_AUTHENTICATED')"
method="POST" requires-channel="https" />
<intercept-url pattern="/api/**" access="isFullyAuthenticated()"
<intercept-url pattern="/api/**" access="isFullyAuthenticated() and hasAuthority('TWOFACTOR_AUTHENTICATED')"
method="PUT" requires-channel="https" />
<intercept-url pattern="/api/**" access="isFullyAuthenticated()"
<intercept-url pattern="/api/**" access="isFullyAuthenticated() and hasAuthority('TWOFACTOR_AUTHENTICATED')"
method="DELETE" requires-channel="https" />
<intercept-url pattern="/api/**" access="isFullyAuthenticated()"
<intercept-url pattern="/api/**" access="isFullyAuthenticated() and hasAuthority('TWOFACTOR_AUTHENTICATED')"
method="HEAD" requires-channel="https" />
<custom-filter after="SECURITY_CONTEXT_FILTER"
ref="basicAuthenticationProcessingFilter" />
<custom-filter ref="twoFactorAuthFilter" after="BASIC_AUTH_FILTER" />
</http>
<beans:bean id="basicAuthenticationEntryPoint"
@@ -83,27 +90,34 @@
erase-credentials="false">
<authentication-provider ref="customAuthenticationProvider" />
</authentication-manager>
</beans:beans>
</beans:beans>
<beans:beans profile="oauth">
<http create-session="stateless" use-expressions="true" pattern="/api/v1/**"
entry-point-ref="oauthAuthenticationEntryPoint"
access-decision-manager-ref="accessDecisionManager">
<anonymous enabled="false" />
<intercept-url pattern="/api//v1/**" method="OPTIONS"
access="permitAll" requires-channel="https" />
<intercept-url pattern="/api/v1/**" access="isFullyAuthenticated()"
access="permitAll" requires-channel="https" />
<intercept-url pattern="/api/*/twofactor" access="isFullyAuthenticated()"
method="GET" requires-channel="https" />
<intercept-url pattern="/api/*/twofactor" access="isFullyAuthenticated()"
method="POST" requires-channel="https" />
<intercept-url pattern="/api/*/twofactor/validate" access="isFullyAuthenticated()"
method="POST" requires-channel="https" />
<intercept-url pattern="/api/v1/**" access="isFullyAuthenticated() and hasAuthority('TWOFACTOR_AUTHENTICATED')"
method="GET" requires-channel="https" />
<intercept-url pattern="/api/v1/**" access="isFullyAuthenticated()"
<intercept-url pattern="/api/v1/**" access="isFullyAuthenticated() and hasAuthority('TWOFACTOR_AUTHENTICATED')"
method="POST" requires-channel="https" />
<intercept-url pattern="/api/v1/**" access="isFullyAuthenticated()"
<intercept-url pattern="/api/v1/**" access="isFullyAuthenticated() and hasAuthority('TWOFACTOR_AUTHENTICATED')"
method="PUT" requires-channel="https" />
<intercept-url pattern="/api/v1/**" access="isFullyAuthenticated()"
<intercept-url pattern="/api/v1/**" access="isFullyAuthenticated() and hasAuthority('TWOFACTOR_AUTHENTICATED')"
method="DELETE" requires-channel="https" />
<intercept-url pattern="/api/v1/**" access="isFullyAuthenticated()"
<intercept-url pattern="/api/v1/**" access="isFullyAuthenticated() and hasAuthority('TWOFACTOR_AUTHENTICATED')"
method="HEAD" requires-channel="https" />
<custom-filter ref="tenantIdentifierProcessingFilter"
position="FIRST" />
<custom-filter before="PRE_AUTH_FILTER" ref="resourceServerFilter" />
<custom-filter ref="twoFactorAuthFilter" after="BASIC_AUTH_FILTER" />
<access-denied-handler ref="oauthAccessDeniedHandler" />
</http>
@@ -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');