Multiple resource server oauth2 clients? Spring OA

2019-04-08 04:13发布

问题:

Good day,

I have setup a working example implementing SSO & the API Gateway pattern (similar to what is described here https://spring.io/guides/tutorials/spring-security-and-angular-js/#_the_api_gateway_pattern_angular_js_and_spring_security_part_iv).

The system consists of separate server components: AUTH-SERVER, API-GATEWAY, SERVICE-DISCOVERY, RESOURCE/UI SERVER.

At the API-GATEWAY (implemented with Spring Boot @EnableZuulProxy @EnableOAuth2Sso) I have configured multiple OAuth providers, including my own OAuth server using JWT:

security:
  oauth2:
    client:
      accessTokenUri: http://localhost:9999/uaa/oauth/token
      userAuthorizationUri: http://localhost:9999/uaa/oauth/authorize
      clientId: acme
      clientSecret: acmesecret
      redirectUri: http://localhost:9000/login
  resource:
    jwt:
      key-value: |
      -----BEGIN PUBLIC KEY-----
      ...public-key...
      -----END PUBLIC KEY-----

facebook:
  client:
    clientId: 233668646673605
    clientSecret: 33b17e044ee6a4fa383f46ec6e28ea1d
    accessTokenUri: https://graph.facebook.com/oauth/access_token
    userAuthorizationUri: https://www.facebook.com/dialog/oauth
    tokenName: oauth_token
    authenticationScheme: query
    clientAuthenticationScheme: form
    redirectUri: http://localhost:8080
  resource:
    userInfoUri: https://graph.facebook.com/me

github:
  client:
    clientId: bd1c0a783ccdd1c9b9e4
    clientSecret: 1a9030fbca47a5b2c28e92f19050bb77824b5ad1
    accessTokenUri: https://github.com/login/oauth/access_token
    userAuthorizationUri: https://github.com/login/oauth/authorize
    clientAuthenticationScheme: form
  resource:
    userInfoUri: https://api.github.com/user

google:
  client:
    clientId: 1091750269931-152sv64o8a0vd5hg8v2lp92qd2d4i00r.apps.googleusercontent.com
    clientSecret: n4I4MRNLKMdv603SU95Ic9lJ
    accessTokenUri: https://www.googleapis.com/oauth2/v3/token
    userAuthorizationUri: https://accounts.google.com/o/oauth2/auth
    authenticationScheme: query
    redirectUri: http://localhost:9000/login/google
    scope:
      - email
      - profile
  resource:
    userInfoUri: https://www.googleapis.com/oauth2/v2/userinfo

The Java Config:

package com.devdream.cloud.apigateway;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso;
import org.springframework.boot.autoconfigure.security.oauth2.resource.UserInfoRestTemplateCustomizer;
import org.springframework.boot.autoconfigure.security.oauth2.resource.UserInfoTokenServices;
import org.springframework.boot.context.embedded.FilterRegistrationBean;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.context.annotation.Bean;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.oauth2.client.OAuth2ClientContext;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.client.filter.OAuth2ClientAuthenticationProcessingFilter;
import org.springframework.security.oauth2.client.filter.OAuth2ClientContextFilter;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.csrf.CsrfFilter;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.csrf.CsrfTokenRepository;
import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.CompositeFilter;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.WebUtils;

@SpringBootApplication
@EnableZuulProxy
@EnableOAuth2Sso
public class APIGatewayApplication extends WebSecurityConfigurerAdapter {

    public static void main(String[] args) {
        SpringApplication.run(APIGatewayApplication.class, args);
    }

    @Autowired
    OAuth2ClientContext oauth2ClientContext;

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http//
        .logout()
                //
                .and()
                //
                .antMatcher("/**")
                //
                .authorizeRequests()
                //
                .antMatchers("/index.html", "/home.html", "/login", "/stomp/**")
                .permitAll()
                //
                .anyRequest()
                .authenticated()
                //
                .and()
                //
                .csrf()
                //
                .csrfTokenRepository(csrfTokenRepository()).and()
                .addFilterAfter(csrfHeaderFilter(), CsrfFilter.class).headers()
                .frameOptions().sameOrigin()//
                .and()//
                .addFilterBefore(ssoFilter(), BasicAuthenticationFilter.class);
    }

    private Filter csrfHeaderFilter() {
        return new OncePerRequestFilter() {
            @Override
            protected void doFilterInternal(HttpServletRequest request,
                    HttpServletResponse response, FilterChain filterChain)
                    throws ServletException, IOException {
                CsrfToken csrf = (CsrfToken) request
                        .getAttribute(CsrfToken.class.getName());
                if (csrf != null) {
                    Cookie cookie = WebUtils.getCookie(request, "XSRF-TOKEN");
                    String token = csrf.getToken();
                    if (cookie == null || token != null
                            && !token.equals(cookie.getValue())) {
                        cookie = new Cookie("XSRF-TOKEN", token);
                        cookie.setPath("/");
                        response.addCookie(cookie);
                    }
                }
                filterChain.doFilter(request, response);
            }
        };
    }

    private CsrfTokenRepository csrfTokenRepository() {
        HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();
        repository.setHeaderName("X-XSRF-TOKEN");
        repository.setParameterName("X-XSRF-TOKEN");
        return repository;

    }

    private Filter ssoFilter() {
        CompositeFilter filter = new CompositeFilter();
        List<Filter> filters = new ArrayList<>();
        filters.add(ssoFilter(facebook(), "/login/facebook"));
        filters.add(ssoFilter(github(), "/login/github"));
        filters.add(ssoFilter(google(), "/login/google"));
        filter.setFilters(filters);
        return filter;
    }

    private Filter ssoFilter(ClientResources client, String path) {
        OAuth2ClientAuthenticationProcessingFilter oAuth2Filter = new OAuth2ClientAuthenticationProcessingFilter(
                path);
        OAuth2RestTemplate oAuth2RestTemplate = new OAuth2RestTemplate(
                client.getClient(), oauth2ClientContext);
        oAuth2Filter.setRestTemplate(oAuth2RestTemplate);
        oAuth2Filter.setTokenServices(new UserInfoTokenServices(client
                .getResource().getUserInfoUri(), client.getClient()
                .getClientId()));
        return oAuth2Filter;
    }

    @Bean
    @ConfigurationProperties("github")
    ClientResources github() {
        return new ClientResources();
    }

    @Bean
    @ConfigurationProperties("facebook")
    ClientResources facebook() {
        return new ClientResources();
    }

    @Bean
    @ConfigurationProperties("google")
    ClientResources google() {
        return new ClientResources();
    }

    @Bean
    public FilterRegistrationBean oauth2ClientFilterRegistration(
            OAuth2ClientContextFilter filter) {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setFilter(filter);
        registration.setOrder(-100);
        return registration;
    }
}

