Search code examples
dockerdocker-composekeycloak

Keycloak using docker, for frontend and backend authentication


I’m trying to configure Keycloak using Docker. The Keycloak is used for authentication in the angular frontend application, and also in the java spring backend application. My docker-compose configuration for these services, is as follows:

    backend:
        image: backend
        container_name: backend
        build:
            context: ./backend
        ports:
            - 8080:8081
        depends_on:
            - db
        networks:
            - net
        restart: always

    frontend:
        image: frontend
        container_name: frontend
        build:
            context: ./frontend
        ports:
            - 80:80
        depends_on:
            - backend
        networks:
            - net
        restart: always

    keycloak:
        image: quay.io/keycloak/keycloak:25.0.4
        command: start
        environment:
            KC_HOSTNAME_PORT: 8080
            KC_HTTP_ENABLED: true
            KC_HOSTNAME_STRICT_HTTPS: false
            KC_HEALTH_ENABLED: true
            KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN_USER}
            KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PWD}
            KC_DB: postgres
            KC_DB_URL: ${KEYCLOAK_DB_URL}
            KC_DB_USERNAME: ${KEYCLOAK_DB_USERNAME}
            KC_DB_PASSWORD: ${KEYCLOAK_DB_PWD}
            KC_HOSTNAME_STRICT: false
            KC_PROXY: edge
            KC_HOSTNAME: http://keycloak:8080
            KC_HOSTNAME_BACKCHANNEL_DYNAMIC: true
        ports:
            - 8083:8080
        restart: always
        networks:
            - net

    db:
        image: 'postgres'
        container_name: db
        environment:
            - POSTGRES_USER=${POSTGRES_USER}
            - POSTGRES_PASSWORD=${POSTGRES_PWD}
        ports:
            - "5432:5432"
        networks:
            - net

The spring property is this:

spring.security.oauth2.resourceserver.jwt.issuer-uri=http://keycloak:8080/realms/<realm-name>

And in the angular, I’m using the following configuration:

config: {
    url: 'http://localhost:8083',
    realm: "<realm-name>",
    clientId: "<client-id>"
}

Authentication in the backend works well, but the problem is the authentication in the frontend application, because as the keycloak hostname is “keycloak”, in frontend application it doesn’t know this hostname. If I change it to localhost, the backend container cannot connect to the keycloak container.

Does anyone know how I can solve this problem?


Solution

  • Why

    We should use the same value for the authorization server hostname across all actors because of OIDC discovery and OpenID tokens validation specifications which require that the Issuer Identifier must be the exact same in:

    • the discovery endpoint URI: {Issuer Identifier}/.well-known/openid-configuration
    • the issuer property of OpenID configuration (exposed at the discovery endpoint)
    • the iss claim in JWTs and introspection endpoint

    For Keycloak, the Issuer Identifier value depends on the hostname configuration property - or request URI if hostname-strict=false.

    As it is used by your Spring resource server (during OIDC discovery and access token validation) and Angular (during authorization code flow and, because you configured Angular as a public OAuth2 client, during OIDC discovery and ID token validation), you should set hostname with a value known within Docker containers and the host that runs the browser you are displaying the SPA with.

    As you seem to be using a reverse proxy (edge which, by the way, was removed in the latest releases), I'd use a URI through that reverse proxy (with care to the hostname, but also scheme and port). I'd also set hostname-strict=true to ensure that everything uses only that URI.

    keycloak is known as hostname only within Docker, which is the reason why http://keycloak:8080 works for the Spring container but not the browser on the Docker host machine. loclahost works within the browser because you expose Keycloak on the host machine, but for the Spring container, localhost loops to itself, not to the host.

    Solutions

    Use a hostname known everywhere

    In this article I wrote as an introduction to Keycloak with Spring, I use the value of the hostname command in a shell prompt on the host machine (on Widows, using Git Bash). The output of this command should be known everywhere.

    In the case where it wouldn't be known by Docker containers, you can add to extra_hosts. Sample in a Docker compose file (replace {hostname}):

    services:
      backend:
        extra_hosts:
        - "{hostname}:host-gateway"
    

    Disable OIDC discovery and issuer validation

    Another option - that I recommend not to use - is to set the Issuer Identifier hostname with a value working for the frontend (localhost for instance), and to disable OIDC discovery and JWT issuer validation in Spring: instead of relying on the OpenID auto-configuration from the issuer-uri, leave it empty and manually set all other provider properties.

    Configure the host machine to resolve Docker services

    A last option is to add the following entry in the hosts file of the host machine (depending on the OS, /etc/hosts or C:\Windows\System32\drivers\etc\hosts):

    127.0.0.1 keycloak
    

    Important side note

    For security reasons, it is now recommended to use only confidential clients (those using a secret and running on the backend). So, the OAuth2 client should not be your Angular app but a middleware on the backend. I wrote another article for that. Note that this OAuth2 BFF pattern still uses the authorization code flow. So, this does not remove the need for the browser to redirect to the authorization server - and for the hostname in the Issuer Identifier to be resolvable from all the involved hosts and containers.