This is an automated email from the ASF dual-hosted git repository.
aleks pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git
The following commit(s) were added to refs/heads/develop by this push:
new c74bfed FINERACT-1012: Upgrading OAuth support to Spring Security 5.6
c74bfed is described below
commit c74bfed1fe4d3f7b024005e3fe30e30b455d1475
Author: Petri Tuomola <[email protected]>
AuthorDate: Sun Dec 26 15:42:35 2021 +0800
FINERACT-1012: Upgrading OAuth support to Spring Security 5.6
---
.../{build-twofactor.yml => build-oauth2.yml} | 16 +-
.github/workflows/build-twofactor.yml | 6 +-
.github/workflows/build.yml | 6 +-
.gitignore | 1 +
build.gradle | 2 +-
fineract-doc/src/docs/en/03_oauth.adoc | 49 +++--
fineract-provider/dependencies.gradle | 2 +-
.../properties/oauth/application.properties | 3 +
.../oauth/twofactor/application.properties | 3 +
.../core/boot/WebXmlOauthConfiguration.java | 44 ----
.../core/config/OAuth2SecurityConfig.java | 145 +++++++++++++
.../exceptionmapper/OAuth2ExceptionEntryPoint.java | 46 +++++
.../security/api/UserDetailsApiResource.java | 105 +++++-----
.../data/FineractJwtAuthenticationToken.java | 42 ++++
.../InsecureTwoFactorAuthenticationFilter.java | 27 ++-
.../filter/TenantAwareTenantIdentifierFilter.java | 2 +-
.../filter/TwoFactorAuthenticationFilter.java | 31 +--
.../security/api/SelfUserDetailsApiResource.java | 7 +-
.../resources/META-INF/spring/securityContext.xml | 167 ---------------
.../src/main/resources/static/api-docs/apiLive.htm | 225 +--------------------
oauth2-tests/build.gradle | 67 ++++++
.../dependencies.gradle | 24 ++-
.../oauth2tests/OAuth2AuthenticationTest.java | 176 ++++++++++++++++
settings.gradle | 1 +
24 files changed, 655 insertions(+), 542 deletions(-)
diff --git a/.github/workflows/build-twofactor.yml
b/.github/workflows/build-oauth2.yml
similarity index 66%
copy from .github/workflows/build-twofactor.yml
copy to .github/workflows/build-oauth2.yml
index 9e55d9f..7b715fe 100644
--- a/.github/workflows/build-twofactor.yml
+++ b/.github/workflows/build-oauth2.yml
@@ -1,4 +1,4 @@
-name: Fineract Gradle build - twofactor
+name: Fineract Gradle build - oauth2
on: [push, pull_request]
jobs:
@@ -14,6 +14,14 @@ jobs:
MARIADB_ROOT_PASSWORD: mysql
options: --health-cmd="mysqladmin ping" --health-interval=5s
--health-timeout=2s --health-retries=3
+ mock-oauth2-server:
+ image: ghcr.io/navikt/mock-oauth2-server:0.4.0
+ ports:
+ - 9000:9000
+ env:
+ SERVER_PORT: 9000
+ JSON_CONFIG: '{ "interactiveLogin": true, "httpServer":
"NettyWrapper", "tokenCallbacks": [ { "issuerId": "auth/realms/fineract",
"tokenExpiry": 120, "requestMappings": [{ "requestParam": "scope", "match":
"fineract", "claims": { "sub": "mifos", "scope": [ "test" ] } } ] } ] }'
+
env:
TZ: Asia/Kolkata
steps:
@@ -23,9 +31,9 @@ jobs:
path: |
~/.gradle/caches
~/.gradle/wrapper
- key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*',
'**/gradle-wrapper.properties') }}
+ key: ${{ runner.os }}-gradle-oauth2-${{ hashFiles('**/*.gradle*',
'**/gradle-wrapper.properties') }}
restore-keys: |
- ${{ runner.os }}-gradle-
+ ${{ runner.os }}-gradle-oauth2-
- name: Checkout
uses: actions/checkout@v2
- name: Set up JDK 11
@@ -49,4 +57,4 @@ jobs:
sudo apt-get update
sudo apt-get install ghostscript -y
- name: Build & Test
- run: ./gradlew --no-daemon -q --console=plain build test --fail-fast
-x :integration-tests:test -Ptwofactor=enabled
+ run: ./gradlew --no-daemon -q --console=plain build test --fail-fast
-x :integration-tests:test -x :twofactor-tests:test -Psecurity=oauth
diff --git a/.github/workflows/build-twofactor.yml
b/.github/workflows/build-twofactor.yml
index 9e55d9f..aac7141 100644
--- a/.github/workflows/build-twofactor.yml
+++ b/.github/workflows/build-twofactor.yml
@@ -23,9 +23,9 @@ jobs:
path: |
~/.gradle/caches
~/.gradle/wrapper
- key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*',
'**/gradle-wrapper.properties') }}
+ key: ${{ runner.os }}-gradle-twofactor-${{ hashFiles('**/*.gradle*',
'**/gradle-wrapper.properties') }}
restore-keys: |
- ${{ runner.os }}-gradle-
+ ${{ runner.os }}-gradle-twofactor-
- name: Checkout
uses: actions/checkout@v2
- name: Set up JDK 11
@@ -49,4 +49,4 @@ jobs:
sudo apt-get update
sudo apt-get install ghostscript -y
- name: Build & Test
- run: ./gradlew --no-daemon -q --console=plain build test --fail-fast
-x :integration-tests:test -Ptwofactor=enabled
+ run: ./gradlew --no-daemon -q --console=plain build test --fail-fast
-x :integration-tests:test -x :oauth2-tests:test -Ptwofactor=enabled
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 908d030..9e89ac6 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -22,9 +22,9 @@ jobs:
path: |
~/.gradle/caches
~/.gradle/wrapper
- key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*',
'**/gradle-wrapper.properties') }}
+ key: ${{ runner.os }}-gradle-basicauth-${{ hashFiles('**/*.gradle*',
'**/gradle-wrapper.properties') }}
restore-keys: |
- ${{ runner.os }}-gradle-
+ ${{ runner.os }}-gradle-basicauth-
- name: Checkout
uses: actions/checkout@v2
- name: Set up JDK 11
@@ -48,4 +48,4 @@ jobs:
sudo apt-get update
sudo apt-get install ghostscript -y
- name: Build & Test
- run: ./gradlew --no-daemon -q --console=plain licenseMain licenseTest
check build test --fail-fast doc -x :twofactor-tests:test
+ run: ./gradlew --no-daemon -q --console=plain licenseMain licenseTest
check build test --fail-fast doc -x :twofactor-tests:test -x :oauth2-test:test
diff --git a/.gitignore b/.gitignore
index b62b52d..e56ba7e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,6 +10,7 @@ build
*.iml
*.ipr
*.iws
+*.swp
*.DS_Store
.idea
.vscode
diff --git a/build.gradle b/build.gradle
index 120046e..25530a8 100644
--- a/build.gradle
+++ b/build.gradle
@@ -29,6 +29,7 @@ buildscript {
'fineract-provider',
'integration-tests',
'twofactor-tests',
+ 'oauth2-tests',
'fineract-client'
].contains(it.name)
}
@@ -111,7 +112,6 @@ allprojects {
// We do not use :+ to get the latest available version available
on Maven Central, as that could suddenly break things.
// We use the Renovate Bot to automatically propose Pull Requests
(PRs) when upgrades for all of these versions are available.
- dependency
'org.springframework.security.oauth:spring-security-oauth2:2.5.1.RELEASE'
dependency 'org.apache.openjpa:openjpa:3.2.0' // when upgrading,
also change OpenJPA version repeated above in buildscript!
dependency 'com.google.guava:guava:31.0.1-jre'
dependency 'com.google.code.gson:gson:2.8.9'
diff --git a/fineract-doc/src/docs/en/03_oauth.adoc
b/fineract-doc/src/docs/en/03_oauth.adoc
index fec07da..933b719 100644
--- a/fineract-doc/src/docs/en/03_oauth.adoc
+++ b/fineract-doc/src/docs/en/03_oauth.adoc
@@ -1,41 +1,66 @@
== OAuth
-Fineract has (basic) OAuth (2.0?) support. Here's how to use it:
+Fineract has a (basic) OAuth2 support based on Spring Boot Security. Here's
how to use it:
=== Build
You must re-build the distribution JAR (or WAR) using the special
`-Psecurity=oauth` flag:
----
-./gradlew bootJAR -Psecurity=oauth
-java -jar build/libs/fineract-provider.jar
+./gradlew bootRun -Psecurity=oauth
----
Downloads from https://fineract.apache.org, or using e.g. the
https://hub.docker.com/r/apache/fineract container image, or on
https://www.fineract.dev, this will not work, because they have not been built
using this flag.
-=== Invoke `/fineract-provider/api/oauth/token`
+Previous versions of Fineract included a built-in authorisation server for
issuing OAuth tokens. However, as the spring-security-oauth2 package was
deprecated and replaced by built-in OAuth support in Spring Security, this is
no longer supported as part of the package. Instead, you need to run a separate
OAuth authorization server (e.g.
https://github.com/spring-projects/spring-authorization-server) or use a
3rd-party OAuth authorization provider
(https://en.wikipedia.org/wiki/List_of_OA [...]
+
+This instruction describes how to get Fineract OAuth working with a Keycloak
(http://keycloak.org) based authentication provider running in a Docker
container. The steps required for other OAuth providers will be similar.
+
+=== Set up Keycloak
+
+1. From terminal, run: 'docker run -p 9000:8080 -e KEYCLOAK_USER=admin -e
KEYCLOAK_PASSWORD=admin quay.io/keycloak/keycloak:15.0.2'
+1. Go to URL 'http://localhost:9000/auth/admin' and login with admin/admin
+1. Hover your mouse over text "Master" and click on "Add realm"
+1. Enter name "fineract" for your realm
+1. Click on tab "Users" on the left, then "Add user" and create user with
username "mifos"
+1. Click on tab "Credentials" at the top, and set password to "password",
turning "temporary" setting to off
+1. Click on tab "Clients" on the left, and create client with ID
'community-app'
+1. In settings tab, set 'access-type' to 'confidential' and enter 'localhost'
in the valid redirect URIs.
+1. In credentials tab, copy string in field 'secret' as this will be needed in
the step to request the access token
+
+Finally we need to change Keycloak configuration so that it uses the username
as a subject of the token:
+
+1. Choose client 'community-app' in the tab 'Clients'
+1. Go to tab 'Mappers' and click on 'Create'
+1. Enter 'usernameInSub' as 'Name'
+1. Choose mapper type 'User Property'
+1. Enter 'username' into the field 'Property' and 'sub' into the field 'Token
Claim Name'. Choose 'String' as 'Claim JSON Type'
+
+You are now ready to test out OAuth:
+
+=== Retrieve an access token from Keycloak
----
-curl [--insecure] --location --request POST \
-'https://localhost:8443/fineract-provider/api/oauth/token' \
---header 'Fineract-Platform-TenantId: default' \
+curl --location --request POST \
+'http://localhost:9000/auth/realms/fineract/protocol/openid-connect/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'username=mifos' \
--data-urlencode 'password=password' \
--data-urlencode 'client_id=community-app' \
--data-urlencode 'grant_type=password' \
---data-urlencode 'client_secret=123'
+--data-urlencode 'client_secret=<enter the client secret from credentials tab>'
----
-Note that the `client_id` and `client_secret` are stored in the
`oauth_client_details` table in the database.
+The reply should contain a field 'access_token'. Copy the field's value and
use it in the API call below:
=== Invoke APIs and pass `Authorization: bearer ...` header
----
curl --location --request GET \
-'https://localhost:8443/fineract-provider/api/v1/clients' \
+'https://localhost:8443/fineract-provider/api/v1/offices' \
--header 'Fineract-Platform-TenantId: default' \
---header 'Authorization: bearer RzfUyQ0wEnxxq4PyFCF1J-XGFCI'
+--header 'Authorization: bearer <enter the value of the access_token field>'
+
----
-NOTE: See also
https://demo.fineract.dev/fineract-provider/api-docs/apiLive.htm#authentication_oauth
\ No newline at end of file
+NOTE: See also
https://demo.fineract.dev/fineract-provider/api-docs/apiLive.htm#authentication_oauth
diff --git a/fineract-provider/dependencies.gradle
b/fineract-provider/dependencies.gradle
index 3378a29..393562f 100644
--- a/fineract-provider/dependencies.gradle
+++ b/fineract-provider/dependencies.gradle
@@ -30,12 +30,12 @@ dependencies {
'org.springframework.boot:spring-boot-starter-web',
'org.springframework.boot:spring-boot-starter-security',
'org.springframework.boot:spring-boot-starter-cache',
+
'org.springframework.boot:spring-boot-starter-oauth2-resource-server',
'org.glassfish.jersey.media:jersey-media-multipart:2.35',
'org.springframework:spring-jms',
'org.springframework:spring-context-support',
- 'org.springframework.security.oauth:spring-security-oauth2',
'com.google.guava:guava',
'com.google.code.gson:gson',
diff --git a/fineract-provider/properties/oauth/application.properties
b/fineract-provider/properties/oauth/application.properties
index 057b361..6936db5 100644
--- a/fineract-provider/properties/oauth/application.properties
+++ b/fineract-provider/properties/oauth/application.properties
@@ -32,3 +32,6 @@ management.endpoints.web.exposure.include=health,info
# FINERACT-914
server.forward-headers-strategy=framework
+
+# OAuth authorisation server endpoint
+spring.security.oauth2.resourceserver.jwt.issuer-uri:
http://localhost:9000/auth/realms/fineract
diff --git
a/fineract-provider/properties/oauth/twofactor/application.properties
b/fineract-provider/properties/oauth/twofactor/application.properties
index dc84c31..3a2e01d 100644
--- a/fineract-provider/properties/oauth/twofactor/application.properties
+++ b/fineract-provider/properties/oauth/twofactor/application.properties
@@ -32,3 +32,6 @@ management.endpoints.web.exposure.include=health,info
# FINERACT-914
server.forward-headers-strategy=framework
+
+# OAuth authorisation server endpoint
+spring.security.oauth2.resourceserver.jwt.issuer-uri:
http://localhost:9000/auth/realms/fineract
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/boot/WebXmlOauthConfiguration.java
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/boot/WebXmlOauthConfiguration.java
deleted file mode 100644
index 7b67579..0000000
---
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/boot/WebXmlOauthConfiguration.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.fineract.infrastructure.core.boot;
-
-import org.springframework.boot.web.servlet.ServletRegistrationBean;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.context.annotation.Profile;
-import org.springframework.web.servlet.DispatcherServlet;
-
-/**
- * This Configuration replaces what formerly was in web.xml. Beans are loaded
only when "oauth" Profile is enabled.
- *
- * @see <a href=
- *
"http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#howto-convert-an-existing-application-to-spring-boot">#howto-convert-an-existing-application-to-spring-boot</a>
- */
-@Configuration
-@Profile("oauth")
-public class WebXmlOauthConfiguration {
-
- @Bean
- public ServletRegistrationBean dispatcherRegistration(DispatcherServlet
dispatcherServlet) {
- ServletRegistrationBean<DispatcherServlet> registrationBean = new
ServletRegistrationBean<DispatcherServlet>(dispatcherServlet);
- registrationBean.addUrlMappings("/api/oauth/token");
- return registrationBean;
- }
-
-}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/OAuth2SecurityConfig.java
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/OAuth2SecurityConfig.java
new file mode 100644
index 0000000..a2226c9
--- /dev/null
+++
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/OAuth2SecurityConfig.java
@@ -0,0 +1,145 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.fineract.infrastructure.core.config;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import
org.apache.fineract.infrastructure.core.exceptionmapper.OAuth2ExceptionEntryPoint;
+import
org.apache.fineract.infrastructure.security.data.FineractJwtAuthenticationToken;
+import
org.apache.fineract.infrastructure.security.filter.TenantAwareTenantIdentifierFilter;
+import
org.apache.fineract.infrastructure.security.filter.TwoFactorAuthenticationFilter;
+import
org.apache.fineract.infrastructure.security.service.TenantAwareJpaPlatformUserDetailsService;
+import
org.apache.fineract.infrastructure.security.vote.SelfServiceUserAccessVote;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Profile;
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.http.HttpMethod;
+import org.springframework.security.access.AccessDecisionManager;
+import org.springframework.security.access.AccessDecisionVoter;
+import org.springframework.security.access.vote.AuthenticatedVoter;
+import org.springframework.security.access.vote.RoleVoter;
+import org.springframework.security.access.vote.UnanimousBased;
+import
org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
+import
org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import
org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.security.crypto.factory.PasswordEncoderFactories;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
+import org.springframework.security.oauth2.jwt.Jwt;
+import
org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
+import org.springframework.security.web.access.expression.WebExpressionVoter;
+import
org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
+import
org.springframework.security.web.context.SecurityContextPersistenceFilter;
+
+@Configuration
+@Profile("oauth")
+@EnableGlobalMethodSecurity(prePostEnabled = true)
+public class OAuth2SecurityConfig extends WebSecurityConfigurerAdapter {
+
+ @Autowired
+ private TwoFactorAuthenticationFilter twoFactorAuthenticationFilter;
+
+ @Autowired
+ private TenantAwareTenantIdentifierFilter
tenantAwareTenantIdentifierFilter;
+
+ @Autowired
+ private TenantAwareJpaPlatformUserDetailsService userDetailsService;
+
+ private static final JwtGrantedAuthoritiesConverter
jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
+
+ @Override
+ protected void configure(HttpSecurity http) throws Exception {
+
+ http //
+ .csrf().disable() // NOSONAR only creating a service that is
used by non-browser clients
+ .antMatcher("/api/**").authorizeRequests() //
+ .antMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() //
+ .antMatchers(HttpMethod.POST, "/api/*/echo").permitAll() //
+ .antMatchers(HttpMethod.POST,
"/api/*/authentication").permitAll() //
+ .antMatchers(HttpMethod.POST,
"/api/*/self/authentication").permitAll() //
+ .antMatchers(HttpMethod.POST,
"/api/*/self/registration").permitAll() //
+ .antMatchers(HttpMethod.POST,
"/api/*/self/registration/user").permitAll() //
+ .antMatchers(HttpMethod.POST,
"/api/*/twofactor/validate").fullyAuthenticated() //
+ .antMatchers("/api/*/twofactor").fullyAuthenticated() //
+ .antMatchers("/api/**").access("isFullyAuthenticated() and
hasAuthority('TWOFACTOR_AUTHENTICATED')") //
+ .accessDecisionManager(accessDecisionManager()).and() //
+ .exceptionHandling().authenticationEntryPoint(new
OAuth2ExceptionEntryPoint()).and()
+ .oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt ->
jwt.jwtAuthenticationConverter(authenticationConverter()))
+ .authenticationEntryPoint(new
OAuth2ExceptionEntryPoint())) //
+ .sessionManagement() //
+ .sessionCreationPolicy(SessionCreationPolicy.STATELESS) //
+ .and() //
+ .addFilterAfter(tenantAwareTenantIdentifierFilter,
SecurityContextPersistenceFilter.class) //
+ .addFilterAfter(twoFactorAuthenticationFilter,
BasicAuthenticationFilter.class) //
+ .requiresChannel(channel ->
channel.antMatchers("/api/**").requiresSecure());
+ }
+
+ @Bean
+ public PasswordEncoder passwordEncoder() {
+ return PasswordEncoderFactories.createDelegatingPasswordEncoder();
+ }
+
+ private Converter<Jwt, FineractJwtAuthenticationToken>
authenticationConverter() {
+ return jwt -> {
+ try {
+ UserDetails user =
userDetailsService.loadUserByUsername(jwt.getSubject());
+ jwtGrantedAuthoritiesConverter.setAuthorityPrefix("");
+ Collection<GrantedAuthority> authorities =
jwtGrantedAuthoritiesConverter.convert(jwt);
+ return new FineractJwtAuthenticationToken(jwt, authorities,
user);
+ } catch (UsernameNotFoundException ex) {
+ throw new OAuth2AuthenticationException(new
OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN), ex);
+ }
+ };
+ }
+
+ @Bean
+ public AccessDecisionManager accessDecisionManager() {
+ List<AccessDecisionVoter<? extends Object>> decisionVoters =
Arrays.asList(new RoleVoter(), new AuthenticatedVoter(),
+ new WebExpressionVoter(), new SelfServiceUserAccessVote());
+
+ return new UnanimousBased(decisionVoters);
+ }
+
+ @Bean
+ public FilterRegistrationBean<TenantAwareTenantIdentifierFilter>
tenantAwareTenantIdentifierFilterRegistration() throws Exception {
+ FilterRegistrationBean<TenantAwareTenantIdentifierFilter> registration
= new FilterRegistrationBean<TenantAwareTenantIdentifierFilter>(
+ tenantAwareTenantIdentifierFilter);
+ registration.setEnabled(false);
+ return registration;
+ }
+
+ @Bean
+ public FilterRegistrationBean<TwoFactorAuthenticationFilter>
twoFactorAuthenticationFilterRegistration() {
+ FilterRegistrationBean<TwoFactorAuthenticationFilter> registration =
new FilterRegistrationBean<TwoFactorAuthenticationFilter>(
+ twoFactorAuthenticationFilter);
+ registration.setEnabled(false);
+ return registration;
+ }
+}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/OAuth2ExceptionEntryPoint.java
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/OAuth2ExceptionEntryPoint.java
new file mode 100644
index 0000000..7b41234
--- /dev/null
+++
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/OAuth2ExceptionEntryPoint.java
@@ -0,0 +1,46 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.core.exceptionmapper;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.io.IOException;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.fineract.infrastructure.core.data.ApiGlobalErrorResponse;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.AuthenticationEntryPoint;
+
+public class OAuth2ExceptionEntryPoint implements AuthenticationEntryPoint {
+
+ @Override
+ public void commence(HttpServletRequest request, HttpServletResponse
response, AuthenticationException authException)
+ throws IOException, ServletException {
+
+ ApiGlobalErrorResponse errorResponse =
ApiGlobalErrorResponse.unAuthenticated();
+ response.setContentType("application/json");
+ response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+ try {
+ ObjectMapper mapper = new ObjectMapper();
+ mapper.writeValue(response.getOutputStream(), errorResponse);
+ } catch (Exception e) {
+ throw new ServletException(e);
+ }
+ }
+}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/api/UserDetailsApiResource.java
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/api/UserDetailsApiResource.java
index 05ce28b..64e96bf 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/api/UserDetailsApiResource.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/api/UserDetailsApiResource.java
@@ -19,7 +19,6 @@
package org.apache.fineract.infrastructure.security.api;
import io.swagger.v3.oas.annotations.Operation;
-import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
@@ -31,24 +30,23 @@ import java.util.Set;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
-import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
import
org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer;
import
org.apache.fineract.infrastructure.security.constants.TwoFactorConstants;
import
org.apache.fineract.infrastructure.security.data.AuthenticatedOauthUserData;
+import
org.apache.fineract.infrastructure.security.data.FineractJwtAuthenticationToken;
import
org.apache.fineract.infrastructure.security.service.SpringSecurityPlatformSecurityContext;
import org.apache.fineract.infrastructure.security.service.TwoFactorUtils;
import org.apache.fineract.useradministration.data.RoleData;
import org.apache.fineract.useradministration.domain.AppUser;
import org.apache.fineract.useradministration.domain.Role;
import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Profile;
import org.springframework.context.annotation.Scope;
-import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
-import
org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
/*
@@ -60,19 +58,15 @@ import org.springframework.stereotype.Component;
@Scope("singleton")
@Tag(name = "Fetch authenticated user details", description = "")
-@SuppressWarnings("deprecation") // TODO FINERACT-1012
public class UserDetailsApiResource {
- private final ResourceServerTokenServices tokenServices;
private final ToApiJsonSerializer<AuthenticatedOauthUserData>
apiJsonSerializerService;
private final SpringSecurityPlatformSecurityContext
springSecurityPlatformSecurityContext;
private final TwoFactorUtils twoFactorUtils;
@Autowired
- public UserDetailsApiResource(@Qualifier("tokenServices") final
ResourceServerTokenServices tokenServices,
- final ToApiJsonSerializer<AuthenticatedOauthUserData>
apiJsonSerializerService,
+ public UserDetailsApiResource(final
ToApiJsonSerializer<AuthenticatedOauthUserData> apiJsonSerializerService,
final SpringSecurityPlatformSecurityContext
springSecurityPlatformSecurityContext, final TwoFactorUtils twoFactorUtils) {
- this.tokenServices = tokenServices;
this.apiJsonSerializerService = apiJsonSerializerService;
this.springSecurityPlatformSecurityContext =
springSecurityPlatformSecurityContext;
this.twoFactorUtils = twoFactorUtils;
@@ -83,48 +77,57 @@ public class UserDetailsApiResource {
@Operation(summary = "Fetch authenticated user details\n", description =
"checks the Authentication and returns the set roles and permissions allowed.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content =
@Content(schema = @Schema(implementation =
UserDetailsApiResourceSwagger.GetUserDetailsResponse.class))) })
- public String fetchAuthenticatedUserData(
- @QueryParam("access_token") @Parameter(description =
"access_token") final String accessToken) {
-
- final Authentication authentication =
this.tokenServices.loadAuthentication(accessToken);
- if (authentication.isAuthenticated()) {
- final AppUser principal = (AppUser) authentication.getPrincipal();
-
- final Collection<String> permissions = new ArrayList<>();
- AuthenticatedOauthUserData authenticatedUserData = new
AuthenticatedOauthUserData(principal.getUsername(), permissions);
-
- final Collection<GrantedAuthority> authorities = new
ArrayList<>(authentication.getAuthorities());
- for (final GrantedAuthority grantedAuthority : authorities) {
- permissions.add(grantedAuthority.getAuthority());
- }
-
- final Collection<RoleData> roles = new ArrayList<>();
- final Set<Role> userRoles = principal.getRoles();
- for (final Role role : userRoles) {
- roles.add(role.toData());
- }
-
- final Long officeId = principal.getOffice().getId();
- final String officeName = principal.getOffice().getName();
-
- final Long staffId = principal.getStaffId();
- final String staffDisplayName = principal.getStaffDisplayName();
-
- final EnumOptionData organisationalRole =
principal.organisationalRoleData();
-
- final boolean requireTwoFactorAuth =
twoFactorUtils.isTwoFactorAuthEnabled()
- &&
!principal.hasSpecificPermissionTo(TwoFactorConstants.BYPASS_TWO_FACTOR_PERMISSION);
- if
(this.springSecurityPlatformSecurityContext.doesPasswordHasToBeRenewed(principal))
{
- authenticatedUserData = new
AuthenticatedOauthUserData(principal.getUsername(), principal.getId(),
accessToken,
- requireTwoFactorAuth);
- } else {
-
- authenticatedUserData = new
AuthenticatedOauthUserData(principal.getUsername(), officeId, officeName,
staffId,
- staffDisplayName, organisationalRole, roles,
permissions, principal.getId(), accessToken, requireTwoFactorAuth);
- }
- return
this.apiJsonSerializerService.serialize(authenticatedUserData);
+ public String fetchAuthenticatedUserData() {
+
+ final SecurityContext context = SecurityContextHolder.getContext();
+ if (context == null) {
+ return null;
+ }
+
+ final FineractJwtAuthenticationToken authentication =
(FineractJwtAuthenticationToken) context.getAuthentication();
+ if (authentication == null) {
+ return null;
+ }
+
+ final AppUser principal = (AppUser) authentication.getPrincipal();
+ if (principal == null) {
+ return null;
+ }
+
+ final Collection<String> permissions = new ArrayList<>();
+ AuthenticatedOauthUserData authenticatedUserData = new
AuthenticatedOauthUserData(principal.getUsername(), permissions);
+
+ final Collection<GrantedAuthority> authorities = new
ArrayList<>(authentication.getAuthorities());
+ for (final GrantedAuthority grantedAuthority : authorities) {
+ permissions.add(grantedAuthority.getAuthority());
+ }
+
+ final Collection<RoleData> roles = new ArrayList<>();
+ final Set<Role> userRoles = principal.getRoles();
+ for (final Role role : userRoles) {
+ roles.add(role.toData());
+ }
+
+ final Long officeId = principal.getOffice().getId();
+ final String officeName = principal.getOffice().getName();
+
+ final Long staffId = principal.getStaffId();
+ final String staffDisplayName = principal.getStaffDisplayName();
+
+ final EnumOptionData organisationalRole =
principal.organisationalRoleData();
+
+ final boolean requireTwoFactorAuth =
twoFactorUtils.isTwoFactorAuthEnabled()
+ &&
!principal.hasSpecificPermissionTo(TwoFactorConstants.BYPASS_TWO_FACTOR_PERMISSION);
+
+ if
(this.springSecurityPlatformSecurityContext.doesPasswordHasToBeRenewed(principal))
{
+ authenticatedUserData = new
AuthenticatedOauthUserData(principal.getUsername(), principal.getId(),
+ authentication.getToken().getTokenValue(),
requireTwoFactorAuth);
+ } else {
+ authenticatedUserData = new
AuthenticatedOauthUserData(principal.getUsername(), officeId, officeName,
staffId, staffDisplayName,
+ organisationalRole, roles, permissions, principal.getId(),
authentication.getToken().getTokenValue(),
+ requireTwoFactorAuth);
}
- return null;
+ return this.apiJsonSerializerService.serialize(authenticatedUserData);
}
}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/FineractJwtAuthenticationToken.java
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/FineractJwtAuthenticationToken.java
new file mode 100644
index 0000000..22e1d54
--- /dev/null
+++
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/FineractJwtAuthenticationToken.java
@@ -0,0 +1,42 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.fineract.infrastructure.security.data;
+
+import java.util.Collection;
+import java.util.Objects;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.oauth2.jwt.Jwt;
+import
org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
+
+public class FineractJwtAuthenticationToken extends JwtAuthenticationToken {
+
+ private final UserDetails user;
+
+ public FineractJwtAuthenticationToken(Jwt jwt,
Collection<GrantedAuthority> authorities, UserDetails user) {
+ super(jwt, authorities, user.getUsername());
+ this.user = Objects.requireNonNull(user, "user");
+ }
+
+ @Override
+ public UserDetails getPrincipal() {
+ return user;
+ }
+}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/InsecureTwoFactorAuthenticationFilter.java
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/InsecureTwoFactorAuthenticationFilter.java
index 7216f11..64e40b8 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/InsecureTwoFactorAuthenticationFilter.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/InsecureTwoFactorAuthenticationFilter.java
@@ -20,12 +20,13 @@ package org.apache.fineract.infrastructure.security.filter;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.List;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
-import org.apache.fineract.useradministration.domain.AppUser;
+import
org.apache.fineract.infrastructure.security.data.FineractJwtAuthenticationToken;
import org.springframework.context.annotation.Profile;
import
org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
@@ -33,6 +34,7 @@ import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
/**
@@ -58,18 +60,21 @@ public class InsecureTwoFactorAuthenticationFilter extends
TwoFactorAuthenticati
}
// Add two-factor authenticated authority if user is authenticated
- if (authentication != null && authentication.isAuthenticated() &&
authentication.getPrincipal() instanceof AppUser) {
- AppUser user = (AppUser) authentication.getPrincipal();
-
- if (user == null) {
- return;
- }
-
+ if (authentication != null && authentication.isAuthenticated()) {
List<GrantedAuthority> updatedAuthorities = new
ArrayList<>(authentication.getAuthorities());
updatedAuthorities.add(new
SimpleGrantedAuthority("TWOFACTOR_AUTHENTICATED"));
- UsernamePasswordAuthenticationToken updatedAuthentication = new
UsernamePasswordAuthenticationToken(
- authentication.getPrincipal(),
authentication.getCredentials(), updatedAuthorities);
- context.setAuthentication(updatedAuthentication);
+
+ if (authentication instanceof UsernamePasswordAuthenticationToken)
{
+ UsernamePasswordAuthenticationToken updatedAuthentication =
new UsernamePasswordAuthenticationToken(
+ authentication.getPrincipal(),
authentication.getCredentials(), updatedAuthorities);
+ context.setAuthentication(updatedAuthentication);
+ } else if (authentication instanceof
FineractJwtAuthenticationToken) {
+ FineractJwtAuthenticationToken fineractJwtAuthenticationToken
= (FineractJwtAuthenticationToken) authentication;
+ FineractJwtAuthenticationToken updatedAuthentication = new
FineractJwtAuthenticationToken(
+ fineractJwtAuthenticationToken.getToken(),
(Collection<GrantedAuthority>) updatedAuthorities,
+ (UserDetails) authentication.getPrincipal());
+ context.setAuthentication(updatedAuthentication);
+ }
}
chain.doFilter(req, res);
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/TenantAwareTenantIdentifierFilter.java
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/TenantAwareTenantIdentifierFilter.java
index 42a26fc..bbc23de 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/TenantAwareTenantIdentifierFilter.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/TenantAwareTenantIdentifierFilter.java
@@ -54,7 +54,7 @@ import org.springframework.web.filter.GenericFilterBean;
*
* Used to support Oauth2 authentication and the service is loaded only when
"oauth" profile is active.
*/
-@Service(value = "tenantIdentifierProcessingFilter")
+@Service
@Profile("oauth")
public class TenantAwareTenantIdentifierFilter extends GenericFilterBean {
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/TwoFactorAuthenticationFilter.java
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/TwoFactorAuthenticationFilter.java
index cc877f0..14dff4b 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/TwoFactorAuthenticationFilter.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/filter/TwoFactorAuthenticationFilter.java
@@ -20,6 +20,7 @@ package org.apache.fineract.infrastructure.security.filter;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.List;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
@@ -28,6 +29,7 @@ import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import
org.apache.fineract.infrastructure.security.constants.TwoFactorConstants;
+import
org.apache.fineract.infrastructure.security.data.FineractJwtAuthenticationToken;
import org.apache.fineract.infrastructure.security.domain.TFAccessToken;
import org.apache.fineract.infrastructure.security.service.TwoFactorService;
import org.apache.fineract.useradministration.domain.AppUser;
@@ -39,7 +41,7 @@ import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
-import org.springframework.security.oauth2.provider.OAuth2Authentication;
+import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import org.springframework.web.filter.GenericFilterBean;
@@ -106,25 +108,28 @@ public class TwoFactorAuthenticationFilter extends
GenericFilterBean {
List<GrantedAuthority> updatedAuthorities = new
ArrayList<>(authentication.getAuthorities());
updatedAuthorities.add(new
SimpleGrantedAuthority("TWOFACTOR_AUTHENTICATED"));
- final Authentication updatedAuthentication =
createUpdatedAuthentication(authentication, updatedAuthorities);
- context.setAuthentication(updatedAuthentication);
+
context.setAuthentication(createUpdatedAuthentication(authentication,
updatedAuthorities));
}
chain.doFilter(req, res);
}
- @SuppressWarnings("deprecation") // TODO FINERACT-1012
private Authentication createUpdatedAuthentication(final Authentication
currentAuthentication,
- final List<GrantedAuthority> updatedAuthorities) {
-
- final UsernamePasswordAuthenticationToken authentication = new
UsernamePasswordAuthenticationToken(
- currentAuthentication.getPrincipal(),
currentAuthentication.getCredentials(), updatedAuthorities);
-
- if (currentAuthentication instanceof OAuth2Authentication) {
- final OAuth2Authentication oAuth2Authentication =
(OAuth2Authentication) currentAuthentication;
- return new
OAuth2Authentication(oAuth2Authentication.getOAuth2Request(), authentication);
+ final List<GrantedAuthority> updatedAuthorities) throws
ServletException {
+
+ if (currentAuthentication instanceof
UsernamePasswordAuthenticationToken) {
+ UsernamePasswordAuthenticationToken updatedAuthentication = new
UsernamePasswordAuthenticationToken(
+ currentAuthentication.getPrincipal(),
currentAuthentication.getCredentials(), updatedAuthorities);
+ return updatedAuthentication;
+ } else if (currentAuthentication instanceof
FineractJwtAuthenticationToken) {
+ FineractJwtAuthenticationToken fineractJwtAuthenticationToken =
(FineractJwtAuthenticationToken) currentAuthentication;
+ FineractJwtAuthenticationToken updatedAuthentication = new
FineractJwtAuthenticationToken(
+ fineractJwtAuthenticationToken.getToken(),
(Collection<GrantedAuthority>) updatedAuthorities,
+ (UserDetails) currentAuthentication.getPrincipal());
+ return updatedAuthentication;
+ } else {
+ throw new ServletException("Unknown authentication type: " +
currentAuthentication.getClass().getName());
}
- return authentication;
}
}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/self/security/api/SelfUserDetailsApiResource.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/self/security/api/SelfUserDetailsApiResource.java
index dda8715..2104cdb 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/self/security/api/SelfUserDetailsApiResource.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/self/security/api/SelfUserDetailsApiResource.java
@@ -19,7 +19,6 @@
package org.apache.fineract.portfolio.self.security.api;
import io.swagger.v3.oas.annotations.Operation;
-import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
@@ -28,7 +27,6 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
-import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import org.apache.fineract.infrastructure.security.api.UserDetailsApiResource;
import org.springframework.beans.factory.annotation.Autowired;
@@ -57,8 +55,7 @@ public class SelfUserDetailsApiResource {
+ "For more info visit this link -
https://demo.fineract.dev/fineract-provider/api-docs/apiLive.htm#selfoauth")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content =
@Content(schema = @Schema(implementation =
SelfUserDetailsApiResourceSwagger.GetSelfUserDetailsResponse.class))) })
- public String fetchAuthenticatedUserData(
- @QueryParam("access_token") @Parameter(description =
"äccess_token") final String accessToken) {
- return
this.userDetailsApiResource.fetchAuthenticatedUserData(accessToken);
+ public String fetchAuthenticatedUserData() {
+ return this.userDetailsApiResource.fetchAuthenticatedUserData();
}
}
diff --git
a/fineract-provider/src/main/resources/META-INF/spring/securityContext.xml
b/fineract-provider/src/main/resources/META-INF/spring/securityContext.xml
deleted file mode 100644
index 255db23..0000000
--- a/fineract-provider/src/main/resources/META-INF/spring/securityContext.xml
+++ /dev/null
@@ -1,167 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-
- 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.
-
--->
-
-<beans:beans xmlns="http://www.springframework.org/schema/security"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:beans="http://www.springframework.org/schema/beans"
- xmlns:oauth="http://www.springframework.org/schema/security/oauth2"
- xsi:schemaLocation="
- http://www.springframework.org/schema/beans
- https://www.springframework.org/schema/beans/spring-beans.xsd
- http://www.springframework.org/schema/security/oauth2
- https://www.springframework.org/schema/security/spring-security-oauth2.xsd
- http://www.springframework.org/schema/security
- https://www.springframework.org/schema/security/spring-security.xsd">
-
- <beans:beans profile="oauth">
- <http create-session="stateless" use-expressions="true"
pattern="/api/v1/**"
- entry-point-ref="oauthAuthenticationEntryPoint"
- access-decision-manager-ref="accessDecisionManager">
- <csrf disabled="true"/>
- <anonymous enabled="false" />
- <intercept-url pattern="/api//v1/**" method="OPTIONS"
- access="permitAll" requires-channel="https" />
- <intercept-url pattern="/api/*/twofactor"
access="isFullyAuthenticated()"
- method="GET" requires-channel="https" />
- <intercept-url pattern="/api/*/twofactor"
access="isFullyAuthenticated()"
- method="POST" requires-channel="https" />
- <intercept-url pattern="/api/*/twofactor/validate"
access="isFullyAuthenticated()"
- method="POST" requires-channel="https" />
- <intercept-url pattern="/api/v1/**" access="isFullyAuthenticated()
and hasAuthority('TWOFACTOR_AUTHENTICATED')"
- method="GET" requires-channel="https" />
- <intercept-url pattern="/api/v1/**" access="isFullyAuthenticated()
and hasAuthority('TWOFACTOR_AUTHENTICATED')"
- method="POST" requires-channel="https" />
- <intercept-url pattern="/api/v1/**" access="isFullyAuthenticated()
and hasAuthority('TWOFACTOR_AUTHENTICATED')"
- method="PUT" requires-channel="https" />
- <intercept-url pattern="/api/v1/**" access="isFullyAuthenticated()
and hasAuthority('TWOFACTOR_AUTHENTICATED')"
- method="DELETE" requires-channel="https" />
- <intercept-url pattern="/api/v1/**" access="isFullyAuthenticated()
and hasAuthority('TWOFACTOR_AUTHENTICATED')"
- method="HEAD" requires-channel="https" />
- <custom-filter ref="tenantIdentifierProcessingFilter"
- position="FIRST" />
- <custom-filter before="PRE_AUTH_FILTER" ref="resourceServerFilter"
/>
- <custom-filter ref="twoFactorAuthFilter" after="BASIC_AUTH_FILTER"
/>
- <access-denied-handler ref="oauthAccessDeniedHandler" />
- </http>
-
- <http pattern="/api/oauth/token" create-session="stateless"
- entry-point-ref="oauthAuthenticationEntryPoint"
use-expressions="true"
- authentication-manager-ref="clientAuthenticationManager">
- <csrf disabled="true"/>
- <intercept-url pattern="/api/oauth/token" method="OPTIONS"
- access="permitAll" requires-channel="https" />
- <intercept-url pattern="/api/oauth/token" method="POST"
- access="isFullyAuthenticated()" requires-channel="https" />
- <anonymous enabled="false" />
-
- <custom-filter ref="tenantIdentifierProcessingFilter"
- position="FIRST" />
- <custom-filter ref="clientCredentialsTokenEndpointFilter"
- before="BASIC_AUTH_FILTER" />
- <access-denied-handler ref="oauthAccessDeniedHandler" />
- </http>
-
- <beans:bean id="oauthAuthenticationEntryPoint"
-
class="org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint">
- <beans:property name="realmName" value="Fineract Platform API" />
- </beans:bean>
-
- <beans:bean id="oauthAccessDeniedHandler"
-
class="org.springframework.security.oauth2.provider.error.OAuth2AccessDeniedHandler"
/>
-
- <beans:bean id="clientCredentialsTokenEndpointFilter"
-
class="org.springframework.security.oauth2.provider.client.ClientCredentialsTokenEndpointFilter">
- <beans:constructor-arg value="/api/oauth/token" />
- <beans:property name="authenticationManager"
ref="clientAuthenticationManager" />
- </beans:bean>
-
- <beans:bean id="accessDecisionManager"
- class="org.springframework.security.access.vote.UnanimousBased">
- <beans:constructor-arg>
- <beans:list>
- <beans:bean
-
class="org.springframework.security.oauth2.provider.vote.ScopeVoter" />
- <beans:bean
class="org.springframework.security.access.vote.RoleVoter" />
- <beans:bean
-
class="org.springframework.security.access.vote.AuthenticatedVoter" />
- <beans:bean
-
class="org.springframework.security.web.access.expression.WebExpressionVoter" />
- <beans:bean
-
class="org.apache.fineract.infrastructure.security.vote.SelfServiceUserAccessVote"
/>
- </beans:list>
- </beans:constructor-arg>
- </beans:bean>
-
- <authentication-manager id="clientAuthenticationManager"
erase-credentials="false">
- <authentication-provider
user-service-ref="clientDetailsUserService" />
- <authentication-provider ref="customAuthenticationProvider" />
- </authentication-manager>
-
- <beans:bean id="clientDetailsUserService"
-
class="org.springframework.security.oauth2.provider.client.ClientDetailsUserDetailsService">
- <beans:constructor-arg ref="clientDetailsService" />
- </beans:bean>
-
- <beans:bean id="clientDetailsService"
-
class="org.springframework.security.oauth2.provider.client.JdbcClientDetailsService">
- <beans:constructor-arg ref="routingDataSource" />
- </beans:bean>
-
- <beans:bean id="tokenStore"
-
class="org.springframework.security.oauth2.provider.token.store.JdbcTokenStore">
- <beans:constructor-arg ref="routingDataSource" />
- </beans:bean>
-
- <beans:bean id="tokenServices"
-
class="org.springframework.security.oauth2.provider.token.DefaultTokenServices">
- <beans:property name="tokenStore" ref="tokenStore" />
- <beans:property name="clientDetailsService"
ref="clientDetailsService" />
- <beans:property name="supportRefreshToken" value="true" />
- <beans:property name="refreshTokenValiditySeconds"
- value="86400" />
- <beans:property name="accessTokenValiditySeconds"
- value="3600" />
- </beans:bean>
-
- <beans:bean id="userApprovalHandler"
-
class="org.springframework.security.oauth2.provider.approval.TokenStoreUserApprovalHandler">
- <beans:property name="tokenStore" ref="tokenStore" />
- <beans:property name="clientDetailsService"
ref="clientDetailsService" />
- <beans:property name="requestFactory" ref="oAuth2RequestFactory" />
- </beans:bean>
-
- <beans:bean id="oAuth2RequestFactory"
-
class="org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestFactory">
- <beans:constructor-arg ref="clientDetailsService" />
- </beans:bean>
-
- <oauth:authorization-server
- client-details-service-ref="clientDetailsService"
token-services-ref="tokenServices"
- user-approval-handler-ref="userApprovalHandler"
token-endpoint-url="/api/oauth/token"
- authorization-endpoint-url="/api/oauth/authorize">
- <oauth:refresh-token />
- <oauth:password />
- </oauth:authorization-server>
-
- <oauth:resource-server id="resourceServerFilter" stateless="false"
- token-services-ref="tokenServices" />
- </beans:beans>
-</beans:beans>
diff --git a/fineract-provider/src/main/resources/static/api-docs/apiLive.htm
b/fineract-provider/src/main/resources/static/api-docs/apiLive.htm
index 61c0ea0..2c8c583 100644
--- a/fineract-provider/src/main/resources/static/api-docs/apiLive.htm
+++ b/fineract-provider/src/main/resources/static/api-docs/apiLive.htm
@@ -2769,22 +2769,6 @@
</tr>
<tr>
<td><a
href="#authenticationoauth">Authentication Oauth2</a></td>
-
<td>oauth/token</td>
- <td><a
href="#oauth">OAuth2 Access and refresh Token Request</a></td>
- <td></td>
- <td></td>
- <td></td>
- </tr>
- <tr>
- <td></td>
- <td></td>
- <td><a
href="#oauth_access_token_req">OAuth2 Access Token Request from refresh
token</a></td>
- <td></td>
- <td></td>
- <td></td>
- </tr>
- <tr>
- <td></td>
<td>authenticated user</td>
<td></td>
<td><a
href="#userdetails_request">Fetch Authenticated user details</a></td>
@@ -4499,7 +4483,7 @@ function executeAjaxRequest(url, verbType, jsonData,
basicAuthKey, successFuncti
function getOauthToken(username, password) {
var jqxhr = $.ajax({
- url : "/fineract-provider/api/oauth/token",
+ url : "URL of the authentication server",
type : 'POST',
dataType : 'json',
data : {
@@ -39103,174 +39087,6 @@ No Request Body
<div class="method-description">
<h3>Authentication Oauth2</h3>
<p>An API capability that allows client
applications to fetch current user details details using Oauth2.</p>
- <table class=matrixHeading>
- <tr class="matrixHeadingBG">
- <td><div
class="fineractHeading2">Field Descriptions</div></td>
- </tr>
- <tr class=alt>
- <td>accessToken</td>
- </tr>
- <tr>
- <td
class=fielddesc>HTTP Auth bearer key. See <a
-
href="#authentication_overview">Authentication Overview</a> for
- an example of
its use.
- </td>
- </tr>
- </tr>
- </table>
- </div>
- </div>
-
- <a id="oauth" name="oauth"
class="old-syle-anchor"> </a>
- <div class="method-section">
- <div class="method-description">
- <h3>Authentication via OAuth2</h3>
- <p>API for requesting OAuth2 access
token and refresh token.</p>
- <table class=matrixHeading>
- <tr class="matrixHeadingBG">
- <td><div
class="fineractHeading2">Field Descriptions</div></td>
- </tr>
- <tr class=alt>
- <td>grant_type</td>
- </tr>
- <tr>
- <td
class=fielddesc>Mandatory field.
- Indicates the requested
grant type. Supported values:
- <span>password</span>.
- </td>
- </tr>
- <tr class=alt>
- <td>client_id</td>
- </tr>
- <tr>
- <td
class=fielddesc>Mandatory field.
- Indicates the client
application identity.
- </td>
- </tr>
- <tr class=alt>
- <td>client_secret</td>
- </tr>
- <tr>
- <td
class=fielddesc>Optional field.
- Indicates the client
application password.
- </td>
- </tr>
- <tr class=alt>
- <td>username</td>
- </tr>
- <tr>
- <td
class=fielddesc>Mandatory field.
- Application
User(resource) login name.
- </td>
- </tr>
- <tr class=alt>
- <td>password</td>
- </tr>
- <tr>
- <td
class=fielddesc>Mandatory field.
- Application
User(resource) password.
- </td>
- </tr>
- </table>
- </div>
- </div>
-
- <a id="oauth_request" name="oauth_request"
class="old-syle-anchor"> </a>
- <div class="method-section">
- <div class="method-description">
- <h4>OAuth2 Refresh and Access Token
Request</h4>
- </div>
- <div class="method-example">
- <code class="method-declaration">
-POST
https://DomainName/api/oauth/token?username={username}&password={password}&client_id={clientId}&grant_type={grant_type}&client_secret={client_secret}
- </code>
- <code class="method-request">
-POST
api/oauth/token?username=mifos&password=password&client_id=community-app&grant_type=password&client_secret=123
-Content-Type: application/json
-No Request Body
- </code>
- <code class="method-response">
-{
- ""access_token": "b771987f-82fc-45ba-b521-bfe280c4e603",
- "token_type": "bearer",
- "refresh_token":"a2a89b23-8d22-4d90-8585-8f464db433b0",
- "expires_in": 3599,
- "scope": "all"
-}
- </code>
- </div>
- </div>
-
-
- <a id="oauth_access_token_req"
name="oauth_access_token_req" class="old-syle-anchor"> </a>
- <div class="method-section">
- <div class="method-description">
- <h3>Authentication via OAuth2</h3>
- <p>API for requesting OAuth2 access
tokens through oAuth2 refresh tokens.</p>
- <table class=matrixHeading>
- <tr class="matrixHeadingBG">
- <td><div
class="fineractHeading2">Field Descriptions</div></td>
- </tr>
- <tr class=alt>
- <td>grant_type</td>
- </tr>
- <tr>
- <td
class=fielddesc>Mandatory field.
- Indicates the requested
grant type. Supported values:
-
<span>refresh_token</span>.
- </td>
- </tr>
- <tr class=alt>
- <td>client_id</td>
- </tr>
- <tr>
- <td
class=fielddesc>Mandatory field.
- Indicates the client
application identity.
- </td>
- </tr>
- <tr class=alt>
- <td>client_secret</td>
- </tr>
- <tr>
- <td
class=fielddesc>Optional field.
- Indicates the client
application password.
- </td>
- </tr>
- <tr class=alt>
- <td>refresh_token</td>
- </tr>
- <tr>
- <td
class=fielddesc>Mandatory field.
- Application refresh
token to generate access token.
- </td>
- </tr>
- </table>
- </div>
- </div>
-
- <a id="oauth_request_with_refresh"
name="oauth_request_with_refresh" class="old-syle-anchor"> </a>
- <div class="method-section">
- <div class="method-description">
- <h4>OAuth2 Token Request through
Refresh Token</h4>
- </div>
- <div class="method-example">
- <code class="method-declaration">
-POST
https://DomainName/api/oauth/token?refresh_token={refresh_token}&client_id={clientId}&grant_type={grant_type}&client_secret={client_secret}
- </code>
- <code class="method-request">
-POST
api/oauth/token?client_id=community-app&grant_type=refresh_token&client_secret=123&refresh_token=a2a89b23-8d22-4d90-8585-8f464db433b0
-Content-Type: application/json
-No Request Body
- </code>
- <code class="method-response">
-{
- ""access_token": "b771987f-82fc-45ba-b521-bfe280c4e643",
- "token_type": "bearer",
- "refresh_token":"a2a89b23-8d22-4d90-8585-8f464db433b0",
- "expires_in": 3599,
- "scope": "all"
-}
- </code>
</div>
</div>
@@ -39282,10 +39098,10 @@ No Request Body
</div>
<div class="method-example">
<code class="method-declaration">
-POST https://DomainName/api/v1/userdetails?access_token={access_token}
+GET https://DomainName/api/v1/userdetails
</code>
<code class="method-request">
-POST userdetails?access_token=bWlmb3M6cGFzc3dvcmQ=
+GET userdetails
Content-Type: application/json
No Request Body
</code>
@@ -39342,22 +39158,6 @@ No Request Body
-
- <code class="method-request">
-POST
api/oauth/token?username=mifos&password=password&client_id=community-app&grant_type=password&client_secret=123
-Content-Type: application/json
-No Request Body
- </code>
- <code class="method-response">
-{
- "developerMessage": "Invalid authentication details were passed in api
request.",
- "developerDocLink":
"https://github.com/openMF/mifosx/wiki/HTTP-API-Error-codes",
- "httpStatusCode": "401",
- "defaultUserMessage": "Unauthenticated. Please login.",
- "userMessageGlobalisationCode": "error.msg.not.authenticated",
- "errors": []
-}
- </code>
</div>
</div>
@@ -47220,10 +47020,10 @@ No Request Body
</div>
<div class="method-example">
<code class="method-declaration">
-POST https://DomainName/api/v1/self/userdetails?access_token={access_token}
+POST https://DomainName/api/v1/self/userdetails
</code>
<code class="method-request">
-POST self/userdetails?access_token=bWlmb3M6cGFzc3dvcmQ=
+POST self/userdetails
Content-Type: application/json
No Request Body
</code>
@@ -47258,21 +47058,6 @@ No Request Body
}
</code>
- <code class="method-request">
-POST
api/oauth/token?username=mifos&password=password&client_id=community-app&grant_type=password&client_secret=123
-Content-Type: application/json
-No Request Body
- </code>
- <code class="method-response">
-{
- "developerMessage": "Invalid authentication details were passed in api
request.",
- "developerDocLink":
"https://github.com/openMF/mifosx/wiki/HTTP-API-Error-codes",
- "httpStatusCode": "401",
- "defaultUserMessage": "Unauthenticated. Please login.",
- "userMessageGlobalisationCode": "error.msg.not.authenticated",
- "errors": []
-}
- </code>
</div>
</div>
diff --git a/oauth2-tests/build.gradle b/oauth2-tests/build.gradle
new file mode 100644
index 0000000..8c805db
--- /dev/null
+++ b/oauth2-tests/build.gradle
@@ -0,0 +1,67 @@
+/**
+ * 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.
+ */
+description = 'Fineract Integration Tests for Oauth2'
+
+apply plugin: 'com.bmuschko.cargo'
+
+// Configuration for the Gradle Cargo plugin
+// https://github.com/bmuschko/gradle-cargo-plugin
+configurations {
+ tomcat
+}
+
+apply from: 'dependencies.gradle'
+
+cargo {
+ containerId "tomcat9x"
+
+ // looks like Cargo doesn't detect the WAR file automatically in the
multi-module setup
+ deployable {
+ file =
file("$rootDir/fineract-provider/build/libs/fineract-provider.war")
+ context = 'fineract-provider'
+ }
+
+ local {
+ installer {
+ installConfiguration = configurations.tomcat
+ downloadDir = file("$buildDir/download")
+ extractDir = file("$buildDir/tomcat")
+ }
+ startStopTimeout = 240000
+ containerProperties {
+ property 'cargo.start.jvmargs', '-Dspring.profiles.active=oauth'
+ property 'cargo.tomcat.connector.keystoreFile',
file("$rootDir/fineract-provider/src/main/resources/keystore.jks")
+ property 'cargo.tomcat.connector.keystorePass', 'openmf'
+ property 'cargo.tomcat.httpSecure', true
+ property 'cargo.tomcat.connector.sslProtocol', 'TLS'
+ property 'cargo.tomcat.connector.clientAuth', false
+ property 'cargo.protocol', 'https'
+ property 'cargo.servlet.port', 8443
+ }
+ }
+}
+
+cargoRunLocal.dependsOn ':fineract-provider:war'
+cargoStartLocal.dependsOn ':fineract-provider:war'
+cargoStartLocal.mustRunAfter 'testClasses'
+
+test {
+ dependsOn cargoStartLocal
+ finalizedBy cargoStopLocal
+}
diff --git a/settings.gradle b/oauth2-tests/dependencies.gradle
similarity index 51%
copy from settings.gradle
copy to oauth2-tests/dependencies.gradle
index 596dc4e..38065ae 100644
--- a/settings.gradle
+++ b/oauth2-tests/dependencies.gradle
@@ -16,9 +16,21 @@
* specific language governing permissions and limitations
* under the License.
*/
-rootProject.name='fineract'
-include ':fineract-provider'
-include ':integration-tests'
-include ':twofactor-tests'
-include ':fineract-client'
-include ':fineract-doc'
+dependencies {
+ // testCompile dependencies are ONLY used in src/test, not src/main.
+ // Do NOT repeat dependencies which are ALREADY in implementation or
runtimeOnly!
+ //
+ tomcat 'org.apache.tomcat:tomcat:9.0.54@zip'
+ testImplementation(
files("$rootDir/fineract-provider/build/classes/java/main/"),
+ project(path: ':fineract-provider', configuration:
'runtimeElements'),
+ 'org.junit.jupiter:junit-jupiter-api'
+ )
+ testImplementation ('io.rest-assured:rest-assured') {
+ exclude group: 'commons-logging'
+ exclude group: 'org.apache.sling'
+ exclude group: 'com.sun.xml.bind'
+ }
+ testRuntimeOnly(
+ 'org.junit.jupiter:junit-jupiter-engine'
+ )
+}
diff --git
a/oauth2-tests/src/test/java/org/apache/fineract/oauth2tests/OAuth2AuthenticationTest.java
b/oauth2-tests/src/test/java/org/apache/fineract/oauth2tests/OAuth2AuthenticationTest.java
new file mode 100644
index 0000000..d688de1
--- /dev/null
+++
b/oauth2-tests/src/test/java/org/apache/fineract/oauth2tests/OAuth2AuthenticationTest.java
@@ -0,0 +1,176 @@
+/**
+ * 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.oauth2tests;
+
+import static io.restassured.RestAssured.given;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import io.restassured.RestAssured;
+import io.restassured.builder.RequestSpecBuilder;
+import io.restassured.builder.ResponseSpecBuilder;
+import io.restassured.http.ContentType;
+import io.restassured.path.json.JsonPath;
+import io.restassured.response.Response;
+import io.restassured.specification.RequestSpecification;
+import io.restassured.specification.ResponseSpecification;
+import java.io.IOException;
+import javax.mail.MessagingException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class OAuth2AuthenticationTest {
+
+ private ResponseSpecification responseSpec;
+ private ResponseSpecification responseSpec403;
+ private ResponseSpecification responseSpec401;
+ private RequestSpecification requestSpec;
+ private RequestSpecification requestFormSpec;
+
+ public static final String TENANT_PARAM_NAME = "tenantIdentifier";
+ public static final String DEFAULT_TENANT = "default";
+ public static final String TENANT_IDENTIFIER = TENANT_PARAM_NAME + '=' +
DEFAULT_TENANT;
+ private static final String HEALTH_URL =
"/fineract-provider/actuator/health";
+
+ @BeforeEach
+ public void setup() throws InterruptedException {
+ initializeRestAssured();
+
+ this.requestSpec = new
RequestSpecBuilder().setContentType(ContentType.JSON).build();
+
+ // Login with basic authentication
+ awaitSpringBootActuatorHealthyUp();
+
+ this.requestSpec = new
RequestSpecBuilder().setContentType(ContentType.JSON).build();
+ this.requestFormSpec = new
RequestSpecBuilder().setContentType(ContentType.URLENC).build();
+ this.responseSpec = new
ResponseSpecBuilder().expectStatusCode(200).build();
+ this.responseSpec403 = new
ResponseSpecBuilder().expectStatusCode(403).build();
+ this.responseSpec401 = new
ResponseSpecBuilder().expectStatusCode(401).build();
+ }
+
+ @Test
+ public void testActuatorAccess() {
+ performServerGet(requestSpec, responseSpec,
"/fineract-provider/actuator/info", null);
+ }
+
+ @Test
+ public void testApiDocsAccess() {
+ performServerGet(requestSpec, responseSpec,
"/fineract-provider/api-docs/apiLive.htm", null);
+ }
+
+ @Test
+ public void testAccessWithoutAuthentication() {
+ performServerGet(requestSpec, responseSpec401,
"/fineract-provider/api/v1/offices/1?" + TENANT_IDENTIFIER, "");
+ }
+
+ @Test
+ public void testOAuth2Login() throws IOException, MessagingException {
+
+ performServerGet(requestSpec, responseSpec401,
"/fineract-provider/api/v1/offices/1?" + TENANT_IDENTIFIER, "");
+
+ String accessToken = performServerPost(requestFormSpec, responseSpec,
"http://localhost:9000/auth/realms/fineract/token",
+
"grant_type=client_credentials&client_id=community-app&client_secret=123123",
"access_token");
+ assertNotNull(accessToken);
+
+ String bearerToken = performServerPost(requestFormSpec, responseSpec,
"http://localhost:9000/auth/realms/fineract/token",
+
"grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=" +
accessToken
+ + "&client_id=community-app&scope=fineract",
+ "access_token");
+ assertNotNull(bearerToken);
+
+ RequestSpecification requestSpecWithToken = new RequestSpecBuilder() //
+ .setContentType(ContentType.JSON) //
+ .addHeader("Authorization", "Bearer " + bearerToken) //
+ .build();
+
+ performServerGet(requestSpecWithToken, responseSpec,
"/fineract-provider/api/v1/offices/1?" + TENANT_IDENTIFIER, "");
+ }
+
+ @Test
+ public void testGetOAuth2UserDetails() {
+ performServerGet(requestSpec, responseSpec401,
"/fineract-provider/api/v1/offices/1?" + TENANT_IDENTIFIER, "");
+
+ String accessToken = performServerPost(requestFormSpec, responseSpec,
"http://localhost:9000/auth/realms/fineract/token",
+
"grant_type=client_credentials&client_id=community-app&client_secret=123123",
"access_token");
+ assertNotNull(accessToken);
+
+ String bearerToken = performServerPost(requestFormSpec, responseSpec,
"http://localhost:9000/auth/realms/fineract/token",
+
"grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=" +
accessToken
+ + "&client_id=community-app&scope=fineract",
+ "access_token");
+ assertNotNull(bearerToken);
+
+ RequestSpecification requestSpecWithToken = new RequestSpecBuilder() //
+ .setContentType(ContentType.JSON) //
+ .addHeader("Authorization", "Bearer " + bearerToken) //
+ .build();
+
+ Boolean authenticationCheck = performServerGet(requestSpecWithToken,
responseSpec,
+ "/fineract-provider/api/v1/userdetails?" + TENANT_IDENTIFIER,
"authenticated");
+ assertEquals(authenticationCheck, true);
+ }
+
+ private static void initializeRestAssured() {
+ RestAssured.baseURI = "https://localhost";
+ RestAssured.port = 8443;
+ RestAssured.keyStore("src/main/resources/keystore.jks", "openmf");
+ RestAssured.useRelaxedHTTPSValidation();
+ }
+
+ private static void awaitSpringBootActuatorHealthyUp() throws
InterruptedException {
+ int attempt = 0;
+ final int max_attempts = 10;
+ Response response = null;
+
+ do {
+ try {
+ response = RestAssured.get(HEALTH_URL);
+
+ if (response.statusCode() == 200) {
+ return;
+ }
+
+ Thread.sleep(3000);
+ } catch (Exception e) {
+ Thread.sleep(3000);
+ }
+ } while (attempt < max_attempts);
+
+ fail(HEALTH_URL + " returned " + response.prettyPrint());
+ }
+
+ @SuppressWarnings("unchecked")
+ private static <T> T performServerGet(final RequestSpecification
requestSpec, final ResponseSpecification responseSpec,
+ final String getURL, final String jsonAttributeToGetBack) {
+ final String json =
given().spec(requestSpec).expect().spec(responseSpec).log().ifError().when().get(getURL).andReturn().asString();
+ if (jsonAttributeToGetBack == null) {
+ return (T) json;
+ }
+ return (T) JsonPath.from(json).get(jsonAttributeToGetBack);
+ }
+
+ @SuppressWarnings("unchecked")
+ public static <T> T performServerPost(final RequestSpecification
requestSpec, final ResponseSpecification responseSpec,
+ final String putURL, final String formBody, final String
jsonAttributeToGetBack) {
+ final String json =
given().spec(requestSpec).body(formBody).expect().spec(responseSpec).log().ifError().when().post(putURL)
+ .andReturn().asString();
+ return (T) JsonPath.from(json).get(jsonAttributeToGetBack);
+ }
+}
diff --git a/settings.gradle b/settings.gradle
index 596dc4e..69f7147 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -20,5 +20,6 @@ rootProject.name='fineract'
include ':fineract-provider'
include ':integration-tests'
include ':twofactor-tests'
+include ':oauth2-tests'
include ':fineract-client'
include ':fineract-doc'