Unable to authenticate using the Authorization header
belgoros opened this issue · 14 comments
I'm trying without success to implement a simple test for the following controller end-points:
@RestController
@RequestMapping("/test")
public class SimpleController {
@RequestMapping(value = "/anonymous", method = RequestMethod.GET)
public ResponseEntity<String> getAnonymous() {
return ResponseEntity.ok("Hello Anonymous");
}
@RequestMapping(value = "/user", method = RequestMethod.GET)
public ResponseEntity<String> getUser() {
return ResponseEntity.ok("Hello User");
}
...
}
The test class:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
class SimpleControllerTest {
@Autowired
private MockMvc mockMvc;
static KeycloakMock keycloakMock = new KeycloakMock(ServerConfig.aServerConfig()
.withPort(8080)
.withRealm("Demo-Realm")
.build());
@BeforeAll
static void setUp() {
keycloakMock.start();
}
@AfterAll
static void tearDown() {
keycloakMock.stop();
}
@Test
public void shouldBeAccessibleForAnybody() throws Exception {
this.mockMvc.perform(get("/test/anonymous"))
.andDo(print())
.andExpect(MockMvcResultMatchers.content().string(containsString("Hello Anonymous")))
.andExpect(status().isOk());
}
@Test
public void shouldBeAccessibleWithUserRole() throws Exception {
TokenConfig tokenConfig = aTokenConfig()
.withRealmRole("app-user")
.withClaim("password", "mypassword")
.withPreferredUsername("employee1")
.withResourceRole("springboot-microservice", "user")
.build();
String accessToken = keycloakMock.getAccessToken(tokenConfig);
mockMvc.perform(get("/test/user")
.header("Authorization", "Bearer " + accessToken))
.andDo(print())
.andExpect(MockMvcResultMatchers.content().string(containsString("Hello User")))
.andExpect(status().isOk());
}
}
The security config class looks like this:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(jsr250Enabled = true)
public class KeycloakSecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.authorizeRequests()
.antMatchers("/test/anonymous").permitAll()
.antMatchers("/test/user").hasAnyRole("user")
.antMatchers("/test/admin").hasAnyRole("admin")
.antMatchers("/test/all-user").hasAnyRole("user", "admin")
.anyRequest()
.permitAll();
http.csrf().disable();
}
...
The first test, for anonymous, passes, but the second fails with:
MockHttpServletRequest:
HTTP Method = GET
Request URI = /test/user
Parameters = {}
Headers = [Authorization:"Bearer eyJraWQiOiJrZXlJZCIsImFsZyI6IlJTMjU2In0.eyJhdWQiOlsic2VydmVyIl0sImlhdCI6MTYyMTQxNjA5MSwiYXV0aF90aW1lIjoxNjIxNDE2MDkxLCJleHAiOjE2MjE0NTIwOTEsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9hdXRoL3JlYWxtcy9EZW1vLVJlYWxtIiwic3ViIjoidXNlciIsInNjb3BlIjoib3BlbmlkIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoiY2xpZW50IiwicHJlZmVycmVkX3VzZXJuYW1lIjoiZW1wbG95ZWUxIiwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImFwcC11c2VyIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsic3ByaW5nYm9vdC1taWNyb3NlcnZpY2UiOnsicm9sZXMiOlsidXNlciJdfX0sInBhc3N3b3JkIjoibXlwYXNzd29yZCJ9.MPY4ynIWuZ0ZaPVh7zljhPSO9lE7i_XIY4NDX7Vb2qBbihRKIISEFqFp8yNG3SYV9UjvNNR2H0pvL9RwKJuHGWeQs-CP8uFgR-GuvSqdyGgBHlj2zs89gaZ07EhaD2cSdHDGzjlXTUZK__qUMM9fEIaGi39BWDJ3XfIiqyORkpRp4a79fLluZ5ip4WW5gShyqDgUcrB2sg9sTglYIuSyt37TlXkkXkH-qN5S28qm5mjiG9G_xZPenNkNrRbZ5zbzOgK2HsgflCsh6cc2bj6FAIdaqGaRxxPLpfWKs7j6gwQX7aTipfg5YhJ87SzPfzu5izby99SHTTfegOPcGEq-MA"]
...
MockHttpServletResponse:
Status = 401
Error message = Unable to authenticate using the Authorization header
Headers = [WWW-Authenticate:"Bearer realm="Demo-Realm", error="invalid_token", error_description="Didn't find publicKey for specified kid"", X-Content-Type-Options:"nosniff", X-XSS-Protection:"1; mode=block", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0", X-Frame-Options:"DENY"]
...
java.lang.AssertionError: Response content
Expected: a string containing "Hello User"
but: was ""
Expected :a string containing "Hello User"
Actual :""
What am I missing here?
Used versions:
- OpenJDK 11
- keycloak-mock
0.7.0
- keycloak version:
13.0
.
Which spring-boot version? No errors in the log? See #74
@mbreevoort The spring-boot version is 2.4.5
but I have no NPE, just wonder if I set all the needed properties in the test example. Because if I use the token generated in the test and use in the Postman, it also fails with 401
error.
Does it work with spring-boot 2.4.4? Then temporary downgrade to netty 4.1.60.Final
on the test scope...
When testing the same end-points from the Postman it works fine with 2.4.5
version (I get the token first, then test /test/user
end-point and others). But the test for /test/user
using the token fails.
Downgrading to the 2.4.4
version had no effect. That's why I think it is rather the test values setup issue.
If I replace the token
value with the one I get from Postman, it works:
@Test
public void shouldBeAccessibleWithUserRole() throws Exception {
TokenConfig tokenConfig = aTokenConfig()
.withRealmRole("app-user")
.withPreferredUsername("employee1")
.withResourceRole("springboot-microservice", "user")
.build();
String accessToken = "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJJUDVES0ZsdU5GUTV1Tml4SmlvXzBvczdTeFFMMTdXakE3MVhSbkRtOTkwIn0.eyJleHAiOjE2MjE0MjEzMDMsImlhdCI6MTYyMTQyMTAwMywianRpIjoiZWNkMWI5YWUtNjNmMy00Mzk3LWI3MTQtNWY5NzA2YWYxNDRiIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL2F1dGgvcmVhbG1zL0RlbW8tUmVhbG0iLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiYWVmYzIyMWMtMTQwMi00MWI0LThmYjAtNGJlMmVhYTAwM2YyIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoic3ByaW5nYm9vdC1taWNyb3NlcnZpY2UiLCJzZXNzaW9uX3N0YXRlIjoiYzc0ODcwMjUtM2I1ZS00ZjQ0LTk1MDktZTJjZGRlNjRjODkwIiwiYWNyIjoiMSIsImFsbG93ZWQtb3JpZ2lucyI6WyJodHRwOi8vbG9jYWxob3N0OjgwODAiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iLCJhcHAtdXNlciJdfSwicmVzb3VyY2VfYWNjZXNzIjp7InNwcmluZ2Jvb3QtbWljcm9zZXJ2aWNlIjp7InJvbGVzIjpbInVzZXIiXX0sImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoiZW1haWwgcHJvZmlsZSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJlbXBsb3llZTEifQ.oliBcu07D_kwUJixHcGEzUV2n8PcGiCY9fgkJty_4z_RclKp5x8IimF8T50rDBX0iQwe7j-NiPGa92qLxtMvllCXI355MXY3ty4btK2vmvZQ0okMsVNFV84LKD2fu4P1pjsfcvH0kWaP3UVd7OOakaDpTxnN6HfXu5-wo-nESWqnWN0XixN2t2Zqj5Du24FzjqaskyjE-UIYYRzfiSE27pPELHgfllqoBAOprOmaB8EWZhGLP0Rg3R9SdLKqpF4v2lFiokzBR67YDLt0iHTik-rBwTc8CWH9mc0R92qE_vuzlkwcaspMK_WAte8WcCUBrsNQA9TSsKLv4JgmFqWgIQ";//keycloakMock.getAccessToken(tokenConfig);
mockMvc.perform(get("/test/user")
.header("Authorization", "Bearer " + accessToken))
.andDo(print())
.andExpect(MockMvcResultMatchers.content().string(containsString("Hello User")))
.andExpect(status().isOk());
}
So it looks like setting thetokenConfig
is wrong in the above test example.
I use the ClassRule
@ClassRule
public static KeycloakMockRule keycloakMock = new KeycloakMockRule(
ServerConfig.aServerConfig()..withPort(8080)
.withRealm("Demo-Realm")
.build())
);
Did you set the url of keycloak to http://localhost:8000/auth
we use a config like this:
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2ResourceServer()
.jwt()
.jwkSetUri(String.format("%s/realms/%s/protocol/openid-connect/certs", serverUrl, realmName))
I don't know how you can use KeycloakMockRule
, - it is not available in keycloakmock
0.7.0 version, where does come from?
Second point, the current security configuration class is defined as follows:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(jsr250Enabled = true)
public class KeycloakSecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.authorizeRequests()
.antMatchers("/test/anonymous").permitAll()
.antMatchers("/test/user").hasAnyRole("user")
.antMatchers("/test/admin").hasAnyRole("admin")
.antMatchers("/test/all-user").hasAnyRole("user", "admin")
.anyRequest()
.permitAll();
http.csrf().disable();
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) {
KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
auth.authenticationProvider(keycloakAuthenticationProvider);
}
@Bean
@Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
}
@Bean
public KeycloakConfigResolver KeycloakConfigResolver() {
return new KeycloakSpringBootConfigResolver();
}
It's from keycloakmock-junit a convenient class.
Forget my config
Try to configure it like https://github.com/TNG/keycloak-mock/tree/master/example-backend/src/
Also check the application.yaml
Hi @belgoros, can you perhaps share your keycloak.json or the Keycloak-specific part in your application.(yaml|properties) where you tell your Spring Boot application what Keycloak server to use and which realm to connect to?
It is not the first project where we are using Keycloak and keycloak-mock
almost the same way, - everything works pretty well. That's why I'm rather convinced that there is something wrong/different with either the realm or test settings.
Here are the steps to reproduce the issue I followed:
- get the token either with Postman client (or other) or
curl
as follows:
curl --location --request POST 'http://localhost:8080/auth/realms/Demo-Realm/protocol/openid-connect/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=password' \
--data-urlencode 'client_id=springboot-microservice' \
--data-urlencode 'client_secret=4e4ca23d-2520-4b93-bede-07ab4933a26c' \
--data-urlencode 'username=employee1' \
--data-urlencode 'password=mypassword'
- hit the end-point
http://localhost:8000/test/user
using the received token (see above):
curl --location --request GET 'http://localhost:8000/test/user' \
--header 'Authorization: Bearer <token>'
- the response should be
Hello User
When testing the same scenario, it fails:
package com.altran.software.factory.Keycloakspringbootmicroservice.controllers;
import com.tngtech.keycloakmock.api.KeycloakMock;
import com.tngtech.keycloakmock.api.ServerConfig;
import com.tngtech.keycloakmock.api.TokenConfig;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import static com.tngtech.keycloakmock.api.TokenConfig.aTokenConfig;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
class SimpleControllerTest {
@Autowired
private MockMvc mockMvc;
static KeycloakMock keycloakMock = new KeycloakMock(ServerConfig.aServerConfig()
.withPort(8080)
.withRealm("Demo-Realm")
.build());
@BeforeAll
static void setUp() {
keycloakMock.start();
}
@AfterAll
static void tearDown() {
keycloakMock.stop();
}
@Test
public void shouldBeAccessibleForAnybody() throws Exception {
this.mockMvc.perform(get("/test/anonymous"))
.andDo(print())
.andExpect(MockMvcResultMatchers.content().string(containsString("Hello Anonymous")))
.andExpect(status().isOk());
}
@Test
public void shouldBeAccessibleWithUserRole() throws Exception {
TokenConfig tokenConfig = aTokenConfig()
.withRealmRole("app-user")
.withPreferredUsername("employee1")
.withResourceRole("springboot-microservice", "user")
.withClaim("password", "mypassword")
.build();
String accessToken = keycloakMock.getAccessToken(tokenConfig);
mockMvc.perform(get("/test/user")
.header("Authorization", "Bearer " + accessToken))
.andDo(print())
.andExpect(MockMvcResultMatchers.content().string(containsString("Hello User")))
.andExpect(status().isOk());
}
}
I attach the keycloak config JSON fille and the Postman collection. You will have just import them into the Keycloak and Postman.
Once again, when replacing the accessToken
value:
String accessToken = keycloakMock.getAccessToken(tokenConfig);
with the token got with Postman, the test passes without problems, so I think the problem is in the generated token:
MockHttpServletResponse:
Status = 401
Error message = Unable to authenticate using the Authorization header
Headers = [WWW-Authenticate:"Bearer realm="Demo-Realm", error="invalid_token", error_description="Didn't find publicKey for specified kid"", X-Content-Type-Options:"nosniff", X-XSS-Protection:"1; mode=block", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0", X-Frame-Options:"DENY"]
Here is my application.yml
file (same for tests and dev):
spring:
application:
name: keycloak-springboot-microservice
allowed-origin: "*"
server:
port: 8000
keycloak:
realm: Demo-Realm
auth-server-url: http://localhost:8080/auth
ssl-required: external
resource: springboot-microservice
credentials:
secret: 4e4ca23d-2520-4b93-bede-07ab4933a26c
use-resource-role-mappings: true
bearer-only: true
When using Postman to get a token, I passed client_secret
as well as username and password. How can I do it when building the TokenConfig
instance? I can't see any methods like with***
there:
TokenConfig tokenConfig = aTokenConfig()
.withRealmRole("app-user")
.withPreferredUsername("employee1")
.withResourceRole("springboot-microservice", "user")
.withClaim("password", "mypassword")
.build();
...
You can find and compare:
After some debugging, it seems like the issue comes from AdapterTokenVerifier
class of the keycloak-adapter-core
in the #getPublicKey
method:
But it is still because of the wrong token it gets.
When comparing the decoded token with jwt.io, the difference is in the header:
Token generated with keycloak-mock:
{
"kid": "keyId",
"alg": "RS256"
}
Token generated by Keycloak server and fetched with Postman:
{
"alg": "RS256",
"typ": "JWT",
"kid": "IP5DKFluNFQ5uNixJio_0os7SxQL17WjA71XRnDm990"
}
Is it normal "kid": "keyId"
to be present in the keycloak-mock header compared to the real one: "kid": "IP5DKFluNFQ5uNixJio_0os7SxQL17WjA71XRnDm990"
? I can see that the value of the KEY_ID
constant in the TokenGenerator
class is just set to keyId
:
public class TokenGenerator {
private static final String KEY_ID = "keyId";
...
And to prove why so, let's just take a look at JWKPublicKeyLocator #lookupCachedKey
method where we'll have to extract a value from the currentKeys
map by kid
value (which is keyId
):
And as you could see, the currentKeys
map does not contain a key keyId
, so it will return NULL
:
It is intentional and wanted that the "kid" value is "keyId", because the mock server only has one key with exactly that ID. What confuses me is that you actually have different keys in your local storage. Are you sure you do not have an actual Keycloak instance running on localhost:8080?
@ostrya Hmm, you were right. I stopped the Keycloak server running locally at localhost:8080
and now getting another error:
2021-05-20 13:37:50.163 WARN 16879 --- [ main] o.keycloak.adapters.KeycloakDeployment : Failed to load URLs from http://localhost:8080/auth/realms/Demo-Realm/.well-known/openid-configuration
org.apache.http.NoHttpResponseException: localhost:8080 failed to respond
...
java.lang.NullPointerException
at java.base/java.net.URI$Parser.parse(URI.java:3104)
at java.base/java.net.URI.<init>(URI.java:600)
at java.base/java.net.URI.create(URI.java:881)
at org.apache.http.client.methods.HttpGet.<init>(HttpGet.java:66)
at org.keycloak.adapters.rotation.JWKPublicKeyLocator.sendRequest(JWKPublicKeyLocator.java:97)
at org.keycloak.adapters.rotation.JWKPublicKeyLocator.getPublicKey(JWKPublicKeyLocator.java:63)
Arr, downgrading the spring-boot from 2.4.5
to 2.4.4
version fixed the problem, - all the tests pass now. Thank you guys!