spring-cloud / spring-cloud-gateway

An API Gateway built on Spring Framework and Spring Boot providing routing and more.
http://cloud.spring.io
Apache License 2.0
4.51k stars 3.31k forks source link

Conditionally skip TokenRelayGatewayFilterFactory authentication flow #3438

Closed HJK181 closed 3 days ago

HJK181 commented 3 months ago

Enhancement

I'm running an Expo React Native application where I use expo-auth-session to authenticate the mobile version of the app with an OIDC Provider - Keycloak in my case. The mobile app stores the received tokens in the secure storage on the mobile and sends the accessToken as part of the Authorization header on every request. Every request goes to Spring Cloud Gateway which should route the request to the appropriate backend service.

However, Expo also supports accessing the application via the Web. In this case, where no secure method exists to store tokens, I don't want to call the OIDC Provider from the web application but instead rely on Spring Cloud Gateway as an Oauth2 client. The gateway should redirect the user to the OIDC Provider's login page. That's why I configured the Gateway in this way:

application.yml

server:
  port: 8080

spring:
  cloud:
    gateway:
      default-filters:
        - name: TokenRelay
      routes:
      ...
      - id: ui
          uri: http://localhost:8081 # Expo app
          predicates:
            - Path=/**
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: http://localhost:9080/keycloak/realms/test
      client:
        registration:
          gateway:
            provider: keycloak
            client-id: test
            scope: openid, profile, offline_access
            client-secret: 123
            authorization-grant-type: authorization_code
            redirect-uri: "{baseUrl}/login/oauth2/code/gateway"
        provider:
          keycloak:
            issuer-uri: http://localhost:9080/keycloak/realms/test

SecurityConfiguration

@Configuration
@EnableWebFluxSecurity
public class SecurityConfiguration {

    @Bean
    public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
        http.authorizeExchange(auth -> auth.anyExchange().authenticated())
                .oauth2Login(Customizer.withDefaults())
                .oauth2ResourceServer((oauth2) -> oauth2.jwt(Customizer.withDefaults()));
        http.csrf(ServerHttpSecurity.CsrfSpec::disable);
        return http.build();
    }
}

Actual state:

When accessing the mobile app via localhost:8080, Spring Cloud Gateway redirects users to Keycloak's login page. After I successfully logged in to Keycloak, I'm present with the login screen of my mobile application, where I need to login once again - because I'm using a different client than on the Gateway - which is a bad user experience.

Expected behavior:

I need a configuration possibility on the TokenRelayGatewayFilterFactory to conditionally skip the authentication flow. If the request comes from the mobile app, the flow must not be triggered, if it comes from the web app, it must be triggered. Sadly I can't configure pathMatches to permitAll access to the resources of the app, as both mobile and app share the same code base. So the only thing I can think of is to add some header to the request, which the Gateway could use to decide whether to trigger the flow or not.

spencergibb commented 3 days ago

Maybe @sjohnr could comment?

sjohnr commented 3 days ago

@HJK181 it seems that you are asking a question. I'm not sure whether the Spring Cloud team typically answers questions via GitHub issues, but this question is actually Spring Security related, and this is a question that would be better suited to Stack Overflow. We prefer to use GitHub issues only for bugs and enhancements.

Having said that, I will comment on your last paragraph to try and give you some pointers in the right direction. However, I don't intend to fully answer your questions which would be best suited for Stack Overflow.

I need a configuration possibility on the TokenRelayGatewayFilterFactory to conditionally skip the authentication flow.

The gateway and specifically the token relay filter is not what's triggering the authentication flow. You are protecting the application with Spring Security. When an anonymous user accesses the application, the login flow with the provider (keycloak) is triggered via Spring Security's ServerAuthenticationEntryPoint which you can read about in Spring Security's reference documentation.

If the request comes from the mobile app, the flow must not be triggered, if it comes from the web app, it must be triggered.

This comes down to how you set up and design the security for this application. As it stands, you seem to have a single SecurityWebFilterChain bean attempting to provide two different types of protection. One using sessions and OAuth2, and another using bearer tokens. Each flow has completely different characteristics and behavior. Therefore, you would want to separate them into multiple filter chains.

However, my recommendation would be to make your mobile app work like your web app and use sessions, but I'm guessing you have strongly decided against doing so for your own reasons.

See above comment.

So the only thing I can think of is to add some header to the request, which the Gateway could use to decide whether to trigger the flow or not.

There are going to be many ways to split your configuration into two filter chains so you can provide different experiences for mobile and web. One would be to have two deployments and use Spring @Profiles to provide two different configurations. Another would be to match on the User-Agent header or similar.

Sadly I can't configure pathMatches to permitAll access to the resources of the app, as both mobile and app share the same code base.

Yet another would be to simply match on the presence of a Bearer token in the Authorization header. In this scenario, you would need another filter chain (ordered earliest in the chain) just for anonymous resources that should be presented without authentication for users of both mobile and web platforms.

In summary, this does not feel to me like a Spring Cloud Gateway issue and is simply a question of setting up your application to meet your requirements. Hopefully this is helpful. I think this issue should be closed as answered. If you have any further questions, please consider asking on stack overflow and update this issue with a link to the re-posted question (so that other people can find it), and I'll be happy to take a look.