Why does Spring Security reject my Keycloak auth token with "No AuthenticationProvider found"? - spring-boot

I'm trying to figure out why my Spring Boot application is rejecting my Keycloak JWT bearer token with a "No AuthenticationProvider found" error message.
I have a few services running in a docker compose environment:
ui (angular) -> proxy (nginx) -> rest api (spring boot) -> auth service (keycloak)
The angular ui pulls the correct keycloak client from the rest service, and then authenticates without issue. I get back a JWT token, and then turn around and hand that to follow on requests to the rest api in a header Authorization: bearer [token].
In the rest API, I can see the correct bearer token come in as a header:
2022-02-11 01:01:31.411 DEBUG 13 --- [nio-8080-exec-4] o.a.coyote.http11.Http11InputBuffer : Received [GET /api/v3/accounts HTTP/1.0
X-Real-IP: 192.168.80.1
X-Forwarded-For: 192.168.80.1
Host: rest-api.mylocal.com
Connection: close
Accept: application/json, text/plain, */*
Authorization: Bearer eyJhbGciO...
...
2022-02-11 01:01:31.421 DEBUG 13 --- [nio-8080-exec-4] o.k.adapters.PreAuthActionsHandler : adminRequest http://rest-api.mylocal.com/api/v3/accounts
...
So the bearer token is there, and with https://jwt.io/ I can verify it's what I would expect:
{
"exp": 1644515847,
...
"iss": "http://auth-service.mylocal.com/auth/realms/LocalTestRealm",
...
"typ": "Bearer",
"azp": "LocalTestClient",
...
"allowed-origins": [
"http://web-ui.mylocal.com"
],
"realm_access": {
"roles": [
"offline_access",
"default-roles-localtestrealm",
"uma_authorization"
]
},
"resource_access": {
"account": {
"roles": [
"manage-account",
"manage-account-links",
"view-profile"
]
}
},
"scope": "openid email profile",
...
}
Processing continues by the rest api - it contacts the keycloak service and pulls the well known config:
...
2022-02-11 01:01:33.321 INFO 13 --- [nio-8080-exec-4] o.keycloak.adapters.KeycloakDeployment : Loaded URLs from http://auth-service.mylocal.com/auth/realms/LocalTestRealm/.well-known/openid-configuration
...
Finally it looks like it successfully parses the bearer token apart, grabs the user and authenticates them:
2022-02-11 01:01:33.521 DEBUG 13 --- [nio-8080-exec-4] o.a.h.impl.conn.tsccm.ConnPoolByRoute : Releasing connection [{}->http://auth-service.mylocal.com:80][null]
2022-02-11 01:01:33.521 DEBUG 13 --- [nio-8080-exec-4] o.a.h.impl.conn.tsccm.ConnPoolByRoute : Pooling connection [{}->http://auth-service.mylocal.com:80][null]; keep alive indefinitely
2022-02-11 01:01:33.521 DEBUG 13 --- [nio-8080-exec-4] o.a.h.impl.conn.tsccm.ConnPoolByRoute : Notifying no-one, there are no waiting threads
2022-02-11 01:01:33.530 DEBUG 13 --- [nio-8080-exec-4] o.k.a.rotation.JWKPublicKeyLocator : Realm public keys successfully retrieved for client LocalTestClient. New kids: [8a7dIQFASdC8BHa0mUWwZX7RBBJSeJItdmzah0Ybpcw]
2022-02-11 01:01:33.546 DEBUG 13 --- [nio-8080-exec-4] o.k.a.BearerTokenRequestAuthenticator : successful authorized
2022-02-11 01:01:33.550 TRACE 13 --- [nio-8080-exec-4] o.k.a.RefreshableKeycloakSecurityContext : checking whether to refresh.
2022-02-11 01:01:33.550 TRACE 13 --- [nio-8080-exec-4] org.keycloak.adapters.AdapterUtils : useResourceRoleMappings
2022-02-11 01:01:33.550 TRACE 13 --- [nio-8080-exec-4] org.keycloak.adapters.AdapterUtils : Setting roles:
2022-02-11 01:01:33.555 DEBUG 13 --- [nio-8080-exec-4] a.s.a.SpringSecurityRequestAuthenticator : Completing bearer authentication. Bearer roles: []
2022-02-11 01:01:33.556 DEBUG 13 --- [nio-8080-exec-4] o.k.adapters.RequestAuthenticator : User 'bf7307ca-9352-4a02-b288-0565e2b57292' invoking 'http://rest-api.mylocal.com/api/v3/accounts' on client 'LocalTestClient'
2022-02-11 01:01:33.556 DEBUG 13 --- [nio-8080-exec-4] o.k.adapters.RequestAuthenticator : Bearer AUTHENTICATED
2022-02-11 01:01:33.556 DEBUG 13 --- [nio-8080-exec-4] f.KeycloakAuthenticationProcessingFilter : Auth outcome: AUTHENTICATED
and then immediately after that fails with the No AuthenticationProvider found error:
2022-02-11 01:01:33.559 TRACE 13 --- [nio-8080-exec-4] f.KeycloakAuthenticationProcessingFilter : Failed to process authentication request
org.springframework.security.authentication.ProviderNotFoundException: No AuthenticationProvider found for org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken
at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:234) ~[spring-security-core-5.5.1.jar!/:5.5.1]
I'm at a loss how it can say Bearer AUTHENTICATED followed by Auth outcome: AUTHENTICATED followed by No AuthenticationProvider found... I'm assuming it somehow can't convert this bearer token into a Keycloak token, even though it definitely came from my Keycloak server.
My app config:
#ComponentScan({"com.mycompany"})
#Configuration
#EnableJpaRepositories(basePackages = "com.mycompany")
#EntityScan("com.mycompany")
#ComponentScan(basePackageClasses = KeycloakSecurityComponents.class)
public class ApplicationConfiguration
extends KeycloakWebSecurityConfigurerAdapter {
#Override
protected void configure(final HttpSecurity http) throws Exception {
super.configure(http);
http
.authorizeRequests()
// These paths (comma separated) are allowed to all
.antMatchers("/api/v3/auth/config").permitAll()
.and()
.authorizeRequests()
// Everything else should be authenticated
.anyRequest().authenticated()
.and()
.csrf().disable();
}
#Bean
#Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new NullAuthenticatedSessionStrategy();
}
#Bean
public KeycloakConfigResolver keycloakConfigResolver() {
// This just pulls the Keycloak config from a DB instead of the config file
return new CustomKeycloakConfigResolver();
// return new KeycloakSpringBootConfigResolver();
}
}

Missing the global config to autowire in a Keycloak auth provider:
#Autowired
public void configureGlobal(final AuthenticationManagerBuilder auth)
throws Exception {
KeycloakAuthenticationProvider keycloakAuthenticationProvider =
keycloakAuthenticationProvider();
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(
new SimpleAuthorityMapper()
);
auth.authenticationProvider(keycloakAuthenticationProvider);
}

Related

CSRF on spring cloud gateway removing formData from POST requests 400 bad request error

I have enabled CSRF on my spring cloud api gateway server.
I have angular as my GUI framework which calls the rest services through the api gateway.
I have used a custom filter to add the CSRF token to the response headers.
When the POST call is made I see that the formData is lost. So I always get 400 Bad request errors.
I disabled CSRF and the request goes through fine without any issues.
Is there something wrong?
Below is my spring cloud gateway configuration. Gateway is used only for routing the requests to other microservices, it does not have any controllers or rest endpoints.
#SpringBootApplication
public class GatewayApplication {
#Autowired
ProfileManager profileManager;
#PostConstruct
public void onInit() {
profileManager.printActiveProfiles();
}
public static void main(String[] args) { SpringApplication.run(GatewayApplication.class, args); }
#Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
http.authorizeExchange().anyExchange().permitAll();
http.csrf().csrfTokenRepository(CookieServerCsrfTokenRepository.withHttpOnlyFalse());
return http.build();
}
}
below is the filter code
#Component
public class CsrfHeaderFilter implements WebFilter {
#Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
Mono<CsrfToken> token = (Mono<CsrfToken>) exchange.getAttributes().get(CsrfToken.class.getName());
if (token != null) {
return token.flatMap(t -> chain.filter(exchange));
}
return chain.filter(exchange);
}
}
My POST rest endpoints are defined with
#RequestParam
below is the code from one of the rest service endpoints. It is an upstream service implemented using the traditional servlet springboot framework.
#RequestMapping(value = "terminate/{listName}", method = RequestMethod.POST, consumes = MediaType.APPLICATION_FORM_URLENCODED)
#CrossOrigin
#Loggable (activityname = ActivityLogConstants.DESCRIPTOR_TERMINATE)
public Response terminate(#Context HttpServletRequest reqContext, #PathVariable String listName, #RequestParam(value = "rowData") String rowData)
throws ServiceException {....}
The formData is lost by the time the request reaches the upstream services.
Looks like the filter in spring cloud gateways is blocking formData
here is my netty configuration:
#Configuration
public class NettyConfiguration implements WebServerFactoryCustomizer<NettyReactiveWebServerFactory> {
#Value("${server.max-initial-line-length:65536}")
private int maxInitialLingLength;
#Value("${server.max-http-header-size:65536}")
private int maxHttpHeaderSize;
public void customize(NettyReactiveWebServerFactory container) {
container.addServerCustomizers(
httpServer -> httpServer.httpRequestDecoder(
httpRequestDecoderSpec -> {
httpRequestDecoderSpec.maxHeaderSize(maxHttpHeaderSize);
httpRequestDecoderSpec.maxInitialLineLength(maxInitialLingLength);
return httpRequestDecoderSpec;
}
)
);
}
}
below is my application.yml
sample log:
2022-07-28 09:18:20.743 DEBUG 26532 --- [ctor-http-nio-5] r.n.http.client.HttpClientOperations : [id:199cd714-5, L:/127.0.0.1:50342 - R:localhost/127.0.0.1:18080] Received response (auto-read:false) : [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, Strict-Transport-Security=max-age=31536000 ; includeSubDomains, X-Frame-Options=DENY, X-Application-Context=application:18080, Date=Thu, 28 Jul 2022 03:48:20 GMT, Connection=close, content-length=0]
2022-07-28 09:18:20.744 DEBUG 26532 --- [ctor-http-nio-5] r.n.r.DefaultPooledConnectionProvider : [id:199cd714-5, L:/127.0.0.1:50342 - R:localhost/127.0.0.1:18080] onStateChange(POST{uri=/cms-service/webapi/terminate/descriptor, connection=PooledConnection{channel=[id: 0x199cd714, L:/127.0.0.1:50342 - R:localhost/127.0.0.1:18080]}}, [response_received])
2022-07-28 09:18:20.744 DEBUG 26532 --- [ctor-http-nio-5] reactor.netty.channel.FluxReceive : [id:199cd714-5, L:/127.0.0.1:50342 - R:localhost/127.0.0.1:18080] FluxReceive{pending=0, cancelled=false, inboundDone=false, inboundError=null}: subscribing inbound receiver
2022-07-28 09:18:20.744 DEBUG 26532 --- [ctor-http-nio-5] r.n.http.client.HttpClientOperations : [id:199cd714-5, L:/127.0.0.1:50342 - R:localhost/127.0.0.1:18080] Received last HTTP packet
2022-07-28 09:18:20.744 DEBUG 26532 --- [ctor-http-nio-5] r.n.http.server.HttpServerOperations : [id:b0f975eb-11, L:/0:0:0:0:0:0:0:1:10443 - R:/0:0:0:0:0:0:0:1:50337] Decreasing pending responses, now 0
2022-07-28 09:18:20.745 DEBUG 26532 --- [ctor-http-nio-5] r.n.http.server.HttpServerOperations : [id:b0f975eb-11, L:/0:0:0:0:0:0:0:1:10443 - R:/0:0:0:0:0:0:0:1:50337] Last HTTP packet was sent, terminating the channel
2022-07-28 09:18:20.745 DEBUG 26532 --- [ctor-http-nio-5] o.s.w.s.adapter.HttpWebHandlerAdapter : [b0f975eb-11, L:/0:0:0:0:0:0:0:1:10443 - R:/0:0:0:0:0:0:0:1:50337] Completed 400 BAD_REQUEST
2022-07-28 09:18:20.745 DEBUG 26532 --- [ctor-http-nio-5] r.n.http.server.HttpServerOperations : [id:b0f975eb-11, L:/0:0:0:0:0:0:0:1:10443 - R:/0:0:0:0:0:0:0:1:50337] Last HTTP response frame
2022-07-28 09:18:20.745 DEBUG 26532 --- [ctor-http-nio-5] c.m.webgateway.handler.RequestLogger : Total time required to process /cms-service/webapi/terminate/descriptor request 60055
2022-07-28 09:18:20.745 DEBUG 26532 --- [ctor-http-nio-5] r.n.r.DefaultPooledConnectionProvider : [id:199cd714, L:/127.0.0.1:50342 - R:localhost/127.0.0.1:18080] onStateChange(POST{uri=/cms-service/webapi/terminate/descriptor, connection=PooledConnection{channel=[id: 0x199cd714, L:/127.0.0.1:50342 - R:localhost/127.0.0.1:18080]}}, [response_completed])
2022-07-28 09:18:20.745 DEBUG 26532 --- [ctor-http-nio-5] r.n.r.DefaultPooledConnectionProvider : [id:199cd714, L:/127.0.0.1:50342 - R:localhost/127.0.0.1:18080] onStateChange(POST{uri=/cms-service/webapi/terminate/descriptor, connection=PooledConnection{channel=[id: 0x199cd714, L:/127.0.0.1:50342 - R:localhost/127.0.0.1:18080]}}, [disconnecting])
2022-07-28 09:18:20.752 DEBUG 26532 --- [ctor-http-nio-5] r.n.resources.PooledConnectionProvider : [id:199cd714, L:/127.0.0.1:50342 ! R:localhost/127.0.0.1:18080] Channel closed, now: 0 active connections, 4 inactive connections and 0 pending acquire requests.
2022-07-28 09:18:20.752 DEBUG 26532 --- [ctor-http-nio-5] r.n.r.DefaultPooledConnectionProvider : [id:199cd714, L:/127.0.0.1:50342 ! R:localhost/127.0.0.1:18080] onStateChange(PooledConnection{channel=[id: 0x199cd714, L:/127.0.0.1:50342 ! R:localhost/127.0.0.1:18080]}, [disconnecting])
2022-07-28 09:18:23.805 DEBUG 26532 --- [ctor-http-nio-5] r.n.http.server.HttpServerOperations : [id:b0f975eb, L:/0:0:0:0:0:0:0:1:10443 - R:/0:0:0:0:0:0:0:1:50337] Increasing pending responses, now 1
2022-07-28 09:18:23.805 DEBUG 26532 --- [ctor-http-nio-5] reactor.netty.http.server.HttpServer : [id:b0f975eb-12, L:/0:0:0:0:0:0:0:1:10443 - R:/0:0:0:0:0:0:0:1:50337] Handler is being applied: org.springframework.http.server.reactive.ReactorHttpHandlerAdapter#7c82616c
2022-07-28 09:18:23.805 DEBUG 26532 --- [ctor-http-nio-5] o.s.w.s.adapter.HttpWebHandlerAdapter : [b0f975eb-12, L:/0:0:0:0:0:0:0:1:10443 - R:/0:0:0:0:0:0:0:1:50337] HTTP GET "/cms-service/webapi/data/descriptor"
below is the link to the sample project.
https://github.com/manjosh1990/webgateway-issues
I tried to ignore FORM URL ENCODED requests and GET request, but it still does not work
private static final Set<HttpMethod> ALLOWED_METHODS = new HashSet<>(
Arrays.asList(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.TRACE, HttpMethod.OPTIONS));
#Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http.authorizeExchange().anyExchange().permitAll().and()
.csrf(csrf -> csrf
.requireCsrfProtectionMatcher(ignoringFormUrlEncodedContentType())
.csrfTokenRepository(CookieServerCsrfTokenRepository.withHttpOnlyFalse()));
return http.build();
}
private ServerWebExchangeMatcher ignoringFormUrlEncodedContentType() {
return (exchange) -> !MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(
exchange.getRequest().getHeaders().getContentType()) || !ALLOWED_METHODS.contains(exchange.getRequest().getMethod())
? ServerWebExchangeMatcher.MatchResult.match()
: ServerWebExchangeMatcher.MatchResult.notMatch();
}
Thanks for the minimal sample to reproduce the issue!
After some testing, I'm unable to come up with a workaround or fix for your configuration that allows a form post (URL-encoded) to pass through the gateway with CSRF protection enabled. My best guess is it has to do with how Spring Security is consuming the request body (which should be cached for subsequent filters to consume) vs how Spring Cloud Gateway is consuming the request body in order to proxy to the downstream service.
I tested this by disabling CSRF protection and adding the following filter:
#Component
public class TestWebFilter implements WebFilter {
#Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return Mono.defer(() -> exchange.getFormData()
.doOnSuccess(System.out::println))
.then(chain.filter(exchange));
}
}
In my testing, this causes the request through the gateway to block for a long time before receiving:
{
"timestamp": "2022-08-10T19:13:54.265+00:00",
"status": 400,
"error": "Bad Request",
"path": "/cms-service/webapi/service/post/test"
}
Since this appears to be a bug in Spring Security, I'd recommend submitting a bug in Spring Security and we can work through it from there.
If you would like to work around the issue in the meantime, you can disable CSRF protection for these types of requests, as follows:
#Configuration
#EnableWebFluxSecurity
public class SecurityConfig {
#Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange((authorize) -> authorize
.anyExchange().authenticated()
)
.csrf((csrf) -> csrf
.requireCsrfProtectionMatcher(ignoringFormUrlEncodedContentType())
.csrfTokenRepository(CookieServerCsrfTokenRepository.withHttpOnlyFalse())
)
.oauth2ResourceServer(ServerHttpSecurity.OAuth2ResourceServerSpec::jwt);
return http.build();
}
private ServerWebExchangeMatcher ignoringFormUrlEncodedContentType() {
return (exchange) -> !MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(
exchange.getRequest().getHeaders().getContentType())
? ServerWebExchangeMatcher.MatchResult.match()
: ServerWebExchangeMatcher.MatchResult.notMatch();
}
}
Important: This is not ideal, because these requests won't be protected. However, this might make sense if these requests were never performed in a browser. In that case, it would make sense to have a separate authentication mechanism, such as requiring a bearer token instead of form login, etc. (as in the example above).

All endpoints return 401/403 in ControllerTest when adding resourceserver in my Spring Boot project

In these days, we are going to migrate our custom JWT filter to Auth0 IDP.
I was using Spring 2.7.1, WebFlux/Kotlin Coroutines in this project. I created a sample project to demo Reactive Spring Security OAuth2 with Auth0 IDP issue.
BTW, I've maintained a WebMvc version to demo Spring Security OAuth2 and OAuth0(updated to Spring 2.7.1), in which the tests are working well.
When I added org.springframework.boot:spring-boot-starter-oauth2-resource-server to our project deps to enable OAuth2 resource server support in our backend API.
But now all API endpoints are protected and returns 401 or 403.
My security config like this.
#Bean
fun springWebFilterChain(http: ServerHttpSecurity, reactiveJwtDecoder: ReactiveJwtDecoder): SecurityWebFilterChain =
http {
csrf { disable() }
httpBasic { disable() }
formLogin { disable() }
logout { disable() }
// enable OAuth2 resource server support
oauth2ResourceServer { jwt { jwtDecoder = reactiveJwtDecoder } }
exceptionHandling {
authenticationEntryPoint = problemSupport
accessDeniedHandler = problemSupport
}
authorizeExchange {
authorize(pathMatchers(GET, "/v1/me"), authenticated)
authorize(anyExchange, permitAll)
}
}
The testing codes is like this, check the complete codes(The tests are ported from other projects, I have not added jwt mock).
#Test
fun `get all posts`() = runTest {
coEvery { posts.findAll() } returns flowOf(
Post(
id = UUID.randomUUID(),
title = "test title",
content = "test content"
)
)
client.get()
.uri("/posts").accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isOk
.expectBodyList(Post::class.java).hasSize(1)
coVerify(exactly = 1) { posts.findAll() }
}
The get all posts endpoints is set to permitAll, when running curl http://localhost:8080/posts command in a opening Powershell, it works well and print all posts in the console.
When running test of GET /posts endpoint. I got the following info from console.
2022-07-01 12:45:27.021 DEBUG 14132 --- [ main] o.s.w.r.f.client.ExchangeFunctions : [6144e499] HTTP GET /posts
2022-07-01 12:45:27.069 DEBUG 14132 --- [ parallel-1] o.s.w.s.adapter.HttpWebHandlerAdapter : [26193fe] HTTP GET "/posts"
2022-07-01 12:45:27.108 DEBUG 14132 --- [ parallel-1] .s.s.w.s.u.m.AndServerWebExchangeMatcher : Trying to match using org.springframework.security.web.server.csrf.CsrfWebFilter$DefaultRequireCsrfProtectionMatcher#6fc32403
2022-07-01 12:45:27.110 DEBUG 14132 --- [ parallel-1] .s.s.w.s.u.m.AndServerWebExchangeMatcher : Did not match
2022-07-01 12:45:27.146 DEBUG 14132 --- [ parallel-2] o.s.w.s.s.DefaultWebSessionManager : Created new WebSession.
2022-07-01 12:45:27.151 DEBUG 14132 --- [ parallel-2] o.s.s.w.s.u.m.OrServerWebExchangeMatcher : Trying to match using PathMatcherServerWebExchangeMatcher{pattern='/logout', method=POST}
2022-07-01 12:45:27.151 DEBUG 14132 --- [ parallel-2] athPatternParserServerWebExchangeMatcher : Request 'GET /posts' doesn't match 'POST /logout'
2022-07-01 12:45:27.152 DEBUG 14132 --- [ parallel-2] o.s.s.w.s.u.m.OrServerWebExchangeMatcher : No matches found
2022-07-01 12:45:27.156 DEBUG 14132 --- [ parallel-2] a.DelegatingReactiveAuthorizationManager : Checking authorization on '/posts' using org.springframework.security.authorization.AuthenticatedReactiveAuthorizationManager#66bdd968
2022-07-01 12:45:27.159 DEBUG 14132 --- [ parallel-2] ebSessionServerSecurityContextRepository : No SecurityContext found in WebSession: 'org.springframework.web.server.session.InMemoryWebSessionStore$InMemoryWebSession#529dcf30'
2022-07-01 12:45:27.160 DEBUG 14132 --- [ parallel-2] o.s.s.w.s.a.AuthorizationWebFilter : Authorization failed: Access Denied
2022-07-01 12:45:27.174 DEBUG 14132 --- [ parallel-2] ebSessionServerSecurityContextRepository : No SecurityContext found in WebSession: 'org.springframework.web.server.session.InMemoryWebSessionStore$InMemoryWebSession#529dcf30'
2022-07-01 12:45:27.184 DEBUG 14132 --- [ parallel-2] o.s.w.s.adapter.HttpWebHandlerAdapter : [26193fe] Completed 401 UNAUTHORIZED
2022-07-01 12:45:27.189 DEBUG 14132 --- [ parallel-2] o.s.w.r.f.client.ExchangeFunctions : [6144e499] [609b7bfb] Response 401 UNAUTHORIZED
2022-07-01 12:45:27.207 ERROR 14132 --- [ main] o.s.t.w.reactive.server.ExchangeResult : Request details for assertion failure:
> GET /posts
> WebTestClient-Request-Id: [1]
> Accept: [application/json]
No content
< 401 UNAUTHORIZED Unauthorized
< WWW-Authenticate: [Bearer]
< Cache-Control: [no-cache, no-store, max-age=0, must-revalidate]
< Pragma: [no-cache]
< Expires: [0]
< X-Content-Type-Options: [nosniff]
< X-Frame-Options: [DENY]
< X-XSS-Protection: [1 ; mode=block]
< Referrer-Policy: [no-referrer]
0 bytes of content (unknown content-type).
java.lang.AssertionError: Status expected:<200 OK> but was:<401 UNAUTHORIZED>
Expected :200 OK
Actual :401 UNAUTHORIZED
<Click to see difference>
I have set logging level to TRACE, but there is no log to check our pathmatcher rules. It seems it does not check the pathmatchers in the authorizeRequest.
I have set disabled to crsf, but crsf filter still participate in the security check progress, it also tired to match the /logout url.
#WebfluxTest / #WebmvcTest load very little conf. I do import my web-security cong like you do in your answer.
You might whish to unit test the production security rules for the controller endpoint. If so, import regular security conf (not a permitAll() test conf) and configure security context with either:
jwt() WebTestClient mutator / WebMvc post-processor from spring-security-test
#WithMockJwtAuth from spring-addons
It seems the WebFluxTest annotated tests did not scan my custom SecurityConfig( which is a simple config NOT extends the XXXSecurityConfigurerAdapater) in the project, but use the Spring Boot built-in default security config instead.
Currently I have to add my SecurityConfig explicitly to enable it in the tests.
class TestsClass{
#TestConfiguration
#Import(SecuirtyConfig::class)
class TestConfig
//test methods
}
I am not sure it is the original design purpose or other reason.

Why does a Keycloak bearer token appear to be truncated during security filter chain processing

I am working with Keycloak 16.1.0, spring boot 2.6.2 and an external application client that sends a bearer token in to my server application to the endpoint http://romanmed-host:8888/actuator/health.
By cranking the debugging level up to maximum, I can see the access token before its processed. I can verify that its accurate by using the JWT Debug site JSON Web Tokens to verify that the signature is correct.
Yet several lines later in the output log the same bearer token appears to be somewhat truncated, its listed with an error saying that it failed to verify. When checked by using the JWT site indicates a signature error, but the token content is correct.
Naturally I would like to know why it appears to be truncated and what I can do about it.
I can match the output from the client program to the server and its not been changed, so truncation must occur within the server program.
The program is accepting the request by a get request, since the token can be checked by JWT as valid at this point, its not truncated by the get request input method.
I have not inserted a filter in the security filter chain, so I can see how any of my code could be doing anything to invalid the token.
Other than the Failed to verify token no other error messages are generated, suggesting that until this point everything is correct.
I can see that the WebAsyncManagerIntegrationFilter, SecurityContextPersistenceFilter, HeaderWriterFilter,KeycloakPreAuthActionsFilter and KeycloakAuthenticationProcessingFilter have all been invoked.
I am assuming that the problem is somewhere within the KeycloakAuthenticationProcessingFilter, but I don't understand why the token appears to have been truncated at this point.
The received bearer token is
eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJ3dUhKc1pvWnduelVsU1Zqc2JyTkxsbUNhR0ZIVkV0cTcyQkI5V0pORTVVIn0.eyJleHAiOjE2NDY0MDI3NTAsImlhdCI6MTY0NjQwMjQ1MCwianRpIjoiMjIyMjUxZDgtNDYxMy00OGQwLWEwNzAtMjU5YTYyY2NhZDkyIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDg1L2F1dGgvcmVhbG1zL0Jvb3RBZG1pbiIsImF1ZCI6WyJybS1jb25maWctc2VydmVyIiwiYXBwLXRvZG8iLCJhY2NvdW50Il0sInN1YiI6Ijc4ZTU1YjhiLWQ5MjAtNGQ0Yi1hNWQ5LWIyZDk3MDYzNDgyYiIsInR5cCI6IkJlYXJlciIsImF6cCI6ImFwcC1hZG1pbiIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOlsiaHR0cDovL2xvY2FsaG9zdDozMDAwMSJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiIsImRlZmF1bHQtcm9sZXMtYm9vdGFkbWluIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsicm0tY29uZmlnLXNlcnZlciI6eyJyb2xlcyI6WyJhY3R1YXRvciJdfSwiYXBwLWFkbWluIjp7InJvbGVzIjpbImFjdHVhdG9yIl19LCJhcHAtdG9kbyI6eyJyb2xlcyI6WyJhY3R1YXRvciJdfSwiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJlbWFpbCBwcm9maWxlIiwiY2xpZW50SWQiOiJhcHAtYWRtaW4iLCJjbGllbnRIb3N0IjoiMTI3LjAuMC4xIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJzZXJ2aWNlLWFjY291bnQtYXBwLWFkbWluIiwiY2xpZW50QWRkcmVzcyI6IjEyNy4wLjAuMSJ9.fwQPLiSIrUSjnRnTBrd1vvGic49OSf7aGDemc0TdmTshZzJ-eYhiEqnAh9-QU2rxDayPIhoIzA9CgBXmGPCnl1Qu4CujDddpBcLpnjszBoBdzwjDgpShgwFpGk0fGCM0fxtSZgMWRfeS_sRjBpRzZ42GelCYZ2E1kZX_E7o_LB3thpiv5oYqgTNucusNmzpm0-iFcEUe5rfnu2ZOHI_hLQvIYKlGURnNld4jov-KDLf2QTh2h3XqjbsGHG9PDq4MbFPhKY_9yF0jQkhF6F3oYrw9MIH4SbemrR-CHw6-aWqGmgucjJ7iKMY5o86HxLPu2tzM06NdaurQZX4ImLCBlQ
Its truncated format is
eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJ3dUhKc1pvWnduelVsU1Zqc2JyTkxsbUNhR0ZIVkV0cTcyQkI5V0pORTVVIn0.eyJleHAiOjE2NDY0MDI3NTAsImlhdCI6MTY0NjQwMjQ1MCwianRpIjoiMjIyMjUxZDgtNDYxMy00OGQwLWEwNzAtMjU5YTYyY2NhZDkyIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDg1L2F1dGgvcmVhbG1zL0Jvb3RBZG1pbiIsImF1ZCI6WyJybS1jb25maWctc2VydmVyIiwiYXBwLXRvZG8iLCJhY2NvdW50Il0sInN1YiI6Ijc4ZTU1YjhiLWQ5MjAtNGQ0Yi1hNWQ5LWIyZDk3MDYzNDgyYiIsInR5cCI6IkJlYXJlciIsImF6cCI6ImFwcC1hZG1pbiIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOlsiaHR0cDovL2xvY2FsaG9zdDozMDAwMSJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiIsImRlZmF1bHQtcm9sZXMtYm9vdGFkbWluIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsicm0tY29uZmlnLXNlcnZlciI6eyJyb2xlcyI6WyJhY3R1YXRvciJdfSwiYXBwLWFkbWluIjp7InJvbGVzIjpbImFjdHVhdG9yIl19LCJhcHAtdG9kbyI6eyJyb2xlcyI6WyJhY3R1YXRvciJdfSwiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJlbWFpbCBwcm9maWxlIiwiY2xpZW50SWQiOiJhcHAtYWRtaW4iLCJjbGllbnRIb3N0IjoiMTI3LjAuMC4xIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJzZXJ2aWNlLWFjY291bnQtYXBwLWFkbWluIiwiY2xpZW50QWRkcmVzcyI6IjEyNy4wLjAuMSJ9
The debug log is
servletPath:/actuator/health
pathInfo:null
headers:
accept-encoding: gzip
user-agent: ReactorNetty/1.0.13
host: romanmed-host:8888
authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJ3dUhKc1pvWnduelVsU1Zqc2JyTkxsbUNhR0ZIVkV0cTcyQkI5V0pORTVVIn0.eyJleHAiOjE2NDY0MDI3NTAsImlhdCI6MTY0NjQwMjQ1MCwianRpIjoiMjIyMjUxZDgtNDYxMy00OGQwLWEwNzAtMjU5YTYyY2NhZDkyIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDg1L2F1dGgvcmVhbG1zL0Jvb3RBZG1pbiIsImF1ZCI6WyJybS1jb25maWctc2VydmVyIiwiYXBwLXRvZG8iLCJhY2NvdW50Il0sInN1YiI6Ijc4ZTU1YjhiLWQ5MjAtNGQ0Yi1hNWQ5LWIyZDk3MDYzNDgyYiIsInR5cCI6IkJlYXJlciIsImF6cCI6ImFwcC1hZG1pbiIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOlsiaHR0cDovL2xvY2FsaG9zdDozMDAwMSJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiIsImRlZmF1bHQtcm9sZXMtYm9vdGFkbWluIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsicm0tY29uZmlnLXNlcnZlciI6eyJyb2xlcyI6WyJhY3R1YXRvciJdfSwiYXBwLWFkbWluIjp7InJvbGVzIjpbImFjdHVhdG9yIl19LCJhcHAtdG9kbyI6eyJyb2xlcyI6WyJhY3R1YXRvciJdfSwiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJlbWFpbCBwcm9maWxlIiwiY2xpZW50SWQiOiJhcHAtYWRtaW4iLCJjbGllbnRIb3N0IjoiMTI3LjAuMC4xIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJzZXJ2aWNlLWFjY291bnQtYXBwLWFkbWluIiwiY2xpZW50QWRkcmVzcyI6IjEyNy4wLjAuMSJ9.fwQPLiSIrUSjnRnTBrd1vvGic49OSf7aGDemc0TdmTshZzJ-eYhiEqnAh9-QU2rxDayPIhoIzA9CgBXmGPCnl1Qu4CujDddpBcLpnjszBoBdzwjDgpShgwFpGk0fGCM0fxtSZgMWRfeS_sRjBpRzZ42GelCYZ2E1kZX_E7o_LB3thpiv5oYqgTNucusNmzpm0-iFcEUe5rfnu2ZOHI_hLQvIYKlGURnNld4jov-KDLf2QTh2h3XqjbsGHG9PDq4MbFPhKY_9yF0jQkhF6F3oYrw9MIH4SbemrR-CHw6-aWqGmgucjJ7iKMY5o86HxLPu2tzM06NdaurQZX4ImLCBlQ
accept: application/vnd.spring-boot.actuator.v2+json, application/vnd.spring-
boot.actuator.v1+json, application/json
Security filter chain: [
WebAsyncManagerIntegrationFilter
SecurityContextPersistenceFilter
HeaderWriterFilter
KeycloakPreAuthActionsFilter
KeycloakAuthenticationProcessingFilter
LogoutFilter
RequestCacheAwareFilter
SecurityContextHolderAwareRequestFilter
KeycloakSecurityContextRequestFilter
KeycloakAuthenticatedActionsFilter
AnonymousAuthenticationFilter
SessionManagementFilter
ExceptionTranslationFilter
FilterSecurityInterceptor
]
************************************************************
2022-03-04 14:03:30.088 TRACE 99667 --- [.1-8888-exec-10] o.s.security.web.FilterChainProxy : Trying to match request against DefaultSecurityFilterChain [RequestMatcher=any request, Filters=[org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter#2caa9666, org.springframework.security.web.context.SecurityContextPersistenceFilter#67683210, org.springframework.security.web.header.HeaderWriterFilter#58a9e64d, org.keycloak.adapters.springsecurity.filter.KeycloakPreAuthActionsFilter#3fecb076, org.keycloak.adapters.springsecurity.filter.KeycloakAuthenticationProcessingFilter#41d84abb, org.springframework.security.web.authentication.logout.LogoutFilter#3e563293, org.springframework.security.web.savedrequest.RequestCacheAwareFilter#25511895, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter#21202507, org.keycloak.adapters.springsecurity.filter.KeycloakSecurityContextRequestFilter#62159fd, org.keycloak.adapters.springsecurity.filter.KeycloakAuthenticatedActionsFilter#28e8dee7, org.springframework.security.web.authentication.AnonymousAuthenticationFilter#263f6e96, org.springframework.security.web.session.SessionManagementFilter#d3b0397, org.springframework.security.web.access.ExceptionTranslationFilter#75d0cac6, org.springframework.security.web.access.intercept.FilterSecurityInterceptor#2267b0bb]] (1/1)
2022-03-04 14:03:30.088 DEBUG 99667 --- [.1-8888-exec-10] o.s.security.web.FilterChainProxy : Securing GET /actuator/health
2022-03-04 14:03:30.089 TRACE 99667 --- [.1-8888-exec-10] o.s.security.web.FilterChainProxy : Invoking WebAsyncManagerIntegrationFilter (1/14)
2022-03-04 14:03:30.089 TRACE 99667 --- [.1-8888-exec-10] o.s.security.web.FilterChainProxy : Invoking SecurityContextPersistenceFilter (2/14)
2022-03-04 14:03:30.089 TRACE 99667 --- [.1-8888-exec-10] w.c.HttpSessionSecurityContextRepository : No HttpSession currently exists
2022-03-04 14:03:30.089 TRACE 99667 --- [.1-8888-exec-10] w.c.HttpSessionSecurityContextRepository : Created SecurityContextImpl [Null authentication]
2022-03-04 14:03:30.089 DEBUG 99667 --- [.1-8888-exec-10] s.s.w.c.SecurityContextPersistenceFilter : Set SecurityContextHolder to empty SecurityContext
2022-03-04 14:03:30.089 TRACE 99667 --- [.1-8888-exec-10] o.s.security.web.FilterChainProxy : Invoking HeaderWriterFilter (3/14)
2022-03-04 14:03:30.089 TRACE 99667 --- [.1-8888-exec-10] o.s.security.web.FilterChainProxy : Invoking KeycloakPreAuthActionsFilter (4/14)
2022-03-04 14:03:30.089 DEBUG 99667 --- [.1-8888-exec-10] o.k.adapters.PreAuthActionsHandler : adminRequest http://romanmed-host:8888/actuator/health
2022-03-04 14:03:30.089 TRACE 99667 --- [.1-8888-exec-10] o.s.security.web.FilterChainProxy : Invoking KeycloakAuthenticationProcessingFilter (5/14)
2022-03-04 14:03:30.089 DEBUG 99667 --- [.1-8888-exec-10] f.KeycloakAuthenticationProcessingFilter : Attempting Keycloak authentication
2022-03-04 14:03:30.089 TRACE 99667 --- [.1-8888-exec-10] o.k.adapters.RequestAuthenticator : --> authenticate()
2022-03-04 14:03:30.089 TRACE 99667 --- [.1-8888-exec-10] o.k.adapters.RequestAuthenticator : try bearer
2022-03-04 14:03:30.089 DEBUG 99667 --- [.1-8888-exec-10] o.k.a.BearerTokenRequestAuthenticator : Found [1] values in authorization header, selecting the first value for Bearer.
2022-03-04 14:03:30.089 DEBUG 99667 --- [.1-8888-exec-10] o.k.a.BearerTokenRequestAuthenticator : Verifying access_token
2022-03-04 14:03:30.089 TRACE 99667 --- [.1-8888-exec-10] o.k.a.BearerTokenRequestAuthenticator : access_token: eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJ3dUhKc1pvWnduelVsU1Zqc2JyTkxsbUNhR0ZIVkV0cTcyQkI5V0pORTVVIn0.eyJleHAiOjE2NDY0MDI3NTAsImlhdCI6MTY0NjQwMjQ1MCwianRpIjoiMjIyMjUxZDgtNDYxMy00OGQwLWEwNzAtMjU5YTYyY2NhZDkyIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDg1L2F1dGgvcmVhbG1zL0Jvb3RBZG1pbiIsImF1ZCI6WyJybS1jb25maWctc2VydmVyIiwiYXBwLXRvZG8iLCJhY2NvdW50Il0sInN1YiI6Ijc4ZTU1YjhiLWQ5MjAtNGQ0Yi1hNWQ5LWIyZDk3MDYzNDgyYiIsInR5cCI6IkJlYXJlciIsImF6cCI6ImFwcC1hZG1pbiIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOlsiaHR0cDovL2xvY2FsaG9zdDozMDAwMSJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiIsImRlZmF1bHQtcm9sZXMtYm9vdGFkbWluIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsicm0tY29uZmlnLXNlcnZlciI6eyJyb2xlcyI6WyJhY3R1YXRvciJdfSwiYXBwLWFkbWluIjp7InJvbGVzIjpbImFjdHVhdG9yIl19LCJhcHAtdG9kbyI6eyJyb2xlcyI6WyJhY3R1YXRvciJdfSwiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJlbWFpbCBwcm9maWxlIiwiY2xpZW50SWQiOiJhcHAtYWRtaW4iLCJjbGllbnRIb3N0IjoiMTI3LjAuMC4xIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJzZXJ2aWNlLWFjY291bnQtYXBwLWFkbWluIiwiY2xpZW50QWRkcmVzcyI6IjEyNy4wLjAuMSJ9.signature
2022-03-04 14:03:30.091 DEBUG 99667 --- [.1-8888-exec-10] o.k.a.BearerTokenRequestAuthenticator : Failed to verify token
2022-03-04 14:03:30.091 DEBUG 99667 --- [.1-8888-exec-10] o.k.adapters.RequestAuthenticator : Bearer FAILED
2022-03-04 14:03:30.091 DEBUG 99667 --- [.1-8888-exec-10] f.KeycloakAuthenticationProcessingFilter : Auth outcome: FAILED
2022-03-04 14:03:30.092 TRACE 99667 --- [.1-8888-exec-10] f.KeycloakAuthenticationProcessingFilter : Failed to process authentication request
org.keycloak.adapters.springsecurity.KeycloakAuthenticationException: Invalid authorization header, see WWW-Authenticate header for detailsr code here
The code is taken from an example by Thomas Darimont at Securing Spring Boot Admin & actuator endpoints with Keycloak and assumed to be correct.
The code is as follows
import lombok.extern.slf4j.Slf4j;
import org.keycloak.KeycloakPrincipal;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.adapters.springboot.KeycloakSpringBootProperties;
import org.keycloak.adapters.springsecurity.KeycloakConfiguration;
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider;
import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter;
import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.health.HealthEndpoint;
import org.springframework.boot.actuate.info.InfoEndpoint;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest;
import java.security.Principal;
#KeycloakConfiguration
#Slf4j
#EnableConfigurationProperties(KeycloakSpringBootProperties.class)
class KeycloakSecurityConfiguration extends KeycloakWebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http
.csrf().disable()
.authorizeRequests()
.requestMatchers(EndpointRequest.to(
InfoEndpoint.class,
HealthEndpoint.class
)).permitAll()
.requestMatchers(EndpointRequest.toAnyEndpoint())
.hasRole("ACTUATOR")
.anyRequest().permitAll()
;
}
/**
* Use {#link KeycloakAuthenticationProvider}
*
* #param auth
* #throws Exception
*/
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
SimpleAuthorityMapper grantedAuthorityMapper = new SimpleAuthorityMapper();
grantedAuthorityMapper.setPrefix("ROLE_");
grantedAuthorityMapper.setConvertToUpperCase(true);
KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(grantedAuthorityMapper);
auth.authenticationProvider(keycloakAuthenticationProvider);
}
#Bean
#Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new RegisterSessionAuthenticationStrategy(buildSessionRegistry());
}
#Bean
protected SessionRegistry buildSessionRegistry() {
return new SessionRegistryImpl();
}
/**
* Allows to inject requests scoped wrapper for {#link KeycloakSecurityContext}.
*
* Returns the {#link KeycloakSecurityContext} from the Spring
* {#link ServletRequestAttributes}'s {#link Principal}.
* <p>
* The principal must support retrieval of the KeycloakSecurityContext, so at
* this point, only {#link KeycloakPrincipal} values and
* {#link KeycloakAuthenticationToken} are supported.
*
* #return the current <code>KeycloakSecurityContext</code>
*/
#Bean
#Scope(scopeName = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public KeycloakSecurityContext provideKeycloakSecurityContext() {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
Principal principal = attributes.getRequest().getUserPrincipal();
if (principal == null) {
return null;
}
if (principal instanceof KeycloakAuthenticationToken) {
principal = Principal.class.cast(KeycloakAuthenticationToken.class.cast(principal).getPrincipal());
}
if (principal instanceof KeycloakPrincipal) {
return KeycloakPrincipal.class.cast(principal).getKeycloakSecurityContext();
}
return null;
}
The problem is the set up of the client and server programs.
The server used the value
auth-server-url: http://localhost:8085/auth
in its application.yml file to define the location of the Keycloak server, the client used the value
auth-server-url: http://romanmed-host:8085/auth
to define the location of Keycloak where the machine name romanmed-host is an alias for localhost. Having changed these values to be the same value, everything works as expected.
The diagnostics generated by the debugging/trace code are confusing, string described as truncated header seems to be truncated the bearer token, with out the signature. What the 'Keycloak` diagnostic is attempting to print is the part of the token which defines the tokens permissions and not the signature section.
Running the entire bearer token through the JWT site does show that the token is valid, because its a correctly encoded token and is legitimate.
The problem is not the token, but the way the token is being used! The client was expecting a legal signed token generated by them instance of Keycloak that it knew about, what it got was a legal signed token generated by Keycloak with a different address, which it correctly objected to.
The problem being the nature of the generated error message, it just claimed that the token signature was invalid, had it said something about an invalid/unexpected hostname, the nature of the problem would have been rather more obvious and resolved much faster. Keycloak is design to be flexible, so error messages tend to more vague to cover all situations, hence the message there is something wrong with your bearer token signature which is correct, but vague.
There seems to several schools of thought on how to resolve issues like this, one is to use an raw ip address which will always resolve to the same value. Thus avoid problems like this. This suffers from if the Keycloak server is moved to another machine there are lots of values to change.
My solution is to define an alias value in the hosts/dns server for the address of the eycloak server and always use that value in the support files. Hence if the Keycloak server is ever moved to another address, there is only one value to change.

