From 7282ead2413c4cd314975764d541a4ced76bfa0e Mon Sep 17 00:00:00 2001 From: Petri Tuomola Date: Sun, 26 Dec 2021 15:42:35 +0800 Subject: [PATCH] FINERACT-1012: Upgrading OAuth support to Spring Security 5.6 --- .github/workflows/build-oauth2.yml | 60 +++++ .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 +++++++++++ .../OAuth2ExceptionEntryPoint.java | 46 ++++ .../security/api/UserDetailsApiResource.java | 105 ++++---- .../data/FineractJwtAuthenticationToken.java | 42 ++++ ...InsecureTwoFactorAuthenticationFilter.java | 27 ++- .../TenantAwareTenantIdentifierFilter.java | 2 +- .../filter/TwoFactorAuthenticationFilter.java | 31 ++- .../api/SelfUserDetailsApiResource.java | 7 +- .../META-INF/spring/securityContext.xml | 167 ------------- .../resources/static/api-docs/apiLive.htm | 225 +----------------- oauth2-tests/build.gradle | 67 ++++++ oauth2-tests/dependencies.gradle | 36 +++ .../oauth2tests/OAuth2AuthenticationTest.java | 176 ++++++++++++++ settings.gradle | 1 + 24 files changed, 721 insertions(+), 532 deletions(-) create mode 100644 .github/workflows/build-oauth2.yml delete mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/boot/WebXmlOauthConfiguration.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/OAuth2SecurityConfig.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/OAuth2ExceptionEntryPoint.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/data/FineractJwtAuthenticationToken.java delete mode 100644 fineract-provider/src/main/resources/META-INF/spring/securityContext.xml create mode 100644 oauth2-tests/build.gradle create mode 100644 oauth2-tests/dependencies.gradle create mode 100644 oauth2-tests/src/test/java/org/apache/fineract/oauth2tests/OAuth2AuthenticationTest.java diff --git a/.github/workflows/build-oauth2.yml b/.github/workflows/build-oauth2.yml new file mode 100644 index 00000000000..7b715fe479f --- /dev/null +++ b/.github/workflows/build-oauth2.yml @@ -0,0 +1,60 @@ +name: Fineract Gradle build - oauth2 +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-20.04 + + services: + mariad: + image: mariadb:10.6 + ports: + - 3306:3306 + env: + 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: + - name: Set up cache + uses: actions/cache@v2 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-oauth2-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle-oauth2- + - name: Checkout + uses: actions/checkout@v2 + - name: Set up JDK 11 + uses: actions/setup-java@v2 + with: + java-version: '11' + distribution: 'zulu' + - name: Validate Gradle wrapper + uses: gradle/wrapper-validation-action@e6e38bacfdf1a337459f332974bb2327a31aaf4b + - name: Verify MariaDB connection + run: | + while ! mysqladmin ping -h"127.0.0.1" -P3306 ; do + sleep 1 + done + - name: Initialise databases + run: | + ./gradlew --no-daemon -q createDB -PdbName=fineract_tenants + ./gradlew --no-daemon -q createDB -PdbName=fineract_default + - name: Install additional software + run: | + 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 -x :twofactor-tests:test -Psecurity=oauth diff --git a/.github/workflows/build-twofactor.yml b/.github/workflows/build-twofactor.yml index 9e55d9f14f7..aac714140a1 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 908d030be17..9e89ac642d2 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 b62b52d54d8..e56ba7eb5d2 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 120046e0f90..25530a8dd64 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 fec07da8b56..933b719a9a4 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_OAuth_providers) + +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=' ---- -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 ' + ---- -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 3378a29ab5c..393562ffbdf 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 057b3615e0e..6936db5277e 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 dc84c31444a..3a2e01d3885 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 7b67579f928..00000000000 --- 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 #howto-convert-an-existing-application-to-spring-boot - */ -@Configuration -@Profile("oauth") -public class WebXmlOauthConfiguration { - - @Bean - public ServletRegistrationBean dispatcherRegistration(DispatcherServlet dispatcherServlet) { - ServletRegistrationBean registrationBean = new ServletRegistrationBean(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 00000000000..a2226c922ea --- /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 authenticationConverter() { + return jwt -> { + try { + UserDetails user = userDetailsService.loadUserByUsername(jwt.getSubject()); + jwtGrantedAuthoritiesConverter.setAuthorityPrefix(""); + Collection 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> decisionVoters = Arrays.asList(new RoleVoter(), new AuthenticatedVoter(), + new WebExpressionVoter(), new SelfServiceUserAccessVote()); + + return new UnanimousBased(decisionVoters); + } + + @Bean + public FilterRegistrationBean tenantAwareTenantIdentifierFilterRegistration() throws Exception { + FilterRegistrationBean registration = new FilterRegistrationBean( + tenantAwareTenantIdentifierFilter); + registration.setEnabled(false); + return registration; + } + + @Bean + public FilterRegistrationBean twoFactorAuthenticationFilterRegistration() { + FilterRegistrationBean registration = new FilterRegistrationBean( + 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 00000000000..7b41234b672 --- /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 05ce28b4568..64e96bf0c0e 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 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 @@ @Scope("singleton") @Tag(name = "Fetch authenticated user details", description = "") -@SuppressWarnings("deprecation") // TODO FINERACT-1012 public class UserDetailsApiResource { - private final ResourceServerTokenServices tokenServices; private final ToApiJsonSerializer apiJsonSerializerService; private final SpringSecurityPlatformSecurityContext springSecurityPlatformSecurityContext; private final TwoFactorUtils twoFactorUtils; @Autowired - public UserDetailsApiResource(@Qualifier("tokenServices") final ResourceServerTokenServices tokenServices, - final ToApiJsonSerializer apiJsonSerializerService, + public UserDetailsApiResource(final ToApiJsonSerializer apiJsonSerializerService, final SpringSecurityPlatformSecurityContext springSecurityPlatformSecurityContext, final TwoFactorUtils twoFactorUtils) { - this.tokenServices = tokenServices; this.apiJsonSerializerService = apiJsonSerializerService; this.springSecurityPlatformSecurityContext = springSecurityPlatformSecurityContext; this.twoFactorUtils = twoFactorUtils; @@ -83,48 +77,57 @@ public UserDetailsApiResource(@Qualifier("tokenServices") final ResourceServerTo @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 permissions = new ArrayList<>(); - AuthenticatedOauthUserData authenticatedUserData = new AuthenticatedOauthUserData(principal.getUsername(), permissions); - - final Collection authorities = new ArrayList<>(authentication.getAuthorities()); - for (final GrantedAuthority grantedAuthority : authorities) { - permissions.add(grantedAuthority.getAuthority()); - } - - final Collection roles = new ArrayList<>(); - final Set 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 permissions = new ArrayList<>(); + AuthenticatedOauthUserData authenticatedUserData = new AuthenticatedOauthUserData(principal.getUsername(), permissions); + + final Collection authorities = new ArrayList<>(authentication.getAuthorities()); + for (final GrantedAuthority grantedAuthority : authorities) { + permissions.add(grantedAuthority.getAuthority()); + } + + final Collection roles = new ArrayList<>(); + final Set 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 00000000000..22e1d54a07c --- /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 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 7216f119357..64e40b8e0ea 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 @@ 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.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 void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) } // 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 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) 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 42a26fc1efa..bbc23de0847 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 @@ * * 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 cc877f0bf0c..14dff4b6874 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 @@ 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.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.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 void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) List 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 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 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) 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 dda871502fe..2104cdbebcb 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 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 SelfUserDetailsApiResource(final UserDetailsApiResource userDetailsApiRes + "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 255db23edc2..00000000000 --- a/fineract-provider/src/main/resources/META-INF/spring/securityContext.xml +++ /dev/null @@ -1,167 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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 61c0ea05090..2c8c5833b0e 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 @@

