Getting oauth2 to work with spring-boot and rest - java

I am trying to get oauth2 to work with spring-boot and protect my rest method calls, sans much success.
I have tried using spring-security-oauth2-javaconfig:1.0.0.CI-SNAPSHOT with rg.springframework.boot:spring-boot-starter-security:1.0.0.RC1.
*gradle:
compile("org.springframework.boot:spring-boot-starter-security:1.0.0.RC1")
compile ('org.springframework.security.oauth:spring-security-oauth2-javaconfig:1.0.0.CI-SNAPSHOT'){
exclude module: 'spring-security-config'
exclude module: 'spring-security-core'
exclude module: 'spring-security-web'
}
For now I am just trying to get the authentication and resource server working. I have copied and tried to modify the existing sparklr2 sample from the spring-security-oauth2-javaconfig sample.
The last error I get is :"error":"invalid_client","error_description":"Bad client credentials
when I run curl -v --data "grant_type=password&username=marissa&password=koala&client_id=tonr&secret=secret" -X POST localhost:8100/oauth/token.
I understand oauth2 from a beginner's perspective and the paucity of resources with regard to oauth2 with spring-boot and rest make it hard. Any suggestions?
If someone could provide a cookbook like approach to configure oauth2 authentication and authorization to protect a rest api call along with the relevant curl commands, that would be awesome.,

Java config support for oauth2 is work in progress, but you might have more success with my fork. If I were you I'd stick to XML for the oauth2 bits for now. Here's a bootified sparklr2 with minimal XML. I haven't checked that it works recently but it shouldn't be in bad shape if you update the boot dependencies to 1.0.0.RC2.
Update: the #Configuration stuff has moved to the main OAuth2 repo, so the fork and its parent are basically redundant now (and will probably be removed soon).
Update: the bootified sample is now also using #Configuration.

Yes. This is what I have done to get it to work that way. I believe it is the right solution (other than using client_credentials for grant_type, but I am not an expert:-) If there is a better solution that would be awesome. Thank you so much for taking the time to help me out.
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
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.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.config.annotation.authentication.configurers.InMemoryClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.OAuth2ServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.OAuth2ServerConfigurer;
import org.springframework.security.oauth2.provider.token.InMemoryTokenStore;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends OAuth2ServerConfigurerAdapter {
private final String applicationName = "restservice";
#Value("${client_id}")
private String client_id;
#Value("${client_secret}")
private String client_secret;
#Value("${grant_type}")
private String grant_type;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.requestMatchers()
.and()
.authorizeRequests()
.antMatchers("/").permitAll()
.anyRequest().authenticated()
.and()
.apply(new OAuth2ServerConfigurer())
.tokenStore(new InMemoryTokenStore())
.resourceId(applicationName);
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(new InMemoryUserDetailsManager(getUserDetails()))
.and()
.apply(new InMemoryClientDetailsServiceConfigurer())
.withClient(client_id)
.resourceIds(applicationName)
.scopes("read", "write")
.authorities("USER")
.authorizedGrantTypes(grant_type)
.secret(client_secret);
}
private static final Collection<UserDetails> getUserDetails() {
List<UserDetails> userDetails = new ArrayList<UserDetails>();
userDetails.add(new User("user", "password", AuthorityUtils.createAuthorityList(
"USER", "read", "write")));
return userDetails;
}
}

Related

Access to XMLHttpRequest at 'http://localhost:8080/blog/updatePost/2' from origin 'http://localhost:3000' has been blocked by CORS policy:

Even though I used #CrossOrigin annotation this error still appears. Spring boot app is running on 8080 port and react app is running on 3000 port.
Error:
If further information is needed, please let me know.
I assume you are using spring security.
#CrossOrigin filter might have a lower precedence over spring security filters. Try to configure CORS using spring security.
I resolved my issue with the answer given by #Sanura. Then blockage done by the CORS is avoided.
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests().antMatchers("/auth/login", "/auth/register","/post/savePost", "/post/**", "/post/getPost/{id}","/post/updatePost/{id}")
.permitAll().anyRequest().authenticated()
.and().exceptionHandling().and().sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
}
I added the routes to the configure method as above.
The below mentioning code sample also can be used instead of this.
package com.myapp.springboot.configs;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
#Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/*").
allowedOrigins("*").
allowedMethods("*").
allowedHeaders("*").
allowCredentials(true);
}
}
Thank you for your support.

OAuth2 flow for Gateway + Backend Service

