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?
Related
I am trying to invoke some backend system which is secured by a client_credentials grant type from a Feign client application.
The access token from the backend system can be retrieved with the following curl structure (just as an example):
curl --location --request POST '[SERVER URL]/oauth/grant' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--header 'Cookie: WebSessionID=172.22.72.1.1558614080219404; b8d49fdc74b7190aacd4ac9b22e85db8=2f0e4c4dbf6d4269fd3349f61c151223' \
--data-raw 'grant_type=client_credentials' \
--data-raw 'client_id=[CLIENT_ID]' \
--data-raw 'client_secret=[CLIENT_SECRET]'
{"accessToken":"V29C90D1917528E9C29795EF52EC2462D091F9DC106FAFD829D0FA537B78147E20","tokenType":"Bearer","expiresSeconds":7200}
This accessToken should then be set in a header to subsequent business calls to the backend system.
So now my question is, how to implement this using Feign and Spring Boot Security 5.
After some research I come to this solution (which doesn't work):
Define my client in the application.yml:
spring:
security:
oauth2:
client:
registration:
backend:
client-id:[CLIENT_ID]
client-secret: [CLIENT_SECRET]
authorization-grant-type: client_credentials
provider:
backend:
token-uri: [SERVER URL]/oauth/grant
Create a OAuth2AuthorizedClientManager Bean to be able to authorize (or re-authorize) an OAuth 2.0 client:
#Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
DefaultOAuth2AuthorizedClientManager authorizedClientManager =
new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
return authorizedClientManager;
}
Create a Feign Request Interceptor that uses the OAuth2AuthorizedClientManager:
public class OAuthRequestInterceptor implements RequestInterceptor {
private OAuth2AuthorizedClientManager manager;
public OAuthRequestInterceptor(OAuth2AuthorizedClientManager manager) {
this.manager = manager;
}
#Override
public void apply(RequestTemplate requestTemplate) {
OAuth2AuthorizedClient client = this.manager.authorize(OAuth2AuthorizeRequest.withClientRegistrationId("backend").principal(createPrincipal()).build());
String accessToken = client.getAccessToken().getTokenValue();
requestTemplate.header(HttpHeaders.AUTHORIZATION, "Bearer" + accessToken);
}
private Authentication createPrincipal() {
return new Authentication() {
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Collections.emptySet();
}
#Override
public Object getCredentials() {
return null;
}
#Override
public Object getDetails() {
return null;
}
#Override
public Object getPrincipal() {
return this;
}
#Override
public boolean isAuthenticated() {
return false;
}
#Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
}
#Override
public String getName() {
return "backend";
}
};
}
}
Create a FeignConfig that uses the Interceptor:
public class FeignClientConfig {
#Bean
public OAuthRequestInterceptor repositoryClientOAuth2Interceptor(OAuth2AuthorizedClientManager manager) {
return new OAuthRequestInterceptor(manager);
}
}
And this is my Feign client:
#FeignClient(name = "BackendRepository", configuration = FeignClientConfig.class, url = "${BACKEND_URL}")
public interface BackendRepository {
#GetMapping(path = "/healthChecks", produces = MediaType.APPLICATION_JSON_VALUE)
public Info healthCheck();
}
When running this code, I get the error:
org.springframework.web.client.UnknownContentTypeException: Could not extract response: no suitable HttpMessageConverter found for response type [class org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse] and content type [text/html;charset=utf-8]
Debugging the code it looks like the DefaultClientCredentialsTokenResponseClient is requesting the auth endpoint using Basic Authentication. Although I never set this up.
Any advise what I can do? Maybe there is a completely different approach to do this.
For this to work with Spring Security 5 and Feign you need to have
a working Spring Security config
a Feign interceptor
a Feign configuration using that interceptor
Working Spring Security Config
Here we will register a generic internal-api client for your oauth2 client credentials. This is where you specify the client-id,client-secret, scopes and grant type.
All basic Spring Security 5 stuff. This also involves setting up a provider (here I am using a custom OpenID Connect provider called "yourprovider"
spring:
security:
oauth2:
client:
registration:
internal-api:
provider: yourprovider
client-id: x
client-secret: y
scope:
- ROLE_ADMIN
authorization-grant-type: client_credentials
provider:
yourprovider:
issuer-uri: yourprovider.issuer-uri
resourceserver:
jwt:
issuer-uri: yourprovider.issuer-uri
Next you need your feign config. This will use a OAuth2FeignRequestInterceptor
public class ServiceToServiceFeignConfiguration extends AbstractFeignConfiguration {
#Bean
public OAuth2FeignRequestInterceptor requestInterceptor() {
return new OAuth2FeignRequestInterceptor(
OAuth2AuthorizeRequest.withClientRegistrationId("internal-api")
.principal(new AnonymousAuthenticationToken("feignClient", "feignClient", createAuthorityList("ROLE_ANONYMOUS")))
.build());
}
}
And a RequestInterceptor that looks like this :
The OAuth2AuthorizedClientManager is a bean that you can configure in your Configuration
public OAuth2AuthorizedClientManager authorizedClientManager(final ClientRegistrationRepository clientRegistrationRepository, final OAuth2AuthorizedClientService authorizedClientService) {
return new AuthorizedClientServiceOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientService);
}
The OAuth2AuthorizeRequest is provided by the Feign Configuration above.
The oAuth2AuthorizedClientManager can authorize the oAuth2AuthorizeRequest, get you the access token, and provide it as an Authorization header to the underlying service
public class OAuth2FeignRequestInterceptor implements RequestInterceptor {
#Inject
private OAuth2AuthorizedClientManager oAuth2AuthorizedClientManager;
private OAuth2AuthorizeRequest oAuth2AuthorizeRequest;
OAuth2FeignRequestInterceptor(OAuth2AuthorizeRequest oAuth2AuthorizeRequest) {
this.oAuth2AuthorizeRequest = oAuth2AuthorizeRequest;
}
#Override
public void apply(RequestTemplate template) {
template.header(AUTHORIZATION,getAuthorizationToken());
}
private String getAuthorizationToken() {
final OAuth2AccessToken accessToken = oAuth2AuthorizedClientManager.authorize(oAuth2AuthorizeRequest).getAccessToken();
return String.format("%s %s", accessToken.getTokenType().getValue(), accessToken.getTokenValue());
}
}
I am quite experienced with Feign and OAuth2 and it took me some good hours to find how to do that.
First, let's say that my app is based on latest Spring libraries, so I am using the following dependencies (managed version for spring-cloud-starter-openfeign is 3.0.0)
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
<version>2.2.4.RELEASE</version>
</dependency>
In my application.properties I have the following
security.oauth2.client.access-token-uri=https://api.twitter.com/oauth2/token
security.oauth2.client.client-id=my-secret-twitter-id
security.oauth2.client.client-secret=my-secret-twitter-secret
security.oauth2.client.grant-type=client_credentials
And finally my configuration beans
package es.spanishkangaroo.ttanalyzer.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.openfeign.security.OAuth2FeignRequestInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.DefaultOAuth2ClientContext;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails;
import feign.RequestInterceptor;
#Configuration
public class FeignClientConfiguration {
#Bean
#ConfigurationProperties(prefix = "security.oauth2.client")
public ClientCredentialsResourceDetails clientCredentialsResourceDetails() {
return new ClientCredentialsResourceDetails();
}
#Bean
public RequestInterceptor oauth2FeignRequestInterceptor(){
return new OAuth2FeignRequestInterceptor(new DefaultOAuth2ClientContext(), clientCredentialsResourceDetails());
}
#Bean
public OAuth2RestTemplate clientCredentialsRestTemplate() {
return new OAuth2RestTemplate(clientCredentialsResourceDetails());
}
}
So then the Feign client is as simple as
package es.spanishkangaroo.ttanalyzer.api;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import es.clovelly.ttanalyzer.model.Trends;
#FeignClient(name = "twitterClient", url = "https://api.twitter.com/1.1/")
public interface TwitterClient {
#GetMapping("/trends/place.json")
Trends[] getTrendsById(#RequestParam Long id);
}
As you may have noticed, the code is automatically getting a token (a bearer token) before the client call. If you are using a non-expiring bearer token you can just use something like
#Bean
public OAuth2ClientContext oAuth2ClientContext() {
DefaultOAuth2ClientContext context = new DefaultOAuth2ClientContext();
context.setAccessToken(bearerToken);
return context;
}
I tried you're approach. Unfortunatelly without success. But this one worked for me: Spring cloud Feign OAuth2 request interceptor is not working. Looks like I use a lot of depredations now, but at least it does work.
I'm trying to integrate a Twitter authentication (ideally SSO) in a Vaadin application. For this I created a Vaadin application from scratch and tried to integrate pac4j (see following steps). Unfortunately I get error "code":32,"message":"Could not authenticate you." despite valid email/password combination. Any ideas how to get this to work?
Download a Vaadin 14.2 project from https://vaadin.com/start/v14
Run Application.java and visit localhost:8080 works fine.
Extend pom.xml with pac4j for Spring Boot:
<!-- https://mvnrepository.com/artifact/org.pac4j/spring-security-pac4j -->
<dependency>
<groupId>org.pac4j</groupId>
<artifactId>spring-security-pac4j</artifactId>
<version>5.1.0</version>
</dependency>
Extend pom.xml with pac4j for Twitter client:
<!-- https://mvnrepository.com/artifact/org.pac4j/pac4j-oauth -->
<dependency>
<groupId>org.pac4j</groupId>
<artifactId>pac4j-oauth</artifactId>
<version>4.0.3</version>
</dependency>
Extend pom.xml with Spring Security
<!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-config -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
</dependency>
Create class Pac4jConfig like in the Spring Security example in this presentation http://www.pac4j.org/gettingstarted.html
import org.pac4j.core.config.Config;
import org.pac4j.oauth.client.TwitterClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
#Configuration
public class Pac4jConfig {
#Bean
public Config config() {
TwitterClient twitterClient = new TwitterClient();
Config config = new Config("http://localhost:8080", twitterClient);
return config;
}
}
Create class SecurityConfig like in the Spring Security example in this presentation http://www.pac4j.org/gettingstarted.html
import org.pac4j.core.config.Config;
import org.pac4j.springframework.security.web.CallbackFilter;
import org.pac4j.springframework.security.web.SecurityFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
#EnableWebSecurity
public class SecurityConfig {
#Configuration
#Order(1)
public static class TwitterWebSecurityConfigurationAdapter
extends WebSecurityConfigurerAdapter {
#Autowired
private Config config;
protected void configure(final HttpSecurity http) throws Exception {
final SecurityFilter filter = new SecurityFilter(config, "TwitterClient");
http.antMatcher("/twitter/**").addFilterBefore(filter, BasicAuthenticationFilter.class);
}
}
#Configuration
public static class DefaultWebSecurityConfigurationAdapter
extends WebSecurityConfigurerAdapter {
#Autowired
private Config config;
protected void configure(final HttpSecurity http) throws Exception {
final CallbackFilter callbackFilter = new CallbackFilter(config);
http.authorizeRequests().anyRequest().permitAll().and().addFilterBefore(callbackFilter,
BasicAuthenticationFilter.class);
}
}
}
Create class TwitterTestApplication like class "Application" in the Spring Security example in this presentation http://www.pac4j.org/gettingstarted.html
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
#Controller
public class TwitterTestApplication {
#RequestMapping("/twitter/index.html")
public String twitter(HttpServletRequest request, HttpServletResponse response,
Map<String, Object> map) {
return "Hello world";
}
}
Calling http://localhost:8080/twitter/index.html lead to this error:
There was an unexpected error (type=Internal Server Error, status=500).
key cannot be blank
org.pac4j.core.exception.TechnicalException: key cannot be blank
at org.pac4j.core.util.CommonHelper.assertTrue(CommonHelper.java:107)
[...]
Extending the constructor call of the TwitterClient by my credentials (which work fine when I copy/paste them at twitter.com) and allowing emails:
TwitterClient twitterClient = new TwitterClient("[my-email]", "[my-password]", true);
Calling http://localhost:8080/twitter/index.html lead to this error:
There was an unexpected error (type=Internal Server Error, status=500).
com.github.scribejava.core.exceptions.OAuthException: Response body is incorrect. Can't extract token and secret from this: '{"errors":[{"code":32,"message":"Could not authenticate you."}]}'
org.pac4j.core.exception.TechnicalException: com.github.scribejava.core.exceptions.OAuthException: Response body is incorrect. Can't extract token and secret from this: '{"errors":[{"code":32,"message":"Could not authenticate you."}]}'
at org.pac4j.oauth.redirect.OAuth10RedirectionActionBuilder.getRedirectionAction(OAuth10RedirectionActionBuilder.java:62)
at org.pac4j.core.client.IndirectClient.getRedirectionAction(IndirectClient.java:109)
at org.pac4j.core.engine.DefaultSecurityLogic.redirectToIdentityProvider(DefaultSecurityLogic.java:224)
at org.pac4j.core.engine.DefaultSecurityLogic.perform(DefaultSecurityLogic.java:157)
at org.pac4j.springframework.security.web.SecurityFilter.doFilter(SecurityFilter.java:73)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:116)
I'm trying to implement sso with Spring Security Oauth2 using Spring-boot and Dave Syer samples
I want to use my custom server provider and it's working fine.
For the client, I want user to be authenticate (so redirected to OAuth2 url) when they try to access client site (eg localhost:8080/) and redirect back to index.html file once authenticated. I also want to implement logout when user on a link in index.html file.
I've come up with this following client sso client:
package org.ikane;
import java.io.IOException;
import java.security.Principal;
import java.util.Arrays;
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.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
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.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.WebUtils;
#SpringBootApplication
#Controller
public class DemoSsoOauth2ClientApplication implements CommandLineRunner {
private static final Logger logger = LoggerFactory.getLogger(DemoSsoOauth2ClientApplication.class);
#Override
public void run(String... arg0) throws Exception {
SecurityContext securityContext = SecurityContextHolder.getContext();
try {
Authentication authentication = securityContext.getAuthentication();
logger.info(authentication.getDetails().toString());
SecurityContextHolder.clearContext();
} catch (Exception e) {
logger.error("Error", e);
}
}
public static void main(String[] args) {
ConfigurableApplicationContext applicationContext = SpringApplication.run(DemoSsoOauth2ClientApplication.class, args);
ConfigurableEnvironment env = applicationContext.getEnvironment();
logger.info("\n\thttp://localhost:{}{}\n\tProfiles:{}\n",
StringUtils.defaultIfEmpty(env.getProperty("server.port"), "8080"),
StringUtils.defaultIfEmpty(env.getProperty("server.contextPath"), "/"),
Arrays.toString(env.getActiveProfiles()));
}
#RequestMapping(value="/")
public String home() {
return "index";
}
#RequestMapping(value="/user")
#ResponseBody
public Principal user(Principal user) {
return user;
}
/**
* The Class OAuthConfiguration that sets up the OAuth2 single sign on
* configuration and the web security associated with it.
*/
#Component
#Controller
#EnableOAuth2Sso
protected static class OAuthClientConfiguration extends WebSecurityConfigurerAdapter {
private static final String CSRF_COOKIE_NAME = "XSRF-TOKEN";
private static final String CSRF_ANGULAR_HEADER_NAME = "X-XSRF-TOKEN";
#Override
public void configure(HttpSecurity http) throws Exception {
http.antMatcher("/**").authorizeRequests()
.antMatchers("/index.html", "/").permitAll().anyRequest()
.authenticated().and().csrf().csrfTokenRepository(csrfTokenRepository())
.and().addFilterAfter(csrfHeaderFilter(), CsrfFilter.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, CSRF_COOKIE_NAME);
String token = csrf.getToken();
if (cookie == null || token != null
&& !token.equals(cookie.getValue())) {
cookie = new Cookie(CSRF_COOKIE_NAME, token);
cookie.setPath("/");
response.addCookie(cookie);
}
}
filterChain.doFilter(request, response);
}
};
}
/**
* Angular sends the CSRF token in a custom header named "X-XSRF-TOKEN"
* rather than the default "X-CSRF-TOKEN" that Spring security expects.
* Hence we are now telling Spring security to expect the token in the
* "X-XSRF-TOKEN" header.
*
* This customization is added to the csrf() filter.
*
* #return
*/
private CsrfTokenRepository csrfTokenRepository() {
HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();
repository.setHeaderName(CSRF_ANGULAR_HEADER_NAME);
return repository;
}
}
}
You can find a GitHub source. Any hints on how to implement this use case?
Thanks in advance
To make your client app redirect to the Authorization Server just add
the annotation #EnableOAuth2Sso on your WebSecurityConfigurerAdapter and
place the proper OAuth2 configurations (client-id, secret, access token uri...) in your properties file.
(I'm assuming that your client app is using Spring Boot as well)
To end the user's session you have to redirect to an endpoint in the authorization server and logout programmatically as shown in this post.
I have created a repository on github with a sample app that has those features that you are looking for.
Please check it out and let me know if it helps you.
I'm working on the spring boot security using basic authentication.I have configured basic application security layer and i can authenticate the user.But,now i would like to know how to authorize the user to show only the granted operations without hard coding the role names in client side or server side code using thymeleaf with spring security.
Let's say i have app with some menus in dashboard to show the granted menus to the logged in user.I have read some thymeleaf tutorials by following this link https://github.com/thymeleaf/thymeleaf-extras-springsecurity but i could not get any samples to work using this.
Could anyone help me to know about this scenario ?
import com.triesten.dost.*;
import com.triesten.dost.customUserService.MyCustomUserDetailsService;
import com.triesten.dost.dao.UserDao;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.core.annotation.Order;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.csrf.CsrfFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
#Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
public class ApplicationSecurity extends WebSecurityConfigurerAdapter {
#Autowired
UserDao userDao;
#Autowired
#Qualifier("userDetailsService")
UserDetailsService userDetailsService;
#Autowired
private RESTAuthenticationEntryPoint authenticationEntryPoint;
#Autowired
private RESTAuthenticationFailureHandler authenticationFailureHandler;
#Autowired
private RESTAuthenticationSuccessHandler authenticationSuccessHandler;
/*#Override
public void configure(WebSecurity web) throws Exception {
web
// Spring Security ignores request to static resources such as
// CSS or JS files.
.ignoring().antMatchers("/static/**");
}*/
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/css/**", "/fonts/**", "/images/**");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/", "/index.html", "/login", "/dost/bannerContent").permitAll();
http.authorizeRequests().anyRequest().fullyAuthenticated().and().httpBasic().and().csrf().disable();
http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
http.formLogin().loginPage("/login").loginProcessingUrl("/login/authenticate")
.successHandler(authenticationSuccessHandler);
http.formLogin().failureHandler(authenticationFailureHandler);
http.logout().logoutRequestMatcher(new AntPathRequestMatcher("/logout")).logoutSuccessUrl("/login")
.invalidateHttpSession(true);
http.exceptionHandling().accessDeniedHandler(accessDeniedHandler());
// CSRF tokens handling
http.addFilterAfter(new CsrfTokenResponseHeaderBindingFilter(), CsrfFilter.class);
}
/**
* Configures the authentication manager bean which processes authentication
* requests.
*/
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// Dao based authentication
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
private AccessDeniedHandler accessDeniedHandler() {
return new AccessDeniedHandler() {
#Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.getWriter().append("Access denied");
response.setStatus(403);
}
};
}
/**
* This is used to hash the password of the user.
*/
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(10);
}
/**
* This bean is load the user specific data when form login is used.
*/
#Bean
public UserDetailsService userDetailsService() {
return new MyCustomUserDetailsService(userDao);
}
}
Thanks in advance...
I have set a up a spring boot (1.2.3) application with spring security and spring-ws. I have configured spring security to use .ldapAuthentication() for authentication in my WebSecurityConfigurerAdapter. I am trying to get the same spring security authenticationManager to authenticate my spring ws SOAP web services using ws-security usernametokens (plain text) in my WsConfigurerAdapter.
I have configured my WebSecurityConfigurerAdapter like this:
package za.co.switchx.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.ldap.core.support.LdapContextSource;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.configuration.WebSecurityConfigurerAdapter;
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Bean
#ConfigurationProperties(prefix="ldap.contextSource")
public LdapContextSource contextSource() {
LdapContextSource contextSource = new LdapContextSource();
return contextSource;
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.ldapAuthentication()
.userSearchBase("cn=Users,dc=SwitchX,dc=co,dc=za")
.userSearchFilter("(uid={0})")
.groupSearchBase("cn=Groups,dc=SwitchX,dc=co,dc=za")
.groupSearchFilter("(&(cn=*)(| (objectclass=groupofUniqueNames)(objectclass=orcldynamicgroup)))")
.contextSource(contextSource());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/ws/**").permitAll()
.anyRequest().authenticated()
.and()
.csrf().disable()
.httpBasic();
}
}
So then I went to configure my WsConfigurerAdapter like this:
package za.co.switchx.config;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.ws.config.annotation.EnableWs;
import org.springframework.ws.config.annotation.WsConfigurerAdapter;
import org.springframework.ws.transport.http.MessageDispatcherServlet;
import org.springframework.ws.wsdl.wsdl11.DefaultWsdl11Definition;
import org.springframework.xml.xsd.SimpleXsdSchema;
import org.springframework.xml.xsd.XsdSchema;
import org.springframework.ws.soap.security.xwss.XwsSecurityInterceptor;
import org.springframework.ws.soap.security.xwss.callback.SpringPlainTextPasswordValidationCallbackHandler;
import org.springframework.ws.server.EndpointInterceptor;
#EnableWs
#Configuration
public class WebServiceConfig extends WsConfigurerAdapter {
#Bean
public ServletRegistrationBean messageDispatcherServlet(ApplicationContext applicationContext) {
MessageDispatcherServlet servlet = new MessageDispatcherServlet();
servlet.setApplicationContext(applicationContext);
servlet.setTransformWsdlLocations(true);
return new ServletRegistrationBean(servlet, "/ws/*");
}
#Bean(name = "ApplicantTypeService")
public DefaultWsdl11Definition defaultWsdl11Definition(XsdSchema applicantTypeServiceSchema) {
DefaultWsdl11Definition wsdl11Definition = new DefaultWsdl11Definition();
wsdl11Definition.setPortTypeName("ApplicantTypePort");
wsdl11Definition.setLocationUri("/ws/ApplicantTypeService");
wsdl11Definition.setTargetNamespace("http://switchx.co.za/services/applicant/types/applicant-type-web-service");
wsdl11Definition.setSchema(applicantTypeServiceSchema);
return wsdl11Definition;
}
#Bean
public XsdSchema applicantTypeSchema() {
return new SimpleXsdSchema(new ClassPathResource("xsd/ApplicantTypeService.xsd"));
}
#Bean
public XwsSecurityInterceptor securityInterceptor() {
XwsSecurityInterceptor securityInterceptor = new XwsSecurityInterceptor();
securityInterceptor.setCallbackHandler(new SpringPlainTextPasswordValidationCallbackHandler());
securityInterceptor.setPolicyConfiguration(new ClassPathResource("securityPolicy.xml"));
return securityInterceptor;
}
#Override
public void addInterceptors(List<EndpointInterceptor> interceptors) {
interceptors.add(securityInterceptor());
}
}
If I use a SimplePasswordValidationCallbackHandler in the XwsSecurityInterceptor it does authenticate the ws usernametoken correctly, so I know there is nothing wrong with the ws-security section. And if I logon via http basic it authenticates my ldap user correctly so I know that works.
The problem is that when I try use my ldap user logon in the ws security usernametoken I get ERROR c.s.xml.wss.logging.impl.filter - WSS1408: UsernameToken Authentication Failed in the logs, so looks like its not using my global ldap authentication defined in the WebSecurityConfigAdapter
I cant seem to figure out how to get the SpringPlainTextPasswordValidationCallbackHandler (which is supposed to use spring security) in the XwsSecurityInterceptor to use the global authenticationManager, please help?? I have really been bashing my head against this for the last day but cant seem to win.
Ok I figured this out so though I would post for anyone trying this in the future.
I resolved this problem by changing my spring boot class to:
#SpringBootApplication
#EnableGlobalMethodSecurity(securedEnabled = true)
public class SwitchxApplication extends WebMvcConfigurerAdapter {
#SuppressWarnings("unused")
private static final Logger log = LoggerFactory.getLogger(SwitchxApplication.class);
#Bean
public ApplicationSecurity applicationSecurity() {
return new ApplicationSecurity();
}
#Configuration
#Order(Ordered.HIGHEST_PRECEDENCE)
protected static class AuthenticationConfiguration extends GlobalAuthenticationConfigurerAdapter {
#Bean
#ConfigurationProperties(prefix="ldap.contextSource")
public LdapContextSource contextSource() {
LdapContextSource contextSource = new LdapContextSource();
return contextSource;
}
#Override
public void init(AuthenticationManagerBuilder auth) throws Exception {
auth
.ldapAuthentication()
.userSearchBase("cn=Users,dc=Blah,dc=co,dc=za")
.userSearchFilter("(uid={0})")
.groupSearchBase("cn=Groups,dc=Blah,dc=co,dc=za")
.groupSearchFilter("(&(cn=*)(|(objectclass=groupofUniqueNames)(objectclass=orcldynamicgroup)))")
.contextSource(contextSource());
}
}
#Order(Ordered.LOWEST_PRECEDENCE - 8)
protected static class ApplicationSecurity extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/ws/**").permitAll()
.anyRequest().authenticated()
.and()
.csrf().disable()
.httpBasic();
}
}
public static void main(String[] args) {
SpringApplication.run(SwitchxApplication.class, args);
}
}
And then made the following relevant changes in my WsConfigurerAdapter to:
#EnableWs
#Configuration
public class WebServiceConfig extends WsConfigurerAdapter {
private static final Logger log = LoggerFactory.getLogger(WebServiceConfig.class);
#Autowired
private AuthenticationManager authenticationManager;
#Bean
public ServletRegistrationBean messageDispatcherServlet(ApplicationContext applicationContext) {
MessageDispatcherServlet servlet = new MessageDispatcherServlet();
servlet.setApplicationContext(applicationContext);
servlet.setTransformWsdlLocations(true);
return new ServletRegistrationBean(servlet, "/ws/*");
}
.....
.....
#Bean
public SpringPlainTextPasswordValidationCallbackHandler callbackHandler() {
SpringPlainTextPasswordValidationCallbackHandler callbackHandler = new SpringPlainTextPasswordValidationCallbackHandler();
try {
callbackHandler.setAuthenticationManager(authenticationManager);
} catch(Exception e) {
log.error(e.getMessage());
}
return callbackHandler;
}
#Bean
public XwsSecurityInterceptor securityInterceptor() {
XwsSecurityInterceptor securityInterceptor = new XwsSecurityInterceptor();
securityInterceptor.setCallbackHandler(callbackHandler());
securityInterceptor.setPolicyConfiguration(new ClassPathResource("securityPolicy.xml"));
return securityInterceptor;
}
#Override
public void addInterceptors(List<EndpointInterceptor> interceptors) {
interceptors.add(securityInterceptor());
}
}
So basically the end result is that for all /ws paths the basic http security is ignored but because of the security intercepter in the WS Config it will use a basic ws-security user name token to authenticate web service calls, allowing you to have both authentication mechanisms using spring security set up with ldap.
I hope this helps someone, was a bit tricky not finding of lot of documentation on the boot and java config documentation on this particular setup and stuff as it still relatively new. But after not getting this working, its pretty awesome and Im very impressed.