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.
Related
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.
Hi I am now using Angluar + Spring Boot to build a web, I am using okta Single-Page App to do authentication. On my frontend, I can login and redirect back, I also add Authorization: Bearer {Token} in the headers when I call backend api.
So the problem is when I installed okta-spring-boot-starter(1.4.0), and I will get the error:
Bearer error="invalid_token", error_description="An error occurred while attempting to decode the Jwt: Invalid token", error_uri="https://tools.ietf.org/html/rfc6750#section-3.1"
I was thinking this might because I didn't setup okta configuration.
Then I add the okta properties in application.yml file:
spring:
profiles:
active: "dev"
main:
banner-mode: "off"
okta.oauth2:
client-id: 0oaebbeiaBCbFFtDU4x6
issuer: https://dev-574831.okta.com
audience: https://dev-574831.okta.com
and use WebSecurityConfigurerAdapter by refering to: https://github.com/okta/samples-java-spring/blob/master/resource-server/src/main/java/com/okta/spring/example/ResourceServerExampleApplication.java
import com.okta.spring.boot.oauth.Okta;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
#Configuration
#EnableWebSecurity
public class OktaOAuth2WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/userinfo").permitAll()
.antMatchers("/health").permitAll()
.anyRequest().authenticated()
.and()
.oauth2ResourceServer().jwt();
http.cors();
Okta.configureResourceServer401ResponseBody(http);
}
}
Still I am getting the same error.
So do I miss something?? or can okta configuration cannot be recognized by my project?
Try this:
okta:
oauth2:
client-id: 0oaebbeiaBCbFFtDU4x6
issuer: https://dev-574831.okta.com/oauth2/default
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
I'm trying to get acclimated with oauth client_credential grant type, to access a protected resource (I'm using this resource server: https://bitbucket.org/hascode/spring-oauth2-example) which works fine given user info existing (implicit flow).
From my understanding, client_credentials has no user info, so it's failing on the userinfo endpoint returning a 401
2016-07-11 15:19:37.335 INFO 9747 --- [nio-9001-exec-2] o.s.b.a.s.o.r.UserInfoTokenServices : Getting user info from: http://rain.okta1.com:1802/oauth2/v1/userinfo
2016-07-11 15:19:37.421 INFO 9747 --- [nio-9001-exec-2] o.s.b.a.s.o.r.UserInfoTokenServices : Could not fetch user details: class org.springframework.web.client.HttpClientErrorException, 401 Unauthorized
I'm also a Spring newbie, so not sure if there are some configs I need to set for this grant type, as I just was going by the tutorial from the same guy: http://www.hascode.com/2016/03/setting-up-an-oauth2-authorization-server-and-resource-provider-with-spring-boot/
There wasn't much config, this is the full resource
package com.hascode.tutorial;
import java.util.UUID;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Scope;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
#SpringBootApplication
#RestController
#EnableResourceServer
public class SampleResourceApplication {
public static void main(String[] args) {
SpringApplication.run(SampleResourceApplication.class, args);
}
#RequestMapping("/")
public String securedCall() {
return "success (id: " + UUID.randomUUID().toString().toUpperCase() + ")";
}
}
In an oauth2 context, you have a client and a resource owner.
With the implicit grant type, the resource owner is the user who uses your application (the client). The access token you get allow you to retrieve resources owned by the user.
With the client credentials grant type, the resource owner is the client. So the access token you get allow you to get resources owned by the client.
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;
}
}