I'm using keycloak 3.4 and spring boot to develop a web app.
I'm using the Active Directory as User Federation to retrieve all users information.
But to use those information inside my web app I think I have to save them inside the "local-webapp" database.
So after the users are logged, how can I save them inside my database?
I'm thinking about a scenario like: "I have an object A which it refers to the user B, so I have to put a relation between them. So I add a foreign key."
In that case I need to have the user on my DB. no?
EDIT
To avoid to get save all users on my DB I'm trying to use the Administrator API, so I added the following code inside a controller.
I also created another client called Test to get all users, in this way I can use client-id and client-secret. Or is there a way to use the JWT to use the admin API?
The client:
Keycloak keycloak2 = KeycloakBuilder.builder()
.serverUrl("http://localhost:8080/auth/admin/realms/MYREALM/users")
.realm("MYREALMM")
.username("u.user")
.password("password")
.clientId("Test")
.clientSecret("cade3034-6ee1-4b18-8627-2df9a315cf3d")
.resteasyClient(new ResteasyClientBuilder().connectionPoolSize(20).build())
.build();
RealmRepresentation realm2 = keycloak2.realm("MYREALMM").toRepresentation();
the error is:
2018-02-05 12:33:06.638 ERROR 16975 --- [nio-8080-exec-7] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Handler dispatch failed; nested exception is java.lang.Error: Unresolved compilation problem:
The method realm(String) is undefined for the type AccessTokenResponse
] with root cause
java.lang.Error: Unresolved compilation problem:
The method realm(String) is undefined for the type AccessTokenResponse
Where am I doing wrong?
EDIT 2
I also tried this:
#Autowired
private HttpServletRequest request;
public ResponseEntity listUsers() {
KeycloakAuthenticationToken token = (KeycloakAuthenticationToken) request.getUserPrincipal();
KeycloakPrincipal principal=(KeycloakPrincipal)token.getPrincipal();
KeycloakSecurityContext session = principal.getKeycloakSecurityContext();
Keycloak keycloak = KeycloakBuilder.builder()
.serverUrl("http://localhost:8080/auth")
.realm("MYREALMM")
.authorization(session.getToken().getAuthorization().toString())
.resteasyClient(new ResteasyClientBuilder().connectionPoolSize(20).build())
.build();
RealmResource r = keycloak.realm("MYREALMM");
List<org.keycloak.representations.idm.UserRepresentation> list = keycloak.realm("MYREALMM").users().list();
return ResponseEntity.ok(list);
but the authorization is always null.
Why?
EDIT 3
Following you can find my spring security config:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled=true)
#ComponentScan(basePackageClasses = KeycloakSecurityComponents.class)
#KeycloakConfiguration
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.httpBasic().disable();
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/webjars/**").permitAll()
.antMatchers("/resources/**").permitAll()
.anyRequest().authenticated()
.and()
.logout()
.logoutUrl("/logout")
.logoutRequestMatcher(new AntPathRequestMatcher("/logout", "GET"))
.permitAll()
.logoutSuccessUrl("/")
.invalidateHttpSession(true);
}
#Autowired
public KeycloakClientRequestFactory keycloakClientRequestFactory;
#Bean
public KeycloakRestTemplate keycloakRestTemplate() {
return new KeycloakRestTemplate(keycloakClientRequestFactory);
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) {
KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
SimpleAuthorityMapper simpleAuthorityMapper = new SimpleAuthorityMapper();
simpleAuthorityMapper.setPrefix("ROLE_");
simpleAuthorityMapper.setConvertToUpperCase(true);
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(simpleAuthorityMapper);
auth.authenticationProvider(keycloakAuthenticationProvider);
}
#Bean
public KeycloakSpringBootConfigResolver keycloakConfigResolver() {
return new KeycloakSpringBootConfigResolver();
}
#Bean
#Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
}
#Override
public void configure(WebSecurity web) throws Exception {
web
.ignoring()
.antMatchers("/resources/**", "/static/**", "/css/**", "/js/**", "/images/**", "/webjars/**");
}
#Bean
#Scope(scopeName = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public AccessToken accessToken() {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
return ((KeycloakSecurityContext) ((KeycloakAuthenticationToken) request.getUserPrincipal()).getCredentials()).getToken();
}
}
EDIT 4
These are the properties inside the applicatoin.properties
#######################################
# KEYCLOAK #
#######################################
keycloak.auth-server-url=http://localhost:8181/auth
keycloak.realm=My Realm
keycloak.ssl-required=external
keycloak.resource=AuthServer
keycloak.credentials.jwt.client-key-password=keystorePwd
keycloak.credentials.jwt.client-keystore-file=keystore.jks
keycloak.credentials.jwt.client-keystore-password=keystorePwd
keycloak.credentials.jwt.alias=AuthServer
keycloak.credentials.jwt.token-expiration=10
keycloak.credentials.jwt.client-keystore-type=JKS
keycloak.use-resource-role-mappings=true
keycloak.confidential-port=0
keycloak.principal-attribute=preferred_username
EDIT 5.
This is my keycloak config:
the user that I'm using to login with view user permission:
EDIT 6
This the log form keycloak after enabling logging:
2018-02-12 08:31:00.274 3DEBUG 5802 --- [nio-8080-exec-1] o.k.adapters.PreAuthActionsHandler : adminRequest http://localhost:8080/utente/prova4
2018-02-12 08:31:00.274 3DEBUG 5802 --- [nio-8080-exec-1] .k.a.t.AbstractAuthenticatedActionsValve : AuthenticatedActionsValve.invoke /utente/prova4
2018-02-12 08:31:00.274 3DEBUG 5802 --- [nio-8080-exec-1] o.k.a.AuthenticatedActionsHandler : AuthenticatedActionsValve.invoke http://localhost:8080/utente/prova4
2018-02-12 08:31:00.274 3DEBUG 5802 --- [nio-8080-exec-1] o.k.a.AuthenticatedActionsHandler : Policy enforcement is disabled.
2018-02-12 08:31:00.275 3DEBUG 5802 --- [nio-8080-exec-1] o.k.adapters.PreAuthActionsHandler : adminRequest http://localhost:8080/utente/prova4
2018-02-12 08:31:00.275 3DEBUG 5802 --- [nio-8080-exec-1] o.k.a.AuthenticatedActionsHandler : AuthenticatedActionsValve.invoke http://localhost:8080/utente/prova4
2018-02-12 08:31:00.275 3DEBUG 5802 --- [nio-8080-exec-1] o.k.a.AuthenticatedActionsHandler : Policy enforcement is disabled.
2018-02-12 08:31:00.276 3DEBUG 5802 --- [nio-8080-exec-1] o.k.adapters.PreAuthActionsHandler : adminRequest http://localhost:8080/utente/prova4
2018-02-12 08:31:00.276 3DEBUG 5802 --- [nio-8080-exec-1] o.k.a.AuthenticatedActionsHandler : AuthenticatedActionsValve.invoke http://localhost:8080/utente/prova4
2018-02-12 08:31:00.276 3DEBUG 5802 --- [nio-8080-exec-1] o.k.a.AuthenticatedActionsHandler : Policy enforcement is disabled.
2018-02-12 08:31:10.580 3DEBUG 5802 --- [nio-8080-exec-1] o.k.a.s.client.KeycloakRestTemplate : Created GET request for "http://localhost:8181/auth/admin/realms/My%20Realm%20name/users"
2018-02-12 08:31:10.580 3DEBUG 5802 --- [nio-8080-exec-1] o.k.a.s.client.KeycloakRestTemplate : Setting request Accept header to [application/json, application/*+json]
2018-02-12 08:31:10.592 3DEBUG 5802 --- [nio-8080-exec-1] o.k.a.s.client.KeycloakRestTemplate : GET request for "http://localhost:8181/auth/admin/realms/My%20Realm%20name/users" resulted in 401 (Unauthorized); invoking error handler
2018-02-12 08:31:10.595 ERROR 5802 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.web.client.HttpClientErrorException: 401 Unauthorized] with root cause
org.springframework.web.client.HttpClientErrorException: 401 Unauthorized
at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:85) ~[spring-web-4.3.13.RELEASE.jar:4.3.13.RELEASE]
at org.springframework.web.client.RestTemplate.handleResponse(RestTemplate.java:707) ~[spring-web-4.3.13.RELEASE.jar:4.3.13.RELEASE]
In order to access the whole list of users, you must verify that the logged user contains at least the view-users role from the realm-management client, see this answer I wrote some time ago. Once the user has this role, the JWT she retrieves will cointain it.
As I can infer from your comments, you seem to lack some bases about the Authorization header. Once the user gets logged in, she gets the signed JWT from keycloak, so as every client in the realm can trust it, without the need to ask Keycloak. This JWT contains the access token, which is later on required in the Authorization header for each of user's request, prefixed by the Bearer keyword (see Token-Based Authentication in https://auth0.com/blog/cookies-vs-tokens-definitive-guide/).
So when user makes the request to your app in order to view the list of users, her access token containing the view-users role already goes into the request headers. Instead of having to parse it manually, create another request yourself to access the Keycloak user endpoint and attach it (as you seem to be doing with KeycloakBuilder), the Keycloak Spring Security adapter already provides a KeycloakRestTemplate class, which is able to perform a request to another service for the current user:
SecurityConfig.java
#Configuration
#EnableWebSecurity
#ComponentScan(basePackageClasses = KeycloakSecurityComponents.class)
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
...
#Autowired
public KeycloakClientRequestFactory keycloakClientRequestFactory;
#Bean
#Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public KeycloakRestTemplate keycloakRestTemplate() {
return new KeycloakRestTemplate(keycloakClientRequestFactory);
}
...
}
Note the scope for the template is PROTOTYPE, so Spring will use a different instance for each of the requests being made.
Then, autowire this template and use it to make requests:
#Service
public class UserRetrievalService{
#Autowired
private KeycloakRestTemplate keycloakRestTemplate;
public List<User> getUsers() {
ResponseEntity<User[]> response = keycloakRestTemplate.getForEntity(keycloakUserListEndpoint, User[].class);
return Arrays.asList(response.getBody());
}
}
You will need to implement your own User class which matches the JSON response returned by the keycloak server.
Note that, when user not allowed to access the list, a 403 response code is returned from the Keycloak server. You could even deny it before yourself, using some annotations like: #PreAuthorize("hasRole('VIEW_USERS')").
Last but not least, I think #dchrzascik's answer is well pointed. To sum up, I would say there's actually another way to avoid either retrieving the whole user list from the keycloak server each time or having your users stored in your app database: you could actually cache them, so as you could update that cache if you do user management from your app.
EDIT
I've implemented a sample project to show how to obtain the whole list of users, uploaded to Github. It is configured for a confidential client (when using a public client, the secret should be deleted from the application.properties).
See also:
https://github.com/keycloak/keycloak-documentation/blob/master/securing_apps/topics/oidc/java/spring-security-adapter.adoc
I suggest double checking if you really need to have your own user store. You should relay solely on Keycloak's users federation to avoid duplicating data and hence avoiding issues that comes with that. Among others, Keycloak is responsible for managing users and you should let it do its job.
Since you are using OIDC there are two things that you benefit from:
In the identity token that you get in form of JWT you have a "sub" field. This field uniquely identifies a user. From the OpenID Connect spec:
REQUIRED. Subject Identifier. A locally unique and never reassigned identifier within the Issuer for the End-User, which is intended to be consumed by the Client, e.g., 24400320 or AItOawmwtWwcT0k51BayewNvutrJUqsvl6qs7A4. It MUST NOT exceed 255 ASCII characters in length. The sub value is a case sensitive string.
In keycloak, "sub" is just a UUID. You can use this field to correlate your "object A" to "user B". In your DB this would be just a regular column, not a foreign key.
In Java, you can access this JWT data using security context. You can also take a look at keycloak's authz-springboot quickstart where it is shown how you can access KeycloakSecurityContext - from there you can get an IDToken which has a getSubject method.
Keycloak provides Admin REST API that has a users resource. This is OIDC supported API so you have to be properly authenticated. Using that API you can perform operations on users - including listing them. You can consume that API directly or through use of Java SDK: keycloak admin client.
In this scenario, you should use the JWT that you get from user in request. Using JWT you are sure that someone who is making a request can list all users in that realm. For instance, please consider following code:
#GetMapping("/users")
public List<UserRepresentation> check(HttpServletRequest request){
KeycloakSecurityContext context = (KeycloakSecurityContext) request.getAttribute(KeycloakSecurityContext.class.getName());
Keycloak keycloak = KeycloakBuilder.builder()
.serverUrl("http://localhost:8080/auth")
.realm("example")
.authorization(context.getTokenString())
.resteasyClient(new ResteasyClientBuilder().connectionPoolSize(20).build())
.build();
List<UserRepresentation> list = keycloak.realm("example").users().list();
return list;
}
In that case we are using HttpServletRequest and token that it contains. We can get the same data through use of org.springframework.security.core.Authentication from spring security or directly getting an Authorization header. The thing is that KeycloakBuilder expects a string as a 'authorization', not an AccessToken - this is the reason why you have that error.
Please keep in mind that in order for this to work, user that is creating a requests, has to have a 'view-users' role from 'realm-management' client. You can assign that role to him in 'Role Mapping' tab for that user or some group to which he belongs.
Besides, you have to be properly authenticated to benefit from security context, otherwise you will get a null. Exemplary spring security keycloak configuration class is:
#Configuration
#EnableWebSecurity
#ComponentScan(basePackageClasses = KeycloakSecurityComponents.class)
class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
auth.authenticationProvider(keycloakAuthenticationProvider);
}
#Bean
public KeycloakSpringBootConfigResolver KeycloakConfigResolver() {
return new KeycloakSpringBootConfigResolver();
}
#Bean
#Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.authorizeRequests()
.antMatchers("/api/users/*")
.hasRole("admin")
.anyRequest()
.permitAll();
}
}
Related
By reference https://www.baeldung.com/spring-security-extra-login-fields
I intend to customize the the functionality of Spring security Authentication UsernamePasswordAuthenticationFilter to get the additional customized "loginForm" fields.
I created one customized filter
CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter
and one customized authentication token
public class CustomAuthenticationToken extends UsernamePasswordAuthenticationToken
By using the below configuration:
#EnableWebSecurity
#Configuration(proxyBeanMethods = false)
public class AuthenticationSecurityConfig extends AbstractHttpConfigurer<AuthenticationSecurityConfig, HttpSecurity> {
#Override
public void configure(HttpSecurity http) throws Exception {
AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);
http.addFilterBefore(authenticationFilter(authenticationManager), UsernamePasswordAuthenticationFilter.class);
}
#Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.requestMatchers("/css/**", "/login")
.permitAll()
.requestMatchers("/resources/**")
.permitAll()
.and()
.formLogin(form -> form
.loginPage("/login")
.permitAll()
)
.logout()
.logoutUrl("/logout")
.and()
.authorizeHttpRequests(
auth -> auth.anyRequest().authenticated()
)
.apply(securityConfig());
return http.build();
}
}
I can successfully make the my AuthenticationProvider's
authenticate
method to be executed, and It can successfully return one "authenticated"
Authentication object.
However it will not redirect to the OAuth2 client's redirect page(I setup my project as one OIDC authorization server), it will stay at login page, the log will be like below:
.HttpSessionRequestCache : Saved request http://192.168.0.107:9000/oauth2/authorize?client_id=messaging-client&redirect_uri=http%3A%2F%2F192.168.0.107%3A8080%2Fauth%2Fsigninwin%2Fmain&response_type=code&scope=openid%20profile%20message.read&state=802af0dcd82a483eb726c1dffff0867d&code_challenge=t_tfBjZPRd228uEZuQJ56clfXokGYqiwkudQqKhWQqo&code_challenge_method=S256&prompt=login&response_mode=query&continue&continue to session
2022-11-30T19:48:05.063+08:00 DEBUG 63801 --- [nio-9000-exec-5] o.s.s.web.DefaultRedirectStrategy : Redirecting to http://192.168.0.107:9000/login
2022-11-30T19:48:05.078+08:00 DEBUG 63801 --- [nio-9000-exec-6] o.s.security.web.FilterChainProxy : Securing GET /login
2022-11-30T19:48:05.078+08:00 DEBUG 63801 --- [nio-9000-exec-6] o.s.security.web.FilterChainProxy : Secured GET /login
2022-11-30T19:48:05.084+08:00 DEBUG 63801 --- [nio-9000-exec-6] o.s.s.w.a.AnonymousAuthenticationFilter : Set SecurityContextHolder to anonymous SecurityContext
If I will not use the customized filter CustomAuthenticationFilter, every thing will be fine(But of couse I can't get the additonal LoginForm fileds).
My build.gradle
plugins {
id "org.springframework.boot" version "3.0.0-RC2"
id "io.spring.dependency-management" version "1.0.11.RELEASE"
id "java"
}
implementation "org.springframework.security:spring-security-oauth2-authorization-server:1.0.0"
Any ideas?
I suspected that the attributes of CustomAuthenticationFilter(use new to create the object) is not the same with attributes of the UsernamePasswordAuthenticationFilter that spring security framework will initialize, need further check the disparity.
There are two solutions,
the difference with previous versions is due to this change: https://github.com/spring-projects/spring-security/issues/11110
*** First solution ***
You can use the old strategy (deprecated) with automatic saving of the SecurityContextHolder setting:
http.securityContext((securityContext) -> securityContext.requireExplicitSave(false))
*** Second solution ***
Define an HttpSessionSecurityContextRepository:
#Bean
public HttpSessionSecurityContextRepository securityContextRepository() {
return new HttpSessionSecurityContextRepository();
}
Set to http configuration:
http.securityContext().securityContextRepository(securityContextRepository())
finally set the context repository to your custom filter:
filter.setSecurityContextRepository(securityContextRepository());
I've a Spring Boot side-project that uses JWTs to authorize users for hitting the end points: /users/** based on Authority in my Web Security Config as .mvcMatchers("/users/**").hasAuthority("USER")
I'm using Postman to test and following these steps:
(1) login using a REST Controller which responses with an access token (works fine as user is authenticated)
(2) with the access token, I chose Bearer Token in the Authorization in Postman and paste it there
(3) But I get 403 Forbidden in Postman
My question:
Why am I getting Access is Denied when the user has the authority USER in my database.
Edit
When I change .mvcMatchers("/users/**").hasAuthority("USER") to
.mvcMatchers("/users/**").authenticated() it works fine but I still want to use hasAuthority('USER')
I tried the following so far:
changed to 'ROLE_USER' in my db for the user
changed hasRole('User') in my Web Security Config
Stack trace
2022-10-25 08:24:37.190 TRACE 17964 --- [nio-8080-exec-5] o.s.s.w.a.i.FilterSecurityInterceptor : Authorizing filter invocation [GET /users/profile/] with attributes [hasAuthority('USER')]
2022-10-25 08:24:37.193 TRACE 17964 --- [nio-8080-exec-5] o.s.s.w.a.expression.WebExpressionVoter : Voted to deny authorization
2022-10-25 08:24:37.193 TRACE 17964 --- [nio-8080-exec-5] o.s.s.w.a.i.FilterSecurityInterceptor : Failed to authorize filter invocation [GET /users/profile/] with attributes [hasAuthority('USER')] using AffirmativeBased [DecisionVoters=[org.springframework.security.web.access.expression.WebExpressionVoter#6ce8bf64], AllowIfAllAbstainDecisions=false]
2022-10-25 08:24:37.197 TRACE 17964 --- [nio-8080-exec-5] o.s.s.w.a.ExceptionTranslationFilter : Sending JwtAuthenticationToken [Principal=org.springframework.security.oauth2.jwt.Jwt#4afa460a, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=null], Granted Authorities=[SCOPE_USER]] to access denied handler since access is denied
org.springframework.security.access.AccessDeniedException: Access is denied
at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:73) ~[spring-security-core-5.7.3.jar:5.7.3]
at org.springframework.security.access.intercept.AbstractSecurityInterceptor.attemptAuthorization(AbstractSecurityInterceptor.java:239) ~[spring-security-core-5.7.3.jar:5.7.3]
My Controller
#GetMapping("/users/profile")
public ResponseEntity<?> getUserData(Principal principal) {
User user = userService.findUserByUsername(principal.getName());
List<UserData> userData = userDataService.getAllUserDataForUser(user.getId());
return ResponseEntity.ok(userData);
}
My Web Security Config
#Configuration
#EnableWebSecurity
public class SecurityConfig {
#Autowired
private JpaUserDetailsService jpaUserDetailsService;
private final RsaKeyProperties rsaKeys;
public SecurityConfig(RsaKeyProperties rsaKeys) {
this.rsaKeys = rsaKeys;
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.mvcMatchers("/css/**", "/js/**", "/", "/about", "/register", "/test", "/login", "/loginrest").permitAll()
.mvcMatchers("/users/**").hasAuthority("USER")
.anyRequest().authenticated()
.and()
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.formLogin()
.loginPage("/login")
.defaultSuccessUrl("/users/profile",true)
.and()
.httpBasic(Customizer.withDefaults());
return http.build();
}
#Bean
JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withPublicKey(rsaKeys.publicKey()).build();
}
#Bean
JwtEncoder jwtEncoder() {
JWK jwk = new RSAKey.Builder(rsaKeys.publicKey()).privateKey(rsaKeys.privateKey()).build();
JWKSource<SecurityContext> jwks = new ImmutableJWKSet<>(new JWKSet(jwk));
return new NimbusJwtEncoder(jwks);
}
#Bean
DaoAuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setPasswordEncoder(passwordEncoder());
provider.setUserDetailsService(jpaUserDetailsService);
return provider;
}
It doesn't matter if a user has an authority in the database when you're using JWT after login, because authorities (roles) are fetched from the token itself - that's the whole point of using self-contained tokens.
So you should "guide" Spring and your JwtDecoder on how to search authorities in your JWT and convert them to GrantedAuthority to be used in SecurityContextHolder.
For that create and configure a bean of JwtAuthenticationConverter type:
#Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
final JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
// here choose a claim name where you stored authorities on login (defaults to "scope" and "scp" if not used)
grantedAuthoritiesConverter.setAuthoritiesClaimName("roles");
// here choose a scope prefix (defaults to "SCOPE_" if not used)
grantedAuthoritiesConverter.setAuthorityPrefix("");
final JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}
This configuration will allow JwtDecoder to convert any authority in the "roles" claim of JWT to GrantedAuthority with no prefix.
I'm trying to create a simple spring boot app with spring security, with custom login form.
Issue:
The app directly opens the default login page instead of the custom login view & it could not resolve any of the other thymeleaf views too.
Directly accessing all other views (/home, /error) renders the error: This localhost page can't be found. Opening http://localhost:8080/login takes only to the default login page & not the custom one.
Note: The html templates are placed under /src/main/resources/templates folder - which is the default place springboot looks into.
The key thing here is that I am using the #EnableWebSecurity
annotation, with SecurityFilterChain bean (which is introduced in
Spring Security 5.4), like this:
#EnableWebSecurity
public class WebSecurityConfig {
#Bean
public SecurityFilterChain configure(HttpSecurity http) throws Exception {
...
}
instead of the more common (but, has been deprecated since Spring Security 5.7.0-M2) way of extending the
WebSecurityConfigurerAdapter class as below.
#Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
...
}
It seems the latter works fine without any issues.
Few of the solutions suggested in various forums - which I have already tried.
Keep the project structure in a way that all the other packages are placed under the base package (the one with the main class with #SprintBootApplication annotation).
Use #ComponentScan={<your_base_package>} - if packages are in the order mentioned in point 1. Or, #ComponentScan={<package-1>, <package-2>, etc}, if the packages are independent of each other.
Both the above solutions were suggested to avoid the 404 error & view not resolved issues.
Use #RestController instead of #Controller.
This was suggested both for WhiteLabel error, and when the view name is returned just as a string, instead of a view.
Keep the mapping url value in the controller methods (like, /login) & the the view name different. If the mapping url as /login, change the view name as loginpage.html (or, something different).
This was suggested for circular path issues - when resolving view names.
Some suggested using #RequestMapping("/login") at class-level, rather than method-level. Although, I didn't see any difference with either approach.
Note that all of the above are based on WebSecurityConfigurerAdapter & not on SecurityFilterChain.
The only references from the official documentation/blogs, that I
could find for this requirement (custom login with
SecurityFilterChain) were these two:
i.
https://docs.spring.io/spring-security/site/docs/4.1.3.RELEASE/guides/html5/form-javaconfig.html
ii.
https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter
Unfortunately, performing the steps given there didn't get the result.
I think the below issue is the same (or, related) to this, but no
solution was given to that either.
https://github.com/spring-projects/spring-security/issues/10542
And, almost all the other git/blogs/websites/video references available, are
using the WebSecurityConfigurerAdapter class only.
Web Security Config class:
#EnableWebSecurity
public class WebSecurityConfig {
// #Bean
public SecurityFilterChain configure(HttpSecurity http) throws Exception {
http
.antMatcher("/**")
.authorizeRequests()
.antMatchers("/", "/home", "/login**","/callback/", "/webjars/**", "/css**", "/error**")
.permitAll()
.anyRequest()
.authenticated()
.and()
.formLogin()
// .loginPage("/loginpage")
.usernameParameter("email")
.passwordParameter("password")
.loginPage("/login").loginProcessingUrl("/login")
.permitAll()
.defaultSuccessUrl("/home")
.failureUrl("/login?message=error")
.and()
// .logout()
// .logoutUrl("/perform_logout")
// .logoutSuccessUrl("/login?message=logout");
.logout()
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
.logoutSuccessUrl("/login")
.permitAll();
return http.build();
}
}
Login Controller:
(Leaving some of the commented code, to hint what else has been tried, so far).
At some point in time, the control entered into the login method & printed the model object's value (string). However, the login view still was not getting resolved & resulted in 404 error only.
//#RestController
#Controller
#ResponseBody
//#RequestMapping("/")
public class LoginController {
#GetMapping({"/", "/home"})
// public String home() {
// #GetMapping({"/", "/showHome"})
public ModelAndView home(ModelAndView mav) {
System.out.println("Inside GetMapping(/home) method of LoginController.");
mav.setViewName("home");
mav.addObject("Using #Controller and #ResponseBody, in the Controller");
System.out.println("View Object: " + mav.getView());
System.out.println("View Name: " + mav.getViewName());
System.out.println("mav.hasView(): " + mav.hasView());
return mav;
// return "home";
}
#GetMapping("/login-error")
// #GetMapping("/error")
#RequestMapping("/login")
public String login(HttpServletRequest request, Model model) {
HttpSession session = request.getSession(false);
String errorMessage = null;
if (session != null) {
AuthenticationException ex = (AuthenticationException) session
.getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
if (ex != null) {
errorMessage = ex.getMessage();
}
}
System.out.println("--->" + errorMessage);
model.addAttribute("errorMessage", errorMessage);
return "login";
}
}
I added a main configuration for MVC too, although I believe these are the default configs which Springboot assumes itself & can work even without this.
Main Configuration class:
#Configuration
#ComponentScan("vlan.test.springboot.customLogin")
public class MainConfiguration implements WebMvcConfigurer {
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = {
"classpath:/static/**", "classpath:/public/**", "classpath:/templates/**", "classpath:/resources/**"
};
#Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**").addResourceLocations(CLASSPATH_RESOURCE_LOCATIONS);
registry.addResourceHandler("/templates/**").addResourceLocations("/templates/");
registry.addResourceHandler("/resources/**").addResourceLocations("/resources/");
}
#Override
public void addViewControllers(ViewControllerRegistry viewRegistry) {
viewRegistry.addViewController("/").setViewName("home");
viewRegistry.addViewController("/home").setViewName("home");
viewRegistry.addViewController("/login").setViewName("login");
}
}
Gradle build file:
implementation 'org.springframework.boot:spring-boot-devtools'<br/>
implementation 'org.springframework.boot:spring-boot-starter-web'<br/>
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'<br/>
implementation 'org.springframework.boot:spring-boot-starter-security'<br/>
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'<br/>
implementation 'org.zalando:logbook-spring-boot-starter:2.14.0'
Project structure:
Extract from the logs - that I found relative/worth-noting.
Check the "..invalid session id..." and "..Failed to authorize filter invocation..." parts.
2022-08-18 12:09:43.297 INFO 16596 --- [http-nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 10 ms
2022-08-18 12:09:43.334 DEBUG 16596 --- [http-nio-8080-exec-1] o.s.security.web.FilterChainProxy : Securing GET /
2022-08-18 12:09:43.351 DEBUG 16596 --- [http-nio-8080-exec-1] s.s.w.c.SecurityContextPersistenceFilter : Set SecurityContextHolder to empty SecurityContext
2022-08-18 12:09:43.363 DEBUG 16596 --- [http-nio-8080-exec-1] ****o.s.s.w.a.AnonymousAuthenticationFilter : Set SecurityContextHolder to anonymous SecurityContext
2022-08-18 12:09:43.364 DEBUG 16596 --- [http-nio-8080-exec-1] o.s.s.w.session.SessionManagementFilter : Request requested invalid session id EF53E44688D581C69527A5442A987DB6
2022-08-18 12:09:43.394 DEBUG 16596 --- [http-nio-8080-exec-1] o.s.s.w.a.i.FilterSecurityInterceptor : Failed to authorize filter invocation [GET /] with attributes [authenticated]
2022-08-18 12:09:43.445 DEBUG 16596 --- [http-nio-8080-exec-1] o.s.s.w.s.HttpSessionRequestCache : Saved request http://localhost:8080/ to session****
2022-08-18 12:09:43.448 DEBUG 16596 --- [http-nio-8080-exec-1] s.w.a.DelegatingAuthenticationEntryPoint : Trying to match using And [Not [RequestHeaderRequestMatcher [expectedHeaderName=X-Requested-With, expectedHeaderValue=XMLHttpRequest]], MediaTypeRequestMatcher [contentNegotiationStrategy=org.springframework.web.accept.HeaderContentNegotiationStrategy#51020050, matchingMediaTypes=[application/xhtml+xml, image/*, text/html, text/plain], useEquals=false, ignoredMediaTypes=[*/*]]]
2022-08-18 12:09:43.450 DEBUG 16596 --- [http-nio-8080-exec-1] s.w.a.DelegatingAuthenticationEntryPoint : Match found! Executing org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint#416ee886
2022-08-18 12:09:43.454 DEBUG 16596 --- [http-nio-8080-exec-1] o.s.s.web.DefaultRedirectStrategy : Redirecting to http://localhost:8080/login
2022-08-18 12:09:43.459 DEBUG 16596 --- [http-nio-8080-exec-1] w.c.HttpSessionSecurityContextRepository : Did not store empty SecurityContext
2022-08-18 12:09:43.466 DEBUG 16596 --- [http-nio-8080-exec-1] w.c.HttpSessionSecurityContextRepository : Did not store empty SecurityContext
2022-08-18 12:09:43.466 DEBUG 16596 --- [http-nio-8080-exec-1] s.s.w.c.SecurityContextPersistenceFilter : Cleared SecurityContextHolder to complete request
2022-08-18 12:09:43.487 DEBUG 16596 --- [http-nio-8080-exec-2] o.s.security.web.FilterChainProxy : Securing GET /login
Complete log here: https://codeshare.io/dwlLDK
(Seems it'd live for only 24 hrs. Pls. let me know if you couldn't access it).
Edit(2022-08-22):
An excerpt from the application.properties file (just the thymeleaf & security config) - which is the reason for the issue (as explained in the accepted [self-identified] answer).
# thymeLeaf
spring.thymeleaf.enabled=true
spring.thymeleaf.cache=false
spring.thymeleaf.check-template=true
spring.thymeleaf.check-template-location=true
spring.thymeleaf.prefix=classpath:/templates
spring.thymeleaf.suffix=.html
#security
server.error.whitelabel.enabled=false
I have an example Spring Boot application here:
https://github.com/Aliuken/JobVacanciesApp_Java11
Hope that it helps.
I fixed the issue myself.
(Adding it as an accepted answer, so that it's easy for people who stumble upon this page.)
The actual issue was a missing trailing / in the thymeleaf prefix config, in the application.properties file:
spring.thymeleaf.prefix=classpath:/templates.
This should have been spring.thymeleaf.prefix=classpath:/templates/.
Once I added it, it started recognizing the templates (including the custom login template/view).
I am trying to setup Resource Server to validate jwt tokens with Authentication server by using NimbusJwtDecoder.withJwkSetUri
Following is my configuration in Resource server
#Configuration
public class ResourceServerConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.oauth2ResourceServer(c -> {
c.jwt(j -> {
j.decoder(jwtDecoder());
});
});
http.authorizeRequests().anyRequest().authenticated();
}
#Bean
public JwtDecoder jwtDecoder(){
RestTemplate rest = new RestTemplate();
List<ClientHttpRequestInterceptor> interceptors = rest.getInterceptors();
interceptors.add(new BasicAuthenticationInterceptor("client1","secret1"));
interceptors.add(new LoggingInterceptor());
rest.setInterceptors(interceptors);
return NimbusJwtDecoder.withJwkSetUri("http://localhost:8080/oauth/token_key").restOperations(rest).build();
}
}
And I have simple endpoint in Resource server to test
#RestController
public class HelloController {
#GetMapping("/hello")
public String hello(){
return "Hello";
}
}
However when I access "/hello" with access token already got from auth server then I get unauthorised response and observe following logs in Resource server.
Response body: {"alg":"SHA256withRSA","value":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAo8ieQxTVHq4jBSM3JpO7UcFOa5UrorX5KhRbMqEtT746yGTqqv+t1EW6l8G31bGc6G/IHy7032vpKNxAgLVcoCrdoOakbGLb1y2+ElB9QmEEEplARWLQ43t47ywd0UA7MhF9WIbud1Z6kqySrsrBTzjPu+fwCElzUFveyaiPsZDlrEAU6yMLQ23nEP3bBCgDtGMVs1a7RsmAzfUsruelqNaAQQamobkjEMWB8ewZWjtsriIldNjGEAUznw4bcJ963ExtmgfMAHS7XhuWqu58yIzdBopxhZvt/falc5cyp7OCP1ZPEjkHJ5TikJksqOgDgWhiIVtr/3cUjd8vnX4y4QIDAQAB\n-----END PUBLIC KEY-----"}
2021-05-15 11:54:47.468 DEBUG 40223 --- [nio-9090-exec-3] o.s.web.client.RestTemplate : Response 200 OK
2021-05-15 11:54:47.468 DEBUG 40223 --- [nio-9090-exec-3] o.s.s.o.s.r.a.JwtAuthenticationProvider : Failed to authenticate since the JWT was invalid
2021-05-15 11:54:47.469 DEBUG 40223 --- [nio-9090-exec-3] w.c.HttpSessionSecurityContextRepository : Did not store empty SecurityContext
2021-05-15 11:54:47.469 DEBUG 40223 --- [nio-9090-exec-3] s.s.w.c.SecurityContextPersistenceFilter : Cleared SecurityContextHolder to complete request
It seems auth server was able to provide public key successfully but resource server could not use this public key to validate provided jwt token.
Any help is highly appreciated.
Hi please add class #EnableAuthorizationServer anotation but not working please read this Outh2 documentation https://projects.spring.io/spring-security-oauth/docs/oauth2.html
Basically I'm trying to access a bearer-only endpoint from a client app which is using a "KeycloakRestTemplate".
I did follow this guidelines 1:1 (it is in German) : https://blog.codecentric.de/2017/09/keycloak-und-spring-security-teil-1-einrichtung-frontend/
My problem is that when I see the logs, the authentication on the side of the bearer only endpoint seems successful, as shown bellow:
Found [1] values in authorization header, selecting the first value for Bearer.
o.k.a.BearerTokenRequestAuthenticator : Verifying access_token
o.k.a.BearerTokenRequestAuthenticator : access_token: [LONG TOKEN HERE]
o.k.a.RefreshableKeycloakSecurityContext : checking whether to refresh.
org.keycloak.adapters.AdapterUtils : use realm role mappings
org.keycloak.adapters.AdapterUtils : Setting roles:
org.keycloak.adapters.AdapterUtils : role: create_vouchers
org.keycloak.adapters.AdapterUtils : role: public_realm_access
org.keycloak.adapters.AdapterUtils : role: overview_orders
org.keycloak.adapters.AdapterUtils : role: uma_authorization
User 'c1500da2-855f-4306-ab65-662160558101' invoking 'http://localhost:8082/articles' on client 'articlesBearerOnlyService'
o.k.adapters.RequestAuthenticator : Bearer AUTHENTICATED
.k.a.t.AbstractAuthenticatedActionsValve : AuthenticatedActionsValve.invoke /articles
o.k.a.AuthenticatedActionsHandler : AuthenticatedActionsValve.invoke http://localhost:8082/articles
cors validation not needed as were not a secure session or origin header was null: {0}
o.k.a.AuthenticatedActionsHandler : Policy enforcement is disabled.
but then directly afterwards on the logs comes this:
o.k.adapters.PreAuthActionsHandler : adminRequest http://localhost:8082/login
o.k.adapters.PreAuthActionsHandler : checkCorsPreflight http://localhost:8082/login
.k.a.t.AbstractAuthenticatedActionsValve : AuthenticatedActionsValve.invoke /login
o.k.a.AuthenticatedActionsHandler : AuthenticatedActionsValve.invoke http://localhost:8082/login
o.k.a.AuthenticatedActionsHandler : Origin: null uri: http://localhost:8082/login
o.k.a.AuthenticatedActionsHandler : cors validation not needed as were not a secure session or origin header was null: {0}
o.k.a.AuthenticatedActionsHandler : Policy enforcement is disabled.
so, it tries to redirect to adminRequest http://localhost:8082/login? why, and how could this be solved?
I did also also tried with postman (getting the acces-token from the token end-point) and pasting it on the Authorization header of this "bearer-only" endpoint, and similarly by seeing the logs, the user seems authorized exacltly like in the first log block above, the diference is that is doesn't try to redirect anywhere but I receive a 401.
{
"timestamp": "2019-09-05T11:18:51.347+0000",
"status": 401,
"error": "Unauthorized",
"message": "Unauthorized",
"path": "/articles" }
Could somebody please provide some guidance into a possible solution?
Thanks in advance!
----------------------------------------
EDITED
----------------------------------------
here is the application properties file:
server.port = 8082
spring.application.name = articleBearerOnlyService
keycloak.auth-server-url=http://localhost:8080/auth
keycloak.realm=[REALM]
keycloak.resource=articlesBearerOnlyService
keycloak.bearer-only=true
keycloak.cors=true
keycloak.credentials.secret=[SECRET]
keycloak.ssl-required = external
# access controlled through spring security
#keycloak.security-constraints[0].auth-roles[0]=overview_orders
#keycloak.security-constraints[0].security-collections[0].patterns[0]=/articles
logging.level.org.keycloak=TRACE
and here the SecurityConfig :
#KeycloakConfiguration
#EnableWebSecurity
class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
private final KeycloakClientRequestFactory keycloakClientRequestFactory;
public SecurityConfig(KeycloakClientRequestFactory keycloakClientRequestFactory) {
this.keycloakClientRequestFactory = keycloakClientRequestFactory;
//to use principal and authentication together with #async
SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
}
/* remove default spring "ROLE_" prefix appending to keycloak's roles*/
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
auth.authenticationProvider(keycloakAuthenticationProvider);
}
#Bean
#Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
// NullAuthenticatedSessionStrategy() for bearer-only services
return new NullAuthenticatedSessionStrategy();
}
/* configure cors & requests handling behaviour*/
#Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.cors()
.and()
.csrf()
.disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.sessionAuthenticationStrategy(sessionAuthenticationStrategy())
.and()
.authorizeRequests()
.antMatchers("/articles").hasRole("overview_orders")
.anyRequest().permitAll();
}
// Spring boot integration
#Bean
public KeycloakConfigResolver keycloakConfigResolver() {
return new KeycloakSpringBootConfigResolver();
}
// *************************** Avoid Bean redefinition ********************************
#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;
}
#Bean
public FilterRegistrationBean keycloakAuthenticatedActionsFilterBean(
KeycloakAuthenticatedActionsFilter filter) {
FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
registrationBean.setEnabled(false);
return registrationBean;
}
#Bean
public FilterRegistrationBean keycloakSecurityContextRequestFilterBean(
KeycloakSecurityContextRequestFilter filter) {
FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
registrationBean.setEnabled(false);
return registrationBean;
}
#Bean
#Override
#ConditionalOnMissingBean(HttpSessionManager.class)
protected HttpSessionManager httpSessionManager() {
return new HttpSessionManager();
}
}
The #SpringBootApplication annotation is a composite of these three annotations: #EnableAutoConfiguration, #ComponentScan and #Configuration. Annotating a class e.g. com.example.demo.DemoApplication with #SpringBootApplication, results in Spring looking for other components, configurations, and services inside com.example.demo and all of its sub-packages.
A class like com.example.config.DemoConfig therefore cannot be found by Spring automatically. If you want, you can give hints to Spring where to look for components via #ComponentScan(basePackages = "com.some.package"). Check out this article if you like to know more.
In this particular case, my #KeycloakConfiguration class SecurityConfig{...}, was completely ignored, and thus the application behaved as if none security config was provided at all.
Now, why was the SecurityConfig ignored?
- it turned out to be (I almost feel shame) path location of the class; I usually would place such a class under:
com.[company].[domain].configuration
In my case (since I'm only prototyping with keycloak + spring and not particularly concerned with class location right now). I did place my SecurityConfig class under:
com.[company].configuration
This made spring boot completely ignore this class.
Follow up question: I'm new to Sprint boot, is it 100% necessary to place all code under "com.[company].[domain].configuration", without modifying the pom (just having a newly created vanilla springboot project via the initializr)?