Skip to content

Detect authorization bearer token properly #6813

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

Closed
wants to merge 2 commits into from
Closed

Detect authorization bearer token properly #6813

wants to merge 2 commits into from

Conversation

TheFonz2017
Copy link

DefaultBearerTokenResolver has a bug that makes it miss authorization bearer tokens, if the "Bearer" is written in lower case as "bearer". The latter can be observed, when running Zuul as an API gateway that forwards authorization bearer tokens to downstream services. This seems to be fixed in spring-security master, but is still an issue in 5.1.x.

DefaultBearerTokenResolver has a bug that makes it miss authorization bearer tokens, if the "Bearer" is written in lower case as "bearer". The latter can be observed, when running Zuul as an API gateway that forwards authorization bearer tokens to downstream services. This seems to be fixed in spring-security master, but is still an issue in 5.1.x.
@pivotal-issuemaster
Copy link

@TheFonz2017 Please sign the Contributor License Agreement!

Click here to manually synchronize the status of this Pull Request.

See the FAQ for frequently asked questions.

@pivotal-issuemaster
Copy link

@TheFonz2017 Thank you for signing the Contributor License Agreement!

@TheFonz2017
Copy link
Author

Note: there is an additional issue in this class. That is in line 39.
There, a matching pattern is given, which also assumes that the Bearer part is written exactly like this. This is wrong, I think.

The pattern used for matching needed to be applied in a case insensitive way.
Note that according to https://tools.ietf.org/html/rfc6750#section-2.1 one could also argue that this implementation was correct and that generally the "Bearer" part should always be written exactly like this. In that case Spring Cloud Security OAuth2 would have a bug, since it does not pay attention to having "Bearer" capitalized.
@jzheaux
Copy link
Contributor

jzheaux commented Apr 24, 2019

@TheFonz2017 thanks for the PR!

This was actually already addressed in 5.2, so we'd likely instead backport 63f2b60 instead of merging this PR.

In addition, this is currently an open discussion over on #6228, and we need to make sure to resolve that before proceeding with a backport.

@jzheaux jzheaux closed this Apr 24, 2019
@TheFonz2017
Copy link
Author

Thanks for the update. I will wait then.
Just some more clarifications:

  • I am using Spring Netflix Zuul in combination with with Spring Security OAuth, Spring Security JWT and Spring Cloud Security (see dependencies below) and the @EnableOAuth2Sso annotation

  • This auto-configures OAuth2 in Zuul and as a result, a Zuul-filter will be added to the Zuul filter chain called OAuth2TokenRelayFilter which is responsible for adding an authorization: bearer <token> header to the proxied request for downstream services to consume.

  • As a result of Line 83 in OAuth2TokenRelayFilter, the resulting header is all lower-case (i.e. authorization: bearer ). In particular, this is the case since OAuth2AuthenticationDetails#getTokenType() returns a lower-case bearer.

  • Thus, if the discussion about case-sensitivity were to conclude with "We'll do it case sensitive in Spring", you will also have to fix the org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationDetails implementation.

  • For all those suffering from the same issue right now: I fixed this issue with a workaround (until Spring Security 5.2 is out) by implementing a custom Zuul PRE-filter, that re-writes the authorization header before it is proxied downstream. See code below.

Dependencies I used - for reference

                <dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
		</dependency>

		<!-- Important: A Eureka Client needs to be on the classpath for Zuul to 
			automatically detect services registered to Eureka, and do an automatic routing 
			based on registered service name. E.g. a service registered as 'users' will 
			be available as /users on the Zuul proxy and requests will be forwarded to 
			the 'users' service. -->
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
		</dependency>

		<!-- Required for health checks and info pages advertised by the service 
			and read by Eureka -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-actuator</artifactId>
		</dependency>

		<!-- For OkHttp-client based routing filter. -->
		<dependency>
			<groupId>com.squareup.okhttp3</groupId>
			<artifactId>okhttp</artifactId>
		</dependency>

		<!-- For @ConfigurationProperties usage in GroovyFilterPathConfig.java -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-configuration-processor</artifactId>
			<optional>true</optional>
		</dependency>

		<!-- Groovy Language for Groovy Filters. Enable this, if you would like 
			to use Groovy filters. Otherwise you can get rid of this dependency. -->
		<dependency>
			<groupId>org.codehaus.groovy</groupId>
			<artifactId>groovy-all</artifactId>
			<version>2.4.9</version>
		</dependency>

		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-oauth2</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.security.oauth</groupId>
			<artifactId>spring-security-oauth2</artifactId>
			<version>2.3.5.RELEASE</version>
		</dependency>

		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-jwt</artifactId>
			<version>1.0.10.RELEASE</version>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>


	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-dependencies</artifactId>
				<version>Greenwich.RELEASE</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>

			<dependency>
				<groupId>org.springframework.security</groupId>
				<artifactId>spring-security-bom</artifactId>
				<version>5.1.5.RELEASE</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>

		</dependencies>
	</dependencyManagement>

Authorization Header Re-Writing Zuul PRE-Filter

import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE;

import java.util.Map;

import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
import org.springframework.cloud.netflix.zuul.filters.discovery.DiscoveryClientRouteLocator;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;

public class ZuulAuthorizationHeaderProxyFilter extends ZuulFilter {

    @SuppressWarnings("unused")
    private ZuulProperties zuulProperties;
    
    @SuppressWarnings("unused")
    private DiscoveryClientRouteLocator routeLocator;
    
    /**
     * Creates a new Zuul filter for canary testing using the given properties.
     * @param zuulProperties the Zuul configurations as given in application.yml
     * @param routeLocator the route locator.
     */
    public ZuulAuthorizationHeaderProxyFilter(ZuulProperties zuulProperties, DiscoveryClientRouteLocator routeLocator) {
        this.zuulProperties = zuulProperties;
        this.routeLocator = routeLocator;
    }
    
    @Override
    public boolean shouldFilter() {
        // Only run, if there is a Zuul request header set for the authorization,
        // and if it contains a bearer token.
        RequestContext ctx = RequestContext.getCurrentContext();
        Map<String, String> zuulRequestHeaders = ctx.getZuulRequestHeaders();
        
        String authorizationHeaderValue = zuulRequestHeaders.get("authorization");
        authorizationHeaderValue = authorizationHeaderValue != null ? authorizationHeaderValue : zuulRequestHeaders.get("Authorization");
        
        return (authorizationHeaderValue != null) && (authorizationHeaderValue.trim().toLowerCase().startsWith("bearer"));
    }

    @Override
    public Object run() throws ZuulException {
        RequestContext ctx = RequestContext.getCurrentContext();
        Map<String, String> zuulRequestHeaders = ctx.getZuulRequestHeaders();
        
        String authorizationHeaderValue = zuulRequestHeaders.get("authorization");
        authorizationHeaderValue = authorizationHeaderValue != null ? authorizationHeaderValue : zuulRequestHeaders.get("Authorization");
        
        if(authorizationHeaderValue == null)
            throw new ZuulException("Error! Could not retrieve authorization header from Zuul request headers.", 500, "Header not found. This must be an illegal state.");
        
        authorizationHeaderValue = authorizationHeaderValue.replace("bearer", "Bearer");
        
        zuulRequestHeaders.put("authorization", authorizationHeaderValue);
        
        return null;
    }

    @Override
    public String filterType() {
        return PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return 11; // one after the OAuth2TokenRelayFilter
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants