dasniko / testcontainers-keycloak

A Testcontainer implementation for Keycloak IAM & SSO.
Apache License 2.0
341 stars 53 forks source link

HTTP 401 Unauthorized response when getting a token #8

Closed belgoros closed 3 years ago

belgoros commented 4 years ago

I'm trying to run a test using the exported from Keycloack server JSON configuration file as follows:

package com.example.controllers;

import org.junit.jupiter.api.Test;
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 static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@AutoConfigureMockMvc
class TestControllerTest extends BaseKeycloakTest {

    @Autowired
    protected MockMvc mockMvc;

    @Test
    public void shouldRespondOKForAdminIfTokenPresent() throws Exception {
        this.mockMvc.perform(get("/test/admin")
                .header("Authorization", "Bearer "
                        + getAccessToken(
                                "employee2",
                                "mypassword",
                                  "springboot-microservice",
                                   "demo")))
                .andExpect(status().isOk());
    }
}

The BaseKeycloakTest class is defined as follows:

@Testcontainers
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class BaseKeycloakTest {
    @Container
    protected static final KeycloakContainer keycloak = new KeycloakContainer()
            .withRealmImportFile("realm-export.json");

    @BeforeAll
    public static void setUp() {
        keycloak.start();
    }
    @AfterAll
    public static void tearDown() {
        keycloak.stop();
    }
...

I'm getting the token in BaseKeycloakTest as follows:

protected static String getAccessToken(String username, String password, String clientId, String realm) {
        RestTemplate restTemplate = new RestTemplate();
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

        LinkedMultiValueMap<String, String> map = new LinkedMultiValueMap<>();
        map.add("grant_type", "password");
        map.add("client_id", clientId);
        map.add("username", username);
        map.add("password", password);
        map.add("client_secret", "85a58c55-dd32-4205-a568-f82ae710edd1");

        KeyCloakToken token = restTemplate.postForObject(
                tokenUrlFor(realm),
                new HttpEntity<>(map, headers), KeyCloakToken.class);

        assert token != null;
        return token.getAccessToken();
    }

    private static String tokenUrlFor(String realm) {
        String url = keycloak.getAuthServerUrl()
                + "/realms/"
                + realm
                + "/protocol/openid-connect/token";
        return url;
    }

    private static class KeyCloakToken {

        private String accessToken;

        @JsonCreator
        KeyCloakToken(@JsonProperty("access_token") final String accessToken) {
            this.accessToken = accessToken;
        }

        public String getAccessToken() {
            return accessToken;
        }
    }

When running the test, it seems like realm-export.json file is not taken in consideration, - the generated token URL looks like that:

http://localhost:32791/auth/realms/demo/protocol/openid-connect/token

instead of:

http://localhost:8080/auth/realms/demo/protocol/openid-connect/token

what raises the error:

org.springframework.web.client.HttpClientErrorException$Unauthorized: 401 Unauthorized: [no body]

    at org.springframework.web.client.HttpClientErrorException.create(HttpClientErrorException.java:105)
    at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:184)

...

The KeucloakConfig class looks like that:

package com.example.config;

import org.keycloak.adapters.KeycloakConfigResolver;
import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver;
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider;
import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class KeycloakConfig 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) throws Exception {
        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();
    }
}

And application.yml for tests looks like that:

spring:
  application:
    name: spring-demo

server:
  port: 8000

keycloak:
  realm: demo
  auth-server-url: http://localhost:8080/auth
  ssl-required: external
  resource: springboot-microservice
  credentials:
    secret: 85a58c55-dd32-4205-a568-f82ae710edd1
  use-resource-role-mappings: true
  bearer-only: true

What is wrong with that?

dasniko commented 4 years ago

The Keycloak-Testcontainer won't configure itself base on some Spring config properties. Testcontainers have no relation to Spring. Generally, Testcontainers start on a random port on each run. That's the concept of Testcontainers.

You have to take care in your Spring test environment/configuration to set the appropriate url/port of Keycloak after Keycloak-Testcontainer has startet. Please consult the Spring docs on how to do that. I'm not a Spring guy, I don't know how this can be achieved, I only know that it is possible and also a well-known and wide-used practice.

belgoros commented 4 years ago

I just wonder if:

manedev79 commented 3 years ago

I'm facing the same issue with Spring. Would be great to have a pointer to the widely used practice.

dasniko commented 3 years ago

Please consult the Spring Boot docs for @ContextConfiguration and ApplicationContextInitializer<ConfigurableApplicationContext>. That‘s what you need.