Spring webflux: ReactiveUserDetails is ignored - spring-security

Here my security configuration:
#Configuration
#EnableWebFluxSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration {
#Bean
public ReactiveJwtDecoder reactiveJwtDecoder() throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec("JAC1O17W1F3QB9E8B4B1MT6QKYOQB36V".getBytes(), mac.getAlgorithm());
return NimbusReactiveJwtDecoder.withSecretKey(secretKey)
.macAlgorithm(MacAlgorithm.HS256)
.build();
}
#Bean
public ReactiveUserDetailsService userDetailsService(
UsuariRepository usuariRepository,
UserDetailsMapper userDetailsMapper
) {
return new GitUserDetailsService(usuariRepository, userDetailsMapper);
}
#Bean
SecurityWebFilterChain springSecurityFilterChain(
ServerHttpSecurity http
) {
final CorsConfigurationSource configurationSource = serverWebExchange -> {
final var cc = new CorsConfiguration();
cc.addAllowedOrigin("*");
cc.addAllowedMethod("*");
cc.addAllowedHeader("*");
return cc;
};
Customizer<CorsSpec> corsCustomizer = (corsSpec) -> corsSpec.configurationSource(configurationSource);
return http
.httpBasic(HttpBasicSpec::disable)
.cors(corsCustomizer)
.csrf(CsrfSpec::disable)
.formLogin(FormLoginSpec::disable)
.anonymous(AnonymousSpec::disable)
.logout(LogoutSpec::disable)
.authorizeExchange((authorize) -> authorize
.pathMatchers("/actuator/**").permitAll()
.pathMatchers("/gicar/**").permitAll()
.anyExchange().authenticated()
)
.oauth2ResourceServer(OAuth2ResourceServerSpec::jwt)
.build();
}
}
As you can see, I'm trying to use my UserDetailsService in order to load user details form database.
Into my methods, I'm using:
#RestController
#RequestMapping(value = "/qdcf")
public class QdCFController {
private final QdCFService qdcfService;
#PreAuthorize("hasRole('ADMIN')")
#GetMapping
public Mono<PageableResponseModel<QdCFPresenter>> all(Pageable pageable) {
return this.qdcfService.getQdCFs(pageable);
}
}
I'm trying to reach my code using this request:
curl -s -X GET "http://$BACKEND/qdcf" -H "Authorization: Bearer $JWT_TOKEN"
I was expecting ReactiveUserDetailsService was called. But it's ignored.
Any ideas?

Replace #EnableGlobalMethodSecurity(prePostEnabled = true) with #EnableReactiveMethodSecurity. First one is for non reactive spring security.

Related

How do I get the Access Token via the OAuth2AuthorizedClientService in the Client Credentials Flow?

