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> <td></td>
</tr> </tr>
<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><a href="#users">User</a></td>
<td>users</td> <td>users</td>
<td><a href="#users_create">Create a User</a></td> <td><a href="#users_create">Create a User</a></td>
@@ -2942,6 +2966,16 @@
external service Configuration</a></td> external service Configuration</a></td>
<td></td> <td></td>
</tr> </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> </table>
</div> </div>
</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 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. 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>
<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> <p>
The platform has been configured to reject plain HTTP requests and The platform has been configured to reject plain HTTP requests and
to expect all API requests to be made over <a to expect all API requests to be made over <a
@@ -4057,6 +4096,142 @@ function executeAjaxRequest(url, verbType, jsonData, authKey, successFunction, e
</code> </code>
</div> </div>
</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> <a id="batch_api" name="batch_api" class="old-syle-anchor">&nbsp;</a>
<div class="method-section"> <div class="method-section">
@@ -19391,6 +19566,73 @@ Content-Type: application/json
</div> </div>
</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 --> <!-- Fund starts here -->
<a id="funds" name="funds" class="old-syle-anchor">&nbsp;</a> <a id="funds" name="funds" class="old-syle-anchor">&nbsp;</a>
<div class="method-section"> <div class="method-section">
+4
View File
@@ -745,4 +745,8 @@ ul.field li {
tt { tt {
font-size: 9.5pt; 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 */ /* Enable Oauth2 authentication based on environment, default to HTTP basic auth */
if (project.hasProperty('security') && project.getProperty('security') == 'oauth') { if (project.hasProperty('security') && project.getProperty('security') == 'oauth') {
copy { if(project.hasProperty('twofactor') && project.getProperty('twofactor') == 'enabled') {
from './properties/oauth/' copy {
into 'src/main/resources/' from './properties/oauth/twofactor/'
include '*.properties' into 'src/main/resources/'
} include '*.properties'
}
} else {
copy {
from './properties/oauth/'
into 'src/main/resources/'
include '*.properties'
}
}
} else { } else {
copy { if(project.hasProperty('twofactor') && project.getProperty('twofactor') == 'enabled') {
from './properties/basicauth/' copy {
into 'src/main/resources/' from './properties/basicauth/twofactor/'
include '*.properties' into 'src/main/resources/'
} include '*.properties'
}
} else {
copy {
from './properties/basicauth/'
into 'src/main/resources/'
include '*.properties'
}
}
} }
task dist(type:Zip){ 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/"; this.href = "/emailcampaigns/configuration/";
return this; 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(); Long getDailyTPTLimit();
void removeGlobalConfigurationPropertyDataFromCache(String propertyName); void removeGlobalConfigurationPropertyDataFromCache(String propertyName);
boolean isSMSOTPDeliveryEnabled();
boolean isEmailOTPDeliveryEnabled();
Integer retrieveOTPCharacterLength();
Integer retrieveOTPLiveTime();
} }
@@ -287,6 +287,43 @@ public class ConfigurationDomainServiceJpa implements ConfigurationDomainService
configurations.remove(key); 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) { private GlobalConfigurationPropertyData getGlobalConfigurationPropertyData(final String propertyName) {
String identifier = ThreadLocalContextUtil.getTenant().getTenantIdentifier(); String identifier = ThreadLocalContextUtil.getTenant().getTenantIdentifier();
String key = identifier + "_" + propertyName; String key = identifier + "_" + propertyName;
@@ -40,7 +40,8 @@ import org.springframework.context.annotation.PropertySource;
* and MariaDB4j (because those differ in the subclasses). * and MariaDB4j (because those differ in the subclasses).
*/ */
@Configuration @Configuration
@Import({ WebXmlConfiguration.class, WebXmlOauthConfiguration.class, WebFrontEndConfiguration.class }) @Import({ WebXmlConfiguration.class, WebXmlOauthConfiguration.class, WebFrontEndConfiguration.class,
WebTwoFactorXmlConfiguration.class })
@ImportResource({ "classpath*:META-INF/spring/appContext.xml" }) @ImportResource({ "classpath*:META-INF/spring/appContext.xml" })
@PropertySource(value="classpath:META-INF/spring/jdbc.properties") @PropertySource(value="classpath:META-INF/spring/jdbc.properties")
@EnableAutoConfiguration(exclude = { DataSourceAutoConfiguration.class, @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.data.EnumOptionData;
import org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer; 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.data.AuthenticatedUserData;
import org.apache.fineract.infrastructure.security.service.SpringSecurityPlatformSecurityContext; 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.data.RoleData;
import org.apache.fineract.useradministration.domain.AppUser; import org.apache.fineract.useradministration.domain.AppUser;
import org.apache.fineract.useradministration.domain.Role; import org.apache.fineract.useradministration.domain.Role;
@@ -56,15 +58,17 @@ public class AuthenticationApiResource {
private final DaoAuthenticationProvider customAuthenticationProvider; private final DaoAuthenticationProvider customAuthenticationProvider;
private final ToApiJsonSerializer<AuthenticatedUserData> apiJsonSerializerService; private final ToApiJsonSerializer<AuthenticatedUserData> apiJsonSerializerService;
private final SpringSecurityPlatformSecurityContext springSecurityPlatformSecurityContext; private final SpringSecurityPlatformSecurityContext springSecurityPlatformSecurityContext;
private final TwoFactorUtils twoFactorUtils;
@Autowired @Autowired
public AuthenticationApiResource( public AuthenticationApiResource(
@Qualifier("customAuthenticationProvider") final DaoAuthenticationProvider customAuthenticationProvider, @Qualifier("customAuthenticationProvider") final DaoAuthenticationProvider customAuthenticationProvider,
final ToApiJsonSerializer<AuthenticatedUserData> apiJsonSerializerService, final ToApiJsonSerializer<AuthenticatedUserData> apiJsonSerializerService,
final SpringSecurityPlatformSecurityContext springSecurityPlatformSecurityContext) { final SpringSecurityPlatformSecurityContext springSecurityPlatformSecurityContext, TwoFactorUtils twoFactorUtils) {
this.customAuthenticationProvider = customAuthenticationProvider; this.customAuthenticationProvider = customAuthenticationProvider;
this.apiJsonSerializerService = apiJsonSerializerService; this.apiJsonSerializerService = apiJsonSerializerService;
this.springSecurityPlatformSecurityContext = springSecurityPlatformSecurityContext; this.springSecurityPlatformSecurityContext = springSecurityPlatformSecurityContext;
this.twoFactorUtils = twoFactorUtils;
} }
@POST @POST
@@ -100,12 +104,16 @@ public class AuthenticationApiResource {
final EnumOptionData organisationalRole = principal.organisationalRoleData(); final EnumOptionData organisationalRole = principal.organisationalRoleData();
boolean isTwoFactorRequired = twoFactorUtils.isTwoFactorAuthEnabled() && !
principal.hasSpecificPermissionTo(TwoFactorConstants.BYPASS_TWO_FACTOR_PERMISSION);
if (this.springSecurityPlatformSecurityContext.doesPasswordHasToBeRenewed(principal)) { 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 { } else {
authenticatedUserData = new AuthenticatedUserData(username, officeId, officeName, staffId, staffDisplayName, 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.data.EnumOptionData;
import org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer; 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.data.AuthenticatedOauthUserData;
import org.apache.fineract.infrastructure.security.service.SpringSecurityPlatformSecurityContext; 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.data.RoleData;
import org.apache.fineract.useradministration.domain.AppUser; import org.apache.fineract.useradministration.domain.AppUser;
import org.apache.fineract.useradministration.domain.Role; import org.apache.fineract.useradministration.domain.Role;
@@ -56,14 +58,17 @@ public class UserDetailsApiResource {
private final ResourceServerTokenServices tokenServices; private final ResourceServerTokenServices tokenServices;
private final ToApiJsonSerializer<AuthenticatedOauthUserData> apiJsonSerializerService; private final ToApiJsonSerializer<AuthenticatedOauthUserData> apiJsonSerializerService;
private final SpringSecurityPlatformSecurityContext springSecurityPlatformSecurityContext; private final SpringSecurityPlatformSecurityContext springSecurityPlatformSecurityContext;
private final TwoFactorUtils twoFactorUtils;
@Autowired @Autowired
public UserDetailsApiResource(@Qualifier("tokenServices") final ResourceServerTokenServices tokenServices, public UserDetailsApiResource(@Qualifier("tokenServices") final ResourceServerTokenServices tokenServices,
final ToApiJsonSerializer<AuthenticatedOauthUserData> apiJsonSerializerService, final ToApiJsonSerializer<AuthenticatedOauthUserData> apiJsonSerializerService,
final SpringSecurityPlatformSecurityContext springSecurityPlatformSecurityContext) { final SpringSecurityPlatformSecurityContext springSecurityPlatformSecurityContext,
final TwoFactorUtils twoFactorUtils) {
this.tokenServices = tokenServices; this.tokenServices = tokenServices;
this.apiJsonSerializerService = apiJsonSerializerService; this.apiJsonSerializerService = apiJsonSerializerService;
this.springSecurityPlatformSecurityContext = springSecurityPlatformSecurityContext; this.springSecurityPlatformSecurityContext = springSecurityPlatformSecurityContext;
this.twoFactorUtils = twoFactorUtils;
} }
@GET @GET
@@ -96,12 +101,16 @@ public class UserDetailsApiResource {
final EnumOptionData organisationalRole = principal.organisationalRoleData(); final EnumOptionData organisationalRole = principal.organisationalRoleData();
final boolean requireTwoFactorAuth = twoFactorUtils.isTwoFactorAuthEnabled()
&& !principal.hasSpecificPermissionTo(TwoFactorConstants.BYPASS_TWO_FACTOR_PERMISSION);
if (this.springSecurityPlatformSecurityContext.doesPasswordHasToBeRenewed(principal)) { if (this.springSecurityPlatformSecurityContext.doesPasswordHasToBeRenewed(principal)) {
authenticatedUserData = new AuthenticatedOauthUserData(principal.getUsername(), principal.getId(), accessToken); authenticatedUserData = new AuthenticatedOauthUserData(principal.getUsername(),
principal.getId(), accessToken, requireTwoFactorAuth);
} else { } else {
authenticatedUserData = new AuthenticatedOauthUserData(principal.getUsername(), officeId, officeName, staffId, staffDisplayName, authenticatedUserData = new AuthenticatedOauthUserData(principal.getUsername(),
organisationalRole, roles, permissions, principal.getId(), accessToken); officeId, officeName, staffId, staffDisplayName, organisationalRole, roles,
permissions, principal.getId(), accessToken, requireTwoFactorAuth);
} }
return this.apiJsonSerializerService.serialize(authenticatedUserData); 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") @SuppressWarnings("unused")
private final boolean shouldRenewPassword; private final boolean shouldRenewPassword;
@SuppressWarnings("unused")
private final boolean isTwoFactorAuthenticationRequired;
public AuthenticatedOauthUserData(final String username, final Collection<String> permissions) { public AuthenticatedOauthUserData(final String username, final Collection<String> permissions) {
this.username = username; this.username = username;
this.userId = null; this.userId = null;
@@ -67,11 +70,13 @@ public class AuthenticatedOauthUserData {
this.roles = null; this.roles = null;
this.permissions = permissions; this.permissions = permissions;
this.shouldRenewPassword = false; this.shouldRenewPassword = false;
this.isTwoFactorAuthenticationRequired = false;
} }
public AuthenticatedOauthUserData(final String username, final Long officeId, final String officeName, final Long staffId, 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 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.username = username;
this.officeId = officeId; this.officeId = officeId;
this.officeName = officeName; this.officeName = officeName;
@@ -84,9 +89,11 @@ public class AuthenticatedOauthUserData {
this.roles = roles; this.roles = roles;
this.permissions = permissions; this.permissions = permissions;
this.shouldRenewPassword = false; 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.username = username;
this.officeId = null; this.officeId = null;
this.officeName = null; this.officeName = null;
@@ -99,5 +106,6 @@ public class AuthenticatedOauthUserData {
this.roles = null; this.roles = null;
this.permissions = null; this.permissions = null;
this.shouldRenewPassword = true; this.shouldRenewPassword = true;
this.isTwoFactorAuthenticationRequired = isTwoFactorAuthenticationRequired;
} }
} }
@@ -54,6 +54,9 @@ public class AuthenticatedUserData {
@SuppressWarnings("unused") @SuppressWarnings("unused")
private final boolean shouldRenewPassword; private final boolean shouldRenewPassword;
@SuppressWarnings("unused")
private final boolean isTwoFactorAuthenticationRequired;
public AuthenticatedUserData(final String username, final Collection<String> permissions) { public AuthenticatedUserData(final String username, final Collection<String> permissions) {
this.username = username; this.username = username;
this.userId = null; this.userId = null;
@@ -67,11 +70,13 @@ public class AuthenticatedUserData {
this.roles = null; this.roles = null;
this.permissions = permissions; this.permissions = permissions;
this.shouldRenewPassword = false; this.shouldRenewPassword = false;
this.isTwoFactorAuthenticationRequired = false;
} }
public AuthenticatedUserData(final String username, final Long officeId, final String officeName, final Long staffId, 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 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.username = username;
this.officeId = officeId; this.officeId = officeId;
this.officeName = officeName; this.officeName = officeName;
@@ -84,9 +89,11 @@ public class AuthenticatedUserData {
this.roles = roles; this.roles = roles;
this.permissions = permissions; this.permissions = permissions;
this.shouldRenewPassword = false; 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.username = username;
this.officeId = null; this.officeId = null;
this.officeName = null; this.officeName = null;
@@ -99,5 +106,6 @@ public class AuthenticatedUserData {
this.roles = null; this.roles = null;
this.permissions = null; this.permissions = null;
this.shouldRenewPassword = true; 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; 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) { public void validateHasReadPermission(final String resourceType) {
final String authorizationMessage = "User has no authority to view " + resourceType.toLowerCase() + "s"; final String authorizationMessage = "User has no authority to view " + resourceType.toLowerCase() + "s";
@@ -48,4 +48,8 @@
overflowToDisk="false" /> overflowToDisk="false" />
<cache name="hooks" maxEntriesLocalHeap="10000" eternal="true" <cache name="hooks" maxEntriesLocalHeap="10000" eternal="true"
overflowToDisk="false" /> overflowToDisk="false" />
<cache name="userTFAccessToken" maxEntriesLocalHeap="10000"
overflowToDisk="false" timeToIdleSeconds="7200" />
<cache name="tfConfig" maxEntriesLocalHeap="10000" eternal="true"
overflowToDisk="false" />
</ehcache> </ehcache>
@@ -42,19 +42,26 @@
method="POST" requires-channel="https" /> method="POST" requires-channel="https" />
<intercept-url pattern="/api/*/self/registration/user" access="permitAll" <intercept-url pattern="/api/*/self/registration/user" access="permitAll"
method="POST" requires-channel="https" /> 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" /> 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" /> 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" /> 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" /> 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" /> method="HEAD" requires-channel="https" />
<custom-filter after="SECURITY_CONTEXT_FILTER" <custom-filter after="SECURITY_CONTEXT_FILTER"
ref="basicAuthenticationProcessingFilter" /> ref="basicAuthenticationProcessingFilter" />
<custom-filter ref="twoFactorAuthFilter" after="BASIC_AUTH_FILTER" />
</http> </http>
<beans:bean id="basicAuthenticationEntryPoint" <beans:bean id="basicAuthenticationEntryPoint"
@@ -83,27 +90,34 @@
erase-credentials="false"> erase-credentials="false">
<authentication-provider ref="customAuthenticationProvider" /> <authentication-provider ref="customAuthenticationProvider" />
</authentication-manager> </authentication-manager>
</beans:beans> </beans:beans>
<beans:beans profile="oauth"> <beans:beans profile="oauth">
<http create-session="stateless" use-expressions="true" pattern="/api/v1/**" <http create-session="stateless" use-expressions="true" pattern="/api/v1/**"
entry-point-ref="oauthAuthenticationEntryPoint" entry-point-ref="oauthAuthenticationEntryPoint"
access-decision-manager-ref="accessDecisionManager"> access-decision-manager-ref="accessDecisionManager">
<anonymous enabled="false" /> <anonymous enabled="false" />
<intercept-url pattern="/api//v1/**" method="OPTIONS" <intercept-url pattern="/api//v1/**" method="OPTIONS"
access="permitAll" requires-channel="https" /> access="permitAll" requires-channel="https" />
<intercept-url pattern="/api/v1/**" 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/v1/**" access="isFullyAuthenticated() and hasAuthority('TWOFACTOR_AUTHENTICATED')"
method="GET" requires-channel="https" /> 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" /> 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" /> 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" /> 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" /> method="HEAD" requires-channel="https" />
<custom-filter ref="tenantIdentifierProcessingFilter" <custom-filter ref="tenantIdentifierProcessingFilter"
position="FIRST" /> position="FIRST" />
<custom-filter before="PRE_AUTH_FILTER" ref="resourceServerFilter" /> <custom-filter before="PRE_AUTH_FILTER" ref="resourceServerFilter" />
<custom-filter ref="twoFactorAuthFilter" after="BASIC_AUTH_FILTER" />
<access-denied-handler ref="oauthAccessDeniedHandler" /> <access-denied-handler ref="oauthAccessDeniedHandler" />
</http> </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');