User

Authentication Oauth2 - oauth/token - OAuth2 Access and refresh Token Request - - - - - - - - OAuth2 Access Token Request from refresh token - - - - - - authenticated user Fetch Authenticated user details @@ -4499,7 +4483,7 @@

Authentication Oauth2

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 @@

Verify authentication

Authentication Oauth2

An API capability that allows client applications to fetch current user details details using Oauth2.

- - - - - - - - - - - -
Field Descriptions
accessToken
HTTP Auth bearer key. See Authentication Overview for - an example of its use. -
-
- - -   -
-
-

Authentication via OAuth2

-

API for requesting OAuth2 access token and refresh token.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Field Descriptions
grant_type
Mandatory field. - Indicates the requested grant type. Supported values: - password. -
client_id
Mandatory field. - Indicates the client application identity. -
client_secret
Optional field. - Indicates the client application password. -
username
Mandatory field. - Application User(resource) login name. -
password
Mandatory field. - Application User(resource) password. -
-
-
- -   -
-
-

OAuth2 Refresh and Access Token Request

-
-
- -POST https://DomainName/api/oauth/token?username={username}&password={password}&client_id={clientId}&grant_type={grant_type}&client_secret={client_secret} - - -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 - - -{ - ""access_token": "b771987f-82fc-45ba-b521-bfe280c4e603", - "token_type": "bearer", - "refresh_token":"a2a89b23-8d22-4d90-8585-8f464db433b0", - "expires_in": 3599, - "scope": "all" -} - -
-
- - -   -
-
-

