FINERACT-1012: Upgrading OAuth support to Spring Security 5.6

This commit is contained in:
Petri Tuomola
2021-12-28 03:08:27 +01:00
committed by Aleksandar Vidakovic
parent b6a9a2791d
commit c74bfed1fe
24 changed files with 719 additions and 530 deletions
+60
View File
@@ -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
+3 -3
View File
@@ -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
+3 -3
View File
@@ -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
+1
View File
@@ -10,6 +10,7 @@ build
*.iml
*.ipr
*.iws
*.swp
*.DS_Store
.idea
.vscode
+1 -1
View File
@@ -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'
+37 -12
View File
@@ -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
+1 -1
View File
@@ -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
@@ -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;
}
}
@@ -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;
}
}
@@ -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);
}
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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);
@@ -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 {
@@ -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;
}
}
@@ -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">&nbsp;</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">&nbsp;</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">&nbsp;</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">&nbsp;</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>
+67
View File
@@ -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
}
+36
View File
@@ -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'
)
}
@@ -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);
}
}
+1
View File
@@ -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'