Repository: fineract Updated Branches: refs/heads/develop 4dbecc7f0 -> c689c143e
http://git-wip-us.apache.org/repos/asf/fineract/blob/1a966e8e/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/domain/TFAccessToken.java ---------------------------------------------------------------------- diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/domain/TFAccessToken.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/domain/TFAccessToken.java new file mode 100644 index 0000000..694046f --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/domain/TFAccessToken.java @@ -0,0 +1,137 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.security.domain; + +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; +import javax.persistence.UniqueConstraint; + +import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.security.data.AccessTokenData; +import org.apache.fineract.useradministration.domain.AppUser; +import org.joda.time.DateTime; +import org.joda.time.LocalDateTime; + +@Entity +@Table(name = "twofactor_access_token", + uniqueConstraints = {@UniqueConstraint(columnNames = { "token", "appuser_id" }, name = "token_appuser_UNIQUE")}) +public class TFAccessToken extends AbstractPersistableCustom<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()); + } +} http://git-wip-us.apache.org/repos/asf/fineract/blob/1a966e8e/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/domain/TFAccessTokenRepository.java ---------------------------------------------------------------------- diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/domain/TFAccessTokenRepository.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/domain/TFAccessTokenRepository.java new file mode 100644 index 0000000..cbdbc8f --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/domain/TFAccessTokenRepository.java @@ -0,0 +1,31 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.security.domain; + +import org.apache.fineract.useradministration.domain.AppUser; +import org.springframework.context.annotation.Profile; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +@Profile("twofactor") +public interface TFAccessTokenRepository extends JpaRepository<TFAccessToken, Long>, JpaSpecificationExecutor<TFAccessToken> { + + TFAccessToken findByUserAndToken(AppUser user, String token); + +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/fineract/blob/1a966e8e/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/domain/TwoFactorConfiguration.java ---------------------------------------------------------------------- diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/domain/TwoFactorConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/domain/TwoFactorConfiguration.java new file mode 100644 index 0000000..e45af00 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/domain/TwoFactorConfiguration.java @@ -0,0 +1,84 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.security.domain; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; + +import org.apache.commons.lang.BooleanUtils; +import org.apache.commons.lang.math.NumberUtils; +import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; +import org.apache.fineract.infrastructure.security.constants.TwoFactorConfigurationConstants; + +@Entity +@Table(name = "twofactor_configuration", + uniqueConstraints = {@UniqueConstraint(columnNames = { "name" }, name = "name_UNIQUE")}) +public class TwoFactorConfiguration extends AbstractPersistableCustom<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); + } +} http://git-wip-us.apache.org/repos/asf/fineract/blob/1a966e8e/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/domain/TwoFactorConfigurationRepository.java ---------------------------------------------------------------------- diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/domain/TwoFactorConfigurationRepository.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/domain/TwoFactorConfigurationRepository.java new file mode 100644 index 0000000..0407f0c --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/domain/TwoFactorConfigurationRepository.java @@ -0,0 +1,34 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.security.domain; + +import java.util.List; + +import org.springframework.context.annotation.Profile; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +@Profile("twofactor") +public interface TwoFactorConfigurationRepository extends + JpaRepository<TwoFactorConfiguration, Long>, JpaSpecificationExecutor<TwoFactorConfiguration> { + + TwoFactorConfiguration findByName(final String name); + + List<TwoFactorConfiguration> findAll(); +} http://git-wip-us.apache.org/repos/asf/fineract/blob/1a966e8e/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/exception/AccessTokenInvalidIException.java ---------------------------------------------------------------------- diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/exception/AccessTokenInvalidIException.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/exception/AccessTokenInvalidIException.java new file mode 100644 index 0000000..4c39bd1 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/exception/AccessTokenInvalidIException.java @@ -0,0 +1,28 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.security.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +public class AccessTokenInvalidIException extends AbstractPlatformDomainRuleException { + + public AccessTokenInvalidIException() { + super("error.msg.twofactor.access.token.invalid", "The provided access token is invalid"); + } +} http://git-wip-us.apache.org/repos/asf/fineract/blob/1a966e8e/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/exception/OTPDeliveryMethodInvalidException.java ---------------------------------------------------------------------- diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/exception/OTPDeliveryMethodInvalidException.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/exception/OTPDeliveryMethodInvalidException.java new file mode 100644 index 0000000..0fe0237 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/exception/OTPDeliveryMethodInvalidException.java @@ -0,0 +1,29 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.security.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +public class OTPDeliveryMethodInvalidException extends AbstractPlatformDomainRuleException { + + public OTPDeliveryMethodInvalidException() { + super("error.msg.twofactor.otp.delivery.invalid", "The requested OTP delivery method " + + "is not supported or not currently unavailable."); + } +} http://git-wip-us.apache.org/repos/asf/fineract/blob/1a966e8e/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/exception/OTPTokenInvalidException.java ---------------------------------------------------------------------- diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/exception/OTPTokenInvalidException.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/exception/OTPTokenInvalidException.java new file mode 100644 index 0000000..12d11e9 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/exception/OTPTokenInvalidException.java @@ -0,0 +1,28 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.security.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +public class OTPTokenInvalidException extends AbstractPlatformDomainRuleException { + + public OTPTokenInvalidException() { + super("error.msg.twofactor.otp.token.invalid", "The provided one time token is invalid"); + } +} http://git-wip-us.apache.org/repos/asf/fineract/blob/1a966e8e/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/InsecureTwoFactorAuthenticationFilter.java ---------------------------------------------------------------------- diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/InsecureTwoFactorAuthenticationFilter.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/InsecureTwoFactorAuthenticationFilter.java new file mode 100644 index 0000000..2f63eba --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/InsecureTwoFactorAuthenticationFilter.java @@ -0,0 +1,82 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.security.filter; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +import org.apache.fineract.useradministration.domain.AppUser; +import org.springframework.context.annotation.Profile; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; + +/** + * A dummy {@link TwoFactorAuthenticationFilter} filter used when 'twofactor' + * environment profile is not active. + * + * This filter adds 'TWOFACTOR_AUTHENTICATED' authority to every authenticated + * platform user. + */ +@Service(value = "twoFactorAuthFilter") +@Profile("!twofactor") +public class InsecureTwoFactorAuthenticationFilter extends TwoFactorAuthenticationFilter { + + public InsecureTwoFactorAuthenticationFilter() { + super(null); + } + + @Override + public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { + + SecurityContext context = SecurityContextHolder.getContext(); + Authentication authentication = null; + if(context != null) { + authentication = context.getAuthentication(); + } + + // Add two-factor authenticated authority if user is authenticated + if(authentication != null && authentication.isAuthenticated()) { + AppUser user = (AppUser) authentication.getPrincipal(); + + if(user == null) { + return; + } + + List<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); + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/fineract/blob/1a966e8e/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/TwoFactorAuthenticationFilter.java ---------------------------------------------------------------------- diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/TwoFactorAuthenticationFilter.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/TwoFactorAuthenticationFilter.java new file mode 100644 index 0000000..6db141e --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/TwoFactorAuthenticationFilter.java @@ -0,0 +1,139 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.security.filter; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.fineract.infrastructure.security.constants.TwoFactorConstants; +import org.apache.fineract.infrastructure.security.domain.TFAccessToken; +import org.apache.fineract.infrastructure.security.service.TwoFactorService; +import org.apache.fineract.useradministration.domain.AppUser; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Profile; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.stereotype.Service; +import org.springframework.web.filter.GenericFilterBean; + + +/** + * This filter is responsible for handling two-factor authentication. + * The filter is enabled when 'twofactor' environment profile is active, otherwise + * {@link InsecureTwoFactorAuthenticationFilter} is used. + * + * This filter validates an access-token provided as a header 'Fineract-Platform-TFA-Token'. + * If a valid token is provided, a 'TWOFACTOR_AUTHENTICATED' authority is added to the current + * authentication. + * If an invalid(non-existent or invalid) token is provided, 403 response is returned. + * + * An authenticated platform user with permission 'BYPASS_TWOFACTOR' will always be granted + * 'TWOFACTOR_AUTHENTICATED' authority regardless of the value of the 'Fineract-Platform-TFA-Token' + * header. + */ +@Service(value = "twoFactorAuthFilter") +@Profile("twofactor") +public class TwoFactorAuthenticationFilter extends GenericFilterBean { + + private final TwoFactorService twoFactorService; + + @Autowired + public TwoFactorAuthenticationFilter(TwoFactorService twoFactorService) { + this.twoFactorService = twoFactorService; + } + + @Override + public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) + throws IOException, ServletException { + + final HttpServletRequest request = (HttpServletRequest) req; + final HttpServletResponse response = (HttpServletResponse) res; + + SecurityContext context = SecurityContextHolder.getContext(); + Authentication authentication = null; + if(context != null) { + authentication = context.getAuthentication(); + } + + // Process two-factor only when user is authenticated + if(authentication != null && authentication.isAuthenticated()) { + AppUser user = (AppUser) authentication.getPrincipal(); + + if(user == null) { + return; + } + + if(!user.hasSpecificPermissionTo(TwoFactorConstants.BYPASS_TWO_FACTOR_PERMISSION)) { + // User can't bypass two-factor auth, check two-factor access token + String token = request.getHeader("Fineract-Platform-TFA-Token"); + if(token != null) { + TFAccessToken accessToken = twoFactorService.fetchAccessTokenForUser(user, token); + // Token is non-existent or invalid + if(accessToken == null || !accessToken.isValid()) { + response.addHeader("WWW-Authenticate", + "Basic realm=\"Fineract Platform API Two Factor\""); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, + "Invalid two-factor access token provided"); + return; + } + } else { + // No token provided + chain.doFilter(req, res); + return; + } + } + + List<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; + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/fineract/blob/1a966e8e/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/AccessTokenGenerationService.java ---------------------------------------------------------------------- diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/AccessTokenGenerationService.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/AccessTokenGenerationService.java new file mode 100644 index 0000000..e477593 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/AccessTokenGenerationService.java @@ -0,0 +1,24 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.security.service; + +public interface AccessTokenGenerationService { + + String generateRandomToken(); +} http://git-wip-us.apache.org/repos/asf/fineract/blob/1a966e8e/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/RandomOTPGenerator.java ---------------------------------------------------------------------- diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/RandomOTPGenerator.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/RandomOTPGenerator.java new file mode 100644 index 0000000..d3bf551 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/RandomOTPGenerator.java @@ -0,0 +1,38 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.security.service; + +public class RandomOTPGenerator { + + private static final String allowedCharacters = "0123456789ABCDEFGHIJKLMNOPQRSTUVQXYZ"; + private final int tokenLength; + + public RandomOTPGenerator(int tokenLength) { + this.tokenLength = tokenLength; + } + + public String generate() { + StringBuilder builder = new StringBuilder(); + for(int i = 0; i < tokenLength; i++) { + builder.append(allowedCharacters.charAt((int) (Math.random() * (allowedCharacters.length())))); + } + + return builder.toString(); + } +} http://git-wip-us.apache.org/repos/asf/fineract/blob/1a966e8e/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TwoFactorConfigurationService.java ---------------------------------------------------------------------- diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TwoFactorConfigurationService.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TwoFactorConfigurationService.java new file mode 100644 index 0000000..aaf9354 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TwoFactorConfigurationService.java @@ -0,0 +1,51 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.security.service; + +import java.util.Map; + +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.security.data.OTPRequest; +import org.apache.fineract.useradministration.domain.AppUser; + +public interface TwoFactorConfigurationService { + + + Map<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); +} http://git-wip-us.apache.org/repos/asf/fineract/blob/1a966e8e/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TwoFactorConfigurationServiceImpl.java ---------------------------------------------------------------------- diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TwoFactorConfigurationServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TwoFactorConfigurationServiceImpl.java new file mode 100644 index 0000000..bd4c108 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TwoFactorConfigurationServiceImpl.java @@ -0,0 +1,304 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.security.service; + + +import java.io.StringReader; +import java.io.StringWriter; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.security.constants.TwoFactorConfigurationConstants; +import org.apache.fineract.infrastructure.security.constants.TwoFactorConstants; +import org.apache.fineract.infrastructure.security.data.OTPRequest; +import org.apache.fineract.infrastructure.security.domain.TwoFactorConfiguration; +import org.apache.fineract.infrastructure.security.domain.TwoFactorConfigurationRepository; +import org.apache.fineract.useradministration.domain.AppUser; +import org.joda.time.LocalDateTime; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; + +import com.github.mustachejava.DefaultMustacheFactory; +import com.github.mustachejava.Mustache; +import com.github.mustachejava.MustacheFactory; + +@Service +@Profile("twofactor") +public class TwoFactorConfigurationServiceImpl implements TwoFactorConfigurationService { + + private static final String DEFAULT_EMAIL_SUBJECT = "Fineract Two-Factor Authentication Token"; + private static final String DEFAULT_EMAIL_BODY = "Hello {username}.\n" + + "Your OTP login token is {token}."; + private static final String DEFAULT_SMS_TEXT = "Your authentication token for Fineract is " + + "{token}."; + + private final TwoFactorConfigurationRepository configurationRepository; + + + @Autowired + public TwoFactorConfigurationServiceImpl(TwoFactorConfigurationRepository configurationRepository) { + this.configurationRepository = configurationRepository; + } + + @Override + @Cacheable(value = "tfConfig", key = "T(org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil).getTenant().getTenantIdentifier()") + public Map<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(); + } +} http://git-wip-us.apache.org/repos/asf/fineract/blob/1a966e8e/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TwoFactorService.java ---------------------------------------------------------------------- diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TwoFactorService.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TwoFactorService.java new file mode 100644 index 0000000..da556d0 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TwoFactorService.java @@ -0,0 +1,43 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.security.service; + +import java.util.List; + +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.security.data.OTPDeliveryMethod; +import org.apache.fineract.infrastructure.security.data.OTPRequest; +import org.apache.fineract.infrastructure.security.domain.TFAccessToken; +import org.apache.fineract.useradministration.domain.AppUser; + +public interface TwoFactorService { + + List<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); + +} http://git-wip-us.apache.org/repos/asf/fineract/blob/1a966e8e/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TwoFactorServiceImpl.java ---------------------------------------------------------------------- diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TwoFactorServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TwoFactorServiceImpl.java new file mode 100644 index 0000000..64361c2 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TwoFactorServiceImpl.java @@ -0,0 +1,229 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.security.service; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.lang.StringUtils; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.domain.EmailDetail; +import org.apache.fineract.infrastructure.core.service.PlatformEmailService; +import org.apache.fineract.infrastructure.security.constants.TwoFactorConstants; +import org.apache.fineract.infrastructure.security.data.OTPDeliveryMethod; +import org.apache.fineract.infrastructure.security.data.OTPRequest; +import org.apache.fineract.infrastructure.security.domain.OTPRequestRepository; +import org.apache.fineract.infrastructure.security.domain.TFAccessToken; +import org.apache.fineract.infrastructure.security.domain.TFAccessTokenRepository; +import org.apache.fineract.infrastructure.security.exception.AccessTokenInvalidIException; +import org.apache.fineract.infrastructure.security.exception.OTPDeliveryMethodInvalidException; +import org.apache.fineract.infrastructure.security.exception.OTPTokenInvalidException; +import org.apache.fineract.infrastructure.sms.domain.SmsMessage; +import org.apache.fineract.infrastructure.sms.domain.SmsMessageRepository; +import org.apache.fineract.infrastructure.sms.scheduler.SmsMessageScheduledJobService; +import org.apache.fineract.useradministration.domain.AppUser; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; + +@Service +@Profile("twofactor") +public class TwoFactorServiceImpl implements TwoFactorService { + + + + private final AccessTokenGenerationService accessTokenGenerationService; + private final PlatformEmailService emailService; + private final SmsMessageScheduledJobService smsMessageScheduledJobService; + + private final OTPRequestRepository otpRequestRepository; + private final TFAccessTokenRepository tfAccessTokenRepository; + private final SmsMessageRepository smsMessageRepository; + + private final TwoFactorConfigurationService configurationService; + + @Autowired + public TwoFactorServiceImpl(AccessTokenGenerationService accessTokenGenerationService, + PlatformEmailService emailService, + SmsMessageScheduledJobService smsMessageScheduledJobService, + OTPRequestRepository otpRequestRepository, + TFAccessTokenRepository tfAccessTokenRepository, + SmsMessageRepository smsMessageRepository, + TwoFactorConfigurationService configurationService) { + this.accessTokenGenerationService = accessTokenGenerationService; + this.emailService = emailService; + this.smsMessageScheduledJobService = smsMessageScheduledJobService; + this.otpRequestRepository = otpRequestRepository; + this.tfAccessTokenRepository = tfAccessTokenRepository; + this.smsMessageRepository = smsMessageRepository; + this.configurationService = configurationService; + } + + + @Override + public List<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); + } +} http://git-wip-us.apache.org/repos/asf/fineract/blob/1a966e8e/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TwoFactorUtils.java ---------------------------------------------------------------------- diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TwoFactorUtils.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TwoFactorUtils.java new file mode 100644 index 0000000..8929246 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TwoFactorUtils.java @@ -0,0 +1,47 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.security.service; + + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +@Component +public class TwoFactorUtils { + + private static final String TWO_FACTOR_PROFILE_NAME = "twofactor"; + + private final Environment environment; + + @Autowired + public TwoFactorUtils(Environment environment) { + this.environment = environment; + } + + + public boolean isTwoFactorAuthEnabled() { + for(final String profile : this.environment.getActiveProfiles()) { + if(TWO_FACTOR_PROFILE_NAME.equals(profile)) { + return true; + } + } + return false; + } +} http://git-wip-us.apache.org/repos/asf/fineract/blob/1a966e8e/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/UUIDAccessTokenGenerationService.java ---------------------------------------------------------------------- diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/UUIDAccessTokenGenerationService.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/UUIDAccessTokenGenerationService.java new file mode 100644 index 0000000..12e09c4 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/UUIDAccessTokenGenerationService.java @@ -0,0 +1,32 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.security.service; + +import java.util.UUID; + +import org.springframework.stereotype.Service; + +@Service +public class UUIDAccessTokenGenerationService implements AccessTokenGenerationService { + + @Override + public String generateRandomToken() { + return UUID.randomUUID().toString().replaceAll("-", ""); + } +} http://git-wip-us.apache.org/repos/asf/fineract/blob/1a966e8e/fineract-provider/src/main/java/org/apache/fineract/useradministration/domain/AppUser.java ---------------------------------------------------------------------- diff --git a/fineract-provider/src/main/java/org/apache/fineract/useradministration/domain/AppUser.java b/fineract-provider/src/main/java/org/apache/fineract/useradministration/domain/AppUser.java index 9c8a86f..d5bcfd5 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/useradministration/domain/AppUser.java +++ b/fineract-provider/src/main/java/org/apache/fineract/useradministration/domain/AppUser.java @@ -491,6 +491,23 @@ public class AppUser extends AbstractPersistableCustom<Long> implements Platform return hasNotPermission; } + /** + * Checks whether the user has a given permission explicitly. + * + * @param permissionCode the permission code to check for. + * @return whether the user has the specified permission + */ + public boolean hasSpecificPermissionTo(final String permissionCode) { + boolean hasPermission = false; + for (final Role role : this.roles) { + if(role.hasPermissionTo(permissionCode)) { + hasPermission = true; + break; + } + } + return hasPermission; + } + public void validateHasReadPermission(final String resourceType) { final String authorizationMessage = "User has no authority to view " + resourceType.toLowerCase() + "s"; http://git-wip-us.apache.org/repos/asf/fineract/blob/1a966e8e/fineract-provider/src/main/resources/META-INF/spring/ehcache.xml ---------------------------------------------------------------------- diff --git a/fineract-provider/src/main/resources/META-INF/spring/ehcache.xml b/fineract-provider/src/main/resources/META-INF/spring/ehcache.xml index b991c69..b5a442d 100644 --- a/fineract-provider/src/main/resources/META-INF/spring/ehcache.xml +++ b/fineract-provider/src/main/resources/META-INF/spring/ehcache.xml @@ -48,4 +48,8 @@ overflowToDisk="false" /> <cache name="hooks" maxEntriesLocalHeap="10000" eternal="true" overflowToDisk="false" /> + <cache name="userTFAccessToken" maxEntriesLocalHeap="10000" + overflowToDisk="false" timeToIdleSeconds="7200" /> + <cache name="tfConfig" maxEntriesLocalHeap="10000" eternal="true" + overflowToDisk="false" /> </ehcache> \ No newline at end of file http://git-wip-us.apache.org/repos/asf/fineract/blob/1a966e8e/fineract-provider/src/main/resources/META-INF/spring/securityContext.xml ---------------------------------------------------------------------- diff --git a/fineract-provider/src/main/resources/META-INF/spring/securityContext.xml b/fineract-provider/src/main/resources/META-INF/spring/securityContext.xml index f03220f..a4db7fe 100644 --- a/fineract-provider/src/main/resources/META-INF/spring/securityContext.xml +++ b/fineract-provider/src/main/resources/META-INF/spring/securityContext.xml @@ -42,19 +42,26 @@ method="POST" requires-channel="https" /> <intercept-url pattern="/api/*/self/registration/user" access="permitAll" method="POST" requires-channel="https" /> - <intercept-url pattern="/api/**" access="isFullyAuthenticated()" + <intercept-url pattern="/api/*/twofactor" access="isFullyAuthenticated()" + method="GET" requires-channel="https" /> + <intercept-url pattern="/api/*/twofactor" access="isFullyAuthenticated()" + method="POST" requires-channel="https" /> + <intercept-url pattern="/api/*/twofactor/validate" access="isFullyAuthenticated()" + method="POST" requires-channel="https" /> + <intercept-url pattern="/api/**" access="isFullyAuthenticated() and hasAuthority('TWOFACTOR_AUTHENTICATED')" method="GET" requires-channel="https" /> - <intercept-url pattern="/api/**" access="isFullyAuthenticated()" + <intercept-url pattern="/api/**" access="isFullyAuthenticated() and hasAuthority('TWOFACTOR_AUTHENTICATED')" method="POST" requires-channel="https" /> - <intercept-url pattern="/api/**" access="isFullyAuthenticated()" + <intercept-url pattern="/api/**" access="isFullyAuthenticated() and hasAuthority('TWOFACTOR_AUTHENTICATED')" method="PUT" requires-channel="https" /> - <intercept-url pattern="/api/**" access="isFullyAuthenticated()" + <intercept-url pattern="/api/**" access="isFullyAuthenticated() and hasAuthority('TWOFACTOR_AUTHENTICATED')" method="DELETE" requires-channel="https" /> - <intercept-url pattern="/api/**" access="isFullyAuthenticated()" + <intercept-url pattern="/api/**" access="isFullyAuthenticated() and hasAuthority('TWOFACTOR_AUTHENTICATED')" method="HEAD" requires-channel="https" /> <custom-filter after="SECURITY_CONTEXT_FILTER" ref="basicAuthenticationProcessingFilter" /> + <custom-filter ref="twoFactorAuthFilter" after="BASIC_AUTH_FILTER" /> </http> <beans:bean id="basicAuthenticationEntryPoint" @@ -83,27 +90,34 @@ erase-credentials="false"> <authentication-provider ref="customAuthenticationProvider" /> </authentication-manager> - </beans:beans> + </beans:beans> <beans:beans profile="oauth"> <http create-session="stateless" use-expressions="true" pattern="/api/v1/**" entry-point-ref="oauthAuthenticationEntryPoint" access-decision-manager-ref="accessDecisionManager"> <anonymous enabled="false" /> <intercept-url pattern="/api//v1/**" method="OPTIONS" - access="permitAll" requires-channel="https" /> - <intercept-url pattern="/api/v1/**" access="isFullyAuthenticated()" + access="permitAll" requires-channel="https" /> + <intercept-url pattern="/api/*/twofactor" access="isFullyAuthenticated()" + method="GET" requires-channel="https" /> + <intercept-url pattern="/api/*/twofactor" access="isFullyAuthenticated()" + method="POST" requires-channel="https" /> + <intercept-url pattern="/api/*/twofactor/validate" access="isFullyAuthenticated()" + method="POST" requires-channel="https" /> + <intercept-url pattern="/api/v1/**" access="isFullyAuthenticated() and hasAuthority('TWOFACTOR_AUTHENTICATED')" method="GET" requires-channel="https" /> - <intercept-url pattern="/api/v1/**" access="isFullyAuthenticated()" + <intercept-url pattern="/api/v1/**" access="isFullyAuthenticated() and hasAuthority('TWOFACTOR_AUTHENTICATED')" method="POST" requires-channel="https" /> - <intercept-url pattern="/api/v1/**" access="isFullyAuthenticated()" + <intercept-url pattern="/api/v1/**" access="isFullyAuthenticated() and hasAuthority('TWOFACTOR_AUTHENTICATED')" method="PUT" requires-channel="https" /> - <intercept-url pattern="/api/v1/**" access="isFullyAuthenticated()" + <intercept-url pattern="/api/v1/**" access="isFullyAuthenticated() and hasAuthority('TWOFACTOR_AUTHENTICATED')" method="DELETE" requires-channel="https" /> - <intercept-url pattern="/api/v1/**" access="isFullyAuthenticated()" + <intercept-url pattern="/api/v1/**" access="isFullyAuthenticated() and hasAuthority('TWOFACTOR_AUTHENTICATED')" method="HEAD" requires-channel="https" /> <custom-filter ref="tenantIdentifierProcessingFilter" position="FIRST" /> <custom-filter before="PRE_AUTH_FILTER" ref="resourceServerFilter" /> + <custom-filter ref="twoFactorAuthFilter" after="BASIC_AUTH_FILTER" /> <access-denied-handler ref="oauthAccessDeniedHandler" /> </http> http://git-wip-us.apache.org/repos/asf/fineract/blob/1a966e8e/fineract-provider/src/main/resources/sql/migrations/core_db/V336__two_factor_authentication.sql ---------------------------------------------------------------------- diff --git a/fineract-provider/src/main/resources/sql/migrations/core_db/V336__two_factor_authentication.sql b/fineract-provider/src/main/resources/sql/migrations/core_db/V336__two_factor_authentication.sql new file mode 100644 index 0000000..bd5d359 --- /dev/null +++ b/fineract-provider/src/main/resources/sql/migrations/core_db/V336__two_factor_authentication.sql @@ -0,0 +1,63 @@ +-- +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance +-- with the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, +-- software distributed under the License is distributed on an +-- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +-- KIND, either express or implied. See the License for the +-- specific language governing permissions and limitations +-- under the License. +-- + +-- Access Token Table + +CREATE TABLE `twofactor_access_token` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `token` varchar(32) NOT NULL, + `appuser_id` bigint(20) NOT NULL, + `valid_from` datetime NOT NULL, + `valid_to` datetime NOT NULL, + `enabled` bit(1) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `token_appuser_UNIQUE` (`token`,`appuser_id`), + KEY `user` (`appuser_id`), + KEY `token` (`token`), + CONSTRAINT `fk_2fa_access_token_user_id` FOREIGN KEY (`appuser_id`) REFERENCES `m_appuser` (`id`) +); + +-- Configuration + +CREATE TABLE `twofactor_configuration` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `name` varchar(40) NOT NULL, + `value` varchar(1024) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key_UNIQUE` (`name`) +); + +INSERT INTO `twofactor_configuration` (`name`, `value`) VALUES + ('otp-delivery-email-enable', 'true'), + ('otp-delivery-email-subject', 'Fineract Two-Factor Authentication Token'), + ('otp-delivery-email-body', 'Hello {{username}}.\nYour OTP login token is {{token}}.'), + ('otp-delivery-sms-enable', 'false'), + ('otp-delivery-sms-provider', '1'), + ('otp-delivery-sms-text', 'Your authentication token for Fineract is {{token}}.'), + ('otp-token-live-time', '300'), + ('otp-token-length', '5'), + ('access-token-live-time', '86400'), + ('access-token-live-time-extended', '604800'); + + +INSERT INTO `m_permission` (`grouping`, `code`, `entity_name`, `action_name`, `can_maker_checker`) VALUES + ('authorisation', 'INVALIDATE_TWOFACTOR_ACCESSTOKEN', 'TWOFACTOR_ACCESSTOKEN', 'INVALIDATE', '0'), + ('configuration', 'READ_TWOFACTOR_CONFIGURATION', 'TWOFACTOR_CONFIGURATION', 'READ', '0'), + ('configuration', 'UPDATE_TWOFACTOR_CONFIGURATION', 'TWOFACTOR_CONFIGURATION', 'UPDATE', '0'), + ('special', 'BYPASS_TWOFACTOR', NULL, NULL, '0'); \ No newline at end of file
