Merge pull request #15 from fynmanoj/keycloak-develop

keycloak-authorization
diff --git a/api/src/main/java/org/apache/fineract/cn/anubis/api/v1/domain/AccountAccess.java b/api/src/main/java/org/apache/fineract/cn/anubis/api/v1/domain/AccountAccess.java
new file mode 100644
index 0000000..427bdd4
--- /dev/null
+++ b/api/src/main/java/org/apache/fineract/cn/anubis/api/v1/domain/AccountAccess.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.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/api/src/main/java/org/apache/fineract/cn/anubis/api/v1/domain/AccountAccessTokenContent.java b/api/src/main/java/org/apache/fineract/cn/anubis/api/v1/domain/AccountAccessTokenContent.java
new file mode 100644
index 0000000..03de5e8
--- /dev/null
+++ b/api/src/main/java/org/apache/fineract/cn/anubis/api/v1/domain/AccountAccessTokenContent.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.cn.anubis.api.v1.domain;
+
+import java.util.List;
+
+/**
+ * @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;
+    }
+
+    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 @@
     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 @@
             [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.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 @@
     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 @@
     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.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 @@
 @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({
     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().disable()).addFilter(filter).sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()).exceptionHandling().accessDeniedHandler((request, response, accessDeniedException) -> {
+   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 @@
   private final List<PermissionSegmentMatcher> servletPathSegmentMatchers;
 
   private final AllowedOperation allowedOperation;
+  private final String accountOperation;
+
 
   private final boolean acceptTokenIntendedForForeignApplication;
 
@@ -51,6 +53,17 @@
       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 @@
     return URL_AUTHORITY;
   }
 
+  public String getAccountOperation() {
+    return accountOperation;
+  }
+
   boolean matches(final FilterInvocation filterInvocation,
                   final ApplicationName applicationName,
                   final AnubisPrincipal principal) {
@@ -82,6 +99,18 @@
         (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));
+ }
+}