FINERACT-1012: Upgrading OAuth support to Spring Security 5.6
This commit is contained in:
committed by
Aleksandar Vidakovic
parent
b6a9a2791d
commit
c74bfed1fe
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -10,6 +10,7 @@ build
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
*.swp
|
||||
*.DS_Store
|
||||
.idea
|
||||
.vscode
|
||||
|
||||
+1
-1
@@ -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'
|
||||
|
||||
@@ -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=<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
|
||||
NOTE: See also https://demo.fineract.dev/fineract-provider/api-docs/apiLive.htm#authentication_oauth
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
-44
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+145
@@ -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;
|
||||
}
|
||||
}
|
||||
+46
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+53
-50
@@ -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) {
|
||||
public String fetchAuthenticatedUserData() {
|
||||
|
||||
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);
|
||||
final SecurityContext context = SecurityContextHolder.getContext();
|
||||
if (context == null) {
|
||||
return 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 this.apiJsonSerializerService.serialize(authenticatedUserData);
|
||||
}
|
||||
}
|
||||
|
||||
+42
@@ -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;
|
||||
}
|
||||
}
|
||||
+16
-11
@@ -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);
|
||||
|
||||
+1
-1
@@ -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 {
|
||||
|
||||
|
||||
+17
-12
@@ -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 List<GrantedAuthority> updatedAuthorities) throws ServletException {
|
||||
|
||||
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);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-5
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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'
|
||||
)
|
||||
}
|
||||
+176
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user