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?