spring boot end point works with browser but not with postman - java

I am new to spring security, I have written test endpoint with google oauth2, I can authencitace with web browser and my end point works, but not working with postman,
here is my properties
spring.security.oauth2.client.registration.google.client-id={{CLIENT_ID}}
spring.security.oauth2.client.registration.google.client-secret={{SECRET}}
spring.security.oauth2.client.registration.google.scope=openid,profile,email
Here is my controller
#RestController
public class UserController {
#GetMapping()
public String get() {
return "testing java 18";
}
}
Here is my security config
#Configuration
#EnableWebSecurity(debug = true)
public class SecurityConfig {
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
.oauth2Login(withDefaults());
return http.build();
}
}
with above configuration, if I test http://localhost:8080 on browser it redirects to google signing and the text testing java 18 appears.
But when I use postman with below config
Type: `OAuth 2.0`
Add authorization to data to: `Request Header`
Configure New Token:
Token Name: {{SOME_RANDOM_NAME}}
Grant Type: `Authorization Code`
Callback URL: `https://oauth.pstmn.io/v1/callback`
Auth URL: `https://accounts.google.com/o/oauth2/auth`
Access Token URL: `https://oauth2.googleapis.com/token`
Client ID: `{{CLIENT_ID}}` same used in application.properties
Client Secret: `{{CLIENT_SECRET}}` same used in application.properties
Scope: `profile email openid`
State: empty
Client Authentication: `Send as basic auth header`
With above if I hit Get New Access Token, I do get new Access Token and id_token with google signing, after I hit Use Token I and send GET on the endpoint I get 403
Also If I use id_token and change Type to Bearer Token I get the same.
with older version If I use below Securityy config, Endpoint works when I send my id_token as Type to Bearer Token
#Configuration
#EnableWebSecurity(debug = false)
public class SecurityConfig extends WebSecurityConfigurerAdapter{
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/**").fullyAuthenticated()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.oauth2ResourceServer().jwt();
}
}
I tried searching in the forum but didn't; got much.

Related

Oauth2 jwt token enriched with userinfo enpoint

I have a spring application that exposes some webflux endpoints, I use a jwt token to authorize the post calls but we I also need the information given by the userinfo endpoint.
I have a SecurityWebFilterChain bean right now and we are using an oauth2ResourceServer configuration then calling the userinfoendpoint for further checks.
What is the best way to validate a jwt token then get the userinfo enpoint information for further validations?
ps: the authorization server is a third part one.
Security configuration without the external call for the user-info
#Bean
public SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
http
.cors()
.and()
.httpBasic().disable()
.formLogin().disable()
.csrf().disable()
.logout().disable()
.oauth2Client()
.and()
.authorizeExchange()
.pathMatchers(HttpMethod.POST).authenticated()
.anyExchange().permitAll()
.and().oauth2ResourceServer().jwt()
;
return http.build();
}
The UserInfo Endpoint is part of OpenID Connect 1.0, and returns user information for an access token. It is not automatically called from a resource server (http.oauth2ResourceServer()) by Spring Security.
Based on your security configuration, it looks like you're wanting to use both OAuth2 Client (http.oauth2Client()) and OAuth2 Resource Server (http.oauth2ResourceServer()) in the same application. OAuth2 Client isn't designed for this use case (calling UserInfo from a resource server), and therefore would require customization to be adapted for such a case. Instead, you can simply use a RestTemplate or WebClient to call the UserInfo endpoint yourself.
You can do this in a custom Converter<Jwt, Collection<GrantedAuthority>> (or in this case the reactive version) like so:
#Configuration
#EnableWebFluxSecurity
public class SecurityConfiguration {
#Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
// #formatter:off
http
.authorizeExchange((authorize) -> authorize
.anyExchange().authenticated()
)
.oauth2ResourceServer((oauth2) -> oauth2
.jwt((jwt) -> jwt
.jwtAuthenticationConverter(jwtAuthenticationConverter())
)
);
// #formatter:on
return http.build();
}
private Converter<Jwt, Mono<AbstractAuthenticationToken>> jwtAuthenticationConverter() {
ReactiveJwtAuthenticationConverter converter = new ReactiveJwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter());
return converter;
}
private Converter<Jwt, Flux<GrantedAuthority>> jwtGrantedAuthoritiesConverter() {
JwtGrantedAuthoritiesConverter delegate = new JwtGrantedAuthoritiesConverter();
return (jwt) -> getUserInfo(jwt.getTokenValue())
.flatMapIterable((userInfo) -> {
Collection<GrantedAuthority> authorities = delegate.convert(jwt);
// TODO: Add authority from userInfo...
return authorities;
});
}
private Mono<Map<String, String>> getUserInfo(String accessToken) {
// TODO: Call user info and extract one or more claims from response
return Mono.just(new HashMap<>());
}
}
It is way more efficient to decode the data from a JWT than querying an external endpoint.
As a consequence, the best option is to configure the authorization-server (even if it is 3rd party) to enrich the JWTs (access and ID tokens) with the data you need for authorization. Most OIDC authorization-servers support it, just refer to its documentation (Keycloak, Auth0, Cognito, ...).
Once all the claims you need are in access-tokens, you can read it on resource-server from the JwtAuthenticationToken instance (or OAuth2AuthenticationToken for client app with oauth2login) in the security-context. This allows to write stuff like:
#PostMapping("/answers/{subject}")
#PreAuthorize("#userSubject == #auth.token.claims['sub']")
public ResponseEntity<String> addAnswer(#PathVariable("subject") String userSubject, #RequestBody #Valid AnswerDto, JwtAuthenticationToken auth) {
...
}

Create principal from JWT and set it to SecurityContext

In one system, I generated the JWT token as follows:
List securityGroups = Arrays.asList("group1");
Map<String, Object> claims = Map.of("username", "user1", "securityGroups", securityGroups);
String token = Jwts.builder()
.setClaims(claims).setSubject("user1").setAudience("web")
.setIssuedAt(<date now>).setExpiration(<some expiration>)
.signWith(SignatureAlgorithm.HS512, "mysecret").compact();
In another application, I want to decrypt this token and set in the SecurityContext. First, I have security config as follows:
#Configuration
public class SecurityConf extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disabled().authorizeRequests()
.antMatchers("/principal").permitAll()
.anyRequests().authenticated();
}
}
With this, when user requests for an endpoint, say /books, the browser will receive 403 error.
Then I implement the /principal to use this token as an authenticated user in spring.
#RequestMapping("/principal")
public class PrincipalController {
#PostMapping
public void setPrincipal(#RequestBody String token) {
// i'm able to decrypt the token here
// use token to create principal
Authentication authentication = ....
SecurityContextHolder.getContext().setAuthentication(...)
}
}
I'm thinking once I set this in SecurityContext, for succeeding request where I attach the token in the Authorization header, I won't be getting anymore 403 or 401 error since user is authenticated and Spring knows that the token corresponds to the principal in the context. But this part I am not sure how to do it. Please advise.
You can achieve this with spring-security built-it JWT support for a server secured with JWT.
First, for a spring-boot application you'll need a dependency:
for Gradle:
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server:2.7.4'
for Maven:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
<version>2.7.4</version>
</dependency>
Then in any configuration class create beans of JwtDecoder type and a converter to convert some JWT claim to a collection of GrantedAuthority.
In your case it can be done like this:
#Bean
public JwtDecoder jwtDecoder() {
final SecretKey key = new SecretKeySpec("mysecret".getBytes(), JwsAlgorithms.HS512);
final NimbusJwtDecoder decoder = NimbusJwtDecoder.withSecretKey(key).build();
return decoder;
}
#Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
final JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
grantedAuthoritiesConverter.setAuthoritiesClaimName("securityGroups");
grantedAuthoritiesConverter.setAuthorityPrefix("");
final JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}
Then just let your application know that you want to use this support by additional security filter chain configuration:
#Configuration
public class SecurityConf {
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disabled().authorizeRequests()
.antMatchers("/principal").permitAll()
.anyRequest().authenticated()
.and()
.oauth2ResourceServer(oauth2Server -> oauth2Server.jwt());
return http.build();
}
}
Note that I don't use WebSecurityConfigurerAdapter because it's deprecated now.
Above configuration will create a filter which will try to authorize users before reaching your controller endpoints and will throw 401 if there's no JWT or it's expired or invalid for other reasons.
So you won't need a separate endpoint like "/principal" for "login" with a token, because Spring will create an Authentication object in a SecurityContextHolder for every request.
Moreover, this configuration will let you authorize users depending on their "securityGroups", so if you decide to configure some role-based or authority-based access to some endpoints Spring will check it for you and return 403 if authorities are insufficient.

Spring security on endpoint using Cognito IAM role?

I am trying to restrict specific endpoints on a Spring boot service depending on what role they have set in the OAuth2 credentials.
This is the endpoint
#RestController
#RequestMapping("/api/admin")
public class AdminController {
#GetMapping(produces = "application/json")
public TestResponse get() {
return new TestResponse("Admin API Response");
}
}
This is then secured using SecurityConfiguration bean
#Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf()
.and()
.authorizeRequests()
.antMatchers("/login", "/", "/home", "/logout", "/ping").permitAll()
.antMatchers("/api/admin").hasRole("arn:aws:iam::xxxxxx:role/spring-sso-test-ADMIN")
.antMatchers("/api/user").hasRole("arn:aws:iam::xxxxxx:role/spring-sso-test-USER")
.and()
.oauth2Login()
.and()
.logout()
.logoutSuccessUrl("/logout");
}
}
I debugged the Principal and can see the correct IAM role in the list of attributes cognito:roles list
However when I hit the endpoint I get a HTTP 403 Unauthorized. Meaning that the user has authenticated successfully, but Spring does not recognize or understand the attributes or how to map them?
I tried using the #Secured annotation but that didn't change anything.
#Secured("arn:aws:iam::xxxxxx:role/spring-sso-test-ADMIN")
#GetMapping(produces = "application/json")
public TestResponse get() {
return new TestResponse("Admin API Response");
}
How do I allow this to work using an IAM role defined in AWS Cognito?
When you use the hasRole DSL method, Spring Security adds the ROLE_ prefix to your authority. So, the authority arn:aws:iam::xxxxxx:role/spring-sso-test-ADMIN will become ROLE_arn:aws:iam::xxxxxx:role/spring-sso-test-ADMIN.
You should use the hasAuthority method instead.
Additionally, you should take the cognito:roles from the attributes and add in the authorities, since it's the property that Spring Security will query to get the authorities.
To map the authorities you can use a OAuth2UserService:
#Bean
SecurityFilterChain app(HttpSecurity http) throws Exception {
http
.oauth2Login(oauth2 -> oauth2
.userInfoEndpoint(userInfo -> userInfo
.oidcUserService(this.oidcUserService())
...
)
);
return http.build();
}
private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
// your custom implementation
}
More details in the documentation.

spring security HTTP Status 403 - Access Denied

Login is success but spring security blocking url even i given access to USER . How can i manage this thing?
#Configuration
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
public void configureGlobalSecurity(AuthenticationManagerBuilder auth)
throws Exception {
auth.inMemoryAuthentication().withUser("sahil").password("123")
.roles("ADMIN","USER");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/login").permitAll()
.antMatchers("/welcome","/inventory/**","/sales/**").access("hasRole('USER')")
.and()
.csrf().disable();
}
LoginController.java
#Controller
public class LoginController {
#RequestMapping(value = { "/", "/login" }, method = RequestMethod.GET)
public String showLoginPage() {
return "login";
}
#RequestMapping(value = "/login", method = RequestMethod.POST)
public String handleUserLogin(ModelMap model, #RequestParam String name, #RequestParam String password) {
if (!service.validateUser(name, password)) {
model.put("errorMsg", "Invalid Credential");
return "login";
}
System.out.println("principal : " + getLoggedInUserName());
model.put("name", name);
model.put("password", password);
return "welcome";
}
private String getLoggedInUserName() {
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
System.out.println("in if");
return ((UserDetails)principal).getUsername();
} else {
System.out.println("in else");
return principal.toString();
}
}
#RequestMapping(value = "/welcome", method = RequestMethod.GET)
public String showWelcomeDashboard() {
return "welcome";
}
}
1 . Once Login success page redirected to welcome page but url is still localhost:8080/login instead of localhost:8080/welcome.
2. After redirecting to URL localhost:8080/sales is it 403 Access denied.
What is spring security
Spring security is all about authentication and authorization, in your case you are missing authentication. There is no configuration of authentication in your security configuration. What you are missing is authentication filter for your spring security. Spring security provides default authentication filter UsernamePasswordAuthenticationFilter that can be configured by .formLogin(). You can use default provided or you can define your own custom authentication filter(Implementation of UsernamePasswordAuthenticationFilter).
Once authentication is success spring security will grant authorities for authenticated user. If authentication is configured correctly, below configuration is responsible for authentication and granting authority
auth.inMemoryAuthentication().withUser("sahil").password("123")
.roles("ADMIN","USER");
Authenticated users each request will be passed through filter FilterSecurityInterceptor and it will verifies authority granted for authenticated user with authorization configured for resources as given in below code.
.antMatchers("/welcome","/inventory/**","/sales/**").access("hasRole('USER')")
You missed all this by not configuring authentication filter.
Now for making it simple use.formLogin() in your http configuration.
#Override
protected void configure(final HttpSecurity http) throws Exception
{
http
.authorizeRequests()
.antMatchers("/welcome","/inventory/**","/sales/**").access("hasRole('USER')")
.and().exceptionHandling()
.accessDeniedPage("/403")
.and().formLogin()
.and().logout()
.logoutSuccessUrl("/login?logout=true")
.invalidateHttpSession(true)
.and()
.csrf()
.disable();
}
.formLogin() without any configuration provides default login page with username and password default form parameters.And after authentication it redirects to "/" If you want to provide your custom login page then use below configuration.
.and().formLogin()
.loginPage("/login")
.usernameParameter("email").passwordParameter("password")
.defaultSuccessUrl("/app/user/dashboard")
.failureUrl("/login?error=true")
.loginPage("") - Your custom login page URL
.usernameParameter("").passwordParameter("") - Your custom login form parameters
.defaultSuccessUrl("") - Page url after successful authentication
.failureUrl("") - Page url after authentication failure
Note: You should not use "/login" POST method in your controller, Even though if you write, it will not be reached from spring security filter chain. As your configuration was wrong before, it was reaching before! Now you remove those from your controller and use conventional approach as mentioned above.

How to disable basic http auth for OPTIONS with SpringBoot Security

I have small rest service that is protected with default Spring Boot security config. It by default requires authorization on every http method including OPTIONS, chrome however does not give a flying duck and won't include authorization header in preflight request which results in 401 response.
How can I disable http basic auth on specific method? So far I tried:
#Configuration
#EnableWebMvc
#EnableGlobalMethodSecurity(securedEnabled = true)
public class Config {
}
And in controller:
#CrossOrigin(origins = {"http://localhost:4200"}, maxAge = 4800)
#RestController
public class MainController {
#Secured("IS_AUTHENTICATED_ANONYMOUSLY")
#RequestMapping(method = RequestMethod.OPTIONS)
public ResponseEntity handle() {
return new ResponseEntity(HttpStatus.OK);
}
}
Did not work obviously.
In the method
#Override
protected void configure(HttpSecurity http) throws Exception
{
add
.antMatchers(HttpMethod.OPTIONS, "/path/to/skip/check").permitAll()

Categories