Skip to content

OAuth2 integration #179

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
malinovskiy-alex opened this issue Jan 29, 2018 · 52 comments
Closed

OAuth2 integration #179

malinovskiy-alex opened this issue Jan 29, 2018 · 52 comments

Comments

@malinovskiy-alex
Copy link

Hello guys,

I have gateway which is written with zuul1 and it's actually @ ResourceServer in terms of oauth2. Do you know if cloud api gateway has an integration with oauth2?

@spencergibb
Copy link
Member

not yet

@uhel-mare
Copy link

Checking one month later - is there some roadmap for implementing this feature please? It would be great to know the plans so that we can adjust ours :)

@re6exp
Copy link
Contributor

re6exp commented Mar 1, 2018

You can do it on separate UAA, not on gateway. In our case, this approach works perfect.

@peter-kori
Copy link

Hello,
I have zuul1 gateway and it's acting as Oauth2 Client (@EnableOAuth2Sso)
I followed the "Spring Security OAuth Boot 2 Autoconfig" to get OAuth2 running
Then I moved from zuul1 gateway @EnableZuulProxy to new spring-cloud-starter-gateway (websocket support in new GW) but due to missing OAuth2 integration I am not able to get routing in the spring cloud gateway running (downstream micro service not found)!

Components: (based on spring boot v. 1.5.9.RELEASE, cloud v. Edgware.RELEASE)

  • cloud config server
  • eureka registry
  • oauth 2 security:
    • cloud foundry UAA - Auth server
    • gateway as oauth2 client (OAuth2Sso)
    • downstream microservices - as oauth2 Resource Servers

Any idea how to migrate the gateway to spring boot 2.0.0?
Plan for oauth2 integration in new spring cloud gateway?

@rworsnop
Copy link

The problem is that Spring Security hasn't completely caught up with the new reactive stuff that the gateway uses. Apparently resource servers will be supported mid-2018: https://spring.io/blog/2018/01/30/next-generation-oauth-2-0-support-with-spring-security

@funnyleolee
Copy link

need this also.how is the progress.

@spencergibb
Copy link
Member

There has been no progress

1 similar comment
@jarvisqi
Copy link

There has been no progress

@rworsnop
Copy link

@spencergibb Is there any chance of a rough timeline for this? I'm sure I'm not the only wondering if it's worth waiting, or if we should migrate to the gateway now and put in a temporary homegrown solution to have it behave like a resource server.

@andye2004
Copy link

andye2004 commented Jul 19, 2018

@rworsnop and anyone else looking, this is actually pretty easy to do yourself in the interim. I'm posting here to help others as I spent the best part of three days trying to work it out. Also, would be good to know from @spencergibb if he can foresee any problems with this approach.

We were implementing a first time gateway and didn't really want to implement a Zuul solution given the SCG was available BUT we also wanted to be able to use token authentication on the SCG and this what we came up with.

I can't post full code samples unfortunately but what I can post should be enough to get people started.

The way the Spring Cloud Gateway works is similar to the normal web security flow in that filters are used for pretty much everything :) so you're going to need one of those. In fact you'll need an AuthenticationWebFilter and this class requires as a minimum a 'Converter', which is actually an implementation of Function<ServerWebExchange, Mono<Authentication>>, and an implementation of a ReactiveAuthenticationManager.

The 'Converter' is responsible for extracting the bearer token from the HTTP Authorization header. This is returned to the filter which then callsReactiveAuthenticationManager.authenticate() to have the authentication done. Assuming the token authenticates OK you're all done. If it doesn't you'll either need to implement a ServerAuthenticationFailureHandler or allow a 401 to be returned.

So, on to the code....

@Component
public class OAuthTokenConverter implements Function<ServerWebExchange, Mono<Authentication>> {

    private static final String BEARER = "bearer ";

    @Override
    public Mono<Authentication> apply(ServerWebExchange exchange) {
        String token = extractToken(exchange.getRequest());
        if (token != null) {
            return Mono.just(new PreAuthenticatedAuthenticationToken(token, ""));
        }
        return Mono.empty();
    }