I'm currently building a web-application using Spring-Boot-Web in combinition with OAuth2 using Amazon-Cognito on AWS.
My target setup looks like this:
In "My-Datacenter" I have 2 applications running:
Gateway-Application
Backend-Application
The Gateway-Application is accessible from the internet. It serves the frontend (html, css, js), contains some display logic and maybe some APIs that are accessible for everyone. It is the initiator for the "Authorization Code Grant"-OAuth 2.0 Flow (it redirects the user to Amazon-Cognito if the user isn't yet logged in).
The Backend-Application is only accessible by the Gateway-Application. It is not accessible from the outside. In the Backend-Application I want to retrieve user-details (Name, E-Mail) from Amazon-Cognito.
What I got to work at the moment:
I registered one client for the Gateway-Application in Amazon-Cognito. The Gateway-Application initiates the "Authorization Code Grant"-Flow and can access the user-information from Amazon-Cognito.
I configured the Gateway-Application to pass the OAuth2-Authorization-Details to the Backend-Application with all my HTTP-Requests. The Backend-Application can successfully check whether the user is authenticated or not.
Gateway-Application configuration
application.yml
spring:
security:
oauth2:
client:
registration:
cognito:
client-name: frontend-local
client-id: #######
client-secret: #########
scope: openid,backend/read,backend/write
redirect-uri: http://localhost:8080/login/oauth2/code/cognito
provider:
cognito:
issuer-uri: https://cognito-idp.eu-central-1.amazonaws.com/<my-aws-pool-id>
user-name-attribute: cognito:username
SecurityConfiguration.java
package io.share.frontend.configuration;
import org.springframework.context.annotation.Configuration;
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 SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.and()
.authorizeRequests().antMatchers("/", "/webjars/**").permitAll().anyRequest().authenticated()
.and()
.oauth2Login()
.and()
.logout().logoutSuccessUrl("/");
}
}
OAuth2WebClientConfiguration.java
package io.share.frontend.configuration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProviderBuilder;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction;
import org.springframework.web.reactive.function.client.WebClient;
#Configuration
public class OAuth2WebClientConfiguration {
#Bean
public OAuth2AuthorizedClientManager authorizedClientManager(ClientRegistrationRepository clientRegistrationRepository, OAuth2AuthorizedClientRepository authorizedClientRepository) {
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials()
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager =
new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
#Bean
WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
oauth2Client.setDefaultClientRegistrationId("cognito");
return WebClient.builder()
.apply(oauth2Client.oauth2Configuration())
.build();
}
}
Backend-Application configuration
application.yml
server:
port: 8081
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://cognito-idp.eu-central-1.amazonaws.com/<my-aws-pool-id>
SecurityConfiguration.java
package io.share.backend.configuration;
import org.springframework.context.annotation.Configuration;
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 SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests().anyRequest().hasAuthority("SCOPE_backend/read")
.and()
.oauth2ResourceServer().jwt();
}
}
What I want to get to work
In my Backend-Application, I want to be able to access to User-Information from Amazon-Cognito. Currently, I have the following basic RestController in my Backend-Application:
package io.share.backend.web;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.security.Principal;
#RestController
public class ObjectResource {
#GetMapping("/username")
public String greeting(#AuthenticationPrincipal Principal user) {
return user.getName();// this currently returns some UUID-Like string, I want to be able to access the username and E-Mail here, just like I can in the Gateway
}
}
But I want to be able to access the real Username and E-Mail in the Backend.
My question
Do I have to create a new client in Amazon-Cognito for the backend? Or do I have to configure the same client-id and client-secret for the Gateway and Backend?
Which additional Spring-Configurations do I have to make in my Applications (both Gateway and Backend) to make this work?
I'm using spring-boot 2.3.6.RELEASE
Good question and you are running into the following common OAuth issue with APIs:
Cognito access tokens contain very little user info, which is good practice - they are not customisable and only include a subject claim
Spring wants to create an AuthenticatedPrincipal from just the JWT - but your API also wants additional claims to be added to the AuthenticatedPrincipal
DESIGN PATTERN
There is a design pattern you can use, or borrow ideas from, and my blog post summarises the behaviour. The idea is to define a Claims Principal first, then to populate it with claims from multiple sources at runtime. In your case one source will be the Cognito User Info endpoint.
WORKING CODE
You can run my Sample Spring Boot API, which also uses AWS Cognito, and the key class is the Authorizer. In some setups you may be able to get an API Gateway to do some of this work for you:
How to Run the API
Spring Boot OAuth Integration with Custom Claims
PROs and CONs
This pattern will give you extensible claims but it will also add some complexity to your API, since you need to override the API technology stack's default OAuth behaviour.

Is there any Spring Security extension for REST services?

