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);
}
}
Related
I have a spring application that exposes some webflux endpoints, I use a jwt token to authorize the post calls but we I also need the information given by the userinfo endpoint.
I have a SecurityWebFilterChain bean right now and we are using an oauth2ResourceServer configuration then calling the userinfoendpoint for further checks.
What is the best way to validate a jwt token then get the userinfo enpoint information for further validations?
ps: the authorization server is a third part one.
Security configuration without the external call for the user-info
#Bean
public SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
http
.cors()
.and()
.httpBasic().disable()
.formLogin().disable()
.csrf().disable()
.logout().disable()
.oauth2Client()
.and()
.authorizeExchange()
.pathMatchers(HttpMethod.POST).authenticated()
.anyExchange().permitAll()
.and().oauth2ResourceServer().jwt()
;
return http.build();
}
The UserInfo Endpoint is part of OpenID Connect 1.0, and returns user information for an access token. It is not automatically called from a resource server (http.oauth2ResourceServer()) by Spring Security.
Based on your security configuration, it looks like you're wanting to use both OAuth2 Client (http.oauth2Client()) and OAuth2 Resource Server (http.oauth2ResourceServer()) in the same application. OAuth2 Client isn't designed for this use case (calling UserInfo from a resource server), and therefore would require customization to be adapted for such a case. Instead, you can simply use a RestTemplate or WebClient to call the UserInfo endpoint yourself.
You can do this in a custom Converter<Jwt, Collection<GrantedAuthority>> (or in this case the reactive version) like so:
#Configuration
#EnableWebFluxSecurity
public class SecurityConfiguration {
#Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
// #formatter:off
http
.authorizeExchange((authorize) -> authorize
.anyExchange().authenticated()
)
.oauth2ResourceServer((oauth2) -> oauth2
.jwt((jwt) -> jwt
.jwtAuthenticationConverter(jwtAuthenticationConverter())
)
);
// #formatter:on
return http.build();
}
private Converter<Jwt, Mono<AbstractAuthenticationToken>> jwtAuthenticationConverter() {
ReactiveJwtAuthenticationConverter converter = new ReactiveJwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter());
return converter;
}
private Converter<Jwt, Flux<GrantedAuthority>> jwtGrantedAuthoritiesConverter() {
JwtGrantedAuthoritiesConverter delegate = new JwtGrantedAuthoritiesConverter();
return (jwt) -> getUserInfo(jwt.getTokenValue())
.flatMapIterable((userInfo) -> {
Collection<GrantedAuthority> authorities = delegate.convert(jwt);
// TODO: Add authority from userInfo...
return authorities;
});
}
private Mono<Map<String, String>> getUserInfo(String accessToken) {
// TODO: Call user info and extract one or more claims from response
return Mono.just(new HashMap<>());
}
}
It is way more efficient to decode the data from a JWT than querying an external endpoint.
As a consequence, the best option is to configure the authorization-server (even if it is 3rd party) to enrich the JWTs (access and ID tokens) with the data you need for authorization. Most OIDC authorization-servers support it, just refer to its documentation (Keycloak, Auth0, Cognito, ...).
Once all the claims you need are in access-tokens, you can read it on resource-server from the JwtAuthenticationToken instance (or OAuth2AuthenticationToken for client app with oauth2login) in the security-context. This allows to write stuff like:
#PostMapping("/answers/{subject}")
#PreAuthorize("#userSubject == #auth.token.claims['sub']")
public ResponseEntity<String> addAnswer(#PathVariable("subject") String userSubject, #RequestBody #Valid AnswerDto, JwtAuthenticationToken auth) {
...
}
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 new to spring security, I have written test endpoint with google oauth2, I can authencitace with web browser and my end point works, but not working with postman,
here is my properties
spring.security.oauth2.client.registration.google.client-id={{CLIENT_ID}}
spring.security.oauth2.client.registration.google.client-secret={{SECRET}}
spring.security.oauth2.client.registration.google.scope=openid,profile,email
Here is my controller
#RestController
public class UserController {
#GetMapping()
public String get() {
return "testing java 18";
}
}
Here is my security config
#Configuration
#EnableWebSecurity(debug = true)
public class SecurityConfig {
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
.oauth2Login(withDefaults());
return http.build();
}
}
with above configuration, if I test http://localhost:8080 on browser it redirects to google signing and the text testing java 18 appears.
But when I use postman with below config
Type: `OAuth 2.0`
Add authorization to data to: `Request Header`
Configure New Token:
Token Name: {{SOME_RANDOM_NAME}}
Grant Type: `Authorization Code`
Callback URL: `https://oauth.pstmn.io/v1/callback`
Auth URL: `https://accounts.google.com/o/oauth2/auth`
Access Token URL: `https://oauth2.googleapis.com/token`
Client ID: `{{CLIENT_ID}}` same used in application.properties
Client Secret: `{{CLIENT_SECRET}}` same used in application.properties
Scope: `profile email openid`
State: empty
Client Authentication: `Send as basic auth header`
With above if I hit Get New Access Token, I do get new Access Token and id_token with google signing, after I hit Use Token I and send GET on the endpoint I get 403
Also If I use id_token and change Type to Bearer Token I get the same.
with older version If I use below Securityy config, Endpoint works when I send my id_token as Type to Bearer Token
#Configuration
#EnableWebSecurity(debug = false)
public class SecurityConfig extends WebSecurityConfigurerAdapter{
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/**").fullyAuthenticated()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.oauth2ResourceServer().jwt();
}
}
I tried searching in the forum but didn't; got much.
I have a requirement to authorize users from a 3rd party app. Basically it's a REST api call, but that's not the problem. Every time I navigate to a page, I get automatically re-directed to the error page, with no explanation at all. There is nothing in the logs, even with my logging: logging.level.org.springframework.security=DEBUG and my root level at WARN
My Security Configuration looks like this:
#Configuration
#EnableWebSecurity
#ConfigurationProperties("security")
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
ELPAuthenticationProvider authenticationProvider;
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
elpLogger.debug("****************Configuring HttpSecurity");
http.authorizeRequests().antMatchers("/hello/**").permitAll();
http.authorizeRequests().anyRequest().authenticated();
}
}
And my Authentication Provider:
#Component
public class ELPAuthenticationProvider implements AuthenticationProvider {
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
logger.debug("In Authenticate");
final List<GrantedAuthority> grantedAuths = new ArrayList<>();
grantedAuths.add(new SimpleGrantedAuthority("ROLE_USER"));
final UserBean principal = new UserBean("admin", "password", grantedAuths);
final Authentication auth = new UsernamePasswordAuthenticationToken(principal, "password", grantedAuths);
return auth;
}
#Override
public boolean supports(Class<? extends Object> authentication) {
return true;
}
}
To me, this looks like it should authenticate anything. But everything but my HelloWorldController ("/hello") I get thrown to my error page with no explanation. My log looks like this:
o.s.security.web.FilterChainProxy : Securing GET /
s.s.w.c.SecurityContextPersistenceFilter : Set SecurityContextHolder to empty SecurityContext
o.s.s.w.a.AnonymousAuthenticationFilter : Set SecurityContextHolder to anonymous SecurityContext
o.s.s.w.a.i.FilterSecurityInterceptor : Failed to authorize filter invocation [GET /] with attributes [authenticated]
o.s.s.w.s.HttpSessionRequestCache : Saved request http://localhost:7080/ to session
o.s.s.w.a.Http403ForbiddenEntryPoint : Pre-authenticated entry point called. Rejecting access
w.c.HttpSessionSecurityContextRepository : Did not store empty SecurityContext
w.c.HttpSessionSecurityContextRepository : Did not store empty SecurityContext
s.s.w.c.SecurityContextPersistenceFilter : Cleared SecurityContextHolder to complete request
c.e.web.controllers.ELPErrorController : ************************ Error handler
So I don't know why my authentication provider is not even being called, and secondly, I have NO idea why an exception is being thrown. (Or why am I being re-directed to the error page and not the not-authorized page)
Any Ideas?
EDIT
I removed the #Component annotation from the AuthenticationProvider and declared it a bean in my main Application.java Autowired it into the SecurityConfiguration. I made the changes in the example above. Exact same problem. No Change.
Given that the authenticate method in the AuthenticationProvider interface takes an authentication as argument, we can rest assured that some kind of initial authentication has to be in place, to give the provider something to work on. The following excerpts are given in Kotlin.
Even with a standard setup with basic authentication
#Configuration
class SecurityAssets {
#Bean
fun passwordEncoder(): PasswordEncoder =
return BCryptPasswordEncoder()
}
#EnableWebSecurity
class Config(val encoder: PasswordEncoder): WebSecurityConfigurerAdapter() {
override fun configure(auth: AuthenticationManagerBuilder) {
auth
.inMemoryAuthentication()
.withUser("user")
.password(encoder.encode("password"))
.roles("USER")
}
override fun configure(http: HttpSecurity) {
http
.authorizeRequests()
.anyRequest()
.authenticated()
}
}
the above will not trigger an authentication to take place. To make this happen, we need to add some method for Spring to create the initial authentication which is then authenticated by the configured provider:
#EnableWebSecurity
class Config(val encoder: PasswordEncoder): WebSecurityConfigurerAdapter() {
...
override fun configure(http: HttpSecurity) {
http
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.httpBasic()
}
}
Now it works!
Similarly in your case, something like this will work:
class CustomAuthenticationProvider : AuthenticationProvider {
override fun authenticate(authentication: Authentication): Authentication {
println("In Authenticate")
val grantedAuths: MutableList<GrantedAuthority> = ArrayList()
grantedAuths.add(SimpleGrantedAuthority("ROLE_USER"))
val principal = AuthenticatedPrincipal { "Name" }
return UsernamePasswordAuthenticationToken(principal, "password", grantedAuths)
}
override fun supports(authentication: Class<*>?): Boolean {
println("In supports")
return true
}
}
#EnableWebSecurity
class Config(val encoder: PasswordEncoder): WebSecurityConfigurerAdapter() {
override fun configure(auth: AuthenticationManagerBuilder) {
auth
.authenticationProvider(CustomAuthenticationProvider())
}
override fun configure(http: HttpSecurity) {
http
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.httpBasic()
}
}
How you make the custom provider available to the configurer is not important, you can use a bean or simply instantiate it in place if you want it.
If you need some different main authentication method, you can change httpBasic to something else. If you need to customize the actual method of authentication (e.g. look for some custom headers or something else for which a filter does not already exists), then you should implement a custom filter which is added to the security filter chain and which then delegates to your provider.
The filters in the security filter chain are what actually processes a request and simply adding a security provider doesn't add a new filter, which, in this case, httpBasic does.
I'm looking for the proper way to add role based authentication where I extract out roles from a JWT.
Ideally, I would like to extract roles from the JWT after authentication has taken place. This will work by inspecting the web token for some fields related to the roles which we are getting from our authentication system, keycloak.
My question is: is it possible to append roles to a request and then use http configuration to require one of these extracted roles?
Below is some relevant code that will help explain what I'm doing.
In my WebSecurityConfigurer, I make the access token available, scoped per request.
#Bean
#Scope(scopeName = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public AccessToken accessToken() {
try {
HttpServletRequest request =
((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes())
.getRequest();
return ((KeycloakSecurityContext) ((KeycloakAuthenticationToken) request
.getUserPrincipal())
.getCredentials()).getToken();
} catch (Exception exc) {
return null;
}
}
Then I override some of the configuration of the http in the configure method.
http
// Disable session management
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// Allow calls to OPTIONS
.authorizeRequests().antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.and()
// Authenticate every other call
.authorizeRequests().anyRequest().authenticated()
.and()
.csrf().disable();
Ideally, what I'd like to achieve is something like:
http.antMatchers("/foo").hasRole("jwt_extracted_role")
I am currently creating custom filters which extract roles from the token and then check for the correct roles, but this is maybe more cumbersome than it needs to be.
Any pointers on which methods of which configuration classes I should be looking to override to extract the roles from the request's, and add them to the request?
I ended up solving this by overriding the KeycloakAuthenticationProvider and providing my override class as a bean in the WebSecurityConfig. My class is below:
public class ResourceAwareAuthenticationProvider extends KeycloakAuthenticationProvider {
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
... here I add granted authorities from the token's credentials ...
}
}
Then in my class WebSecurityConfigurer extends KeycloakWebSecurityConfigurerAdapter I override the AuthenticationProvider:
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return new ProviderManager(Lists.newArrayList(new ResourceAwareAuthenticationProvider()));
}
This allows me to do configuration like:
http.authorizeRequests()
.antMatchers("/**").hasAuthority("my-resource-authority")