I have created a client with Spring Security. As Authorization Server I use Keycloak.
Now I want to gain access to the access token, using the parameter annotation #RegisteredOAuth2AuthorizedClient(registrationId = "keycloak")
I get access to the token, but when I go through the OAuth2AuthorizedClientService I only get back NULL, and I don't understand why.
Here is my implementation:
application.properties:
spring.security.oauth2.client.registration.keycloak.client-id=test-client
spring.security.oauth2.client.registration.keycloak.client-secret=b466bd4e-8dec-4af2-b90a-ab3fc06009b6
spring.security.oauth2.client.registration.keycloak.authorization-grant-type=client_credentials
spring.security.oauth2.client.provider.keycloak.issuer-uri=http://localhost:8081/auth/realms/master
SecurityConfiguration.class
#Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter
{
#Override
public void configure(HttpSecurity http) throws Exception
{
http.oauth2Client();
}
}
RestController:
#RestController
#RequestMapping("client")
public class ExampleRestController
{
#Autowired
private OAuth2AuthorizedClientService authorizedClientService;
#GetMapping("resource")
public void runSecure(#RegisteredOAuth2AuthorizedClient(registrationId = "keycloak") OAuth2AuthorizedClient client)
{
var serviceClient = authorizedClientService.loadAuthorizedClient("keycloak", "anonymousUser");
System.out.println(serviceClient.getAccessToken()); // NULL
System.out.println((client.getAccessToken())); // NOT NULL
}
}
Hope for help, thanks :)
EDIT:
Here is my solution to get a flexible access to the access-token using a service:
application.properties:
spring.security.oauth2.client.registration.keycloak.client-id=test-client
spring.security.oauth2.client.registration.keycloak.client-secret=b466bd4e-8dec-4af2-b90a-ab3fc06009b6
spring.security.oauth2.client.provider.keycloak.token-uri=http://localhost:8081/auth/realms/master/protocol/openid-connect/token
Client registration:
#Configuration
public class ClientRegistrationConfiguration
{
private static final String KEYCLOAK = "keycloak";
#Bean
public ClientRegistration clientRegistration(OAuth2ClientProperties properties)
{
return withRegistrationId(KEYCLOAK)
.tokenUri(properties.getProvider().get(KEYCLOAK).getTokenUri())
.clientId(properties.getRegistration().get(KEYCLOAK).getClientId())
.clientSecret(properties.getRegistration().get(KEYCLOAK).getClientSecret())
.authorizationGrantType(CLIENT_CREDENTIALS)
.build();
}
#Bean
public ClientRegistrationRepository clientRegistrationRepository(ClientRegistration clientRegistration)
{
return new InMemoryClientRegistrationRepository(clientRegistration);
}
#Bean
public OAuth2AuthorizedClientService oAuth2AuthorizedClientService(ClientRegistrationRepository clientRegistrationRepository)
{
return new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository);
}
#Bean
public AuthorizedClientServiceOAuth2AuthorizedClientManager authorizedClientServiceOAuth2AuthorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientService authorizedClientService)
{
var authorizedClientProvider = builder().clientCredentials().build();
var authorizedClientManager = new AuthorizedClientServiceOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientService);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
}
My custom token service:
#Service
public class DefaultTokenService implements TokenService
{
#Autowired
private AuthorizedClientServiceOAuth2AuthorizedClientManager clientManager;
#Override
public OAuth2AccessToken getOAuth2AccessToken()
{
return Objects.requireNonNull(clientManager.authorize(withClientRegistrationId("keycloak").principal("Keycloak").build())).getAccessToken();
}
}
You need to autowire OAuth2AuthorizedClientManager bean and explicitly send a token request:
authorizedClientManager.authorize(request)
See https://docs.spring.io/spring-security/reference/servlet/oauth2/client/authorization-grants.html

How can a jwt protected resource server call userinfo?

