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.
Related
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) {
...
}
I have a problem with my spring boot application (version 2.6.3).
I have configured reactive spring security like there:
MyApplication.java:
#SpringBootApplication
#EnableWebFlux
#EnableWebFluxSecurity
#EnableReactiveMethodSecurity
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class);
}
#Bean
public SecurityWebFilterChain springSecurityFilterChain(final ServerHttpSecurity http, final ReactiveOpaqueTokenIntrospector reactiveOpaqueTokenIntrospector) {
return http.authorizeExchange()
.anyExchange().authenticated()
.and()
.httpBasic().disable()
.cors().and()
.logout().disable()
.formLogin().disable()
.oauth2ResourceServer()
.opaqueToken()
.introspector(reactiveOpaqueTokenIntrospector)
.and().and()
.csrf()
.disable()
.build();
}
}
And this is my web resource (controller):
MyWebResource.java:
#RestController
public class MyWebResource implements MyWebResourceApi {
#PreAuthorize("hasRole('ROLE_USER')")
#Override
public Mono<String> details(String userId, ServerWebExchange exchange) {
return exchange.getPrincipal().map(Principal::getName);
}
}
It's work fine, when my access token is expired or incorrect the request should be denied. However when PreAuthorized allow request, my user principal will be never resolved in my exchange...
In reactive application authentication information is stored in the Reactive flow and accessible from Mono/Flux. You could use ReactiveSecurityContextHolder to obtain the currently authenticated principal, or an authentication request token.
#PreAuthorize("hasRole('ROLE_USER')")
public Mono<String> details() {
return ReactiveSecurityContextHolder.getContext()
.map(ctx -> ((Principal) ctx.getAuthentication().getPrincipal()).getName());
}
I found this answer looking for a way to add the access token to my webclient requests. If we are using OAuth2 or OpenID Connect and want to access the token instead of the principal's name, then this is not possible via the principal in the security context.
Instead we need to create a ServerOAuth2AuthorizedClientExchangeFilterFunction and register it as a filter function to the WebClient:
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth =
new ServerOAuth2AuthorizedClientExchangeFilterFunction(
clientRegistrations,
authorizedClients);
oauth.setDefaultOAuth2AuthorizedClient(true);
where clientRegistrations is an injectabile bean of type ReactiveClientRegistrationRepository and authorizedClients is an injectable bean of type ServerOAuth2AuthorizedClientRepository.
We can then use the filters method for the builder to add our filter function to the exchangeFilterFunctions:
WebClient.builder()
.filters(exchangeFilterFunctions -> {
exchangeFilterFunctions.add(oauth);
})
.build();
Baeldung has a nice background article about this, which explains it in more detail: https://www.baeldung.com/spring-webclient-oauth2
In my controller I have two endpoints where one has PreAuthorize annotation and the other does not:
#GetMapping
public UserResponse getUser(){
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
//get and return User...
}
#PreAuthorize("hasRole('ROLE_ADMIN')")
#PostMapping
public UserResponse createUser(#RequestBody UserRequestDetails userDetails){...}
Secured endpoint its ok, works only when I am logged and token with right role is placed in request header. But when I want access to endpoint without PreAuthorize annotation I always got status 403 forbidden. I want access to the getUser endpoint when users are logged and regardless of the possible roles they have.
Here is my security configuration:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurity extends WebSecurityConfigurerAdapter{
#Override
protected void configure(HttpSecurity http) throws Exception{
http.csrf().disable()
.authorizeRequests()
.antMatchers(HttpMethod.POST, "/login").permitAll()
.anyRequest().authenticated()
.and()
.addFilterBefore(getAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(jwtAuthorizationFilterBean(), UsernamePasswordAuthenticationFilter.class)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().cors();
}
}
thank!
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.
I'm looking for the proper way to add role based authentication where I extract out roles from a JWT.
Ideally, I would like to extract roles from the JWT after authentication has taken place. This will work by inspecting the web token for some fields related to the roles which we are getting from our authentication system, keycloak.
My question is: is it possible to append roles to a request and then use http configuration to require one of these extracted roles?
Below is some relevant code that will help explain what I'm doing.
In my WebSecurityConfigurer, I make the access token available, scoped per request.
#Bean
#Scope(scopeName = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public AccessToken accessToken() {
try {
HttpServletRequest request =
((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes())
.getRequest();
return ((KeycloakSecurityContext) ((KeycloakAuthenticationToken) request
.getUserPrincipal())
.getCredentials()).getToken();
} catch (Exception exc) {
return null;
}
}
Then I override some of the configuration of the http in the configure method.
http
// Disable session management
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// Allow calls to OPTIONS
.authorizeRequests().antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.and()
// Authenticate every other call
.authorizeRequests().anyRequest().authenticated()
.and()
.csrf().disable();
Ideally, what I'd like to achieve is something like:
http.antMatchers("/foo").hasRole("jwt_extracted_role")
I am currently creating custom filters which extract roles from the token and then check for the correct roles, but this is maybe more cumbersome than it needs to be.
Any pointers on which methods of which configuration classes I should be looking to override to extract the roles from the request's, and add them to the request?
I ended up solving this by overriding the KeycloakAuthenticationProvider and providing my override class as a bean in the WebSecurityConfig. My class is below:
public class ResourceAwareAuthenticationProvider extends KeycloakAuthenticationProvider {
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
... here I add granted authorities from the token's credentials ...
}
}
Then in my class WebSecurityConfigurer extends KeycloakWebSecurityConfigurerAdapter I override the AuthenticationProvider:
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return new ProviderManager(Lists.newArrayList(new ResourceAwareAuthenticationProvider()));
}
This allows me to do configuration like:
http.authorizeRequests()
.antMatchers("/**").hasAuthority("my-resource-authority")