Run a Spring Boot oAuth2 application as resource server AND serving web content - spring-boot

I'm using Spring Boot 1.5.13 and with that Spring Security 4.2.6 and Spring Security oAuth2 2.0.15.
I want to find a best practice setup for our Spring Boot applications that serve a mixed set of content: A REST API, and some web pages that provide a convenience "landing page" for developers with some links on it, plus Swagger based API documentation, which is also web content.
I have a configuration that allows me to run the app with proper authorization code flow, hence I can access all web content via Browser and get authenticated by the configured IdP (in my case PingFederate), plus I can make API calls from within the Browser, i.e. directly or with a REST Client, e.g. with RESTClient.
This is my security configuration:
#Slf4j
#Configuration
#EnableWebSecurity
#EnableOAuth2Sso // this annotation must stay here!
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/login**", "/webjars/**", "/css/**").permitAll()
.antMatchers("/cfhealth").permitAll()
.antMatchers("/").permitAll()
.antMatchers("/protected", "/api/**").authenticated();
}
#Bean
public RequestContextListener requestContextListener() {
return new RequestContextListener();
}
}
and the oAuth2 configuration:
#Configuration
#Slf4j
public class OAuth2Config extends ResourceServerConfigurerAdapter {
#Value("${pingfederate.pk-uri}")
String pingFederatePublicKeyUri;
#Autowired
PingFederateKeyUtils pingFederateKeyUtils;
#Override
public void configure(ResourceServerSecurityConfigurer config) {
config.tokenServices(tokenServices());
}
#Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
#Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
String certificate = pingFederateKeyUtils.getKeyFromServer(pingFederatePublicKeyUri);
String publicKey = pingFederateKeyUtils.extractPublicKey(certificate);
converter.setVerifier(pingFederateKeyUtils.createSignatureVerifier(publicKey));
return converter;
}
#Bean
#Primary
public DefaultTokenServices tokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
return defaultTokenServices;
}
}
But when I want to call a REST API programmatically/outside the Browser with a bearer token in the header, e.g. with curl, the authorization code flow kicks in and redirects to the local login endpoint. What I want is that API calls accept the bearer token for authentication, without creating a session, and that all web content/mvc calls in the Browser establish a session.
curl -i -H "Accept: application/json" -H "Authorization: Bearer $TOKEN" -X GET http://localhost:8080/authdemo/api/hello
Adding the #EnableResourceServer annotation to the above SecurityConfig class (and adding security.oauth2.resource.filter-order=3 in the application properties file, I can make the curl command work, but then the authorization code flow is broken, I get the following output in the Browser for all URLs in my application:
<oauth>
<error_description>
Full authentication is required to access this resource
</error_description>
<error>unauthorized</error>
</oauth>
Now is there a way to get this szenario working nicely? If yes, how would that look like? Or is it only supported in later versions of Spring Boot+Security+oAuth2?
The question at Spring Boot with Security OAuth2 - how to use resource server with web login form? is quite similar

I found the solution: It takes multiple HttpSecurity configurations. I found out by reading the great article written by Matt Raible at https://developer.okta.com/blog/2018/02/13/secure-spring-microservices-with-oauth where he introduced me to the notion of requestMatchers(.). This is how I finally implemented it:
#Configuration
#EnableResourceServer
#EnableWebSecurity(debug = true)
#EnableOAuth2Sso
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
#Bean
public RequestContextListener requestContextListener() {
return new RequestContextListener();
}
#Override
public void configure(HttpSecurity http) throws Exception {
http
.requestMatcher(new RequestHeaderRequestMatcher("Authorization"))
.authorizeRequests().anyRequest().fullyAuthenticated();
}
}
With that I can access the service with a Browser, leading to a authorization code flow. But accessing the API (or actually any part of the service) leads to a validation of the provided Bearer token.
And to illustrate the way how some endpoints can be exluded/made public in such a case, here's how I configure the actuator endpoints and one very simple 'ping' endpoint I've added myself:
#Configuration
#Order(1)
public class ActuatorSecurity extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.requestMatcher(new OrRequestMatcher(EndpointRequest.to("health", "info"),
new AntPathRequestMatcher("/cfhealth"))).authorizeRequests().anyRequest().permitAll();
}
}
And my implementation of the /cfhealth endpoint:
#Controller
#Slf4j
public class MainController {
#GetMapping(value = "/cfhealth")
#ResponseBody
public String cfhealth() {
return "ok";
}
}
I'm happy to learn from others if that's the best practice way of Spring Security configuration or if there are better ways to do it. I've spent quite some time on the topic in the last few weeks on it, and it takes quite some effort to grasp the basic Spring Security concepts.

Related

Evaluate Web Services Interceptor Before Spring Security Filter Chain

I have a SOAP-based web services application which is leveraging Spring Web Services (and Spring WS Security) as well as Spring Security. I am using a custom AbstractWsSecurityInterceptor to authenticate the incoming requests (using an injected AuthenticationManager) and to add the successful authentications to the SecurityContext. I then have a custom AcessDecisionManager which is using a custom WebSecurityExpressionHandler to validate a certain property from the principal added to the context by the interceptor.
Below is an idea of what my configuration files look like:
SecurityConfig.java:
#Getter
#Setter
#Configuration
#RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final AccessDecisionManager customAccessDecisionManager;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.
cors()
.and().csrf().disable()
.authorizeRequests()
.accessDecisionManager(customAccessDecisionManager)
.antMatchers(GET, "/actuator/**").permitAll()
.anyRequest().access("customAccessMethod()")
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
WebServiceConfig.java:
#EnableWs
#Configuration
#RequiredArgsConstructor
public class WebServiceConfig extends WsConfigurerAdapter {
private final AuthenticationManager authenticationManager;
#Bean
public ServletRegistrationBean<MessageDispatcherServlet> messageDispatcherServlet(ApplicationContext applicationContext) {
MessageDispatcherServlet servlet = new MessageDispatcherServlet();
servlet.setApplicationContext(applicationContext);
servlet.setTransformWsdlLocations(true);
return new ServletRegistrationBean<>(servlet, "/ws/*");
}
...
...
#Bean
AbstractWsSecurityInterceptor customAuthenticationInterceptor() {
return new CustomAuthenticationInterceptor(authenticationManager);
}
#Override
public void addInterceptors(List<EndpointInterceptor> interceptors) {
interceptors.add(customAuthenticationInterceptor());
}
}
The issue with this setup is that the Spring Security filter chain is evaluated first and fails the authentication because the AccessDecisionManager is evaluated before the request has a chance to enter the custom AbstractWsSecurityInterceptor and place the authentication in the SecurityContext.
Is there any way to evaluate the interceptor and handling of the request on the Web Services and WS Security side of things before it then hits the Spring Security filter chain? Is this a possibility?
Thank you in advance for the help!

Spring Boot Security - How to disable security for Swagger UI

I have an application with only REST endpoints. I have enabled oauth2 token security via:
#Configuration
#EnableAuthorizationServer
public class AuthServerOAuth2Config extends AuthorizationServerConfigurerAdapter {
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("xxx").secret("xxx").accessTokenValiditySeconds(3600)
.authorizedGrantTypes("client_credentials")
.scopes("xxx", "xxx")
.and()
.withClient("xxx").secret("xxx").accessTokenValiditySeconds(3600)
.authorizedGrantTypes("password", "refresh_token")
.scopes("xxx", "xxx");
}
}
Now if I try to access any of my endpoints I get 401 Unauthorized, and I first have to get the access_token via the /oauth/token?grant_type=client_credentials or /oauth/token?grant_type=password calls. The REST endpoints work as expected if I add the proper Authorization header with the token returned in previous call.
However, I am unable to access the swagger-ui page. I have enabled swagger via:
#Configuration
#EnableSwagger2
public class SwaggerConfig {
#Bean
public Docket productApi() {
return new Docket(DocumentationType.SWAGGER_2)
.select().apis(RequestHandlerSelectors.basePackage("com.xxx"))
.paths(PathSelectors.regex("/xxx/.*"))
.build();
}
}
If I go to localhost:8080/swagger-ui.html I get:
<oauth>
<error_description>
Full authentication is required to access this resource
</error_description>
<error>unauthorized</error>
</oauth>
So I added the following to be able to access Swagger:
#Configuration
public class ResourceServerConfig extends WebSecurityConfigurerAdapter {
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/swagger-ui.html")
.antMatchers("/webjars/springfox-swagger-ui/**")
.antMatchers("/swagger-resources/**")
.antMatchers("/v2/api-docs");
}
#Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
}
}
And in #EnableWebMvc class I added:
#Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("swagger-ui.html")
.addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
}
Now I can access the Swagger UI page, but my security for the REST endpoints is messed up. By that I mean, the client_credentials endpoints no longer require a token, and the password endpoints give a 403 Forbidden no matter what I do.
I think my approach is wrong but I don't know what. Basically I want:
Oauth token security on all my REST endpoints (beginning with /api/* for example)
Swagger UI page should be accessible
The endpoints on the swagger page should have a way to specify the access_token
How do I achieve this?
This is how I fixed it. I removed the class that extends WebSecurityConfigurerAdapter (see above) and replaced with this:
#Configuration
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
#Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/xxx/**").authenticated();
http.authorizeRequests().anyRequest().permitAll();
http.csrf().disable();
}
}
To enable token authentication on the swagger page I followed this tutorial: http://www.baeldung.com/swagger-2-documentation-for-spring-rest-api

How to protect the same resource using both spring-session and spring-security-oauth

I have a requirement to use two kinds of authentication,
for web we #EnableRedisHttpSession and for other consumers like mobile we use #EnableAuthorizationServer with #EnableResourceServer.
suppose we try to protect a controller common to both the authentication mechanisms for e.g /api/v1/test
i have hit a roadblock.
i am only able to use one kind of authentication scheme
if i set #WebSecurityConfigurerAdapter #order(2) and #ResourceServerConfigurerAdapter #order(3) then i can only access the resource via web
and if i set #ResourceServerConfigurerAdapter #order(2) and #WebSecurityConfigurerAdapter #order(3) then only OAuth works.
i am unable to use both the mechanism at the same time.how can we make the two work together, for e.g if the request comes from web use the filter responsible for that and if the request comes from mobile use the appropriate filter. web uses cookies and API Consumers use Authorization : Bearer header.
please help
It's sounds very strange. I suggest you to review how REST API is used and why it should be used by browser users. Better to separate web views and REST API, don't mix it.
However, answering your question "can I use two kinds of authentication for some URI at once" - yes, you can.
You need custom RequestMatcher that will decide how to route incoming request.
So:
for "API consumers" - check existence of Authorization header
contains "Bearer"
for "browser users" - just inverse first rule
Code example:
public abstract class AntPathRequestMatcherWrapper implements RequestMatcher {
private AntPathRequestMatcher delegate;
public AntPathRequestMatcherWrapper(String pattern) {
this.delegate = new AntPathRequestMatcher(pattern);
}
#Override
public boolean matches(HttpServletRequest request) {
if (precondition(request)) {
return delegate.matches(request);
}
return false;
}
protected abstract boolean precondition(HttpServletRequest request);
}
OAuth2 authentication
#EnableResourceServer
#Configuration
public class ResourceServerConfigurer extends ResourceServerConfigurerAdapter {
#Override
public void configure(HttpSecurity http) throws Exception {
http.requestMatcher(new AntPathRequestMatcherWrapper("/api/v1/test") {
#Override
protected boolean precondition(HttpServletRequest request) {
return String.valueOf(request.getHeader("Authorization")).contains("Bearer");
}
}).authorizeRequests().anyRequest().authenticated();
}
}
Web authentication
#Configuration
#EnableWebSecurity
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.requestMatcher(new AntPathRequestMatcherWrapper("/api/v1/test") {
#Override
protected boolean precondition(HttpServletRequest request) {
return !String.valueOf(request.getHeader("Authorization")).contains("Bearer");
}
}).authorizeRequests().anyRequest().authenticated();
}
}
Using this configuration it's possible to use two different authentication types for one URI /api/v1/test.
In addition, I highly recommended to read the article about Spring Security architecture by Dave Syer, to understand how does it work:
https://spring.io/guides/topicals/spring-security-architecture/#_web_security

Unable to set up basic authentication with Spring Boot REST API

I'm trying to set up a RESTful API with Spring Boot and I'm trying to enable basic authentication. How come I keep hitting a 403 Access Denied error? I'm sending my credentials as a header in Postman (see image attached). If I remove .anyRequest.authenticated(), it works fine. I don't want to remove that though because I would like basic authentication for every endpoint. What am I doing wrong?
Application.java
#SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
SecurityConfiguration.java
#Configuration
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/h2-console/*").permitAll()
.anyRequest().authenticated();
http.csrf().disable();
http.headers().frameOptions().disable();
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("user").password("password").roles("USER");
}
}
Controller.java
#RestController
public class Controller {
#RequestMapping("/test")
public String index() {
return "Greetings from Spring Boot!";
}
}
After digging around in the Spring docs, it seems I understand what each of the chained method calls are for.
Anyway, the simple answer is that I needed .and().httpBasic() to enable Basic HTTP Authentication over my REST API.
.anyRequest().authenticated() simply mandates that every request is authenticated, but did not specify what method. Adding basic authentication means we can use basic auth to authenticate a user.
See more.

Spring ResourceServer using RemoteTokenServices with uri to check access_token

I have a configuration that enables web security and resource server as below
#EnableWebSecurity
#EnableResourceServer
public class SpringSecurityConfiguration extends ResourceServerConfigurerAdapter {
#Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated();
}
}
I also added the following properties
security.oauth2.resource.user-info-uri: http://localhost:9090/oauth2/tokeninfo
security.oauth2.resource.token-info-uri: http://localhost:9090/oauth2/tokeninfo
But somehow the authentication manager in the created OAuth2AuthenticationProcessingFilter uses the DefaultTokenServices to loadAuthentication. How can I let it use the RemoteTokenServices by calling the uri I provided in the configuration to check the access_token sent by the client.

Resources