microsoft / azure-spring-boot

Spring Boot Starters for Azure services
MIT License
374 stars 460 forks source link

Why we are getting I/O error the JWK Set source login.microsoftonline.com when applying Proxy? #958

Closed acarlstein closed 1 year ago

acarlstein commented 1 year ago

First, thank you for taking the time to read over this issue. Hopefully someone will have an answer that could help us. If by any chance, this issue is located in the wrong repository, please help us by pointing to the right one. Thank you.

Summary

We are trying to have our Spring Boot project to connect with OAuth2 with Microsoft, via a proxy, when connected to our VPN.

Previously, we have successfully implemented OAuth 2.0 security in our Spring Boot project via the VPN until the proxy was put in effect.

Using httping, we can confirm that we can reach login.microsoftonline.com, when connected:

httping -x <out-proxy-url-goes-here>:83 -g login.microsoftonline.com 

However, when we modify the code in SecurityConfig.kt, to use the proxy, we get the message:

An I/O error occurred while reading from the JWK Set source: login.microsoftonline.com"

We tried to enable the debug @EnableWebSecurity(debug=true) plus other configuration that were supposed to give us more debug information but nothing extra shows up that can point us to the right direction.

We tried many other "solutions" we found online, but we keep getting the same error. Plus, we tried adding breakpoints and going throughout the flow but after quite a while, it didn't got us further either.

Any help would be appreciated.

Description

In the application-default.yml, we have:

security:
  oauth2:
    resource:
      jwk:
        key-set-uri: https://login.microsoftonline.com/common/discovery/v2.0/keys
      id: <our-id-goes-here>

Our current code, after trying so many different ideas we found online looks as following:

package com.connectingtooauth.config

import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Configuration
import org.springframework.http.client.SimpleClientHttpRequestFactory
import org.springframework.security.config.Customizer
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.EnableWebSecurity
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer
import org.springframework.security.oauth2.jwt.*
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter
import org.springframework.web.client.RestTemplate
import java.net.InetSocketAddress
import java.net.Proxy

@Configuration
@EnableResourceServer
@EnableWebSecurity(debug=true)
@EnableGlobalMethodSecurity(prePostEnabled = true)
class SecurityConfig : ResourceServerConfigurerAdapter() {

    // We also tried to set static values here instead of using the @Value annotation

    @Value("\${security.oauth2.resource.jwk.key-set-uri}")
    lateinit var jwkSetUri: String

    @Value("\${security.oauth2.resource.id}")
    lateinit var resourceId: String

    private var host = "<hardcoded-our-proxy-here>"
    private var port = 88

    @Throws(Exception::class)
    override fun configure(http: HttpSecurity) {
        http
            .authorizeRequests(Customizer { authorize: ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry ->
                authorize
                    .antMatchers("/latestChargeSessions").authenticated()
                    .antMatchers("/v3/api-docs/**", "/swagger-resources/**", "/swagger-ui/**")
                    .permitAll()
            })
            .oauth2ResourceServer { oauth2: OAuth2ResourceServerConfigurer<HttpSecurity?> ->
                oauth2
                    .jwt(Customizer { jwt: OAuth2ResourceServerConfigurer<HttpSecurity?>.JwtConfigurer ->
                        jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())
                            .decoder(jwtDecoder())                           
                    })
            }
    }

    fun jwtDecoder(): JwtDecoder? {
        val proxy = Proxy(Proxy.Type.HTTP, InetSocketAddress(host, port))
        val requestFactory = SimpleClientHttpRequestFactory()
        requestFactory.setProxy(proxy)
        return NimbusJwtDecoder
            .withJwkSetUri(jwkSetUri)
            .restOperations(RestTemplate(requestFactory)).build()
    }

    @Throws(Exception::class)
    override fun configure(resources: ResourceServerSecurityConfigurer) {
        resources.resourceId(resourceId)
    }

    private fun jwtAuthenticationConverter(): JwtAuthenticationConverter {
        val grantedAuthoritiesConverter = JwtGrantedAuthoritiesConverter()
        grantedAuthoritiesConverter.setAuthoritiesClaimName("roles")
        grantedAuthoritiesConverter.setAuthorityPrefix("")
        val jwtAuthenticationConverter = JwtAuthenticationConverter()
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter)
        return jwtAuthenticationConverter
    }

}
acarlstein commented 1 year ago

We tried also variations such as:

 JwtDecoder customDecoder() {
            SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
            Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyPort));
            requestFactory.setProxy(proxy);
            RestOperations restOperations = new RestTemplate(requestFactory);
            String jwtKeysUri = issuerUri.concat("/discovery/keys/");
            final NimbusJwtDecoder tokenDecoder = NimbusJwtDecoder
                    .withJwkSetUri(jwtKeysUri)
                    .restOperations(restOperations)
                    .build();
            tokenDecoder.setJwtValidator(getJwtValidator());
            return tokenDecoder;
        }

         OAuth2TokenValidator<Jwt> getJwtValidator() {
            OAuth2TokenValidator<Jwt> audienceValidator = audienceValidator();
            OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuerUri);
            return new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);
        }

        OAuth2TokenValidator<Jwt> audienceValidator() {
            return new JwtClaimValidator<List<String>>(AUD, aud -> aud.contains(audience));
        }

Note: due security reasons, I can't provide the data that forms the the audience used in the code above, nor the value for spring.security.oauth2.resourceserver.jwt.issuer-uri.

acarlstein commented 1 year ago

Here is an additional log when we try to hit one of our endpoints:

 o.s.security.web.FilterChainProxy        : Securing GET /endpoint?id=1XXXXXXXXXXX3
 s.s.w.c.SecurityContextPersistenceFilter : Set SecurityContextHolder to empty SecurityContext
 p.a.OAuth2AuthenticationProcessingFilter : Authentication request failed: error="server_error", error_description="An I/O error occurred while reading from the JWK Set source: login.microsoftonline.com"
 s.s.o.p.e.DefaultOAuth2ExceptionRenderer : Written [error="server_error", error_description="An I/O error occurred while reading from the JWK Set source: login.microsoftonline.com"] as "application/json" using [org.springframework.http.converter.json.MappingJackson2HttpMessageConverter@f2a0360]
 s.s.w.c.SecurityContextPersistenceFilter : Cleared SecurityContextHolder to complete request
acarlstein commented 1 year ago

We also tried to add .csrf().disable()

      http.csrf().disable()
            .authorizeRequests(Customizer { authorize: ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry ->
                authorize
                    .antMatchers("/latestChargeSessions").authenticated()
                    .antMatchers("/v3/api-docs/**", "/swagger-resources/**", "/swagger-ui/**")
                    .permitAll()
            })
            .oauth2ResourceServer { oauth2: OAuth2ResourceServerConfigurer<HttpSecurity?> ->
                oauth2
                    .jwt(Customizer { jwt: OAuth2ResourceServerConfigurer<HttpSecurity?>.JwtConfigurer ->
                        jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())
                            .decoder(jwtDecoder())                           
                    })
            }
acarlstein commented 1 year ago

We found the solution. We needed to provide the proxy information via the VM Options such as:

-Dspring.profiles.active=dev 
-Dhttp.proxyHost=our-proxy.com
-Dhttp.proxyPort=88 
-Dhttp.nonProxyHosts=**.our-proxy.com|localhost* 

Now, we are trying to see if we can set those programmatically so we don't have to pass them as a VM Option.