    private String extractToken(ServerHttpRequest request) {
        String token = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
        if (StringUtils.isBlank(token) || !token.toLowerCase().startsWith(BEARER)) {
            return null;
        }
        return token.substring(BEARER.length());
    }
    
}

The OAuthTokenConverter class is pretty self explanatory, it extracts the bearer token, if it exists, and returns it as a Mono<Authentication> implemented using a PreAuthenticatedAuthenticationToken framework class. If there is no bearer token then it simply returns Mono.empty(). I believe it's possible that the token could also be presented as a query parameter? If so then this class would also need to check the query params as well. This is not the case for us so we only check the headers.

@Component
public class OAuth2AuthenticationManagerAdapter implements ReactiveAuthenticationManager {

    private final AuthenticationManager authenticationManager;

    @Autowired
    public OAuth2AuthenticationManagerAdapter(ResourceServerTokenServices tokenServices) {
        this.authenticationManager = oauthManager(tokenServices);
    }

    public Mono<Authentication> authenticate(Authentication token) {
        return Mono.just(token).publishOn(Schedulers.elastic()).flatMap(t -> {
            try {
                return Mono.just(this.authenticationManager.authenticate(t));
            } catch (Exception x) {
                return Mono.error(new BadCredentialsException("Invalid or expired access token presented"));
            }
        }).filter(Authentication::isAuthenticated);
    }

    private AuthenticationManager oauthManager(ResourceServerTokenServices tokenServices) {
        OAuth2AuthenticationManager oauthAuthenticationManager = new OAuth2AuthenticationManager();
        oauthAuthenticationManager.setResourceId("");
        oauthAuthenticationManager.setTokenServices(tokenServices);
        return oauthAuthenticationManager;
    }

}

The OAuth2AuthenticationManagerAdapter class is inspired by the framework class ReactiveAuthenticationManagerAdapter and this class effectively does the authentication. What we actually do here is wrap the Spring OAuth2AuthenticationManager class which the authenticator injected into the oauth filter generated by the @EnableResourceServer annotation. Because of this, a lot of the config that can be used with a normal resource server can be used here.

We then simply delegate the authenticate call to OAuth2AuthenticationManager.authenticate(). We also have to catch and wrap any exceptions that are thrown by this call and for your purposes returning Mono.error(x) may be sufficient (it can result in a 500 being returned which does carry an appropriate error message in the body) however we want to force a 401 and that means returning a Mono.error(BadCredentialsException).

@Configuration
public class OAuth2Config {

    @Bean("oauthAuthenticationWebFilter")
    AuthenticationWebFilter oauthAuthenticationWebFilter(
            OAuth2AuthenticationManagerAdapter authManager, OAuthTokenConverter tokenConverter) {

        AuthenticationWebFilter filter = new AuthenticationWebFilter(authManager);
        filter.setAuthenticationConverter(tokenConverter);
        return filter;
    }

    @Bean
    ResourceServerTokenServices tokenServices(TokenStore tokenStore) {
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        tokenServices.setSupportRefreshToken(false);
        return tokenServices;
    }

}

We just need to wire these things together now and OAuth2Config does exactly that for us. It just instantiates a new AuthenticationWebFilter using the wrapped OAuth2AuthenticationManager and sets the converter. Once we have this all that is required is to insert it into the security filter chain and we do this using the code below, we are effectively taking the place of the HTTP Basic Auth filter as we have disabled that completely, you're requirements may be different.

@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
    ...
    /**
     * The configuration defined here is what really drives the edge service.
     * Any request that doesn't get handled by the other two configs will be
     * handled by this one. All requests coming through here must have a
     * valid access token
     */
    @Bean
    SecurityWebFilterChain oauthTokenAuthConfig(
            ServerHttpSecurity security, AuthenticationWebFilter oauthAuthenticationWebFilter) {

        return security
                .csrf().disable()
                .logout().disable()
                .httpBasic().disable()
                .formLogin().disable()
                .exceptionHandling().and()
                .securityMatcher(notMatches("/unsecuredRoutes/**"))
                .addFilterAt(oauthAuthenticationWebFilter, SecurityWebFiltersOrder.HTTP_BASIC)
                .authorizeExchange().anyExchange().authenticated()
                .and().build();
    }

    private ServerWebExchangeMatcher matches(String ... routes) {
        return ServerWebExchangeMatchers.pathMatchers(routes);
    }

    private ServerWebExchangeMatcher notMatches(String ... routes) {
        return new NegatedServerWebExchangeMatcher(matches(routes));
    }
    ...
}