The documentation at spring security is missing important detail. Our idp does not provide an introspection link, and our resource server is not a client in its own right. It receives JWT access tokens from the actual client, and "needs to know" details about the user associated with the access token.
In our case standard jwt processing gives us a useful start, but we need to fill out the authentication with the claims from userinfo.
How do we 1. get a baseline valid oauth2 authentication, 2. fill it out with the results of the userinfo call.
public class UserInfoOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
private final OpaqueTokenIntrospector delegate =
new NimbusOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret");
private final WebClient rest = WebClient.create();
#Override
public OAuth2AuthenticatedPrincipal introspect(String token) {
OAuth2AuthenticatedPrincipal authorized = this.delegate.introspect(token);
return makeUserInfoRequest(authorized);
}
}
Current implementation using a converter:
#Configuration
public class JWTSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired JwtConverterWithUserInfo jwtConverter;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors()
.and()
.csrf().disable()
.authorizeRequests(authz -> authz
.antMatchers("/api/**").authenticated()
.anyRequest().permitAll())
.oauth2ResourceServer().jwt().jwtAuthenticationConverter(jwtConverter);
}
}
#Configuration
public class WebClientConfig {
/**
* Provides a Web-Client Bean containing the bearer token of the authenticated user.
*/
#Bean
WebClient webClient(){
HttpClient httpClient = HttpClient.create()
.responseTimeout(Duration.ofSeconds(5))
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000);
return WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.filter(new ServletBearerExchangeFilterFunction())
.build();
}
}
#Component
#Log4j2
public class JwtConverterWithUserInfo implements Converter<Jwt, AbstractAuthenticationToken> {
#Autowired WebClient webClient;
#Value("${userinfo-endpoint}")
String userinfoEndpoint;
#SuppressWarnings("unchecked")
#Override
public AbstractAuthenticationToken convert(Jwt jwt) {
String token = jwt.getTokenValue();
log.debug("Calling userinfo endpoint for token: {}", token);
String identityType = jwt.getClaimAsString("identity_type");
Map<String,Object> userInfo = new HashMap<>();
if ("user".equals(identityType)) {
// invoke the userinfo endpoint
userInfo =
webClient.get()
.uri(userinfoEndpoint)
.headers(h -> h.setBearerAuth(token))
.retrieve()
.onStatus(s -> s.value() >= HttpStatus.SC_BAD_REQUEST, response -> response.bodyToMono(String.class).flatMap(body -> {
return Mono.error(new HttpException(String.format("%s, %s", response.statusCode(), body)));
}))
.bodyToMono(Map.class)
.block();
log.debug("User info Map is: {}",userInfo);
// construct an Authentication including the userinfo
OidcIdToken oidcIdToken = new OidcIdToken(jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaims());
OidcUserInfo oidcUserInfo = new OidcUserInfo(userInfo);
List<OidcUserAuthority> authorities = new ArrayList<>();
if (oidcIdToken.hasClaim("scope")) {
String scope = String.format("SCOPE_%s", oidcIdToken.getClaimAsString("scope"));
authorities.add(new OidcUserAuthority(scope, oidcIdToken, oidcUserInfo));
}
OidcUser oidcUser = new DefaultOidcUser(authorities, oidcIdToken, oidcUserInfo, IdTokenClaimNames.SUB);
//TODO replace this OAuth2 Client authentication with a more appropriate Resource Server equivalent
return new OAuth2AuthenticationTokenWithCredentials(oidcUser, authorities, oidcUser.getName());
} else {
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
if (jwt.hasClaim("scope")) {
authorities.add(new SimpleGrantedAuthority(String.format("SCOPE_%s", jwt.getClaimAsString("scope"))));
}
return new JwtAuthenticationToken(jwt, authorities);
}
}
}
public class OAuth2AuthenticationTokenWithCredentials extends OAuth2AuthenticationToken {
public OAuth2AuthenticationTokenWithCredentials(OAuth2User principal,
Collection<? extends GrantedAuthority> authorities,
String authorizedClientRegistrationId) {
super(principal, authorities, authorizedClientRegistrationId);
}
#Override
public Object getCredentials() {
return ((OidcUser) this.getPrincipal()).getIdToken();
}
}
Instead of a custom OpaqueTokenIntrospector, try a custom JwtAuthenticationConverter:
#Component
public class UserInfoJwtAuthenticationConverter implements Converter<Jwt, BearerTokenAuthentication> {
private final ClientRegistrationRepository clients;
private final JwtGrantedAuthoritiesConverter authoritiesConverter =
new JwtGrantedAuthoritiesConverter();
#Override
public BearerTokenAuthentication convert(Jwt jwt) {
// Spring Security has already verified the JWT at this point
OAuth2AuthenticatedPrincipal principal = invokeUserInfo(jwt);
Instant issuedAt = jwt.getIssuedAt();
Instant expiresAt = jwt.getExpiresAt();
OAuth2AccessToken token = new OAuth2AccessToken(
BEARER, jwt.getTokenValue(), issuedAt, expiresAt);
Collection<GrantedAuthority> authorities = this.authoritiesConverter.convert(jwt);
return new BearerTokenAuthentication(principal, token, authorities);
}
private OAuth2AuthenticatedPrincipal invokeUserInfo(Jwt jwt) {
ClientRegistration registration =
this.clients.findByRegistrationId("registration-id");
OAuth2UserRequest oauth2UserRequest = new OAuth2UserRequest(
registration, jwt.getTokenValue());
return this.oauth2UserService.loadUser(oauth2UserRequest);
}
}
And then wire into the DSL like so:
#Bean
SecurityFilterChain web(
HttpSecurity http, UserInfoJwtAuthenticationConverter authenticationConverter) {
http
.oauth2ResourceServer((oauth2) -> oauth2
.jwt((jwt) -> jwt.jwtAuthenticationConverter())
);
return http.build();
}
our resource server is not a client in its own right
oauth2-client is where Spring Security's support for invoking /userinfo lives and ClientRegistration is where the application's credentials are stored for addressing /userinfo. If you don't have those, then you are on your own to invoke the /userinfo endpoint yourself. Nimbus provides good support, or you may be able to simply use RestTemplate.

RSocket Java Spring #AuthenticationPrincipal with JWT

