This is an automated email from the ASF dual-hosted git repository. avikg pushed a commit to branch develop in repository https://gitbox.apache.org/repos/asf/fineract-cn-anubis.git
The following commit(s) were added to refs/heads/develop by this push: new ef40781 keycloak-authorization new 9c4c2c0 Merge pull request #15 from fynmanoj/keycloak-develop ef40781 is described below commit ef40781a0936f62d5e94d20a6aaf4634c4e67202 Author: Manoj <ma...@fynarfin.io> AuthorDate: Wed Aug 4 11:03:48 2021 +0530 keycloak-authorization --- .../cn/anubis/api/v1/domain/AccountAccess.java | 53 ++++-- .../api/v1/domain/AccountAccessTokenContent.java | 37 ++-- library/build.gradle | 4 + .../cn/anubis/config/AnubisImportSelector.java | 11 +- .../config/AnubisSecurityConfigurerAdapter.java | 2 + .../fineract/cn/anubis/config/EnableAnubis.java | 3 +- .../FinKeycloakSecurityConfigurerAdapter.java | 159 ++++++++++++++++ .../anubis/provider/FinKeycloakRsaKeyProvider.java | 45 +++++ .../security/AccountLevelAccessVerifierCustom.java | 55 ++++++ .../cn/anubis/security/ApplicationPermission.java | 29 +++ .../FinKeycloakAuthenticationProvider.java | 202 +++++++++++++++++++++ .../security/FinKeycloakTenantAuthenticator.java | 141 ++++++++++++++ 12 files changed, 701 insertions(+), 40 deletions(-) diff --git a/library/src/main/java/org/apache/fineract/cn/anubis/config/EnableAnubis.java b/api/src/main/java/org/apache/fineract/cn/anubis/api/v1/domain/AccountAccess.java similarity index 55% copy from library/src/main/java/org/apache/fineract/cn/anubis/config/EnableAnubis.java copy to api/src/main/java/org/apache/fineract/cn/anubis/api/v1/domain/AccountAccess.java index e333306..427bdd4 100644 --- a/library/src/main/java/org/apache/fineract/cn/anubis/config/EnableAnubis.java +++ b/api/src/main/java/org/apache/fineract/cn/anubis/api/v1/domain/AccountAccess.java @@ -16,23 +16,38 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.fineract.cn.anubis.config; - -import org.springframework.context.annotation.Import; - -import java.lang.annotation.*; - -@SuppressWarnings({"unused", "WeakerAccess"}) -@Target({ElementType.TYPE}) -@Retention(RetentionPolicy.RUNTIME) -@Inherited -@Import({ - AnubisConfiguration.class, - AnubisImportSelector.class, - AnubisSecurityConfigurerAdapter.class -}) -public @interface EnableAnubis { - boolean provideSignatureRestController() default true; - boolean provideSignatureStorage() default true; - boolean generateEmptyInitializeEndpoint() default false; +package org.apache.fineract.cn.anubis.api.v1.domain; + +import java.util.Set; + +/** + * @author manoj + */ +public class AccountAccess { + private String number; + private Set<String> access; + + public AccountAccess() { + } + + public AccountAccess(String number, Set<String> access) { + this.number = number; + this.access = access; + } + + public String getNumber() { + return number; + } + + public void setNumber(String number) { + this.number = number; + } + + public Set<String> getAccess() { + return access; + } + + public void setAccess(Set<String> access) { + this.access = access; + } } diff --git a/library/src/main/java/org/apache/fineract/cn/anubis/config/EnableAnubis.java b/api/src/main/java/org/apache/fineract/cn/anubis/api/v1/domain/AccountAccessTokenContent.java similarity index 58% copy from library/src/main/java/org/apache/fineract/cn/anubis/config/EnableAnubis.java copy to api/src/main/java/org/apache/fineract/cn/anubis/api/v1/domain/AccountAccessTokenContent.java index e333306..03de5e8 100644 --- a/library/src/main/java/org/apache/fineract/cn/anubis/config/EnableAnubis.java +++ b/api/src/main/java/org/apache/fineract/cn/anubis/api/v1/domain/AccountAccessTokenContent.java @@ -16,23 +16,28 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.fineract.cn.anubis.config; +package org.apache.fineract.cn.anubis.api.v1.domain; -import org.springframework.context.annotation.Import; +import java.util.List; -import java.lang.annotation.*; +/** + * @author manoj + */ +public class AccountAccessTokenContent { + private List<AccountAccess> accounts; + + public AccountAccessTokenContent() { + } + + public AccountAccessTokenContent(List<AccountAccess> accounts) { + this.accounts = accounts; + } + + public List<AccountAccess> getAccounts() { + return accounts; + } -@SuppressWarnings({"unused", "WeakerAccess"}) -@Target({ElementType.TYPE}) -@Retention(RetentionPolicy.RUNTIME) -@Inherited -@Import({ - AnubisConfiguration.class, - AnubisImportSelector.class, - AnubisSecurityConfigurerAdapter.class -}) -public @interface EnableAnubis { - boolean provideSignatureRestController() default true; - boolean provideSignatureStorage() default true; - boolean generateEmptyInitializeEndpoint() default false; + public void setAccounts(List<AccountAccess> accounts) { + this.accounts = accounts; + } } diff --git a/library/build.gradle b/library/build.gradle index 5e18c95..7ffeac5 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -39,6 +39,9 @@ dependencyManagement { imports { mavenBom 'org.springframework.cloud:spring-cloud-netflix:1.2.0.RELEASE' } + imports { + mavenBom 'org.keycloak.bom:keycloak-adapter-bom:4.0.0.Final' + } } dependencies { @@ -46,6 +49,7 @@ dependencies { [group: 'org.springframework.cloud', name: 'spring-cloud-starter-feign'], [group: 'org.springframework.cloud', name: 'spring-cloud-starter-eureka'], [group: 'org.springframework.cloud', name: 'spring-cloud-starter-security'], + [group: 'org.keycloak', name: 'keycloak-spring-boot-starter', version: '4.0.0.Final'], [group: 'org.hibernate', name: 'hibernate-validator', version: versions.hibernatevalidator], [group: 'io.jsonwebtoken', name: 'jjwt', version: versions.jjwt], [group: 'org.apache.fineract.cn', name: 'lang', version: versions.frameworklang], diff --git a/library/src/main/java/org/apache/fineract/cn/anubis/config/AnubisImportSelector.java b/library/src/main/java/org/apache/fineract/cn/anubis/config/AnubisImportSelector.java index a214d24..260cff8 100644 --- a/library/src/main/java/org/apache/fineract/cn/anubis/config/AnubisImportSelector.java +++ b/library/src/main/java/org/apache/fineract/cn/anubis/config/AnubisImportSelector.java @@ -22,13 +22,11 @@ import org.apache.fineract.cn.anubis.controller.EmptyInitializeResourcesRestCont import org.apache.fineract.cn.anubis.controller.PermittableRestController; import org.apache.fineract.cn.anubis.controller.SignatureCreatorRestController; import org.apache.fineract.cn.anubis.controller.SignatureRestController; +import org.apache.fineract.cn.anubis.provider.FinKeycloakRsaKeyProvider; import org.apache.fineract.cn.anubis.provider.SystemRsaKeyProvider; import org.apache.fineract.cn.anubis.provider.TenantRsaKeyProvider; import org.apache.fineract.cn.anubis.repository.TenantAuthorizationDataRepository; -import org.apache.fineract.cn.anubis.security.GuestAuthenticator; -import org.apache.fineract.cn.anubis.security.IsisAuthenticatedAuthenticationProvider; -import org.apache.fineract.cn.anubis.security.SystemAuthenticator; -import org.apache.fineract.cn.anubis.security.TenantAuthenticator; +import org.apache.fineract.cn.anubis.security.*; import org.apache.fineract.cn.anubis.service.PermittableService; import org.apache.fineract.cn.anubis.token.SystemAccessTokenSerializer; import org.apache.fineract.cn.anubis.token.TenantAccessTokenSerializer; @@ -49,6 +47,7 @@ class AnubisImportSelector implements ImportSelector { final Set<Class> classesToImport = new HashSet<>(); classesToImport.add(TenantRsaKeyProvider.class); classesToImport.add(SystemRsaKeyProvider.class); + classesToImport.add(FinKeycloakRsaKeyProvider.class); classesToImport.add(SystemAccessTokenSerializer.class); classesToImport.add(TenantAccessTokenSerializer.class); @@ -62,6 +61,10 @@ class AnubisImportSelector implements ImportSelector { classesToImport.add(PermittableRestController.class); classesToImport.add(PermittableService.class); + classesToImport.add(FinKeycloakAuthenticationProvider.class); + classesToImport.add(FinKeycloakTenantAuthenticator.class); + classesToImport.add(AccountLevelAccessVerifierCustom.class); + final boolean provideSignatureRestController = (boolean)importingClassMetadata .getAnnotationAttributes(EnableAnubis.class.getTypeName()) .get("provideSignatureRestController"); diff --git a/library/src/main/java/org/apache/fineract/cn/anubis/config/AnubisSecurityConfigurerAdapter.java b/library/src/main/java/org/apache/fineract/cn/anubis/config/AnubisSecurityConfigurerAdapter.java index 91da9de..3a45c3d 100644 --- a/library/src/main/java/org/apache/fineract/cn/anubis/config/AnubisSecurityConfigurerAdapter.java +++ b/library/src/main/java/org/apache/fineract/cn/anubis/config/AnubisSecurityConfigurerAdapter.java @@ -28,6 +28,7 @@ import org.apache.http.HttpStatus; import org.slf4j.Logger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -55,6 +56,7 @@ import java.util.List; @SuppressWarnings("WeakerAccess") @Configuration @EnableWebSecurity +@ConditionalOnProperty("authentication.service.anubis") public class AnubisSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter { final private Logger logger; final private ApplicationName applicationName; diff --git a/library/src/main/java/org/apache/fineract/cn/anubis/config/EnableAnubis.java b/library/src/main/java/org/apache/fineract/cn/anubis/config/EnableAnubis.java index e333306..a23471c 100644 --- a/library/src/main/java/org/apache/fineract/cn/anubis/config/EnableAnubis.java +++ b/library/src/main/java/org/apache/fineract/cn/anubis/config/EnableAnubis.java @@ -29,7 +29,8 @@ import java.lang.annotation.*; @Import({ AnubisConfiguration.class, AnubisImportSelector.class, - AnubisSecurityConfigurerAdapter.class + AnubisSecurityConfigurerAdapter.class, + FinKeycloakSecurityConfigurerAdapter.class }) public @interface EnableAnubis { boolean provideSignatureRestController() default true; diff --git a/library/src/main/java/org/apache/fineract/cn/anubis/config/FinKeycloakSecurityConfigurerAdapter.java b/library/src/main/java/org/apache/fineract/cn/anubis/config/FinKeycloakSecurityConfigurerAdapter.java new file mode 100644 index 0000000..7860409 --- /dev/null +++ b/library/src/main/java/org/apache/fineract/cn/anubis/config/FinKeycloakSecurityConfigurerAdapter.java @@ -0,0 +1,159 @@ +/* + * 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.cn.anubis.config; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.fineract.cn.anubis.filter.IsisAuthenticatedProcessingFilter; +import org.apache.fineract.cn.anubis.security.FinKeycloakAuthenticationProvider; +import org.apache.fineract.cn.anubis.security.UrlPermissionChecker; +import org.apache.fineract.cn.lang.ApplicationName; +import org.keycloak.KeycloakPrincipal; +import org.keycloak.KeycloakSecurityContext; +import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver; +import org.keycloak.adapters.springsecurity.KeycloakSecurityComponents; +import org.keycloak.adapters.springsecurity.account.KeycloakRole; +import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider; +import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter; +import org.keycloak.adapters.springsecurity.filter.KeycloakAuthenticationProcessingFilter; +import org.keycloak.adapters.springsecurity.filter.KeycloakPreAuthActionsFilter; +import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken; +import org.keycloak.representations.AccessToken; +import org.slf4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.access.AccessDecisionManager; +import org.springframework.security.access.AccessDecisionVoter; +import org.springframework.security.access.vote.UnanimousBased; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.UrlAuthorizationConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper; +import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy; +import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy; + +import javax.servlet.Filter; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** + * @author manoj + */ +@Configuration +@EnableWebSecurity +@ComponentScan(basePackageClasses = KeycloakSecurityComponents.class) +@ConditionalOnProperty({"authentication.service.keycloak"}) +public class FinKeycloakSecurityConfigurerAdapter extends KeycloakWebSecurityConfigurerAdapter { + final private Logger logger; + final private ApplicationName applicationName; + + public FinKeycloakSecurityConfigurerAdapter(final @Qualifier(AnubisConstants.LOGGER_NAME) Logger logger, + final ApplicationName applicationName) { + this.logger = logger; + this.applicationName = applicationName; + } + + static class CustomKeycloakAccessToken extends AccessToken { + @JsonProperty("roles") + protected Set<String> roles; + + public Set<String> getRoles() { + return roles; + } + + public void setRoles(Set<String> roles) { + this.roles = roles; + } + } + + @Override + protected KeycloakAuthenticationProvider keycloakAuthenticationProvider() { + return new KeycloakAuthenticationProvider() { + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + KeycloakAuthenticationToken token = (KeycloakAuthenticationToken) authentication; + List<GrantedAuthority> grantedAuthorities = new ArrayList<>(); + + for (String role : ((CustomKeycloakAccessToken)((KeycloakPrincipal<KeycloakSecurityContext>)token.getPrincipal()).getKeycloakSecurityContext().getToken()).getRoles()) { + grantedAuthorities.add(new KeycloakRole(role)); + } + + return new KeycloakAuthenticationToken(token.getAccount(), token.isInteractive(), new SimpleAuthorityMapper().mapAuthorities(grantedAuthorities)); + } + + }; + } + + @Autowired + public void configureGlobal( + final AuthenticationManagerBuilder auth, + @SuppressWarnings("SpringJavaAutowiringInspection") final FinKeycloakAuthenticationProvider provider) + throws Exception { + auth.authenticationProvider(provider); + } + + @Bean + @Override + protected SessionAuthenticationStrategy sessionAuthenticationStrategy() { + return new NullAuthenticatedSessionStrategy(); + } + @Bean + public KeycloakSpringBootConfigResolver KeycloakConfigResolver() { + return new KeycloakSpringBootConfigResolver(); + } + + @Bean + public FilterRegistrationBean keycloakAuthenticationProcessingFilterRegistrationBean( + KeycloakAuthenticationProcessingFilter filter) { + FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter); + registrationBean.setEnabled(false); + return registrationBean; + } + + @Bean + public FilterRegistrationBean keycloakPreAuthActionsFilterRegistrationBean(KeycloakPreAuthActionsFilter filter) { + FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter); + registrationBean.setEnabled(false); + return registrationBean; + } + + private AccessDecisionManager defaultAccessDecisionManager() { + final List<AccessDecisionVoter<?>> voters = new ArrayList<>(); + voters.add(new UrlPermissionChecker(logger, applicationName));return new UnanimousBased(voters); + } + + protected void configure(HttpSecurity http) throws Exception { + Filter filter = new IsisAuthenticatedProcessingFilter(super.authenticationManager()); + ((HttpSecurity)((HttpSecurity)((HttpSecurity)((HttpSecurity)((UrlAuthorizationConfigurer.StandardInterceptUrlRegistry)((UrlAuthorizationConfigurer.AuthorizedUrl)((UrlAuthorizationConfigurer)((HttpSecurity)((HttpSecurity)http.httpBasic().disable()).csrf().disable()).apply(new UrlAuthorizationConfigurer(this.getApplicationContext()))).getRegistry().anyRequest()).hasAuthority("maats_feather").accessDecisionManager(this.defaultAccessDecisionManager())).and()).formLogin().disable()).logout( [...] + response.setStatus(404); + }); + } + +} \ No newline at end of file diff --git a/library/src/main/java/org/apache/fineract/cn/anubis/provider/FinKeycloakRsaKeyProvider.java b/library/src/main/java/org/apache/fineract/cn/anubis/provider/FinKeycloakRsaKeyProvider.java new file mode 100644 index 0000000..4c1d071 --- /dev/null +++ b/library/src/main/java/org/apache/fineract/cn/anubis/provider/FinKeycloakRsaKeyProvider.java @@ -0,0 +1,45 @@ +/* + * 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.cn.anubis.provider; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; +import java.util.Base64; + +/** + * @author manoj + */ +@Component +public class FinKeycloakRsaKeyProvider { + @Value("${fin.keycloak.realm.publicKey}") + private String rsaPublicKey; + + public PublicKey getPublicKey() throws InvalidKeySpecException, NoSuchAlgorithmException { + + X509EncodedKeySpec keySpec = new X509EncodedKeySpec(Base64.getDecoder().decode(rsaPublicKey)); + KeyFactory kf = KeyFactory.getInstance("RSA"); + return kf.generatePublic(keySpec); + } +} diff --git a/library/src/main/java/org/apache/fineract/cn/anubis/security/AccountLevelAccessVerifierCustom.java b/library/src/main/java/org/apache/fineract/cn/anubis/security/AccountLevelAccessVerifierCustom.java new file mode 100644 index 0000000..5e66118 --- /dev/null +++ b/library/src/main/java/org/apache/fineract/cn/anubis/security/AccountLevelAccessVerifierCustom.java @@ -0,0 +1,55 @@ +/* + * 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.cn.anubis.security; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; + +import java.util.Collection; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @author manoj + */ +@Service +public class AccountLevelAccessVerifierCustom { + private final static String OWNER = "OWNER"; + + @Value("${conf.enableAccountLevelAccessVerification}") + private String isAccountLevelAccessVerificationEnabled; + + public void validate(String accountNo, String operation){ + if(!"true".equals(isAccountLevelAccessVerificationEnabled)) return; + AnubisAuthentication authentication = (AnubisAuthentication)SecurityContextHolder.getContext().getAuthentication(); + String acctPermission = "ACCT_ACCESS_" + accountNo; + final Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); + final Set<String> accountOperation = authorities.stream() + .map(x -> (ApplicationPermission) x) + .filter(x -> x.matches(acctPermission, "get", authentication.getPrincipal().getForApplicationName(), authentication.getPrincipal())) + .map(ApplicationPermission::getAccountOperation) + .collect(Collectors.toSet()); + + if(accountOperation.size() == 0 || !(accountOperation.contains(OWNER) || accountOperation.contains(operation))) { + throw AmitAuthenticationException.internalError("Access Denied, " + operation + " on " + accountNo); + } + } +} diff --git a/library/src/main/java/org/apache/fineract/cn/anubis/security/ApplicationPermission.java b/library/src/main/java/org/apache/fineract/cn/anubis/security/ApplicationPermission.java index c24a7c5..987f3e9 100644 --- a/library/src/main/java/org/apache/fineract/cn/anubis/security/ApplicationPermission.java +++ b/library/src/main/java/org/apache/fineract/cn/anubis/security/ApplicationPermission.java @@ -43,6 +43,8 @@ public class ApplicationPermission implements GrantedAuthority { private final List<PermissionSegmentMatcher> servletPathSegmentMatchers; private final AllowedOperation allowedOperation; + private final String accountOperation; + private final boolean acceptTokenIntendedForForeignApplication; @@ -51,6 +53,17 @@ public class ApplicationPermission implements GrantedAuthority { final AllowedOperation allowedOperation, final boolean acceptTokenIntendedForForeignApplication) { this.allowedOperation = allowedOperation; + this.accountOperation = null; + servletPathSegmentMatchers = PermissionSegmentMatcher.getServletPathSegmentMatchers(servletPath); + this.acceptTokenIntendedForForeignApplication = acceptTokenIntendedForForeignApplication; + } + + public ApplicationPermission( + final String servletPath, + final String accountOperation, + final boolean acceptTokenIntendedForForeignApplication) { + this.allowedOperation = AllowedOperation.READ; + this.accountOperation = accountOperation; servletPathSegmentMatchers = PermissionSegmentMatcher.getServletPathSegmentMatchers(servletPath); this.acceptTokenIntendedForForeignApplication = acceptTokenIntendedForForeignApplication; } @@ -64,6 +77,10 @@ public class ApplicationPermission implements GrantedAuthority { return URL_AUTHORITY; } + public String getAccountOperation() { + return accountOperation; + } + boolean matches(final FilterInvocation filterInvocation, final ApplicationName applicationName, final AnubisPrincipal principal) { @@ -82,6 +99,18 @@ public class ApplicationPermission implements GrantedAuthority { (matcher, segment) -> matcher.matches(segment, principal, acceptTokenIntendedForForeignApplication, isSu)); } + boolean matches(final String path, String method, + final String applicationName, + final AnubisPrincipal principal) { + if (!acceptTokenIntendedForForeignApplication && !applicationName.equals(principal.getForApplicationName())) + return false; + boolean isSu = principal.getUser().equals(ApiConstants.SYSTEM_SU); + return matchesHelper( + path, + method, + (matcher, segment) -> matcher.matches(segment, principal, acceptTokenIntendedForForeignApplication, isSu)); + } + private boolean matchesHelper(final String servletPath, final String method, @Nonnull final BiPredicate<PermissionSegmentMatcher, String> segmentMatcher) { final boolean opMatches = allowedOperation.containsHttpMethod(method); diff --git a/library/src/main/java/org/apache/fineract/cn/anubis/security/FinKeycloakAuthenticationProvider.java b/library/src/main/java/org/apache/fineract/cn/anubis/security/FinKeycloakAuthenticationProvider.java new file mode 100644 index 0000000..c236f0b --- /dev/null +++ b/library/src/main/java/org/apache/fineract/cn/anubis/security/FinKeycloakAuthenticationProvider.java @@ -0,0 +1,202 @@ +/* + * 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.cn.anubis.security; + +import io.jsonwebtoken.*; +import org.apache.fineract.cn.anubis.api.v1.TokenConstants; +import org.apache.fineract.cn.anubis.provider.FinKeycloakRsaKeyProvider; +import org.apache.fineract.cn.anubis.provider.SystemRsaKeyProvider; +import org.apache.fineract.cn.anubis.provider.TenantRsaKeyProvider; +import org.apache.fineract.cn.anubis.token.TokenType; +import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider; +import org.slf4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; + +import javax.annotation.Nonnull; +import java.security.Key; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Optional; + +import static org.apache.fineract.cn.anubis.config.AnubisConstants.LOGGER_NAME; + +@Component +public class FinKeycloakAuthenticationProvider extends KeycloakAuthenticationProvider { + private final SystemRsaKeyProvider systemRsaKeyProvider; + private final TenantRsaKeyProvider tenantRsaKeyProvider; + private final SystemAuthenticator systemAuthenticator; + private final FinKeycloakRsaKeyProvider keycloakRsaKeyProvider; + private final FinKeycloakTenantAuthenticator tenantAuthenticator; + private final GuestAuthenticator guestAuthenticator; + private final Logger logger; + + @Autowired + public FinKeycloakAuthenticationProvider( + final SystemRsaKeyProvider systemRsaKeyProvider, + final TenantRsaKeyProvider tenantRsaKeyProvider, + final SystemAuthenticator systemAuthenticator, + final FinKeycloakRsaKeyProvider keycloakRsaKeyProvider, + final FinKeycloakTenantAuthenticator tenantAuthenticator, + final GuestAuthenticator guestAuthenticator, + final @Qualifier(LOGGER_NAME) Logger logger) { + this.systemRsaKeyProvider = systemRsaKeyProvider; + this.tenantRsaKeyProvider = tenantRsaKeyProvider; + this.systemAuthenticator = systemAuthenticator; + this.keycloakRsaKeyProvider = keycloakRsaKeyProvider; + this.tenantAuthenticator = tenantAuthenticator; + this.guestAuthenticator = guestAuthenticator; + this.logger = logger; + } + + @Override public boolean supports(final Class<?> clazz) { + return PreAuthenticatedAuthenticationToken.class.isAssignableFrom(clazz); + } + + @Override public Authentication authenticate(Authentication authentication) + throws AuthenticationException { + if (!PreAuthenticatedAuthenticationToken.class.isAssignableFrom(authentication.getClass())) + { + throw AmitAuthenticationException.internalError( + "authentication called with unexpected authentication object."); + } + + final PreAuthenticatedAuthenticationToken preAuthentication = (PreAuthenticatedAuthenticationToken) authentication; + + final String user = (String) preAuthentication.getPrincipal(); + Assert.hasText(user, "user cannot be empty. This should have been assured in preauthentication"); + + return convert(user, (String)preAuthentication.getCredentials()); + } + + private Authentication convert(final @Nonnull String user, final String authenticationHeader) { + final Optional<String> token = getJwtTokenString(authenticationHeader); + return (Authentication)token.map(x -> { + + final TokenInfo tokenInfo = getTokenInfo(x);//new TokenInfo(TokenType.TENANT, getKeyTimestamp());//getTokenInfo(x);//;// + + switch (tokenInfo.getType()) { + case TENANT: + case SYSTEM: + return tenantAuthenticator.authenticate(user, x, tokenInfo.getKeyTimestamp()); + default: + logger.debug("Authentication failed for a token with a token type other than tenant or system."); + throw AmitAuthenticationException.invalidTokenIssuer(tokenInfo.getType().getIssuer()); + } + }).orElseGet(() -> guestAuthenticator.authenticate(user)); + } + + private Optional<String> getJwtTokenString(final String authenticationHeader) { + if ((authenticationHeader == null) || authenticationHeader.equals( + TokenConstants.NO_AUTHENTICATION)){ + return Optional.empty(); + } + + if (!authenticationHeader.startsWith(TokenConstants.PREFIX)) { + logger.debug("Authentication failed for a token which does not begin with the token prefix."); + throw AmitAuthenticationException.invalidHeader(); + } + return Optional.of(authenticationHeader.substring(TokenConstants.PREFIX.length()).trim()); + } + + @Nonnull private TokenInfo getTokenInfo(final String token) + { + try { + @SuppressWarnings("unchecked") + final Jwt<Header, Claims> jwt = Jwts.parser().setSigningKeyResolver(new SigningKeyResolver() { + @Override public Key resolveSigningKey(final JwsHeader header, final Claims claims) { + final TokenType tokenType = getTokenTypeFromClaims(claims);// TokenType.TENANT;//getTokenTypeFromClaims(claims); + final String keyTimestamp = getKeyTimestampFromClaims(claims); + + try { + switch (tokenType) { + case TENANT: + case SYSTEM: + return keycloakRsaKeyProvider.getPublicKey(); + default: + logger.debug("Authentication failed in token type discovery for a token with a token type other than tenant or system."); + throw AmitAuthenticationException.invalidTokenIssuer(tokenType.getIssuer()); + } + } + catch (final IllegalArgumentException e) + { + logger.debug("Authentication failed because no tenant was provided."); + throw AmitAuthenticationException.missingTenant(); + } + catch (final InvalidKeySpecException | NoSuchAlgorithmException e) + { + logger.debug("Authentication failed because the provided rsa public key."); + throw AmitAuthenticationException.invalidTokenKeyTimestamp(tokenType.getIssuer(), keyTimestamp); + } + } + + @Override public Key resolveSigningKey(final JwsHeader header, final String plaintext) { + return null; + } + }).parse(token); + + final String alg = jwt.getHeader().get("alg").toString(); + final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.forName(alg); + if (!signatureAlgorithm.isRsa()) { + logger.debug("Authentication failed because the token is signed with an algorithm other than RSA."); + throw AmitAuthenticationException.invalidTokenAlgorithm(alg); + } + + final String keyTimestamp = getKeyTimestampFromClaims(jwt.getBody()); + final TokenType tokenType = getTokenTypeFromClaims(jwt.getBody()); + + return new TokenInfo(tokenType, keyTimestamp); + } + catch (final JwtException e) + { + logger.debug("Authentication failed because token parsing failed."); + throw AmitAuthenticationException.invalidToken(); + } + } + + private @Nonnull String getKeyTimestampFromClaims(final Claims claims) { + SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH_mm_ss"); + //Integer millis = claims.get("iat", Integer.class); + Date date = claims.getIssuedAt(); + return formatter.format(date); + } + + private @Nonnull String getKeyTimestamp() { + SimpleDateFormat sdf1 = new SimpleDateFormat("yyyy-MM-dd'T'HH_mm_ss"); + Date date = new Date(); + return sdf1.format(date); + } + + private @Nonnull TokenType getTokenTypeFromClaims(final Claims claims) { + final String issuer = claims.get("authType", String.class); + final Optional<TokenType> tokenType = TokenType.valueOfIssuer(issuer); + if (!tokenType.isPresent()) { + logger.debug("Authentication failed for a token with a missing or invalid token type."); + throw AmitAuthenticationException.invalidTokenIssuer(issuer); + } + return tokenType.get(); + } +} diff --git a/library/src/main/java/org/apache/fineract/cn/anubis/security/FinKeycloakTenantAuthenticator.java b/library/src/main/java/org/apache/fineract/cn/anubis/security/FinKeycloakTenantAuthenticator.java new file mode 100644 index 0000000..75b3d16 --- /dev/null +++ b/library/src/main/java/org/apache/fineract/cn/anubis/security/FinKeycloakTenantAuthenticator.java @@ -0,0 +1,141 @@ +/* + * 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.cn.anubis.security; + +import com.google.gson.Gson; +import io.jsonwebtoken.*; +import org.apache.fineract.cn.anubis.annotation.AcceptedTokenType; +import org.apache.fineract.cn.anubis.api.v1.TokenConstants; +import org.apache.fineract.cn.anubis.api.v1.domain.AccountAccess; +import org.apache.fineract.cn.anubis.api.v1.domain.AccountAccessTokenContent; +import org.apache.fineract.cn.anubis.api.v1.domain.TokenContent; +import org.apache.fineract.cn.anubis.api.v1.domain.TokenPermission; +import org.apache.fineract.cn.anubis.provider.FinKeycloakRsaKeyProvider; +import org.apache.fineract.cn.anubis.service.PermittableService; +import org.apache.fineract.cn.lang.ApplicationName; +import org.slf4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Component; + +import javax.annotation.Nonnull; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.apache.fineract.cn.anubis.config.AnubisConstants.LOGGER_NAME; + +/** + * @author manoj + */ +@Component +public class FinKeycloakTenantAuthenticator { + private final FinKeycloakRsaKeyProvider keycloakRsaKeyProvider; + private final String applicationNameWithVersion; + private final Gson gson; + private final Set<ApplicationPermission> guestPermissions; + private final Logger logger; + + @Autowired + public FinKeycloakTenantAuthenticator( + final FinKeycloakRsaKeyProvider keycloakRsaKeyProvider, + final ApplicationName applicationName, + final PermittableService permittableService, + final @Qualifier("anubisGson") Gson gson, + final @Qualifier(LOGGER_NAME) Logger logger) { + this.keycloakRsaKeyProvider = keycloakRsaKeyProvider; + this.applicationNameWithVersion = applicationName.toString(); + this.gson = gson; + this.guestPermissions + = permittableService.getPermittableEndpointsAsPermissions(AcceptedTokenType.GUEST); + this.logger = logger; + } + + AnubisAuthentication authenticate( + final @Nonnull String user, + final @Nonnull String token, + final @Nonnull String keyTimestamp) { + try { + final JwtParser parser = Jwts.parser() + .setSigningKey(keycloakRsaKeyProvider.getPublicKey()); + + @SuppressWarnings("unchecked") Jwt<Header, Claims> jwt = parser.parse(token); + + final String serializedTokenContent = jwt.getBody().get("tokenPermissions", String.class); + + + final String sourceApplication = "Keycloak"; + final TokenContent tokenContent = gson.fromJson(serializedTokenContent, TokenContent.class); + if (tokenContent == null) + throw AmitAuthenticationException.missingTokenContent(); + + final Set<ApplicationPermission> permissions = translatePermissions(tokenContent.getTokenPermissions()); + permissions.addAll(guestPermissions); + + + if(jwt.getBody().get("fin") != null){ + final String serializedAccountAccess = jwt.getBody().get("fin", String.class); + final AccountAccessTokenContent accountAccess = gson.fromJson(serializedAccountAccess, AccountAccessTokenContent.class); + final Set<ApplicationPermission> acctPermissions = translateAccountPermissions(accountAccess.getAccounts()); + permissions.addAll(acctPermissions); + } + + + logger.info("Tenant token for user {}, with key timestamp {} authenticated successfully.", user, keyTimestamp); + + return new AnubisAuthentication(TokenConstants.PREFIX + token, + jwt.getBody().get("preferred_username", String.class), applicationNameWithVersion, sourceApplication, permissions + ); + } + catch (final JwtException | InvalidKeySpecException | NoSuchAlgorithmException e) { + logger.info("Tenant token for user {}, with key timestamp {} failed to authenticate. Exception was {}", user, keyTimestamp, e); + throw AmitAuthenticationException.invalidToken(); + } + } + + private Set<ApplicationPermission> translatePermissions( + @Nonnull final List<TokenPermission> tokenPermissions) + { + return tokenPermissions.stream() + .filter(x -> x.getPath().startsWith(applicationNameWithVersion)) + .flatMap(this::getAppPermissionFromTokenPermission) + .collect(Collectors.toSet()); + } + + private Set<ApplicationPermission> translateAccountPermissions( + @Nonnull final List<AccountAccess> tokenPermissions) + { + return tokenPermissions.stream() + .flatMap(this::getAppPermissionFromAcctPermission) + .collect(Collectors.toSet()); + } + + private Stream<ApplicationPermission> getAppPermissionFromTokenPermission(final TokenPermission tokenPermission) { + final String servletPath = tokenPermission.getPath().substring(applicationNameWithVersion.length()); + return tokenPermission.getAllowedOperations().stream().map(x -> new ApplicationPermission(servletPath, x, false)); + } + + private Stream<ApplicationPermission> getAppPermissionFromAcctPermission(final AccountAccess tokenPermission) { + final String servletPath = "ACCT_ACCESS_"+ tokenPermission.getNumber(); + return tokenPermission.getAccess().stream().map(x -> new ApplicationPermission(servletPath, x, false)); + } +}