One final thing worth mentioning here is the NegatedServerWebExchangeMatcher class, this is actually not available in the current version of the SCG but if you have a look in Spring Security 5.1 you can find it there, all we have done is copy it over until the SCG is updated with Security 5.1 or later.

I really hope this can help others get over the hurdle until the Spring guys can get this implemented properly in the SCG. I'm sure whatever they come up with will be a lot more elegant than this solution but at least this works in the interim.

I'd really appreciate any comments that anyone may have, good, bad or otherwise - oh, and apologies for the long post!

@rworsnop
Copy link

Thanks for taking the time to write this, @andye2004 . It looks really useful.

@Alos
Copy link

Alos commented Aug 7, 2018

@andye2004 What does your POM look like?

@andye2004
Copy link

@Alos, we're currently running SB 2.0.3 and we use our own top level parent POM so we can over-ride some of the default versions of things like lombok that come with SB and also to ensure all of our services have specific things included by default (cloud config, common banners, discovery-client, tracing etc). I can't post our POMs as is so I've quickly pieced together what you will probably need to make the above work. If something doesn't work post back and I can probably modify this as necessary.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <!-- Standard Maven properties we use in every project -->
        <java.version>1.8</java.version>
        <maven.compiler.source>${java.version}</maven.compiler.source>
        <maven.compiler.target>${java.version}</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

        <spring-cloud.version>Finchley.RELEASE</spring-cloud.version>
        <spring-security-jwt.version>1.0.9.RELEASE</spring-security-jwt.version>
        <spring-security-oauth2.version>2.0.14.RELEASE</spring-security-oauth2.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security.oauth</groupId>
            <artifactId>spring-security-oauth2</artifactId>
            <version>${spring-security-oauth2.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-jwt</artifactId>
            <version>${spring-security-jwt.version}</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>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            ....
            <plugin>
                <!-- Builds executable Springboot fatjars -->
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <executable>true</executable>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>build-info</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            ....
        </plugins>
    </build>

</project>

@nearzk
Copy link

nearzk commented Aug 24, 2018

@spencergibb Hi, we found that the spring cloud gateway already has the latest version (Greenwich.BUILD-SNAPSHOT), is it already able to support OAuth2 integration?

@spencergibb
Copy link
Member

No

@chsi13
Copy link

chsi13 commented Aug 24, 2018

Does the problem coming from scg directly or it is about spring security only?

@minisha
Copy link

minisha commented Sep 25, 2018

@andye2004 I am trying to validate the token from a url like @Resourceserver does, looks like the skeleton you gave is validating the token against a inMemory store.

How to configure the url to validate the token? Any pointers?

@andye2004
Copy link

@minisha are you using a 3rd party OAuth provider? If not, what I've shown here is probably the most performant way to do it, e.g. use the same ResourceServerTokenServices, remember JWT tokens are not stored in a DB by default so as long as the same key is used in your OAuth2 server then it doesn't really make much sense to add in another HTTP hop to validate the token.

If you are using a 3rd party OAuth provider then you would need to write your own implementation of 'ResourceServerTokenServices' to actually make the call out to the 3rd party to validate the token. A look at the Spring code base should give a really good head start on how to do this.

@minisha
Copy link

minisha commented Sep 25, 2018

@andye2004 Yes, I am using a third party service. In spring boot app the below configuration will make sure the signature of the token is correct by fetching the key from the given url and validating I suppose.
I am trying to get the same behavior.

security:
oauth2:
resource:
jwt:
key-uri: https://xxxx/oauth/token_key

@minisha
Copy link

minisha commented Sep 25, 2018

@andye2004 Your code expect the token to be store inmemory after authentication isnt? Then in the subsequent request, the incoming token is validated against the token inMemory? Did I get this correctly?

@andye2004
Copy link

andye2004 commented Sep 25, 2018

@minisha, the code your looking at in my post is a very simplified version of our actual codebase and could be easily adapted to fetch the key from a 3rd party OAuth provider at start-up then store it in memory. I believe there is an example on the JHipster (by Matt Raible) website that explains exactly how to do this. Sorry, I don't have time at the moment to go looking for it.

Apologies, it wasn't Matt Raible, nor JHipster. It was Okta and Brian Demers, see here https://developer.okta.com/blog/2018/04/02/client-creds-with-spring-boot, they have a link to Spring boot starter that might also help you out.

@lllllpylllll
Copy link

Spring Security 5.1 support WebFlux- OAuth2.

https://docs.spring.io/spring-security/site/docs/5.1.0.RELEASE/reference/htmlsingle/#new

Has anyone tested it with Spring Cloud Gateway?

@tatharoy
Copy link

tatharoy commented Oct 9, 2018

Spring Security 5.1 support WebFlux- OAuth2.

https://docs.spring.io/spring-security/site/docs/5.1.0.RELEASE/reference/htmlsingle/#new

Has anyone tested it with Spring Cloud Gateway?

I have tried enabling this faced a lot of compatibility issues. have raised this as part of issue #585

@rwinch
Copy link

rwinch commented Oct 23, 2018

Here is an example of it working using milestones https://github.com/rwinch/spring-security51-by-example-reactive

@amitpokhrel
Copy link

amitpokhrel commented Dec 21, 2018

@andye2004 I have used your code to make it work. However I am struggling with configuring CORS with this configuration. I have tried all ways mentioned in the documentation for CORS configuration but it is not working. (I am using spring security starter 2.0.0)

@ghost
Copy link

ghost commented Feb 28, 2019

Hello all,
Spring boot 1.x EOL is near: https://spring.io/blog/2018/07/30/spring-boot-1-x-eol-aug-1st-2019.
Is there a chance that this issue will not be solved until then?

This issue is stopping us from migrating to Spring boot 2 so I would need to find another solution if Spring cloud gateway will not support OAuth2.

@making
Copy link
Contributor

making commented Feb 28, 2019

@bassmake OAuth2 Integration exists in Spring Cloud Security. Did you check it?

https://cloud.spring.io/spring-cloud-security/single/spring-cloud-security.html#_client_token_relay_in_spring_cloud_gateway
https://github.com/spring-cloud-samples/sample-gateway-oauth2login

@ghost
Copy link

ghost commented Feb 28, 2019

@making
thanks, but I wasn't clear enough: I would like to use cloud-gateway as a resource server, so not only forwarding tokens but validating them as well, which is not described in provided link.

@making
Copy link
Contributor

making commented Feb 28, 2019

@bassmake in the sample above, resource sever validates JWT.
https://github.com/spring-cloud-samples/sample-gateway-oauth2login/blob/master/resource-server/src/main/java/sample/SecurityConfig.java#L33
Doesn’t this work for you?

@andye2004
Copy link

@bassmake I put a (rather lengthy) post here that explains how to do pretty much what you are looking for. It looks more daunting than it actually is, take the time to understand what it is doing and you'll see it's actually quite simple.

If you are using a 3rd party OAuth2 provider you would need to call their endpoint to get the JWKS in order to authenticate the token. That's about the only bit missing from that example.

@andye2004
Copy link

@bassmake in the sample above, resource sever validates JWT.
https://github.com/spring-cloud-samples/sample-gateway-oauth2login/blob/master/resource-server/src/main/java/sample/SecurityConfig.java#L33
Doesn’t this work for you?

I didn't even notice that this was now in there. Need to look a bit deeper now.....

@andye2004
Copy link

@amitpokhrel

@andye2004 I have used your code to make it work. However I am struggling with configuring CORS with this configuration. I have tried all ways mentioned in the documentation for CORS configuration but it is not working. (I am using spring security starter 2.0.0)

Apologies Amit, I've been away from work for a few months and I have only just seen this. Did you resolve your problem?

@ghost
Copy link

ghost commented Feb 28, 2019

@andye2004
thanks, I saw your comment but I would like to see some "official" statement to this issue.

@ryanjbaxter
Copy link
Contributor

@bassmake I think your issue is different than what this issue was originally describing (which is the gateway acting as an oauth2 client). You want the gateway to validate tokens. I guess I dont actually understand what is blocking you from doing that today.

@ghost
Copy link

ghost commented Feb 28, 2019

this is the first comment:

Hello guys,

I have gateway which is written with zuul1 and it's actually @ ResourceServer in terms of oauth2. Do you know if cloud api gateway has an integration with oauth2?

so maybe this issue could be closed and create more specific one if this is already possible.

@ryanjbaxter
Copy link
Contributor

@ghost
Copy link

ghost commented Mar 6, 2019

thank you @ryanjbaxter,
I will have definitely have a look.

spencergibb pushed a commit that referenced this issue Mar 7, 2019
We have been using `WebClientHttpRoutingFilter` configured with a `WebClient` that relays OAuth2 access tokens (including refreshing tokens), making use of [`ServerOAuth2AuthorizedClientExchangeFilterFunction`](https://github.com/spring-projects/spring-security/blob/5.1.4.RELEASE/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunction.java#L93).

This has worked well for us (also cf. #179 and [#171 from spring-cloud-security](spring-attic/spring-cloud-security#171)).
@ghost
Copy link

ghost commented Mar 8, 2019

@ryanjbaxter provided link works for my case, thanks again

@chrisovergaauw
Copy link

@bassmake OAuth2 Integration exists in Spring Cloud Security. Did you check it?

https://cloud.spring.io/spring-cloud-security/single/spring-cloud-security.html#_client_token_relay_in_spring_cloud_gateway
https://github.com/spring-cloud-samples/sample-gateway-oauth2login

Can this example somehow be used in conjunction with cloud.gateway.discovery.locator.enabled = true?

@ryanjbaxter
Copy link
Contributor

@chrisovergaauw I dont see why not, you are just changing how the routes are created.

@chrisovergaauw
Copy link

@ryanjbaxter thanks for responding so quickly.
I think a better way to phrase my question is: How to retain the default predicate path and rewrite when using fluent java routes? (or how to apply that filter using the yaml configuration)

I do not intend to abuse the issue tracker though. What channel is best used for questions regarding SCG? Stackoverflow maybe?

@ryanjbaxter
Copy link
Contributor

Stackoverflow or Gitter

@vicykie

This comment has been minimized.

@spencergibb
Copy link
Member

I believe that this is resolved, closing

@peter-kori
Copy link

@spencergibb according to https://github.com/spring-projects/spring-security/wiki/OAuth-2.0-Features-Matrix
I don't see that OAuth2 Client SSO (@EnableOAuth2Sso) - see Column "Spring Security OAuth (2.2+)"

I have zuul1 gateway and it's acting as OAuth2 Client SSO (using @EnableOAuth2Sso)
What is the status of SSO support for OAuth2 Client in new spring security?

@ryanjbaxter
Copy link
Contributor

@HopscotchSen
Copy link

@ryanjbaxter
Do you think the API Gateway should be integrated with oauth2? The gateway integrates oauth2 as a resource service and is responsible for forwarding tokens and authentication tokens. Or don't integrate oauth2 just forward tokens

@spencergibb
Copy link
Member

Since gateway is just a webflux application, any other integration would be via spring security

@yuanyifeng
Copy link

I would like to use spring-cloud-gateway now, how can I refer to oauth2 to the project, I want the interface to access the gateway for forwarding, and then authenticate, this is done at the gateway.

@ryanjbaxter
Copy link
Contributor

#179 (comment)

@ChrisLeejing
Copy link

ChrisLeejing commented Mar 4, 2020

I would like to use spring-cloud-gateway now, how can I refer to oauth2 to the project, I want the interface to access the gateway for forwarding, and then authenticate, this is done at the gateway.

Hello, has it got out ? I am awaiting.

@ghost
Copy link

ghost commented Jul 29, 2021

Is this issue resolved? Wondering..

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

No branches or pull requests