How can we use #AuthenticationPrincipal with a RSocket Method #AuthenticationPrincipal Mono token
public Mono<String> uppercase(String s, #AuthenticationPrincipal Mono<JwtAuthenticationToken> token) {
//Token is always null
return Mono.just(s.toUpperCase());
}
I created a RSocketSecurityConfiguration class:
#Configuration
#EnableRSocketSecurity
#EnableReactiveMethodSecurity
#Slf4j
public class RSocketSecurityConfiguration {
#Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
private String issuerUri;
#Bean
PayloadSocketAcceptorInterceptor rsocketInterceptor(RSocketSecurity rsocket) {
rsocket
.authorizePayload(authorize ->
authorize
.anyRequest().authenticated()
.anyExchange().permitAll()
)
.jwt(jwtSpec -> {
jwtSpec.authenticationManager(jwtReactiveAuthenticationManager(reactiveJwtDecoder()));
});
return rsocket.build();
}
#Bean
ReactiveJwtDecoder reactiveJwtDecoder() {
NimbusReactiveJwtDecoder decoder = (NimbusReactiveJwtDecoder)
ReactiveJwtDecoders.fromOidcIssuerLocation(issuerUri);
return decoder;
}
#Bean
public JwtReactiveAuthenticationManager jwtReactiveAuthenticationManager(ReactiveJwtDecoder reactiveJwtDecoder) {
JwtReactiveAuthenticationManager jwtReactiveAuthenticationManager = new JwtReactiveAuthenticationManager(reactiveJwtDecoder);
JwtAuthenticationConverter authenticationConverter = new JwtAuthenticationConverter();
JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
jwtGrantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
authenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
jwtReactiveAuthenticationManager.setJwtAuthenticationConverter( new ReactiveJwtAuthenticationConverterAdapter(authenticationConverter));
return jwtReactiveAuthenticationManager;
}
#Bean
RSocketMessageHandler messageHandler(RSocketStrategies strategies) {
RSocketMessageHandler mh = new RSocketMessageHandler();
mh.getArgumentResolverConfigurer().addCustomResolver(new AuthenticationPrincipalArgumentResolver());
mh.setRSocketStrategies(strategies);
return mh;
}
Full UpperCaseController:
#Slf4j
#Controller
public class UpperCaseController {
#MessageMapping("uppercase")
public Mono<String> uppercase(String s, #AuthenticationPrincipal Mono<JwtAuthenticationToken> token) {
JwtAuthenticationToken currentToken = token.block();
if ( currentToken == null ) {
log.info("token is null");
}
return Mono.just(s.toUpperCase());
}
}
Full ConnectController:
#Slf4j
#Controller
public class ConnectController {
#ConnectMapping("connect")
void connectShellClientAndAskForTelemetry(RSocketRequester requester,
#Payload String client) {
requester.rsocket()
.onClose()
.doFirst(() -> {
// Add all new clients to a client list
log.info("Client: {} CONNECTED.", client);
})
.doOnError(error -> {
// Warn when channels are closed by clients
log.warn("Channel to client {} CLOSED", client);
})
.doFinally(consumer -> {
// Remove disconnected clients from the client list
log.info("Client {} DISCONNECTED", client);
})
.subscribe();
}
}
RSocket Client:
#Component
#Slf4j
public class RSocketClient {
private static final MimeType SIMPLE_AUTH = MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.getString());
MimeType BEARER_AUTH =
MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.getString());
private static final String BEARER_TOKEN = "....";
private final RSocketRequester requester;
private RSocketStrategies rsocketStrategies;
public RSocketClient(RSocketRequester.Builder requesterBuilder,
#Qualifier("rSocketStrategies") RSocketStrategies strategies) {
this.rsocketStrategies = strategies;
SocketAcceptor responder = RSocketMessageHandler.responder(rsocketStrategies, new RSocketClientHandler());
requester = requesterBuilder
.setupRoute("connect")
.setupData("MyTestClient")
.setupMetadata(new BearerTokenMetadata(BEARER_TOKEN), BEARER_AUTH)
.rsocketStrategies(builder ->
builder.encoder(new BearerTokenAuthenticationEncoder()))
.rsocketConnector(connector -> connector.acceptor(responder))
.connectTcp("localhost", 7000)
.block();
requester.rsocket()
.onClose()
.doOnError(error -> log.warn("Connection CLOSED"))
.doFinally(consumer -> log.info("Client DISCONNECTED"))
.subscribe();
}
public void uppercase() {
String response = requester
.route("uppercase")
.metadata(BEARER_TOKEN, BEARER_AUTH)
.data("Hello")
.retrieveMono(String.class).block();
log.info(response);
}
}
I have done something very similar for Spring REST and it works fine but for RSocket the token is always null.
I assume you have started with https://spring.io/blog/2020/06/17/getting-started-with-rsocket-spring-security
I was able to get this working for my codebase using a different type than #Payload
#ConnectMapping
fun handle(requester: RSocketRequester, #AuthenticationPrincipal jwt: String) {
logger.debug("connected $jwt")
}
#MessageMapping("runCommand")
suspend fun runCommand(request: CommandRequest, rSocketRequester: RSocketRequester, #AuthenticationPrincipal jwt: String): Flow<CommandResponse> {
...
}
I am using Keycloak as jwt Issuer. Just follow up this git repo. Only thing that didn't work for me is
#CurrentUserProfile Mono<UserProfile> currentUserProfile
Solution for that w.r.t above git repo will be using either of the below
#CurrentUserProfile Mono<Jwt> currentUserProfile
or directly use
#AuthenticationPrincipal Jwt currentUserProfile
Hope this will work for you. Enjoy!