Spring with two security configurations - failed API login redirects to form login page. How to change?

I have a Spring Boot application with two security configurations (two WebSecurityConfigurerAdapters), one for a REST API with "/api/**" endpoints, and one for a web front-end at all other endpoints. The security configuration is here on Github and here's some relevant parts of it:
#Configuration
#Order(1)
public static class APISecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
JWTAuthenticationFilter jwtAuthenticationFilter = new JWTAuthenticationFilter(authenticationManager());
jwtAuthenticationFilter.setFilterProcessesUrl("/api/login");
jwtAuthenticationFilter.setPostOnly(true);
http.antMatcher("/api/**")
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.csrf().disable()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.addFilter(jwtAuthenticationFilter)
.addFilter(new JWTAuthorizationFilter(authenticationManager()));
}
}
#Configuration
public static class FrontEndSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.formLogin()
.loginPage("/login").permitAll()
.defaultSuccessUrl("/")
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/?logout")
.and()
.authorizeRequests()
.mvcMatchers("/").permitAll()
.mvcMatchers("/home").authenticated()
.anyRequest().denyAll()
.and();
}
}
The JWTAuthenticationFilter is a custom subclass of UsernamePasswordAuthenticationFilter that processes sign-in attempts to the REST API (by HTTP POST with a JSON body to /api/login) and returns a JWT token in the "Authorization" header if successful.
So here's the issue: failed login attempts to /api/login (either with bad credentials or missing JSON body) are redirecting to the HTML login form /api/login. Non-authenticated requests to other "/api/**" endpoints result in a simple JSON response such as:
{
"timestamp": "2019-11-22T21:03:07.892+0000",
"status": 403,
"error": "Forbidden",
"message": "Access Denied",
"path": "/api/v1/agency"
}
{
"timestamp": "2019-11-22T21:04:46.663+0000",
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/api/v1/badlink"
}
Attempts to access other protected URLs (not starting with "/api/") by a non-authenticated user redirect to the login form /login, which is the desired behavior. But I don't want API calls to /api/login to redirect to that form!
How can I code the correct behavior for failed API logins? Is it a question of adding a new handler for that filter? Or maybe adding an exclusion to some behavior I've already defined?
More detail on the exception and handling:
I looked at the logs, and the exception being thrown for either bad credentials or malformed JSON is a subclass of org.springframework.security.authentication.AuthenticationException. The logs show, for example:
webapp_1 | 2019-11-25 15:30:16.048 DEBUG 1 --- [nio-8080-exec-5] n.j.w.g.config.JWTAuthenticationFilter : Authentication request failed: org.springframework.security.authentication.BadCredentialsException: Bad credentials
(...stack trace...)
webapp_1 | 2019-11-25 15:30:16.049 DEBUG 1 --- [nio-8080-exec-5] n.j.w.g.config.JWTAuthenticationFilter : Updated SecurityContextHolder to contain null Authentication
webapp_1 | 2019-11-25 15:30:16.049 DEBUG 1 --- [nio-8080-exec-5] n.j.w.g.config.JWTAuthenticationFilter : Delegating to authentication failure handler org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler#7f9648b6
webapp_1 | 2019-11-25 15:30:16.133 DEBUG 1 --- [nio-8080-exec-6] n.j.webapps.granite.home.HomeController : Accessing /login page.
When I access another URL, for example one that doesn't exist such as /api/x, it looks very different. It's pretty verbose but it looks like the server is trying to redirect to /error and is not finding that to be an authorized URL. Interestingly if I try this in a web browser I get the error formatted with my custom error page (error.html), but if I access it with Postman I just get a JSON message. A sample of the logs:
webapp_1 | 2019-11-25 16:07:22.157 DEBUG 1 --- [nio-8080-exec-6] o.s.s.w.a.ExceptionTranslationFilter : Access is denied (user is anonymous); redirecting to authentication entry point
webapp_1 |
webapp_1 | org.springframework.security.access.AccessDeniedException: Access is denied
...
webapp_1 | 2019-11-25 16:07:22.174 DEBUG 1 --- [nio-8080-exec-6] o.s.s.w.a.ExceptionTranslationFilter : Calling Authentication entry point.
webapp_1 | 2019-11-25 16:07:22.175 DEBUG 1 --- [nio-8080-exec-6] o.s.s.w.a.Http403ForbiddenEntryPoint : Pre-authenticated entry point called. Rejecting access
...
webapp_1 | 2019-11-25 16:07:22.211 DEBUG 1 --- [nio-8080-exec-6] o.s.s.w.u.matcher.AntPathRequestMatcher : Checking match of request : '/error'; against '/api/**'
...
webapp_1 | 2019-11-25 16:07:22.214 DEBUG 1 --- [nio-8080-exec-6] o.s.security.web.FilterChainProxy : /error reached end of additional filter chain; proceeding with original chain
webapp_1 | 2019-11-25 16:07:22.226 DEBUG 1 --- [nio-8080-exec-6] o.s.web.servlet.DispatcherServlet : "ERROR" dispatch for GET "/error", parameters={}
webapp_1 | 2019-11-25 16:07:22.230 DEBUG 1 --- [nio-8080-exec-6] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
webapp_1 | 2019-11-25 16:07:22.564 DEBUG 1 --- [nio-8080-exec-6] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Using 'application/json', given [*/*] and supported [application/json, application/*+json, application/json, application/*+json]
webapp_1 | 2019-11-25 16:07:22.577 DEBUG 1 --- [nio-8080-exec-6] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Writing [{timestamp=Mon Nov 25 16:07:22 GMT 2019, status=403, error=Forbidden, message=Access Denied, path=/a (truncated)...]
webapp_1 | 2019-11-25 16:07:22.903 DEBUG 1 --- [nio-8080-exec-6] w.c.HttpSessionSecurityContextRepository : SecurityContext is empty or contents are anonymous - context will not be stored in HttpSession.
webapp_1 | 2019-11-25 16:07:22.905 DEBUG 1 --- [nio-8080-exec-6] o.s.web.servlet.DispatcherServlet : Exiting from "ERROR" dispatch, status 403
So it looks like what I maybe need to do is to configure the "authentication failure handler" for the REST API to go to "/error" instead of going to "/login", but only for endpoints under /api/**.
I added an #Override of unsuccessfulAuthentication() to my Authentication filter
The grandparent class (AbstractAuthenticationProcessingFilter) has a method for unsuccessful authentications (i.e. AuthenticationException) which delegates to an authentication failure handler class. I could have created my own custom authentication failure handler, but instead decided to simply override the unsuccessfulAuthentication method with some code that sends back a response with a 401 status and a JSON error message:
#Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
// TODO: enrich/improve error messages
response.setStatus(response.SC_UNAUTHORIZED);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
response.getWriter().write("{\"error\": \"authentication error?\"}");
}
...and added a custom AuthenticationEntryPoint to make the other errors match
This doesn't have the exact form of the error messages I had seen at other endpoints, so I also created a custom AuthenticationEntryPoint (the class that handles unauthorized requests to protected endpoints) and it does basically the same thing. My implementation:
public class RESTAuthenticationEntryPoint implements AuthenticationEntryPoint {
#Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
// TODO: enrich/improve error messages
response.setStatus(response.SC_UNAUTHORIZED);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
response.getWriter().write("{\"error\": \"unauthorized?\"}");
}
}
Now the security configuration for the REST endpoints looks like this (note the addition of ".exceptionHandling().authenticationEntryPoint()":
#Configuration
#Order(1)
public static class APISecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
JWTAuthenticationFilter jwtAuthenticationFilter = new JWTAuthenticationFilter(authenticationManager());
jwtAuthenticationFilter.setFilterProcessesUrl("/api/login");
http.antMatcher("/api/**")
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.csrf().disable()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.exceptionHandling()
.authenticationEntryPoint(new RESTAuthenticationEntryPoint())
.and()
.addFilter(jwtAuthenticationFilter)
.addFilter(new JWTAuthorizationFilter(authenticationManager()));
}
}
I'll need to work on my error messages so they're more informative and secure.
And this doesn't exactly answer my original question, which was how to get those default-style JSON responses I liked, but it does allow me to customize the errors from all types of access failures independently of the web configuration. (Endpoints outside of /api/** still work with the web form login.)
Full code as of the current commit: github

Spring boot client fails to register Spring boot admin (version 2.x)

I am running spring boot application with basic authentication enabled and spring boot admin with UI security enabled .
My spring boot client fails to register to the spring boot admin server . Below are the logs :
2018-08-23 15:17:09.676 DEBUG 4992 --- [gistrationTask1] o.s.web.client.RestTemplate : Created POST request for "http://localhost:9001/instances"
2018-08-23 15:17:09.699 DEBUG 4992 --- [gistrationTask1] o.s.web.client.RestTemplate : Setting request Accept header to [application/json, application/*+json]
2018-08-23 15:17:09.724 DEBUG 4992 --- [gistrationTask1] o.s.web.client.RestTemplate : Writing [Application(name=spring-boot-application, managementUrl=http://localhost:8001/relay/actuator, healthUrl=http://localhost:8001/relay/actuator/health, serviceUrl=http://localhost:8001/relay)] as "application/json" using [org.springframework.http.converter.json.MappingJackson2HttpMessageConverter#12c0c0b3]
2018-08-23 15:17:09.864 DEBUG 4992 --- [gistrationTask1] s.n.www.protocol.http.HttpURLConnection : sun.net.www.MessageHeader#5953ec3b8 pairs: {POST /instances HTTP/1.1: null}{Accept: application/json}{Content-Type: application/json}{Authorization: Basic YWRtaW46YWRtaW4=}{User-Agent: Java/1.8.0_161}{Host: localhost:9001}{Connection: keep-alive}{Content-Length: 287}
2018-08-23 15:17:10.124 DEBUG 4992 --- [gistrationTask1] s.n.www.protocol.http.HttpURLConnection : sun.net.www.MessageHeader#126c0bea10 pairs: {null: HTTP/1.1 403}{Cache-Control: no-cache, no-store, max-age=0, must-revalidate}{Pragma: no-cache}{Expires: 0}{X-Content-Type-Options: nosniff}{X-Frame-Options: DENY}{X-XSS-Protection: 1 ; mode=block}{Content-Type: text/plain}{Transfer-Encoding: chunked}{Date: Thu, 23 Aug 2018 09:47:10 GMT}
2018-08-23 15:17:10.128 DEBUG 4992 --- [gistrationTask1] o.s.web.client.RestTemplate : POST request for "http://localhost:9001/instances" resulted in 403 (null); invoking error handler
2018-08-23 15:17:10.138 WARN 4992 --- [gistrationTask1] d.c.b.a.c.r.ApplicationRegistrator : Failed to register application as Application(name=spring-boot-application, managementUrl=http://localhost:8001/relay/actuator, healthUrl=http://localhost:8001/relay/actuator/health, serviceUrl=http://localhost:8001/relay) at spring-boot-admin ([http://localhost:9001/instances]): 403 null. Further attempts are logged on DEBUG level
Below are my webSecurity config class from admin server
#SpringBootApplication
#EnableAdminServer
#EnableAutoConfiguration
#Configuration
public class SpringStarterAdminApplication {
public static void main(String[] args) {
SpringApplication.run(SpringStarterAdminApplication.class, args);
}
// Added for spring boot security login ui for admin
public static class SecuritySecureConfig extends WebSecurityConfigurerAdapter {
private final String adminContextPath;
public SecuritySecureConfig(AdminServerProperties adminServerProperties) {
this.adminContextPath = adminServerProperties.getContextPath();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
// #formatter:off
SavedRequestAwareAuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
successHandler.setTargetUrlParameter("redirectTo");
successHandler.setDefaultTargetUrl(adminContextPath + "/");
http.authorizeRequests()
.antMatchers(adminContextPath + "/assets/**").permitAll()
.antMatchers(adminContextPath + "/login").permitAll()
.anyRequest().authenticated()
.and()
.formLogin().loginPage(adminContextPath + "/login").successHandler(successHandler).and()
.logout().logoutUrl(adminContextPath + "/logout").and()
.httpBasic().and()
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.ignoringAntMatchers(
"/instances",
"/actuator/**"
);
// #formatter:on
}
}
Not sure what is preventing the client to register to admin Server .
The inner configuration class should be annotated with #Configuration too.
It's even better if you refactor it into a separate class.

Resources