When an unauthenticated request is sent to the gateway, the request is redirected the the AUTH-SERVER as expected, here I present options to sign in with my AUTH-SERVER as well as social options configured above by providing a link which essentially takes the user back the the API-GATEWAY to be intercepted by the associated OAuth filter paths as configured above. My AUTH-SERVER issuing JWT tokens works as expected, serving my resource data & ui but when I successfully auth with Google for example, I get the following response from the resource/ui server:

This XML file does not appear to have any style information associated with it. The document tree is shown below.
<oauth>
<error_description>Cannot convert access token to JSON</error_description>
<error>invalid_token</error>
</oauth>

I then realised this may be due to the Resource server's OAuth config?

security:
  oauth2:
    client:
      client-id: acme
      client-secret: acmesecret
    resource:
      jwt:
        key-value: |
          -----BEGIN PUBLIC KEY-----
          ...public-key...
          -----END PUBLIC KEY-----

How would the resource server know to decode the token sent by the external OAuth provider? Can multiple OAuth2 clients be configured in the resource server? Is my thinking here flawed?

Upon debugging the request to resource server, I discovered the token value sent from Google in the org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter class:

ya29..xwLBw5mz3XoTo-xuaSGbwhuE3_wqtAwL8tP7sGe5wMRvChk6pxeH8CpPnPg83OlbnA

There seems to be no payload in the token?

I also saw that the verifier being used was using the jwt key-value as configured above.

How would I configure multiple resource server oauth resources & how would the resource server know which one to use?