How to set user authorities from user claims return by an oauth server in spring security

I recently wrote a spring boot project that uses spring security oauth2, the auth server is IdentityServer4 for some reason, I can successfully login and get username in my project but I cannot find any way to set user's authority/role.
request.isUserInRole always return false.
#PreAuthorize("hasRole('rolename')") always lead me to 403.
Where can I place some code to set the authorities?
The server has returned some user claims through userinfo endpoint, and my project received them, and I can even see it in the principle param of my controller.
This method always return 403
#ResponseBody
#RequestMapping("admin")
#PreAuthorize("hasRole('admin')")
public String admin(HttpServletRequest request){
return "welcome, you are admin!" + request.isUserInRole("ROLE_admin");
}
application.properties
spring.security.oauth2.client.provider.test.issuer-uri = http://localhost:5000
spring.security.oauth2.client.provider.test.user-name-attribute = name
spring.security.oauth2.client.registration.test.client-id = java
spring.security.oauth2.client.registration.test.client-secret = secret
spring.security.oauth2.client.registration.test.authorization-grant-type = authorization_code
spring.security.oauth2.client.registration.test.scope = openid profile
I print the claims
#ResponseBody
#RequestMapping()
public Object index(Principal user){
OAuth2AuthenticationToken token = (OAuth2AuthenticationToken)user;
return token.getPrincipal().getAttributes();
}
and get the result show there is a claim named 'role'
{"key":"value","role":"admin","preferred_username":"bob"}
Anybody can help me and give me a solution please?
EDIT 1:
The reason is oauth2 client has removed the extracter, and I have to implement the userAuthoritiesMapper.
Finally I got this work by adding the following class:
#Configuration
public class AppConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.oauth2Login().userInfoEndpoint().userAuthoritiesMapper(this.userAuthoritiesMapper());
//.oidcUserService(this.oidcUserService());
super.configure(http);
}
private GrantedAuthoritiesMapper userAuthoritiesMapper() {
return (authorities) -> {
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
authorities.forEach(authority -> {
if (OidcUserAuthority.class.isInstance(authority)) {
OidcUserAuthority oidcUserAuthority = (OidcUserAuthority)authority;
OidcUserInfo userInfo = oidcUserAuthority.getUserInfo();
if (userInfo.containsClaim("role")){
String roleName = "ROLE_" + userInfo.getClaimAsString("role");
mappedAuthorities.add(new SimpleGrantedAuthority(roleName));
}
} else if (OAuth2UserAuthority.class.isInstance(authority)) {
OAuth2UserAuthority oauth2UserAuthority = (OAuth2UserAuthority)authority;
Map<String, Object> userAttributes = oauth2UserAuthority.getAttributes();
if (userAttributes.containsKey("role")){
String roleName = "ROLE_" + (String)userAttributes.get("role");
mappedAuthorities.add(new SimpleGrantedAuthority(roleName));
}
}
});
return mappedAuthorities;
};
}
}
The framework changes so fast and the demos on the web is too old!
I spent a few hours and I find the solution. The problem is with spring oauth security, by default it obtain the user roles from the token using the key 'authorities'. So, I implemented a custom token converter.
The first you need is the custom user token converter, here is the class:
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");
}
}
}
}
The you need a custom token converter, here is:
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;
}
}
And finally you ResourceServerConfiguration looks like this:
import hello.helper.CustomAccessTokenConverter;
import hello.helper.CustomUserTokenConverter;
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 {
#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("resourceId");
tokenServices.setClientSecret("resource.secret");
tokenServices.setCheckTokenEndpointUrl("http://localhost:5001/connect/introspect");
tokenServices.setAccessTokenConverter(accessTokenConverter());
return tokenServices;
}
#Bean
public CustomAccessTokenConverter accessTokenConverter() {
final CustomAccessTokenConverter converter = new CustomAccessTokenConverter();
converter.setUserTokenConverter(new CustomUserTokenConverter());
return converter;
}
}
Apparently #wjsgzcn answer (EDIT 1) DOES NOT WORK for reasons below
If you print the attributes returned by the Oauth2UserAuthirty class you will soon notice the contents of the JSON data does not have the role key instead has an authorities key hence you need to use that key to iterate over the list of authorities (roles) to get the actual role name.
Hence the following lines of code will not work as there is no role key in the JSON data returned by the oauth2UserAuthority.getAttributes();
OAuth2UserAuthority oauth2UserAuthority = (OAuth2UserAuthority)authority;
Map<String, Object> userAttributes = oauth2UserAuthority.getAttributes();
if (userAttributes.containsKey("role")){
String roleName = "ROLE_" + (String)userAttributes.get("role");
mappedAuthorities.add(new SimpleGrantedAuthority(roleName));
}
So instead use the following to get the actual role from the getAttributes
if (userAttributes.containsKey("authorities")){
ObjectMapper objectMapper = new ObjectMapper();
ArrayList<Role> authorityList =
objectMapper.convertValue(userAttributes.get("authorities"), new
TypeReference<ArrayList<Role>>() {});
log.info("authList: {}", authorityList);
for(Role role: authorityList){
String roleName = "ROLE_" + role.getAuthority();
log.info("role: {}", roleName);
mappedAuthorities.add(new SimpleGrantedAuthority(roleName));
}
}
Where the Role is a pojo class like so
#Data
#AllArgsConstructor
#NoArgsConstructor
public class Role {
#JsonProperty
private String authority;
}
That way you will be able to get the ROLE_ post prefix which is the actual role granted to the user after successfully authenticated to the Authorization server and the client is returned the LIST of granted authorities (roles).
Now the complete GrantedAuthoritesMapper look like the following:
private GrantedAuthoritiesMapper userAuthoritiesMapper() {
return (authorities) -> {
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
authorities.forEach(authority -> {
if (OidcUserAuthority.class.isInstance(authority)) {
OidcUserAuthority oidcUserAuthority = (OidcUserAuthority)authority;
OidcIdToken idToken = oidcUserAuthority.getIdToken();
OidcUserInfo userInfo = oidcUserAuthority.getUserInfo();
// Map the claims found in idToken and/or userInfo
// to one or more GrantedAuthority's and add it to mappedAuthorities
if (userInfo.containsClaim("authorities")){
ObjectMapper objectMapper = new ObjectMapper();
ArrayList<Role> authorityList = objectMapper.convertValue(userInfo.getClaimAsMap("authorities"), new TypeReference<ArrayList<Role>>() {});
log.info("authList: {}", authorityList);
for(Role role: authorityList){
String roleName = "ROLE_" + role.getAuthority();
log.info("role: {}", roleName);
mappedAuthorities.add(new SimpleGrantedAuthority(roleName));
}
}
} else if (OAuth2UserAuthority.class.isInstance(authority)) {
OAuth2UserAuthority oauth2UserAuthority = (OAuth2UserAuthority)authority;
Map<String, Object> userAttributes = oauth2UserAuthority.getAttributes();
log.info("userAttributes: {}", userAttributes);
// Map the attributes found in userAttributes
// to one or more GrantedAuthority's and add it to mappedAuthorities
if (userAttributes.containsKey("authorities")){
ObjectMapper objectMapper = new ObjectMapper();
ArrayList<Role> authorityList = objectMapper.convertValue(userAttributes.get("authorities"), new TypeReference<ArrayList<Role>>() {});
log.info("authList: {}", authorityList);
for(Role role: authorityList){
String roleName = "ROLE_" + role.getAuthority();
log.info("role: {}", roleName);
mappedAuthorities.add(new SimpleGrantedAuthority(roleName));
}
}
}
});
log.info("The user authorities: {}", mappedAuthorities);
return mappedAuthorities;
};
}
Now you are able to use the userAuthorityMapper in your oauth2Login as follows
#Override
public void configure(HttpSecurity http) throws Exception {
http.antMatcher("/**").authorizeRequests()
.antMatchers("/", "/login**").permitAll()
.antMatchers("/clientPage/**").hasRole("CLIENT")
.anyRequest().authenticated()
.and()
.oauth2Login()
.userInfoEndpoint()
.userAuthoritiesMapper(userAuthoritiesMapper());
}