I am building REST API with Spring Boot and I use Spring Security. I started here but found some other tutorials and blog posts with this issue and managed to get it work after implementing custom stuff. This and this SO posts answer some of my questions, but I have one more:
Is there any extension that implements some of the things like REST AuthenticationEntryPoint that returns 401 instead of redirect, or JWT generating and verifying or I should just implement same things for every REST service?
Thank you for your answers.
I also use Springboot but for the security I rely on Apache Shiro project which fundamentally, depending how you store the users accounts (mines are in a MongoDb instance),
takes care of the login - currentUser.login(token);
If fails throws an exception so you can handle the response
If succeed inject the authentication cookie in the response
For any other request, decode the cookie and inject the user with the proper authorizations
In few words Shiro does not redirect the HTTPRequest because it just care for the security leaving further decision, redirect in your case, to your controller logic.
You can add it to your project with a simple Maven dependence.
#brownies.....
try this one....
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
#Component
public class RESTAuthenticationEntryPoint implements AuthenticationEntryPoint {
#Override
public void commence(final HttpServletRequest request, final HttpServletResponse response, final AuthenticationException authException) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
}
}
#Autowired
private RESTAuthenticationEntryPoint restAuthenticationEntryPoint;
#Override
protected void configure(final HttpSecurity http) throws Exception {
http.cors().and().exceptionHandling().authenticationEntryPoint(restAuthenticationEntryPoint).and().authorizeRequests()......
add above RESTAuthenticationEntryPoint and config in your security configuration class then it will return 401 if auth fails.

How to give different role permissions to same url with different request method types in spring boot secuirty

I am using spring boot security to authenticate and authorize my rest http apis.
I see that we can set authorization setttings like this
import org.springframework.context.annotation.Configuration;
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.config.http.SessionCreationPolicy;
#EnableWebSecurity
#Configuration
class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
// .anyRequest().permitAll()
.antMatchers("/user/health_check/*").permitAll()
.antMatchers("/user/registration").permitAll()
.antMatchers("/user/login","/user/logout").hasAnyRole("USER","ADMIN","MANAGER")
.antMatchers("/admin/*").fullyAuthenticated()
.and().httpBasic()
.and().csrf().disable();
}
}
I wanted to know how to give different permission to different urls which differ in request methods?
Eg:
if i have to two urls like
// GET /api/account/{id}
// POST /api/account/{id}
// PUT /api/account/{id}
i wish to give only admin acces to PUT and user access to GET and both user and admin access to POST.. how do i achieve this?
You can pass request method in antMatchers.
Try with this:
http
.httpBasic().and()
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/api/account/**").hasRole("ADMIN")
.antMatchers(HttpMethod.POST, "/api/account/**").hasRole("ADMIN")
.antMatchers(HttpMethod.PUT, "/api/account/**").hasRole("ADMIN");
You can use #PreAuthorize annotation on controller's methods:
#GetMapping("/api/account/{id}")
#PreAuthorize("hasAutority('readAccountById')")
public Account getAccount(#PathVariable Integer id){
...
}
It works with Spring security context, and you can check user roles, authorities and many more.
For reference have a look at this article https://www.baeldung.com/spring-security-method-security

Preventing spring-security to redirect invalid urls to login page

I've setup a spring-boot + spring-mvc + spring-security project.
Everything work as expected right now except for the invalid urls.
If I issue:
http://siteaddr.com/invalid/url
I expect to see my 404 not found page, but spring-security redirects to login page first and after authentication shows 404 not found!
I don't think this is how it should work!
Here is my Websecurity config:
package com.webitalkie.webapp.config;
import java.util.EnumSet;
import javax.servlet.DispatcherType;
import javax.servlet.ServletContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
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.crypto.password.PasswordEncoder;
import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;
import com.webitalkie.webapp.security.DatabaseUserDetailsServic;
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private DatabaseUserDetailsServic userDetailsService;
#Autowired
private ServletContext servletContext;
#Override
protected void configure(HttpSecurity http) throws Exception {
servletContext.getFilterRegistration(AbstractSecurityWebApplicationInitializer
.DEFAULT_FILTER_NAME).addMappingForUrlPatterns(EnumSet
.allOf(DispatcherType.class), false, "/*");
http.csrf().disable();
http.authorizeRequests()
.antMatchers("/home/", "/home", "/home/index", "/", "/favicon.ico").permitAll()
.antMatchers("/contents/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/home/loginsec").failureUrl("/loginsecerror").permitAll()
.and()
.logout()
.logoutUrl("/home/logout")
.logoutSuccessUrl("/home/index")
.invalidateHttpSession(true);
}
#Override
#org.springframework.beans.factory.annotation.Autowired
protected void configure(
org.springframework.security.config.annotation
.authentication.builders.AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(getPasswordEncoder());
}
public PasswordEncoder getPasswordEncoder() {
return new BcryptPasswordEncoder(11);
}
}
Do you guys have any idea?
To customize your particular use case apply the inverted logic as suggested. You could do like this:
1) Replace
.anyRequest().authenticated()
by
.anyRequest().anonymous()
2) Add
.antMatchers("/protected-urls/**").authenticated()
The rule in 2) must come before that in 1) as the first match applies. Unless you have a common url prefix for protected resources you'll have to declare all the authenticated urls one by one.
You can also apply additional configuration overriding the
public void configure(WebSecurity web)...
for example to ignore static resources:
web.ignoring().antMatchers("/favicon.ico", "*.css")
Hope that helps.
This is a security feature, not a problem.
Your security model is "deny all unless explicitly allowed". If a request path is protected (i.e. doesn't match an explicitly permitAll path), then you would not want to reveal that it does not exist until the user was authenticated. In certain situations the 404 could leak private information
.../user/jones is 404? Hmm... something happened to Jones
This is the reason well designed login forms don't tell you "user not found" or "invalid password", and instead just say "invalid credentials" in all failure cases to avoid giving away too much.
The only way to get invalid URLs to bypass security would be to invert your security model, making everything public unless explicitly protected ("allow unless explicitly prohibited"). Which has its own set of issues, such as having to remember to update the definition every time a new root path is created.

Categories