Secure Spring boot Rest Api
-
Hi all,
I'm trying to integrate Spring with FusionAuth. Following this tutorial: https://fusionauth.io/blog/2018/10/24/easy-integration-of-fusionauth-and-spring/ I managed to successfully authenticate and authorize different users with different roles for an UI application and it's resources.I also try to do the same (appropriate authentication and authorization) for Spring Rest API. Using the same configuration I do not manage to access certain endpoints that are annotated with "@PreAuthorize("hasAuthority('user') or hasAuthority('admin')")". I'm getting "403 Forbidden" error.
So, I run an Api call using oAuth2 authentication. The client (in my case Postman) generates token after proper authentication into FusionAuth (using credentials form user registered into the app having the required roll). Then this token I set into call's header. However, I'm getting "403 Forbiden". I manage to access the endpoint only then when it is annotated as "@PreAuthorize("permitAll()")".
Could somebody give a brief explanation about how should I approach and properly configure my Rest Controller ?
Thanks in advance!
-
Hi @konstantin-dzekov ,
Welcome to the FusionAuth community!
What does your JWT look like? You can customize it with the JWT populate lambda, documented here.
This answer indicates you need to add an
scp
claim (if you are using spring security 5.2): https://stackoverflow.com/questions/59379645/spring-security-5-populating-authorities-based-on-jwt-claimsThis older answer says that the spring framework looks for an
authorities
claim: https://stackoverflow.com/questions/55609083/how-to-set-user-authorities-from-user-claims-return-by-an-oauth-server-in-spring/56259665But in general it appears from some looking around that you need to figure out what claims the JWT is expected to provide and then map the FusionAuth roles (which are assigned to users registered to an application, or in a group and registered to an application) to the correct claims the framework expects.
Let us know what you find, please!
-
HI @dan ,
So, I reconfigured my application following the solution given in: https://stackoverflow.com/questions/55609083/how-to-set-user-authorities-from-user-claims-return-by-an-oauth-server-in-spring/56259665
Basically I have the following classes for JWT configuration:import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; import org.springframework.security.oauth2.provider.token.RemoteTokenServices; @Configuration @EnableResourceServer public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { @Value("${fusionAuth.accessTokenUri}") private String accessTokenUri; @Value("${fusionAuth.clientId}") private String clientId; @Value("${fusionAuth.clientSecret}") private String clientSecret; @Override public void configure(final HttpSecurity http) throws Exception { // @formatter:off http.authorizeRequests() .anyRequest().access("hasAnyAuthority('Admin')"); } @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { resources.resourceId("arawaks"); } @Bean @Primary public RemoteTokenServices tokenServices() { final RemoteTokenServices tokenServices = new RemoteTokenServices(); tokenServices.setClientId(clientId); tokenServices.setClientSecret(clientSecret); tokenServices.setCheckTokenEndpointUrl(accessTokenUri); tokenServices.setAccessTokenConverter(accessTokenConverter()); return tokenServices; } @Bean public CustomAccessTokenConverter accessTokenConverter() { final CustomAccessTokenConverter converter = new CustomAccessTokenConverter(); converter.setUserTokenConverter(new CustomUserTokenConverter()); return converter; } }
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.oauth2.provider.token.UserAuthenticationConverter; import org.springframework.util.StringUtils; import java.util.Collection; import java.util.LinkedHashMap; import java.util.Map; public class CustomUserTokenConverter implements UserAuthenticationConverter { private Collection<? extends GrantedAuthority> defaultAuthorities; private UserDetailsService userDetailsService; private final String AUTHORITIES = "role"; private final String USERNAME = "preferred_username"; private final String USER_IDENTIFIER = "sub"; public CustomUserTokenConverter() { } public void setUserDetailsService(UserDetailsService userDetailsService) { this.userDetailsService = userDetailsService; } public void setDefaultAuthorities(String[] defaultAuthorities) { this.defaultAuthorities = AuthorityUtils.commaSeparatedStringToAuthorityList(StringUtils.arrayToCommaDelimitedString(defaultAuthorities)); } public Map<String, ?> convertUserAuthentication(Authentication authentication) { Map<String, Object> response = new LinkedHashMap(); response.put(USERNAME, authentication.getName()); if (authentication.getAuthorities() != null && !authentication.getAuthorities().isEmpty()) { response.put(AUTHORITIES, AuthorityUtils.authorityListToSet(authentication.getAuthorities())); } return response; } public Authentication extractAuthentication(Map<String, ?> map) { if (map.containsKey(USER_IDENTIFIER)) { Object principal = map.get(USER_IDENTIFIER); Collection<? extends GrantedAuthority> authorities = this.getAuthorities(map); if (this.userDetailsService != null) { UserDetails user = this.userDetailsService.loadUserByUsername((String)map.get(USER_IDENTIFIER)); authorities = user.getAuthorities(); principal = user; } return new UsernamePasswordAuthenticationToken(principal, "N/A", authorities); } else { return null; } } private Collection<? extends GrantedAuthority> getAuthorities(Map<String, ?> map) { if (!map.containsKey(AUTHORITIES)) { return this.defaultAuthorities; } else { Object authorities = map.get(AUTHORITIES); if (authorities instanceof String) { return AuthorityUtils.commaSeparatedStringToAuthorityList((String)authorities); } else if (authorities instanceof Collection) { return AuthorityUtils.commaSeparatedStringToAuthorityList(StringUtils.collectionToCommaDelimitedString((Collection)authorities)); } else { throw new IllegalArgumentException("Authorities must be either a String or a Collection"); } } } }
and
import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter; import org.springframework.stereotype.Component; import java.util.Map; @Component public class CustomAccessTokenConverter extends DefaultAccessTokenConverter { @Override public OAuth2Authentication extractAuthentication(Map<String, ?> claims) { OAuth2Authentication authentication = super.extractAuthentication(claims); authentication.setDetails(claims); return authentication; } }
-
An it seems that there is some progress. I'm getting: "401 Anauthorized" response with this body:
{ "error": "invalid_token", "error_description": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjA5MDkzNmZhZSJ9.eyJhdWQiOiIyYmVhNTQzNS1hZGEwLTQ0Y2QtYTJmMi00OTM1ZDYzMWQ2ZDgiLCJleHAiOjE2MTI4ODY4ODYsImlhdCI6MTYxMjg4MzI4NiwiaXNzIjoiYWNtZS5jb20iLCJzdWIiOiJmZGZlYzExZi02ZGExLTRmN2YtOGIwZi0xMGFhMzhjNTg4ZWQiLCJqdGkiOiJkZTZmNTg2Yi0xNzlhLTRiMTUtOTA1OC1iNWEyNDc5ZmU1ZTYiLCJhdXRoZW50aWNhdGlvblR5cGUiOiJQQVNTV09SRCIsImVtYWlsIjoia29uc3RhbnRpbi5kemVrb3ZAc2VtYW50aWMtd2ViLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJhcHBsaWNhdGlvbklkIjoiMmJlYTU0MzUtYWRhMC00NGNkLWEyZjItNDkzNWQ2MzFkNmQ4Iiwicm9sZXMiOlsiYWRtaW4iXX0.82M8lLFe2hjmNuEe3hQ9bdGxXj3WeZkHplUwKuVfR9I" }
In order to get the token I use this endpoint: {fusionauth-host}/api/login, with the body:
{ "loginId": "konstantin.dzekov@semantic-web.com", "password": "fa-kdzekov-21", "applicationId": "2bea5435-ada0-44cd-a2f2-4935d631d6d8", "noJWT" : false, "ipAddress": "localhost" }
For which I'm getting this token:
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjA5MDkzNmZhZSJ9.eyJhdWQiOiIyYmVhNTQzNS1hZGEwLTQ0Y2QtYTJmMi00OTM1ZDYzMWQ2ZDgiLCJleHAiOjE2MTI4ODY4ODYsImlhdCI6MTYxMjg4MzI4NiwiaXNzIjoiYWNtZS5jb20iLCJzdWIiOiJmZGZlYzExZi02ZGExLTRmN2YtOGIwZi0xMGFhMzhjNTg4ZWQiLCJqdGkiOiJkZTZmNTg2Yi0xNzlhLTRiMTUtOTA1OC1iNWEyNDc5ZmU1ZTYiLCJhdXRoZW50aWNhdGlvblR5cGUiOiJQQVNTV09SRCIsImVtYWlsIjoia29uc3RhbnRpbi5kemVrb3ZAc2VtYW50aWMtd2ViLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJhcHBsaWNhdGlvbklkIjoiMmJlYTU0MzUtYWRhMC00NGNkLWEyZjItNDkzNWQ2MzFkNmQ4Iiwicm9sZXMiOlsiYWRtaW4iXX0.82M8lLFe2hjmNuEe3hQ9bdGxXj3WeZkHplUwKuVfR9I".
What bothers me (I also found something on other stack-overflow threads) that:
@Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { resources.resourceId("arawaks"); }
" resources.resourceId("arawaks")" might causes the issue. To be honest, I don't know how to get appropriate value for the resourceId.
-
Those tokens look similar, and when I decode the token I get:
{ "aud": "2bea5435-ada0-44cd-a2f2-4935d631d6d8", "exp": 1612886886, "iat": 1612883286, "iss": "acme.com", "sub": "fdfec11f-6da1-4f7f-8b0f-10aa38c588ed", "jti": "de6f586b-179a-4b15-9058-b5a2479fe5e6", "authenticationType": "PASSWORD", "email": "konstantin.dzekov@example.com", "email_verified": true, "applicationId": "2bea5435-ada0-44cd-a2f2-4935d631d6d8", "roles": [ "admin" ] }
(Info in JWTs are public, you might want to hide your email address as I did.)
Does that have all the claims you expect? From looking at this tutorial and the JWT generated, it appears that spring expects the resourceId to be the audience.
So could you try
@Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { resources.resourceId("2bea5435-ada0-44cd-a2f2-4935d631d6d8"); }
And see if that helps?
-
{
"aud": "2bea5435-ada0-44cd-a2f2-4935d631d6d8",
"exp": 1612886886,
"iat": 1612883286,
"iss": "acme .com",
"sub": "fdfec11f-6da1-4f7f-8b0f-10aa38c588ed",
"jti": "de6f586b-179a-4b15-9058-b5a2479fe5e6",
"authenticationType": "PASSWORD",
"email": "konstantin.dzekov@example.com",
"email_verified": true,
"applicationId": "2bea5435-ada0-44cd-a2f2-4935d631d6d8",
"roles": [
"admin"
]
}