How to secure Apache Camel rest endpoint with Spring Security and OAuth2

I'm working on Spring Boot application with configured SSO/OAuth2 security.
Authentication works fine for my rest controllers and now I need to secure my Apache Camel route with a rest endpoint.
As I understand there are several ways how to do it:
By adding auth processor to my route
By adding policy (SpringSecurityAuthorizationPolicy) to my route
By handlers option to jetty endpoint
I'm trying to do it by adding new auth processor to my rest endpoint but I stuck on this exception:
org.springframework.security.oauth2.common.exceptions.OAuth2Exception:
No AuthenticationProvider found for
org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken
During debugging I see that org.springframework.security.authentication.ProviderManager.getProviders() contains only one provider AnonymousAuthenticationProvider so probably I have to register appropriate provider...
Can someone help me to find the right way to solve this problem please?
#Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests().anyRequest().permitAll();
}
#Configuration
#EnableResourceServer
protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
#Value("${oauth2.token.endpoint}")
private String tokenEndpoint;
#Bean
public ResourceServerTokenServices tokenService() {
RemoteTokenServices tokenServices = new RemoteTokenServices();
tokenServices.setClientId("clientId");
tokenServices.setClientSecret("clientSecret");
tokenServices.setCheckTokenEndpointUrl(tokenEndpoint);
return tokenServices;
}
#Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated();
}
}
}
#Configuration
public class EmbeddedServerRoute {
#Bean
public RoutesBuilder embeddedServer() {
return new RouteBuilder() {
#Override
public void configure() throws Exception {
restConfiguration().component("jetty").port("8081").bindingMode(RestBindingMode.json);
}
};
}
}
#Component
public class RestTestRoute extends RouteBuilder {
#Autowired
private AuthProcessor authProcessor;
#Override
public void configure() throws Exception {
from("rest:get:/test").process(authProcessor).to("mock:end").end();
}
}
#Component
public class AuthProcessor implements Processor {
#Autowired
private AuthenticationManager authenticationManager;
private TokenExtractor tokenExtractor = new BearerTokenExtractor();
private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new OAuth2AuthenticationDetailsSource();
#Override
public void process(Exchange exchange) throws Exception {
HttpServletRequest request = exchange.getIn().getBody(HttpServletRequest.class);
Subject subject = new Subject();
Authentication auth = getAuth(request);
subject.getPrincipals().add(auth);
exchange.getIn().setHeader(Exchange.AUTHENTICATION, subject);
}
private Authentication getAuth(HttpServletRequest request) throws OAuth2Exception {
Authentication authentication = null;
try {
authentication = tokenExtractor.extract(request);
if (authentication != null) {
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, authentication.getPrincipal());
if (authentication instanceof AbstractAuthenticationToken) {
AbstractAuthenticationToken needsDetails = (AbstractAuthenticationToken) authentication;
needsDetails.setDetails(authenticationDetailsSource.buildDetails(request));
}
return authenticationManager.authenticate(authentication);
}
} catch (Exception e) {
throw new OAuth2Exception(e.getMessage());
}
throw new OAuth2Exception("Not Authorized to view resource");
}
}
As a final solution I decided to use Spring Boot embedded servlet container instead of Apache Camel rest component. So it could be easily secured by Spring Security. This could be done by creating additional beans:
#Bean
public ServletRegistrationBean servletRegistrationBean() {
SpringServerServlet serverServlet = new SpringServerServlet();
ServletRegistrationBean regBean = new ServletRegistrationBean(serverServlet, "/camel/*");
Map<String, String> params = new HashMap<>();
params.put("org.restlet.component", "restletComponent");
regBean.setInitParameters(params);
return regBean;
}
#Bean
public Component restletComponent() {
return new Component();
}
#Bean
public RestletComponent restletComponentService() {
return new RestletComponent(restletComponent());
}

Resources