2

I'm having trouble getting Keycloak to work in docker compose. I have a basic Spring Boot REST API that uses the new OAuth stack from Spring Security to work as a resource server. I've set up the Keycloak authentication server to import a realm on first start up. I'm trying to use the client-credentials flow. It works fine when the API is run locally, but when I run it in docker-compose, it fails with a 401 Unauthorized before it even reaches the endpoint. From what I've read, it is potentially a cors error in that the preflight request is what triggers the 401, but none of the configuration I've played with has worked. When I try to access with curl, the response indicates that it's an invalid token. I've looked into that too for some common issues like timezone drift and whatnot, but also doesn't seem to be the culprit. I've updated my etc/hosts file so I can change the iss claim to be keycloak:8081 but that doesn't help either. To be clear, I can log in no problem, and if I turn off the .authorizeRequests().anyRequest().authenticated().and().oauth2ResourceServer().jwt() it also works.

version: "3.8"

services:
  mariadb:
    image: mariadb:10.5.3
    environment:
      MYSQL_ROOT_PASSWORD: root_password
      MYSQL_DATABASE: keycloak
      MYSQL_USER: some_user
      MYSQL_PASSWORD: password
    container_name: mariadb-10.5.3
    networks:
      - app
    volumes:
      - "auth:/var/lib/mysql"

  auth:
    build:
      context: ./auth/
      args:
        - JAVA_VERSION=14
    image: auth:1.0.0.1
    container_name: auth-1.0.0.1
    ports:
      - "8081:8081"
    environment:
      KEYCLOAK_DB_PROTOCOL: mysql
      KEYCLOAK_DB_HOST: mariadb
      KEYCLOAK_DB_PORT: 3306
      KEYCLOAK_DB_NAME: keycloak
      KEYCLOAK_DB_USERNAME: some_user
      KEYCLOAK_DB_PASSWORD: password
      KEYCLOAK_DB_DRIVER: org.mariadb.jdbc.Driver
      KEYCLOAK_CONTEXT_PATH: /auth
    depends_on:
      - mariadb
    entrypoint:
      ["./wait-for-it.sh", "mariadb:3306", "--", "java", "-jar", "/app.jar"]
    networks:
      - app

  api:
    build:
      context: ./api/
      args:
        - JAVA_VERSION=14
    image: api:1.0.0.1
    container_name: api-1.0.0.1
    ports:
      - "8080:8080"
    environment:
      KEYCLOAK_HOST: keycloak
      KEYCLOAK_PORT: 8081
      KEYCLOAK_CONTEXT_PATH: /auth
      KEYCLOAK_REALM: api
      ELASTICSEARCH_HOST: elasticsearch
      ELASTICSEARCH_PORT: 9200
    depends_on:
      - elasticsearch
      - auth
    entrypoint:
      [
        "./wait-for-it.sh",
        "elasticsearch:9200",
        "--",
        "java",
        "-jar",
        "/app.jar",
      ]
    networks:
      - app

  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch-oss:7.7.1
    container_name: elasticsearch-7.7.1
    environment:
      discovery.type: single-node
    ports:
      - "9200:9200"
      - "9300:9300"
    networks:
      - app
      - elk

  kibana:
    image: docker.elastic.co/kibana/kibana-oss:7.7.1
    container_name: kibana-7.7.1
    depends_on:
      - elasticsearch
    ports:
      - "5601:5601"
    networks:
      - elk

  logstash:
    build:
      context: ./elk/logstash/
    image: logstash:7.7.1
    container_name: logstash-7.7.1
    depends_on:
      - elasticsearch
    environment:
      PIPELINE_WORKERS: 1
    networks:
      - elk

networks:
  elk:
    name: elk
  app:
    name: app

volumes:
  auth:
    name: auth
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)  // enables PreAuthorize annotation
public class SecurityConfig extends WebSecurityConfigurerAdapter {
  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.cors().and()
          .authorizeRequests()
            .anyRequest()
              .authenticated()
        .and()
          .oauth2ResourceServer()
            .jwt();
  }
}

api - application.properties

spring.security.oauth2.resourceserver.jwt.issuer-uri=http://\${KEYCLOAK_HOST:localhost}:\${KEYCLOAK_PORT}/\${KEYCLOAK_CONTEXT_PATH:auth}/realms/\${KEYCLOAK_REALM}
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://\${KEYCLOAK_HOST:localhost}:\${KEYCLOAK_PORT}/\${KEYCLOAK_CONTEXT_PATH:auth}/realms/\${KEYCLOAK_REALM}/protocol/openid-connect/certs

keycloak - application.yaml

keycloak:
  cors: true
  server:
    contextPath: ${KEYCLOAK_CONTEXT_PATH:/auth}
    adminUser:
      username: admin
      password: admin
    realmImportFile:
      - api-realm.json

controller

@RestController
@RequestMapping("/")
public class Controller {
  @Autowired
  IDocumentRepository documentRepository;

  @GetMapping("/{id}")
  @PreAuthorize("@authorizationService.checkClientIDMatchesID(#token, #id)")
  public Iterable<Documents> getAll(@AuthenticationPrincipal Jwt token, @PathVariable("id") String id) {
    return documentRepository.findById(id);
  }
}

custom authorization method

@Service
public class AuthorizationService {
  private static final Logger logger = LoggerFactory.getLogger(AuthorizationService.class);

  public boolean checkClientIDMatchesID(Jwt token, String id)
  {
    if (id == null || token == null || token.getClaimAsBoolean("clientId") == null) return false;
    return ((String)token.getClaim("clientId")).equalsIgnoreCase(id);
  }
}

Here are the logs for the API: enter image description here

And for Keycloak: enter image description here

curl response: enter image description here

I played around with decoding it on jwt.io, and it's saying the signature isn't verified, the public key doesn't seem to make it valid.

errorline1
  • 350
  • 1
  • 6
  • 18
  • Do you get any error trace at server side? – Aritz Jun 22 '20 at 20:29
  • I've added screenshots. There's not much in the way of information, but I added another edit as I was playing around with JWT signatures – errorline1 Jun 23 '20 at 13:32
  • /auth/realms/master/protocol/openid-connect/token I guess this is the token endpoint. How did you manage to communicate with api and keycloak – Gurkan İlleez Jun 23 '20 at 14:42
  • https://stackoverflow.com/questions/57974630/oauth2-client-credentials-flow-via-spring-boot-keycloak-integration – Gurkan İlleez Jun 23 '20 at 14:54
  • @Gurkanİlleez I read the link but it doesn't seem to address the question. The API uses the oauth2ResourceServer method from Spring Security, allowing it to act as a relying party. It reads `spring.security.oauth2.resourceserver.jwt.issuer-uri` and `spring.security.oauth2.resourceserver.jwt.jwk-set-uri` to check tokens. I think! – errorline1 Jun 24 '20 at 16:52
  • Did you import client by api-realm.json? you can try to get token by the given url '/auth/realms/${realm}/protocol/openid-connect/token'. If it does not work your configuration can be misconfigured. – Gurkan İlleez Jun 30 '20 at 12:29

0 Answers0