ch4mpy / spring-addons

Ease spring OAuth2 resource-servers configuration and testing
Apache License 2.0
521 stars 84 forks source link

Handle CORS Requests with Keycloak's "allowed-origins" claim like the keycloak adapter (now deprecated) #202

Closed ulk200 closed 1 month ago

ulk200 commented 4 months ago

I would like to mimic the behaviour of the adapter that was maintained by the keycloak team, but that is now deprecated. In class org.keycloak.adapters.AuthenticatedActionsHandler they used to read the allowed-origins claim and validate the Origin header of the HTTP request.

This claim is populated with the urls that are configured on the client, in the Admin Console : Clients -> #the client# -> Settings -> Web Origins

A nice feature is that when we set this form field with a plus sign + every valid redirect URI is also a valid web-origin that is copied in the token.

It seems that configuring cors with spring's CorsConfigurationSource is not dynamic enough to read from the token with each request. What would be the cleanest way of doing it ?

Thanks

ch4mpy commented 4 months ago

Hi @ulk200 and thanks for reaching out.

allowed-origins is a private claim. If other authorization servers were providing with equivalents, it would probably in other claims. In such a case, it could be worth to implement something similar to how authorities mapping is done (expose some configuration property accepting a JSON path to allowed origins claim). But I don't know any other provider exposing the origins it allows in claims, reason why I'm not quite inclined to add such a feature to spring-addons-starter-oidc.

However, your use-case is interesting and I'll try to put together the Java configuration to add (might take a few days to find the time for that).

ulk200 commented 4 months ago

Yes a code sample or a tutorial would be enough since this is not standard. Your project always comes along when searching for a replacement for the adapters that the Keycloak team used to maintain, so i think it's an ideal place to find everything that can permit to fully replace it. Thank you

ch4mpy commented 1 month ago

@ulk200 I finally have a few holidays and had time to get a closer look. You could try to register a CorsFilter like that:

@Bean
CorsFilter corsFilter(JwtDecoder jwtDecoder) {
    return new CorsFilter(new CustomCorsConfigurationSource(jwtDecoder));
}

public static class CustomCorsConfigurationSource implements CorsConfigurationSource {

    private static final String ALLOWED_ORIGINS_CLAIM = "allowed-origins";

    private final JwtDecoder jwtDecoder;
    private final UrlBasedCorsConfigurationSource defaultConfigSource;

    public CustomCorsConfigurationSource(JwtDecoder jwtDecoder) {
        this.jwtDecoder = jwtDecoder;
        this.defaultConfigSource = defaultConfigSource();
    }

    @Override
    @Nullable
    public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
        final var delegate = getAllowedOrigins(request).map(allowedOrigins -> {
            final var configSource = defaultConfigSource();
            final var configs = configSource.getCorsConfigurations();
            configs.forEach((k, v) -> {
                v.setAllowedOrigins(allowedOrigins);
            });
            configSource.setCorsConfigurations(configs);
            return configSource;
        }).orElse(this.defaultConfigSource);

        return delegate.getCorsConfiguration(request);
    }

    private Optional<String> getBearer(HttpServletRequest request) {
        return Optional
            .ofNullable(request.getHeader(HttpHeaders.AUTHORIZATION))
            .filter(h -> h.toLowerCase().startsWith("bearer "))
            .map(h -> h.substring(7))
            .filter(StringUtils::hasText);
    }

    private Optional<Jwt> getJwt(HttpServletRequest request) {
        final var bearer = getBearer(request);
        return bearer.map(jwtDecoder::decode);
    }

    private Optional<List<String>> getAllowedOrigins(HttpServletRequest request) {
        final var jwt = getJwt(request);
        return jwt.map(token -> token.getClaimAsStringList(ALLOWED_ORIGINS_CLAIM));
    }

    private static UrlBasedCorsConfigurationSource defaultConfigSource() {
        final var configSource = new UrlBasedCorsConfigurationSource();
        final var configuration = new CorsConfiguration();
        configuration.setAllowedMethods(List.of("*"));
        // insert here a default value for allowed origins
        // Also update the path matcher below if you don't want the config to apply to all your resources
        configSource.registerCorsConfiguration("/**", configuration);
        return configSource;
    }
}

For now, spring-addons CORS configuration backs off only if cors properties are left empty (it should soon be improved to check that no CorsFilter bean is registered). So check that your application.properties or application.yaml does not contain cors configuration for spring-addons.