Authentication via OAuth2

-

API for requesting OAuth2 access tokens through oAuth2 refresh tokens.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Field Descriptions
grant_type
Mandatory field. - Indicates the requested grant type. Supported values: - refresh_token. -
client_id
Mandatory field. - Indicates the client application identity. -
client_secret
Optional field. - Indicates the client application password. -
refresh_token
Mandatory field. - Application refresh token to generate access token. -
-
-
- -   -
-
-

OAuth2 Token Request through Refresh Token

-
-
- -POST https://DomainName/api/oauth/token?refresh_token={refresh_token}&client_id={clientId}&grant_type={grant_type}&client_secret={client_secret} - - -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 - - -{ - ""access_token": "b771987f-82fc-45ba-b521-bfe280c4e643", - "token_type": "bearer", - "refresh_token":"a2a89b23-8d22-4d90-8585-8f464db433b0", - "expires_in": 3599, - "scope": "all" -} -
@@ -39282,10 +39098,10 @@

Fetch authenticated user details

-POST https://DomainName/api/v1/userdetails?access_token={access_token} +GET https://DomainName/api/v1/userdetails -POST userdetails?access_token=bWlmb3M6cGFzc3dvcmQ= +GET userdetails Content-Type: application/json No Request Body @@ -39342,22 +39158,6 @@

Fetch authenticated user details

- - -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 - - -{ - "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": [] -} -
@@ -47220,10 +47020,10 @@

Fetch authenticated user details

-POST https://DomainName/api/v1/self/userdetails?access_token={access_token} +POST https://DomainName/api/v1/self/userdetails -POST self/userdetails?access_token=bWlmb3M6cGFzc3dvcmQ= +POST self/userdetails Content-Type: application/json No Request Body @@ -47258,21 +47058,6 @@

Fetch authenticated user details

} - -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 - - -{ - "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": [] -} -
diff --git a/oauth2-tests/build.gradle b/oauth2-tests/build.gradle new file mode 100644 index 00000000000..8c805db4b59 --- /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/oauth2-tests/dependencies.gradle b/oauth2-tests/dependencies.gradle new file mode 100644 index 00000000000..38065ae1bf4 --- /dev/null +++ b/oauth2-tests/dependencies.gradle @@ -0,0 +1,36 @@ +/** + * 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. + */ +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 00000000000..d688de1f53f --- /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 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 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 596dc4ee953..69f714751b4 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'