I am trying to build a #Restcontroller with some basic function and authenticate/authorize thru spring security 4 with an ldap backend, all in java config. I have some questions:
storing passwords in ldap are realworld scenarios or they should be somewhere in a database and that involves a custom userDetailService implementation with dao functions?
If passwords should be stored in ldap then PasswordComparisonAuthenticator is how performance optimized(speed, constant connection toward ldap)?
When calling the #RestController how should the caller authenticate and what java config needs to be done there?
My current implementation works with simple httpBasic authentication against the #RestController but when changing the authenticationProvider to ldap I get some error(I see the bad credentials error but i don't know why I gets it), am I missing some conceptual thing?:
Basic Authentication Authorization header found for user 'jfryer'
[DEBUG] [http-bio-8080-exec-3 04:06:02] (ProviderManager.java:authenticate:162) Authentication attempt using org.springframework.security.ldap.authentication.LdapAuthenticationProvider
[DEBUG] [http-bio-8080-exec-3 04:06:02] (AbstractLdapAuthenticationProvider.java:authenticate:67) Processing authentication request for user: jfryer
[DEBUG] [http-bio-8080-exec-3 04:06:02] (BasicAuthenticationFilter.java:doFilterInternal:196) Authentication request for failed: org.springframework.security.authentication.BadCredentialsException: Bad credentials
[DEBUG] [http-bio-8080-exec-3 04:06:02] (DelegatingAuthenticationEntryPoint.java:commence:78) Trying to match using RequestHeaderRequestMatcher [expectedHeaderName=X-Requested-With, expectedHeaderValue=XMLHttpRequest]
[DEBUG] [http-bio-8080-exec-3 04:06:02] (DelegatingAuthenticationEntryPoint.java:commence:91) No match found. Using default entry point org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint#56f05ca6
[DEBUG] [http-bio-8080-exec-3 04:06:02] (SecurityContextPersistenceFilter.java:doFilter:105) SecurityContextHolder now cleared, as request processing completed
And the java config:
#Configuration
#EnableWebSecurity
#ComponentScan("my packages")
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Bean
public DefaultSpringSecurityContextSource contextSource() {
DefaultSpringSecurityContextSource contextSource = new DefaultSpringSecurityContextSource("ldap://localhost:11389/o=sevenSeas");
contextSource.setUserDn("uid=admin,ou=system");
contextSource.setPassword("admin");
return contextSource;
}
#Bean
public PasswordComparisonAuthenticator ldapAuthenticator(DefaultSpringSecurityContextSource contextSource) {
PasswordComparisonAuthenticator authenticator = new PasswordComparisonAuthenticator(contextSource);
String[] userDn = {"cn={0},ou=people"};
authenticator.setUserDnPatterns(userDn);
authenticator.setPasswordAttributeName("userPassword");
authenticator.setPasswordEncoder(new LdapShaPasswordEncoder());
return authenticator;
}
#Bean
public DefaultLdapAuthoritiesPopulator authoritiesPopulator(DefaultSpringSecurityContextSource contextSource) {
DefaultLdapAuthoritiesPopulator populator = new DefaultLdapAuthoritiesPopulator(contextSource, "ou=groups");
populator.setGroupRoleAttribute("cn");
return populator;
}
#Bean
public LdapAuthenticationProvider ldapAuthenticationProvider(DefaultSpringSecurityContextSource contextSource) {
return new LdapAuthenticationProvider(this.ldapAuthenticator(contextSource), authoritiesPopulator(contextSource));
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth, DefaultSpringSecurityContextSource contextSource) throws Exception {
//auth.userDetailsService(userDetailsService);
auth.authenticationProvider(ldapAuthenticationProvider(contextSource));
}
#Override
public void configure(HttpSecurity http) throws Exception {
http.httpBasic().and().authorizeRequests().antMatchers("/hello/**").hasRole("ADMIN").antMatchers("/date/**").hasRole("USER").and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().csrf().disable().exceptionHandling()
.authenticationEntryPoint(new AuthenticationHandler()).accessDeniedHandler(new PermissionHandler());
}
}
The calling code:
final String helloUri = "http://localhost:8080/Security/hello/Zoltan";
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
String auth = "jfryer:alma";
byte[] encodedAuth = Base64.encodeBase64(auth.getBytes(Charset.forName("US-ASCII")));
String authHeader = "Basic " + new String(encodedAuth);
headers.add("Authorization", authHeader);
HttpEntity<String> entity = new HttpEntity<String>("parameters", headers);
ResponseEntity<String> result = restTemplate.exchange(helloUri, HttpMethod.POST, entity, String.class);
Related
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.
I am trying to implement authentication using JWT in Spring Boot. In the login function I am setting the authentication in the SecurityContextHolder in order to be able to get it when requested. The login functionality works, but when I try to get the current logged user, I am getting unathorized. I debugged and the SecurityContextHolder gives anonymous user. Why is this happening?
UserController class:
#RestController
#CrossOrigin(origins = "http://localhost:3000")
#RequestMapping("/api")
public class UserController {
#Autowired
private UserService userService;
#Autowired
private CustomAuthenticationManager authenticationManager;
#Autowired
private JwtEncoder jwtEncoder;
#PostMapping("/user/login")
public ResponseEntity<User> login(#RequestBody #Valid AuthDto request) {
try {
Authentication authentication = authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken(request.getEmail(), request.getPassword()));
String userEmail = (String) authentication.getPrincipal();
User user = userService.findUserByEmail(userEmail);
Instant now = Instant.now();
long expiry = 36000L;
String scope = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(joining(" "));
JwtClaimsSet claims = JwtClaimsSet.builder()
.issuer("uni.pu")
.issuedAt(now)
.expiresAt(now.plusSeconds(expiry))
.subject(format("%s,%s", user.getId(), user.getEmail()))
.claim("roles", scope)
.build();
String token = this.jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
SecurityContextHolder.getContext().setAuthentication(authentication);
return ResponseEntity.ok()
.header(HttpHeaders.AUTHORIZATION, token)
.body(user);
} catch (BadCredentialsException ex) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
}
#GetMapping("/user/current")
public ResponseEntity<User> getLoggedUser(){
try{
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
return ResponseEntity.ok()
.body((User)auth.getPrincipal());
}
catch(Exception e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
}
}
WebSecurityConfig:
#EnableWebSecurity
#EnableGlobalMethodSecurity(securedEnabled = true, jsr250Enabled = true, prePostEnabled = true)
public class WebSecurityConfig {
private static final String[] WHITE_LIST_URLS = {"/api/user/login", "/api/user/current"};
#Autowired
private MyUserDetailsService userDetailsService;
#Value("${jwt.public.key}")
private RSAPublicKey rsaPublicKey;
#Value("${jwt.private.key}")
private RSAPrivateKey rsaPrivateKey;
#Bean
public PasswordEncoder encoder() {
return new BCryptPasswordEncoder(10);
}
#Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(encoder());
return authProvider;
}
#Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// Enable CORS and disable CSRF
http = http.cors().and().csrf().disable();
// Set session management to stateless
http = http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and();
// Set unauthorized requests exception handler
http = http.exceptionHandling(
(exceptions) -> exceptions.authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint())
.accessDeniedHandler(new BearerTokenAccessDeniedHandler()));
http = http.authenticationProvider(authenticationProvider());
// Set permissions on endpoints
http.authorizeHttpRequests().antMatchers(WHITE_LIST_URLS).permitAll().antMatchers("/api/**").authenticated()
// Our private endpoints
.anyRequest().authenticated()
// Set up oauth2 resource server
.and().httpBasic(Customizer.withDefaults()).oauth2ResourceServer().jwt();
return http.build();
}
#Bean
public JwtEncoder jwtEncoder() {
JWK jwk = new RSAKey.Builder(this.rsaPublicKey).privateKey(this.rsaPrivateKey).build();
JWKSource<SecurityContext> jwks = new ImmutableJWKSet<>(new JWKSet(jwk));
return new NimbusJwtEncoder(jwks);
}
// Used by JwtAuthenticationProvider to decode and validate JWT tokens
#Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withPublicKey(this.rsaPublicKey).build();
}
// Extract authorities from the roles claim
#Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("roles");
jwtGrantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}
#Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
#Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration)
throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
#Bean
public RoleHierarchy roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
String hierarchy = "ROLE_ADMIN > ROLE_INSPECTOR \n ROLE_INSPECTOR > ROLE_STUDENT";
roleHierarchy.setHierarchy(hierarchy);
return roleHierarchy;
}
}
In Spring documentation, section Storing the SecurityContext between requests says :
Depending on the type of application, there may need to be a strategy
in place to store the security context between user operations. In a
typical web application, a user logs in once and is subsequently
identified by their session Id. The server caches the principal
information for the duration session. In Spring Security, the
responsibility for storing the SecurityContext between requests falls
to the SecurityContextPersistenceFilter, which by default stores the
context as an HttpSession attribute between HTTP requests. It restores
the context to the SecurityContextHolder for each request and,
crucially, clears the SecurityContextHolder when the request completes
So basically, when you create the security context manually no session object is created. Only when the request finishes processing does the Spring Security mechanism realize that the session object is null (when it tries to store the security context to the session after the request has been processed).
At the end of the request Spring Security creates a new session object and session ID. However this new session ID never makes it to the browser because it occurs at the end of the request, after the response to the browser has been made. This causes the new session ID (and hence the Security context containing my manually logged on user) to be lost when the next request contains the previous session ID.
I found two solutions to hande this situation:
1.First solution : Save SecurityContext object in session and then extract it from session when needed :
HttpSession session = request.getSession(true);
session.setAttribute("SPRING_SECURITY_CONTEXT", securityContext);
and then, extract it from session.
Second solution according to this answer would be to refactor your login function like this:
private void doAutoLogin(String username, String password, HttpServletRequest request) {
try {
// Must be called from request filtered by Spring Security, otherwise SecurityContextHolder is not updated
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);
token.setDetails(new WebAuthenticationDetails(request));
Authentication authentication = this.authenticationProvider.authenticate(token);
logger.debug("Logging in with [{}]", authentication.getPrincipal());
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (Exception e) {
SecurityContextHolder.getContext().setAuthentication(null);
logger.error("Failure in autoLogin", e);
}
};
This is how you shoud get authenticationProvider :
#Configuration public class WebConfig extends WebSecurityConfigurerAdapter {
#Bean
public AuthenticationManager authenticationProvider() throws Exception{
return super.authenticationManagerBean();
}
}
How to correctly get the users's session oauth2 token ?
I implemented an OAuth2 Authorization/Resource server using spring-security-oauth2-autoconfigure.
I implemented a client app, that uses the authorization server to login the user and gets his access token. The login phase is working perfectly and so the retreive of the login data (using the access token by the oauth2 filters). The Principal in the client app requests correctly shows all authorities filled by the authorization server.
I'd like to use the client app as a proxy to send Rest Request using the given Access Token of the user that requested the call.
I already tried to use #EnableOAuth2Client but that does not work. The OAuth2RestTemplate is null when tried to be autowired.
I had to reimplement a request scoped bean of a RestTemplate which get the tokenValue from the SecurityContext. This works, but I do not find this clean. This behavior is quite common, so I should miss something.
application.yml
spring:
application.name: client
security:
oauth2:
client:
registration:
myclient:
client-id: client-id
client-secret: client-secret
redirect-uri: http://localhost:8081/login/oauth2/code/
authorization-grant-type: authorization_code
provider:
myclient:
authorization-uri: http://localhost:8090/oauth/authorize
token-uri: http://localhost:8090/oauth/token
user-info-uri: http://localhost:8090/me
user-name-attribute: name
SecurityConfiguration
#Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
// #formatter:off
http.authorizeRequests()
.antMatchers("/", "/login").permitAll()
.antMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and().oauth2Login()
.and().oauth2Client()
;
// #formatter:on
}
#Bean
#Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
#Qualifier("oauth2RestTemplate")
public RestTemplate oauth2RestTemplate() {
RestTemplate restTemplate = new RestTemplate();
restTemplate.getInterceptors().add(new ClientHttpRequestInterceptor() {
#Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
throws IOException {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.isAuthenticated() && auth instanceof OAuth2AuthenticationToken) {
#SuppressWarnings("unchecked")
Map<String, Object> details = (Map<String, Object>) ((OAuth2AuthenticationToken) auth).getPrincipal().getAttributes().get("details");
String tokenValue = (String) details.get("tokenValue");
if (tokenValue != null) {
request.getHeaders().add("Authorization", "Bearer " + tokenValue);
}
}
return execution.execute(request, body);
}
});
return restTemplate;
}
}
In WebController
private #Autowired #Qualifier("oauth2RestTemplate") RestTemplate oauth2RestTemplate;
#GetMapping("/remote")
public Map<String, Object> remote() {
#SuppressWarnings("unchecked")
Map<String, Object> resp = oauth2RestTemplate.getForObject(URI.create("http://localhost:8090/api/test"), Map.class);
return resp;
}
It works, but I do not think I should configure the RestTemplate myself.
Unfortunately, you have to define OAuth2RestTemplate. However, this is a more clean implementation.
#Bean
public OAuth2RestTemplate oauth2RestTemplate() {
ClientCredentialsResourceDetails resourceDetails = new ClientCredentialsResourceDetails();
resourceDetails.setAccessTokenUri(format("%s/oauth/token", authServerUrl));
resourceDetails.setClientId("client_id");
resourceDetails.setClientSecret("client_secret");
resourceDetails.setGrantType("client_credentials");
resourceDetails.setScope(asList("read", "write"));
DefaultOAuth2ClientContext clientContext = new DefaultOAuth2ClientContext();
return new OAuth2RestTemplate(resourceDetails, clientContext);
}
In this case, your Resource server will communicate with the authorization server on your behalf using its own credentials.
I have a small Spring Boot 2.1.6 webapp with JWT authententication. Call flow is as follows:
User enters username and password and sends a POST request to /authenticate
A filter is watching this URL (setFilterProcessesUrl), when a request comes, it hashes the password and checks it against the hash stored in DB
If matches, and user is not locked, it creates a JWT with username and granted roles, and returns it in response
User must include this JWT in all further requests
Also, CSRF is disabled in the WebSecurityConfigurerAdapter.
The solution itself is working fine, but I have to create unit tests as well. I ended up with the following test case:
#RunWith(SpringRunner.class)
#WebMvcTest
#ContextConfiguration(classes = { ConfigReaderMock.class })
public class ControllerSecurityTest {
private static final String VALID_USERNAME = "username";
private static final String VALID_PASSWORD = "password";
#Autowired
private MockMvc mockMvc;
private String createAuthenticationBody(String username, String passwordHash) {
return "username=" + URLEncoder.encode(username, StandardCharsets.UTF_8) + "&password="
+ URLEncoder.encode(passwordHash, StandardCharsets.UTF_8);
}
#Test
public void testValidLogin() throws Exception {
MvcResult result = mockMvc
.perform(MockMvcRequestBuilders.post("/authenticate")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.content(createAuthenticationBody(VALID_USERNAME, VALID_PASSWORD)).accept(MediaType.ALL))
.andExpect(status().isOk()).andReturn();
String authHeader = result.getResponse().getHeader(SecurityConstants.TOKEN_HEADER);
mockMvc.perform(MockMvcRequestBuilders.get("/main?" + SecurityConstants.TOKEN_QUERY_PARAM + "="
+ URLEncoder.encode(authHeader, StandardCharsets.UTF_8))).andExpect(status().isOk());
}
}
What I expect, is that the server accepts the username and password provided, and returns the JWT, which I can use in the subsequent request to access the next page (the same is implemented in the front end). Instead I get HTTP 403 from the authentication filter:
MockHttpServletRequest:
HTTP Method = POST
Request URI = /authenticate
Parameters = {username=[username], password=[password]}
Headers = [Content-Type:"application/x-www-form-urlencoded", Accept:"*/*"]
Body = <no character encoding set>
Session Attrs = {org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository.CSRF_TOKEN=org.springframework.security.web.csrf.DefaultCsrfToken#4ac0fdc7}
Handler:
Type = null
Async:
Async started = false
Async result = null
Resolved Exception:
Type = null
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
Attributes = null
MockHttpServletResponse:
Status = 403
Error message = Forbidden
Headers = [X-Content-Type-Options:"nosniff", X-XSS-Protection:"1; mode=block", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0", X-Frame-Options:"DENY"]
Content type = null
Body =
Forwarded URL = null
Redirected URL = null
Cookies = []
I noticed it is sending a CSRF token for some reason in the Session Attributes. Further checking the logs, I can see the belo messages:
2019-07-29 08:09:17,438 DEBUG o.s.b.f.s.DefaultSingletonBeanRegistry [main] Creating shared instance of singleton bean 'org.springframework.boot.autoconfigure.security.servlet.WebSecurityEnablerConfiguration'
2019-07-29 08:09:17,443 DEBUG o.s.s.c.a.a.c.AuthenticationConfiguration$EnableGlobalAuthenticationAutowiredConfigurer [main] Eagerly initializing {org.springframework.boot.autoconfigure.security.servlet.WebSecurityEnablerConfiguration=org.springframework.boot.autoconfigure.security.servlet.WebSecurityEnablerConfiguration$$EnhancerBySpringCGLIB$$236da03c#4e68aede}
2019-07-29 08:09:17,444 DEBUG o.s.b.f.s.DefaultSingletonBeanRegistry [main] Creating shared instance of singleton bean 'inMemoryUserDetailsManager'
2019-07-29 08:09:17,445 DEBUG o.s.b.f.s.DefaultSingletonBeanRegistry [main] Creating shared instance of singleton bean 'org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration'
2019-07-29 08:09:17,454 DEBUG o.s.b.f.s.DefaultSingletonBeanRegistry [main] Creating shared instance of singleton bean 'spring.security-org.springframework.boot.autoconfigure.security.SecurityProperties'
2019-07-29 08:09:17,457 DEBUG o.s.b.f.s.ConstructorResolver [main] Autowiring by type from bean name 'inMemoryUserDetailsManager' via factory method to bean named 'spring.security-org.springframework.boot.autoconfigure.security.SecurityProperties'
2019-07-29 08:09:17,462 INFO o.s.b.a.s.s.UserDetailsServiceAutoConfiguration [main]
Using generated security password: 963b2bac-d953-4793-a8cd-b3f81586823e
...
2019-07-29 08:09:17,783 DEBUG o.s.s.w.c.HttpSessionSecurityContextRepository [main] No HttpSession currently exists
2019-07-29 08:09:17,784 DEBUG o.s.s.w.c.HttpSessionSecurityContextRepository [main] No SecurityContext was available from the HttpSession: null. A new one will be created.
2019-07-29 08:09:17,794 DEBUG o.s.s.w.c.CsrfFilter [main] Invalid CSRF token found for http://localhost/authenticate
2019-07-29 08:09:17,795 DEBUG o.s.s.w.h.w.HstsHeaderWriter [main] Not injecting HSTS header since it did not match the requestMatcher org.springframework.security.web.header.writers.HstsHeaderWriter$SecureRequestMatcher#1c15a6aa
2019-07-29 08:09:17,796 DEBUG o.s.s.w.c.HttpSessionSecurityContextRepository$SaveToSessionResponseWrapper [main] SecurityContext is empty or contents are anonymous - context will not be stored in HttpSession.
2019-07-29 08:09:17,799 DEBUG o.s.s.w.c.SecurityContextPersistenceFilter [main] SecurityContextHolder now cleared, as request processing completed
So it seems like Spring Security is creating it's own security configuration, instead of using the class I created, extending WebSecurityConfigurerAdapter. Question is, why? And how can I force it to use my security config, as I'm dependent on it with the database login?
Update: added WebSecurityConfigurerAdapter
#EnableWebSecurity
#EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private AICAuthenticationService authenticationService;
#Autowired
private AICUserDetailsService aicUserDetailsService;
#Autowired
private AICLogoutSuccessHandler aicLogoutSuccessHandler;
#Override
public void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.cors()
.and()
.authorizeRequests()
.antMatchers("/resources/**", "/login", "/").permitAll()
.anyRequest().authenticated()
.and()
.addFilter(new JwtAuthenticationFilter(authenticationManager()))
.addFilter(new JwtAuthorizationFilter(authenticationManager()))
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessHandler(aicLogoutSuccessHandler)
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID", "error");
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(aicUserDetailsService);
}
#Override
protected AuthenticationManager authenticationManager() throws Exception {
return authenticationService;
}
#Bean
public AuthenticationManager custromAuthenticationManager() throws Exception {
return authenticationManager();
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(aicUserDetailsService);
}
I was able to get it done with TestRestTemplate, like this:
#RunWith(SpringRunner.class)
#SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class ControllerSecurityTest {
private static final String VALID_USERNAME = "username";
private static final String VALID_PASSWORD = "password";
#LocalServerPort
private int port;
#Autowired
private TestRestTemplate restTemplate;
#Test
public void testValidLogin() throws Exception {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.setAccept(Arrays.asList(MediaType.ALL));
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.add("username", VALID_USERNAME);
map.add("password", VALID_PASSWORD);
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(map, headers);
ResponseEntity<String> tokenResponse = restTemplate
.postForEntity("http://localhost:" + port + "/authenticate", request, String.class);
assertEquals(200, tokenResponse.getStatusCodeValue());
String authHeader = tokenResponse.getHeaders().getFirst(SecurityConstants.TOKEN_HEADER);
assertNotNull(authHeader);
ResponseEntity<String> mainResponse = restTemplate.getForEntity("http://localhost:" + port + "/main?"
+ SecurityConstants.TOKEN_QUERY_PARAM + "=" + URLEncoder.encode(authHeader, StandardCharsets.UTF_8),
String.class);
assertEquals(200, mainResponse.getStatusCodeValue());
}
}
I am trying to do a simple thing.
Want to make a request to a single endpoint and send a bearer token (from a client), I want this token to be validated and depending on the role assigned on keycloak accept/deny request on my endpoint.
I followed many tutorials and even books but most of all them I simply dont understand.
Followed this to setup my keycloak info (realm, role, user)
https://medium.com/#bcarunmail/securing-rest-api-using-keycloak-and-spring-oauth2-6ddf3a1efcc2
So,
I basically set up my keycloak with a client, a user with a specific role "user" and configured it like this:
#Configuration
#KeycloakConfiguration
//#ComponentScan(basePackageClasses = KeycloakSecurityComponents.class)
public class SecurityConf extends KeycloakWebSecurityConfigurerAdapter
{
/**
* Registers the KeycloakAuthenticationProvider with the authentication manager.
*/
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(keycloakAuthenticationProvider());
}
/**
* Defines the session authentication strategy.
*/
#Bean
#Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
}
#Bean
public KeycloakSpringBootConfigResolver KeycloakConfigResolver() {
return new KeycloakSpringBootConfigResolver();
}
#Bean
public FilterRegistrationBean keycloakAuthenticationProcessingFilterRegistrationBean(
KeycloakAuthenticationProcessingFilter filter) {
FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
registrationBean.setEnabled(false);
return registrationBean;
}
#Bean
public FilterRegistrationBean keycloakPreAuthActionsFilterRegistrationBean(
KeycloakPreAuthActionsFilter filter) {
FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
registrationBean.setEnabled(false);
return registrationBean;
}
#Override
protected void configure(HttpSecurity http) throws Exception
{
super.configure(http);
http
.authorizeRequests()
.antMatchers("/user/*").hasRole("admin")
.antMatchers("/admin*").hasRole("user")
}
}
I dont understand why at many tutorials I see this(as the last rule):
.anyRequest().permitAll();
Basically when I set that I have no security, I can call the endpoints without a bearer token.
But when I add this as last rule
.anyRequest().denyAll();
I always get a 403.
Debbugging I found this:
Request is to process authentication
f.KeycloakAuthenticationProcessingFilter : Attempting Keycloak authentication
o.k.a.BearerTokenRequestAuthenticator : Found [1] values in authorization header, selecting the first value for Bearer.
o.k.a.BearerTokenRequestAuthenticator : Verifying access_token
o.k.a.BearerTokenRequestAuthenticator : successful authorized
a.s.a.SpringSecurityRequestAuthenticator : Completing bearer authentication. Bearer roles: []
o.k.adapters.RequestAuthenticator : User 'testuser' invoking 'http://localhost:9090/api/user/123' on client 'users'
o.k.adapters.RequestAuthenticator : Bearer AUTHENTICATED
f.KeycloakAuthenticationProcessingFilter : Auth outcome: AUTHENTICATED
o.s.s.authentication.ProviderManager : Authentication attempt using org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider
o.s.s.core.session.SessionRegistryImpl : Registering session 5B871A0E2AF55B70DC8E3B7436D79333, for principal testuser
f.KeycloakAuthenticationProcessingFilter : Authentication success using bearer token/basic authentication. Updating SecurityContextHolder to contain: org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken#355f68d6: Principal: testuser; Credentials: [PROTECTED]; Authenticated: true; Details: org.keycloak.adapters.springsecurity.account.SimpleKeycloakAccount#5d7a32a9; Not granted any authorities
[nio-9090-exec-3] o.s.security.web.FilterChainProxy : /api/user/123 at position 8 of 15 in additional filter chain; firing Filter: 'RequestCacheAwareFilter'
nio-9090-exec-3] o.s.s.w.s.DefaultSavedRequest : pathInfo: both null (property equals)
[nio-9090-exec-3] o.s.s.w.s.DefaultSavedRequest : queryString: both null (property equals)
Seems like I get no bearer roles ...
My dependencies:
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-boot-starter</artifactId>
<version>6.0.1</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-security-adapter</artifactId>
<version>6.0.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
My problem?
I request an access token sending:
client_id -> my client from keycloak
username -> my user from keycloak
password -> my password from keycloak
grant_type -> password
client_secret -> from keycloak
I get a token and then I use to request to my app endoint. My requests are always valid no matter what endpoint I use (the one with role user or with role admin).
At my properties I have something like this:
keycloak:
auth-server-url: http://localhost:8080/auth/
resource: users-api
credentials:
secret : my-secret
use-resource-role-mappings : true
realm: my-realm
realmKey: my-key
public-client: true
principal-attribute: preferred_username
bearer-only: true
Any idea how to actually enabling the roles in this case?
Do I have to configure a client to use JWT? any ideas?
I also added the annotations on my endpoint
#Secured("admin")
#PreAuthorize("hasAnyAuthority('admin')")
but seems they dont do anything...
-- EDIT --
After fixed the url to match the resource I still get 403.
"realm_access": {
"roles": [
"offline_access",
"admin",
"uma_authorization"
]
},
"resource_access": {
"account": {
"roles": [
"manage-account",
"manage-account-links",
"view-profile"
]
}
},
Is it somehow related the resource_access with my problem?
in Debug stack: I see you are calling /api/user/123 and in your security configs you are securing /user/* which is not the same, change your security to:
.antMatchers("/api/user/*").hasRole("user")
.antMatchers("/api/admin*").hasRole("admin")
P.S: you don't need to register KeycloakAuthenticationProcessingFilter and KeycloakPreAuthActionsFilter
I know this is an old post but I'm just writing this for future reference in case anyone else has the same problem.
If you look into the logs, Keycloak successfully authenticated the access token but there are not any granted authorities. That's why Spring doesn't authorize the request and you get HTTP 403 Forbidden:
f.KeycloakAuthenticationProcessingFilter : Authentication success using bearer token/basic authentication. Updating SecurityContextHolder to contain: org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken#355f68d6: Principal: testuser; Credentials: [PROTECTED]; Authenticated: true; Details: org.keycloak.adapters.springsecurity.account.SimpleKeycloakAccount#5d7a32a9; Not granted any authorities
That's because Keycloak adapter is configured to use resource (i.e. client-level) role mappings instead of realm-level role mappings:
use-resource-role-mappings: If set to true, the adapter will look inside the token for application-level role mappings for the user. If false, it will look at the realm level for user role mappings. This is OPTIONAL. The default value is false.
Here is the link about adapter configurations.
So, if you want to get authorized via realm roles, properties should be like this:
keycloak:
auth-server-url: http://localhost:8080/auth/
resource: users-api
credentials:
secret : my-secret
use-resource-role-mappings : false
realm: my-realm
realmKey: my-key
public-client: true
principal-attribute: preferred_username
bearer-only: true
Note: If you want to use both realm-level and client-level role mappings, then you should override KeycloakAuthenticationProvider.authenticate() method to provide the necessary roles by combining them yourself.
permitAll:
Whenever you want to allow any request to access the particular resource/URL you can use permitAll. For example, the Login URL should be accessible to everyone.
denyAll:
Whenever you want to block the access of particular URL no matter from where the request comes or who is making request(ADMIN)
You also have miss-match with URL and Role (you are granting URL with admin to USER and vise-versa). (It's a good practice to use the role as ROLE_ADMIN or ADMIN or USER) Form your stack I can see Not granted any authorities so please recheck the code with authorities
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/api/user/**").hasRole("ADMIN")
.antMatchers("/api/admin/**").hasRole("USER")
.anyRequest().authenticated();
Do you try without the #Configuration ? I think you only need #KeycloakConfiguration annotation on your SecurityConf class.
Do your antMatchers respect case sensitivity ?
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/api/user/**").hasRole("user")
.antMatchers("/api/admin/**").hasRole("admin")
.anyRequest().authenticated();
Please also try this configuration, to remove the ROLE_* conventions defined by Java :
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) {
KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
// SimpleAuthorityMapper is used to remove the ROLE_* conventions defined by Java so
// we can use only admin or user instead of ROLE_ADMIN and ROLE_USER
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
auth.authenticationProvider(keycloakAuthenticationProvider);
}
If all your endpoints have the same logics, the security config should be enough, you don't need others annotations. But if you have another endpoint with the admin role, which is not in your "/api/admin" controller, you can try :
#PreAuthorize("hasRole('admin')")
2022 update
Keycloak adapters for Spring are deprecated. Don't use it. Use spring-boot-starter-oauth2-resource-server instead.
Easy solution
With a very handy set of libs on top of spring-boot-starter-oauth2-resource-server, configuration can be as simple as:
#EnableMethodSecurity
#Configuration
public static class SecurityConfig {
#Bean
ExpressionInterceptUrlRegistryPostProcessor expressionInterceptUrlRegistryPostProcessor() {
return (ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry) -> registry
.antMatchers("/api/user/**").hasAuthority("USER")
.antMatchers("/api/admin/**").hasAuthority("ADMIN")
.anyRequest().authenticated();
}
}
}
com.c4-soft.springaddons.security.issuers[0].location=https://localhost:8443/realms/master
com.c4-soft.springaddons.security.issuers[0].authorities.claims=realm_access.roles,resource_access.employee-service.roles,resource_access.other-client.roles
com.c4-soft.springaddons.security.cors[0].path=/api/**
com.c4-soft.springaddons.security.permit-all=/actuator/health/readiness,/actuator/health/liveness,/v3/api-docs/**
Spring only solution
To do the same with spring-boot-starter-oauth2-resource-server only, there is quite some Java conf to write:
#EnableWebSecurity
#EnableMethodSecurity
#Configuration
public class SecurityConfig {
public interface Jwt2AuthoritiesConverter extends Converter<Jwt, Collection<? extends GrantedAuthority>> {
}
#SuppressWarnings("unchecked")
#Bean
public Jwt2AuthoritiesConverter authoritiesConverter() {
// This is a converter for roles as embedded in the JWT by a Keycloak server
// Roles are taken from both realm_access.roles & resource_access.{client}.roles
return jwt -> {
final var realmAccess = (Map<String, Object>) jwt.getClaims().getOrDefault("realm_access", Map.of());
final var realmRoles = (Collection<String>) realmAccess.getOrDefault("roles", List.of());
final var resourceAccess = (Map<String, Object>) jwt.getClaims().getOrDefault("resource_access", Map.of());
// We assume here you have "employee-service" (as in the tutorial referenced in the question) and "other-client" clients configured with "client roles" mapper in Keycloak
final var confidentialClientAccess = (Map<String, Object>) resourceAccess.getOrDefault("employee-service", Map.of());
final var confidentialClientRoles = (Collection<String>) confidentialClientAccess.getOrDefault("roles", List.of());
final var publicClientAccess = (Map<String, Object>) resourceAccess.getOrDefault("other-client", Map.of());
final var publicClientRoles = (Collection<String>) publicClientAccess.getOrDefault("roles", List.of());
return Stream.concat(realmRoles.stream(), Stream.concat(confidentialClientRoles.stream(), publicClientRoles.stream()))
.map(SimpleGrantedAuthority::new).toList();
};
}
public interface Jwt2AuthenticationConverter extends Converter<Jwt, AbstractAuthenticationToken> {
}
#Bean
public Jwt2AuthenticationConverter authenticationConverter(Jwt2AuthoritiesConverter authoritiesConverter) {
return jwt -> new JwtAuthenticationToken(jwt, authoritiesConverter.convert(jwt));
}
#Bean
public SecurityFilterChain filterChain(HttpSecurity http, Jwt2AuthenticationConverter authenticationConverter, ServerProperties serverProperties)
throws Exception {
// Enable OAuth2 with custom authorities mapping
http.oauth2ResourceServer().jwt().jwtAuthenticationConverter(authenticationConverter);
// Enable anonymous
http.anonymous();
// Enable and configure CORS
http.cors().configurationSource(corsConfigurationSource());
// State-less session (state in access-token only)
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// Enable CSRF with cookie repo because of state-less session-management
http.csrf().disable();
// Return 401 (unauthorized) instead of 403 (redirect to login) when authorization is missing or invalid
http.exceptionHandling().authenticationEntryPoint((request, response, authException) -> {
response.addHeader(HttpHeaders.WWW_AUTHENTICATE, "Basic realm=\"Restricted Content\"");
response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
});
// If SSL enabled, disable http (https only)
if (serverProperties.getSsl() != null && serverProperties.getSsl().isEnabled()) {
http.requiresChannel().anyRequest().requiresSecure();
} else {
http.requiresChannel().anyRequest().requiresInsecure();
}
// Route security: authenticated to all routes but actuator and Swagger-UI
// #formatter:off
http.authorizeRequests()
.antMatchers("/actuator/health/readiness", "/actuator/health/liveness", "/v3/api-docs", "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll()
.antMatchers("/api/user/**").hasAuthority("USER")
.antMatchers("/api/admin/**").hasAuthority("ADMIN")
.anyRequest().authenticated();
// #formatter:on
return http.build();
}
private CorsConfigurationSource corsConfigurationSource() {
// Very permissive CORS config...
final var configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("*"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setExposedHeaders(Arrays.asList("*"));
// Limited to API routes (neither actuator nor Swagger-UI)
final var source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", configuration);
return source;
}
}
spring.security.oauth2.resourceserver.jwt.issuer-uri=https://localhost:8443/realms/master
Important notes
Both configuration above make no transformation to keycloak roles (case unchanged, no ROLE_ prefix), reason for using hasAuthority(...) instead of hasRole(...).
Also only roles defined at following levels are considered:
"realm"
"employee-service" client (has defined in the tutorial referenced in the question
"other-client" (just to demo that any other arbitrary client(s) can be used)
Late answer, but hope it will help other facing the same issue.
I was facing the exact same problem as you, and for me, in the configuration class, i has to change the default keycloakAuthenticationProvider by setting a granted authority mapper (the #Override method is just for debugging):
#Bean
public KeycloakSpringBootConfigResolver KeycloakConfigResolver() {
KeycloakAuthenticationProvider keycloakAuthenticationProvider = new KeycloakAuthenticationProvider() {
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
System.out.println("===========+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> authenticate ");
KeycloakAuthenticationToken token = (KeycloakAuthenticationToken) authentication;
for (String role : token.getAccount().getRoles()) {
System.out.println("===========+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> Role : " + role);
}
return super.authenticate(authentication);
}
};
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
auth.authenticationProvider(keycloakAuthenticationProvider);
}
}