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:
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.