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));
+ }
+}