Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Add BearerContent filter to enable bearer auth #2197

Merged
merged 8 commits into from
Mar 16, 2022
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import org.zowe.apiml.security.common.config.CertificateAuthenticationProvider;
import org.zowe.apiml.security.common.config.HandlerInitializer;
import org.zowe.apiml.security.common.content.BasicContentFilter;
import org.zowe.apiml.security.common.content.BearerContentFilter;
import org.zowe.apiml.security.common.content.CookieContentFilter;
import org.zowe.apiml.security.common.filter.CategorizeCertsFilter;
import org.zowe.apiml.security.common.login.LoginFilter;
Expand Down Expand Up @@ -228,7 +229,8 @@ private HttpSecurity mainframeCredentialsConfiguration(HttpSecurity http, Authen
// endpoints protection
.and()
.addFilterBefore(basicFilter(authenticationManager), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(cookieFilter(authenticationManager), UsernamePasswordAuthenticationFilter.class);
.addFilterBefore(cookieFilter(authenticationManager), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(bearerContentFilter(authenticationManager), UsernamePasswordAuthenticationFilter.class);

return http;
}
Expand Down Expand Up @@ -266,6 +268,17 @@ private CookieContentFilter cookieFilter(AuthenticationManager authenticationMan
authConfigurationProperties);
}

/**
* Secures content with a Bearer token
*/
private BearerContentFilter bearerContentFilter(AuthenticationManager authenticationManager) {
return new BearerContentFilter(
authenticationManager,
handlerInitializer.getAuthenticationFailureHandler(),
handlerInitializer.getResourceAccessExceptionHandler()
);
}

@Bean
public LogoutSuccessHandler logoutSuccessHandler() {
return new ApiCatalogLogoutSuccessHandler(authConfigurationProperties);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* This program and the accompanying materials are made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v20.html
*
* SPDX-License-Identifier: EPL-2.0
*
* Copyright Contributors to the Zowe Project.
*/
package org.zowe.apiml.security.common.content;

import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.zowe.apiml.constants.ApimlConstants;
import org.zowe.apiml.security.common.error.ResourceAccessExceptionHandler;
import org.zowe.apiml.security.common.token.TokenAuthentication;

import javax.servlet.http.HttpServletRequest;
import java.util.Optional;

/**
* Authenticate the JWT token stored in Bearer header
*/
public class BearerContentFilter extends AbstractSecureContentFilter {

public BearerContentFilter(AuthenticationManager authenticationManager,
AuthenticationFailureHandler failureHandler,
ResourceAccessExceptionHandler resourceAccessExceptionHandler) {
super(authenticationManager, failureHandler, resourceAccessExceptionHandler, new String[0]);
}

public BearerContentFilter(AuthenticationManager authenticationManager,
AuthenticationFailureHandler failureHandler,
ResourceAccessExceptionHandler resourceAccessExceptionHandler,
String[] endpoints) {
super(authenticationManager, failureHandler, resourceAccessExceptionHandler, endpoints);
}

/**
* Extract the JWT token from the authorization header
*
* @param request the http request
* @return the JWT token
*/
protected Optional<AbstractAuthenticationToken> extractContent(HttpServletRequest request) {
return Optional.ofNullable(
request.getHeader(HttpHeaders.AUTHORIZATION)
).filter(
header -> header.startsWith(ApimlConstants.BEARER_AUTHENTICATION_PREFIX)
).map(
header -> {
header = header.replaceFirst(ApimlConstants.BEARER_AUTHENTICATION_PREFIX, "").trim();
return new TokenAuthentication(header);
}
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ void shouldNotAuthenticateWithNoGateway() throws ServletException, IOException {
}

@Test
void shouldNotFilterWithNoCookie() throws ServletException, IOException {
void shouldNotFilterWithNoCredentials() throws ServletException, IOException {
basicContentFilter.doFilter(request, response, filterChain);

verify(authenticationManager, never()).authenticate(any());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/*
taban03 marked this conversation as resolved.
Show resolved Hide resolved
* This program and the accompanying materials are made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v20.html
*
* SPDX-License-Identifier: EPL-2.0
*
* Copyright Contributors to the Zowe Project.
*/
package org.zowe.apiml.security.common.content;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpHeaders;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.zowe.apiml.security.common.error.ResourceAccessExceptionHandler;
import org.zowe.apiml.security.common.token.TokenAuthentication;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import java.io.IOException;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

class BearerContentFilterTest {
private BearerContentFilter bearerContentFilter;
private final MockHttpServletRequest request = new MockHttpServletRequest();
private final MockHttpServletResponse response = new MockHttpServletResponse();
private final FilterChain filterChain = mock(FilterChain.class);
private final AuthenticationManager authenticationManager = mock(AuthenticationManager.class);
private final AuthenticationFailureHandler authenticationFailureHandler = mock(AuthenticationFailureHandler.class);
private final ResourceAccessExceptionHandler resourceAccessExceptionHandler = mock(ResourceAccessExceptionHandler.class);
private final static String BEARER_AUTH = "Bearer token";

@BeforeEach
void setUp() {
bearerContentFilter = new BearerContentFilter(
authenticationManager,
authenticationFailureHandler,
resourceAccessExceptionHandler);
}

@Nested
class GivenValidBearerHeader {
@Nested
class WhenAuthenticate {
@Test
void thenSuccess() throws ServletException, IOException {
String token = "token";
TokenAuthentication tokenAuthentication = new TokenAuthentication(token);
request.addHeader(HttpHeaders.AUTHORIZATION, BEARER_AUTH);

bearerContentFilter.doFilter(request, response, filterChain);

verify(authenticationManager).authenticate(tokenAuthentication);
verify(filterChain).doFilter(request, response);
verify(authenticationFailureHandler, never()).onAuthenticationFailure(any(), any(), any());
verify(resourceAccessExceptionHandler, never()).handleException(any(), any(), any());
}
}

@Nested
class whenAuthenticateWithNoGateway {
@Test
void thenAuthenticationFails() throws ServletException, IOException {
String token = "token";
RuntimeException exception = new RuntimeException("No Gateway");

TokenAuthentication tokenAuthentication = new TokenAuthentication(token);
request.addHeader(HttpHeaders.AUTHORIZATION, BEARER_AUTH);
when(authenticationManager.authenticate(tokenAuthentication)).thenThrow(exception);

bearerContentFilter.doFilter(request, response, filterChain);

verify(authenticationManager).authenticate(tokenAuthentication);
verify(filterChain, never()).doFilter(any(), any());
verify(authenticationFailureHandler, never()).onAuthenticationFailure(any(), any(), any());
verify(resourceAccessExceptionHandler).handleException(request, response, exception);
}
}
}

@Nested
class WhenGatewayEndpoint {
@Test
void thenSkipFilter() throws ServletException, IOException {
String[] endpoints = {"/gateway"};

request.setContextPath(endpoints[0]);

BearerContentFilter bearerContentFilter = new BearerContentFilter(authenticationManager,
authenticationFailureHandler,
resourceAccessExceptionHandler,
endpoints);

bearerContentFilter.doFilter(request, response, filterChain);

verify(authenticationManager, never()).authenticate(any());
verify(authenticationFailureHandler, never()).onAuthenticationFailure(any(), any(), any());
verify(resourceAccessExceptionHandler, never()).handleException(any(), any(), any());
}
}

@Nested
class GivenInValidToken {
@Nested
class WhenAuthenticate {
@Test
void thenAuthenticationFails() throws ServletException, IOException {
String token = "token";
AuthenticationException exception = new BadCredentialsException("Token not valid");

TokenAuthentication tokenAuthentication = new TokenAuthentication(token);
request.addHeader(HttpHeaders.AUTHORIZATION, BEARER_AUTH);
when(authenticationManager.authenticate(tokenAuthentication)).thenThrow(exception);

bearerContentFilter.doFilter(request, response, filterChain);

verify(authenticationManager).authenticate(tokenAuthentication);
verify(filterChain, never()).doFilter(any(), any());
verify(authenticationFailureHandler).onAuthenticationFailure(request, response, exception);
verify(resourceAccessExceptionHandler, never()).handleException(any(), any(), any());
}
}
}

@Nested
class WhenNoBearerHeader {
@Test
void thenNotFilter() throws ServletException, IOException {
bearerContentFilter.doFilter(request, response, filterChain);

verify(authenticationManager, never()).authenticate(any());
verify(filterChain).doFilter(request, response);
verify(authenticationFailureHandler, never()).onAuthenticationFailure(any(), any(), any());
verify(resourceAccessExceptionHandler, never()).handleException(any(), any(), any());
}

@Test
void thenReturnEmpty() {
Optional<AbstractAuthenticationToken> content = bearerContentFilter.extractContent(request);

assertEquals(Optional.empty(), content);
}
}

@Nested
class WhenBearerHeader {
@Test
void thenExtractContent() {
request.addHeader(HttpHeaders.AUTHORIZATION, BEARER_AUTH);
Optional<AbstractAuthenticationToken> content = bearerContentFilter.extractContent(request);

TokenAuthentication actualToken = new TokenAuthentication("token");

assertTrue(content.isPresent());
assertEquals(actualToken, content.get());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import org.zowe.apiml.security.common.config.AuthConfigurationProperties;
import org.zowe.apiml.security.common.config.HandlerInitializer;
import org.zowe.apiml.security.common.content.BasicContentFilter;
import org.zowe.apiml.security.common.content.BearerContentFilter;
import org.zowe.apiml.security.common.content.CookieContentFilter;

import java.util.Collections;
Expand Down Expand Up @@ -79,6 +80,7 @@ protected void configure(HttpSecurity http) throws Exception {
.and())
.addFilterBefore(basicFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(cookieFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(bearerContentFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.antMatchers("/**").authenticated()
.and()
Expand Down Expand Up @@ -165,6 +167,7 @@ protected void configure(HttpSecurity http) throws Exception {
baseConfigure(http.antMatcher("/discovery/**"))
.addFilterBefore(basicFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(cookieFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(bearerContentFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class)
.httpBasic().realmName(DISCOVERY_REALM);
if (verifySslCertificatesOfServices || nonStrictVerifySslCertificatesOfServices) {
http.authorizeRequests().anyRequest().authenticated().and()
Expand Down Expand Up @@ -193,6 +196,17 @@ private CookieContentFilter cookieFilter(AuthenticationManager authenticationMan
securityConfigurationProperties);
}

/**
* Secures content with a Bearer token
*/
private BearerContentFilter bearerContentFilter(AuthenticationManager authenticationManager) {
return new BearerContentFilter(
authenticationManager,
handlerInitializer.getAuthenticationFailureHandler(),
handlerInitializer.getResourceAccessExceptionHandler()
);
}

private UserDetailsService x509UserDetailsService() {
return username -> new User("eurekaClient", "", Collections.emptyList());
}
Expand Down
Loading